package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"slices"
"strings"
"sync"
"time"
"github.com/spf13/cobra"
"golang.org/x/sys/unix"
"github.com/lxc/incus/v6/internal/linux"
"github.com/lxc/incus/v6/internal/server/instance/instancetype"
"github.com/lxc/incus/v6/shared/logger"
"github.com/lxc/incus/v6/shared/subprocess"
"github.com/lxc/incus/v6/shared/util"
)
var servers = make(map[string]*http.Server, 2)
var errChan = make(chan error)
type cmdAgent struct {
global *cmdGlobal
}
func (c *cmdAgent) Command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = "incus-agent [--debug]"
cmd.Short = "Incus virtual machine agent"
cmd.Long = `Description:
Incus virtual machine agent
This daemon is to be run inside virtual machines managed by Incus.
It will normally be started through init scripts present or injected
into the virtual machine.
`
cmd.RunE = c.Run
return cmd
}
func (c *cmdAgent) Run(cmd *cobra.Command, args []string) error {
err := logger.InitLogger("", "", c.global.flagLogVerbose, c.global.flagLogDebug, nil)
if err != nil {
os.Exit(1)
}
logger.Info("Starting")
defer logger.Info("Stopped")
files, err := templatesApply("files/")
if err != nil {
return err
}
if util.PathExists("/proc/sys/kernel/hostname") && slices.Contains(files, "/etc/hostname") {
src, err := os.Open("/etc/hostname")
if err != nil {
return err
}
dst, err := os.Create("/proc/sys/kernel/hostname")
if err != nil {
return err
}
_, err = io.Copy(dst, src)
if err != nil {
return err
}
_ = src.Close()
err = dst.Close()
if err != nil {
return err
}
}
if util.PathExists("/etc/cloud") && slices.Contains(files, "/var/lib/cloud/seed/nocloud-net/meta-data") {
logger.Info("Seeding cloud-init")
cloudInitPath := "/run/cloud-init"
if util.PathExists(cloudInitPath) {
logger.Info(fmt.Sprintf("Removing %q", cloudInitPath))
err = os.RemoveAll(cloudInitPath)
if err != nil {
return err
}
}
logger.Info("Rebooting")
_, _ = subprocess.RunCommand("reboot")
time.Sleep(300 * time.Second)
}
reconfigureNetworkInterfaces()
if !util.PathExists("/dev/vsock") {
logger.Info("Loading vsock module")
err = linux.LoadModule("vsock")
if err != nil {
return fmt.Errorf("Unable to load the vsock kernel module: %w", err)
}
for i := 0; i < 5; i++ {
if !util.PathExists("/dev/vsock") {
time.Sleep(1 * time.Second)
}
}
}
c.mountHostShares()
d := newDaemon(c.global.flagLogDebug, c.global.flagLogVerbose)
err = startHTTPServer(d, c.global.flagLogDebug)
if err != nil {
return fmt.Errorf("Failed to start HTTP server: %w", err)
}
if util.PathExists("agent.conf") {
f, err := os.Open("agent.conf")
if err != nil {
return err
}
err = setConnectionInfo(d, f)
if err != nil {
_ = f.Close()
return err
}
_ = f.Close()
if d.DevIncusEnabled {
err = startDevIncusServer(d)
if err != nil {
return err
}
}
}
ctx, cancelFunc := context.WithCancel(context.Background())
cancelStatusNotifier := c.startStatusNotifier(ctx, d.chConnected)
if os.Getenv("NOTIFY_SOCKET") != "" {
_, err := subprocess.RunCommand("systemd-notify", "READY=1")
if err != nil {
cancelStatusNotifier()
cancelFunc()
return fmt.Errorf("Failed to notify systemd of readiness: %w", err)
}
}
chSignal := make(chan os.Signal, 1)
signal.Notify(chSignal, unix.SIGTERM)
exitStatus := 0
select {
case <-chSignal:
case err := <-errChan:
fmt.Fprintln(os.Stderr, err)
exitStatus = 1
}
cancelStatusNotifier()
cancelFunc()
os.Exit(exitStatus)
return nil
}
func (c *cmdAgent) startStatusNotifier(ctx context.Context, chConnected <-chan struct{}) context.CancelFunc {
_ = c.writeStatus("STARTED")
wg := sync.WaitGroup{}
exitCtx, exit := context.WithCancel(ctx)
cancel := func() {
exit()
wg.Wait()
}
wg.Add(1)
go func() {
defer wg.Done()
ticker := time.NewTicker(time.Duration(time.Second) * 5)
defer ticker.Stop()
for {
select {
case <-chConnected:
_ = c.writeStatus("CONNECTED")
case <-ticker.C:
_ = c.writeStatus("STARTED")
case <-exitCtx.Done():
_ = c.writeStatus("STOPPED")
return
}
}
}()
return cancel
}
func (c *cmdAgent) writeStatus(status string) error {
if util.PathExists("/dev/virtio-ports/org.linuxcontainers.incus") {
vSerial, err := os.OpenFile("/dev/virtio-ports/org.linuxcontainers.incus", os.O_RDWR, 0600)
if err != nil {
return err
}
defer vSerial.Close()
_, err = vSerial.Write([]byte(fmt.Sprintf("%s\n", status)))
if err != nil {
return err
}
}
return nil
}
func (c *cmdAgent) mountHostShares() {
agentMountsFile := "./agent-mounts.json"
if !util.PathExists(agentMountsFile) {
return
}
b, err := os.ReadFile(agentMountsFile)
if err != nil {
logger.Errorf("Failed to load agent mounts file %q: %v", agentMountsFile, err)
}
var agentMounts []instancetype.VMAgentMount
err = json.Unmarshal(b, &agentMounts)
if err != nil {
logger.Errorf("Failed to parse agent mounts file %q: %v", agentMountsFile, err)
return
}
for _, mount := range agentMounts {
if !slices.Contains([]string{"9p", "virtiofs"}, mount.FSType) {
logger.Infof("Unsupported mount fstype %q", mount.FSType)
continue
}
err = tryMountShared(mount.Source, mount.Target, mount.FSType, mount.Options)
if err != nil {
logger.Infof("Failed to mount %q (Type: %q, Options: %v) to %q: %v", mount.Source, "virtiofs", mount.Options, mount.Target, err)
continue
}
logger.Infof("Mounted %q (Type: %q, Options: %v) to %q", mount.Source, mount.FSType, mount.Options, mount.Target)
}
}
func tryMountShared(src string, dst string, fstype string, opts []string) error {
if !strings.HasPrefix(dst, "/") {
dst = fmt.Sprintf("/%s", dst)
}
if !util.PathExists(dst) {
err := os.MkdirAll(dst, 0755)
if err != nil {
return fmt.Errorf("Failed to create mount target %q", dst)
}
} else if linux.IsMountPoint(dst) {
return nil
}
sharedArgs := []string{}
p9Args := []string{}
for _, opt := range opts {
if strings.HasPrefix(opt, "trans=") || strings.HasPrefix(opt, "msize=") {
p9Args = append(p9Args, "-o", opt)
continue
}
sharedArgs = append(sharedArgs, "-o", opt)
}
args := []string{"-t", "virtiofs", src, dst}
args = append(args, sharedArgs...)
_, err := subprocess.RunCommand("mount", args...)
if err == nil {
return nil
} else if fstype == "virtiofs" {
return err
}
args = []string{"-t", "9p", src, dst}
args = append(args, sharedArgs...)
args = append(args, p9Args...)
_, err = subprocess.RunCommand("mount", args...)
if err != nil {
return err
}
return nil
}