From b2c56365b2d1282330caee3f6c53eb26c5d7d583 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Tue, 7 Nov 2023 21:40:32 -0500 Subject: [PATCH] Refactor Ignition configuration for virt providers Creates a common SetIgnitionFile function in pkg/machine/ignition.go which creates the new VMFile that will represent the machine's ignition file. It assigns the VMFile to the provided location. Creates an IgnitionBuilder type to generate the ignition configuration for a given virt provider. [NO NEW TESTS NEEDED] Signed-off-by: Jake Correnti --- pkg/machine/applehv/config.go | 4 +- pkg/machine/applehv/machine.go | 92 +++++++-------- pkg/machine/hyperv/config.go | 4 +- pkg/machine/hyperv/machine.go | 199 ++++++++++++++++----------------- pkg/machine/ignition.go | 63 +++++++++++ pkg/machine/qemu/config.go | 8 +- pkg/machine/qemu/machine.go | 91 +++++++-------- 7 files changed, 242 insertions(+), 219 deletions(-) diff --git a/pkg/machine/applehv/config.go b/pkg/machine/applehv/config.go index be4dfabbe9..28ab6db64e 100644 --- a/pkg/machine/applehv/config.go +++ b/pkg/machine/applehv/config.go @@ -129,11 +129,9 @@ func (v AppleHVVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, return nil, err } - ignitionPath, err := define.NewMachineFile(filepath.Join(configDir, m.Name)+".ign", nil) - if err != nil { + if err := machine.SetIgnitionFile(&m.IgnitionFile, vmtype, m.Name); err != nil { return nil, err } - m.IgnitionFile = *ignitionPath // Set creation time m.Created = time.Now() diff --git a/pkg/machine/applehv/machine.go b/pkg/machine/applehv/machine.go index 8da6f3f74c..31e1ca1b05 100644 --- a/pkg/machine/applehv/machine.go +++ b/pkg/machine/applehv/machine.go @@ -50,6 +50,22 @@ type VfkitHelper struct { VirtualMachine *vfConfig.VirtualMachine } +// appleHVReadyUnit is a unit file that sets up the virtual serial device +// where when the VM is done configuring, it will send an ack +// so a listening host knows it can begin interacting with it +const appleHVReadyUnit = `[Unit] +Requires=dev-virtio\\x2dports-%s.device +After=remove-moby.service sshd.socket sshd.service +OnFailure=emergency.target +OnFailureJobMode=isolate +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/bin/sh -c '/usr/bin/echo Ready | socat - VSOCK-CONNECT:2:1025' +[Install] +RequiredBy=default.target +` + type MacMachine struct { // ConfigPath is the fully qualified path to the configuration file ConfigPath define.VMFile @@ -136,51 +152,6 @@ func (m *MacMachine) addMountsToVM(opts machine.InitOptions, virtiofsMnts *[]mac return nil } -// writeIgnitionConfigFile generates the ignition config and writes it to the filesystem -func (m *MacMachine) writeIgnitionConfigFile(opts machine.InitOptions, key string, virtiofsMnts *[]machine.VirtIoFs) error { - // Write the ignition file - ign := machine.DynamicIgnition{ - Name: opts.Username, - Key: key, - VMName: m.Name, - VMType: machine.AppleHvVirt, - TimeZone: opts.TimeZone, - WritePath: m.IgnitionFile.GetPath(), - UID: m.UID, - Rootful: m.Rootful, - } - - if err := ign.GenerateIgnitionConfig(); err != nil { - return err - } - - // ready is a unit file that sets up the virtual serial device - // where when the VM is done configuring, it will send an ack - // so a listening host knows it can being interacting with it - ready := `[Unit] - Requires=dev-virtio\\x2dports-%s.device - After=remove-moby.service sshd.socket sshd.service - OnFailure=emergency.target - OnFailureJobMode=isolate - [Service] - Type=oneshot - RemainAfterExit=yes - ExecStart=/bin/sh -c '/usr/bin/echo Ready | socat - VSOCK-CONNECT:2:1025' - [Install] - RequiredBy=default.target - ` - readyUnit := machine.Unit{ - Enabled: machine.BoolToPtr(true), - Name: "ready.service", - Contents: machine.StrToPtr(fmt.Sprintf(ready, "vsock")), - } - virtiofsUnits := generateSystemDFilesForVirtiofsMounts(*virtiofsMnts) - ign.Cfg.Systemd.Units = append(ign.Cfg.Systemd.Units, readyUnit) - ign.Cfg.Systemd.Units = append(ign.Cfg.Systemd.Units, virtiofsUnits...) - - return ign.Write() -} - func (m *MacMachine) Init(opts machine.InitOptions) (bool, error) { var ( key string @@ -280,6 +251,17 @@ func (m *MacMachine) Init(opts machine.InitOptions) (bool, error) { return false, err } + builder := machine.NewIgnitionBuilder(machine.DynamicIgnition{ + Name: opts.Username, + Key: key, + VMName: m.Name, + VMType: machine.AppleHvVirt, + TimeZone: opts.TimeZone, + WritePath: m.IgnitionFile.GetPath(), + UID: m.UID, + Rootful: m.Rootful, + }) + if len(opts.IgnitionPath) < 1 { key, err = machine.CreateSSHKeys(m.IdentityPath) if err != nil { @@ -289,14 +271,22 @@ func (m *MacMachine) Init(opts machine.InitOptions) (bool, error) { } if len(opts.IgnitionPath) > 0 { - inputIgnition, err := os.ReadFile(opts.IgnitionPath) - if err != nil { - return false, err - } - return false, os.WriteFile(m.IgnitionFile.GetPath(), inputIgnition, 0644) + return false, builder.BuildWithIgnitionFile(opts.IgnitionPath) + } + + if err := builder.GenerateIgnitionConfig(); err != nil { + return false, err } + + builder.WithUnit(machine.Unit{ + Enabled: machine.BoolToPtr(true), + Name: "ready.service", + Contents: machine.StrToPtr(fmt.Sprintf(appleHVReadyUnit, "vsock")), + }) + builder.WithUnit(generateSystemDFilesForVirtiofsMounts(virtiofsMnts)...) + // TODO Ignition stuff goes here - err = m.writeIgnitionConfigFile(opts, key, &virtiofsMnts) + err = builder.Build() callbackFuncs.Add(m.IgnitionFile.Delete) return err == nil, err diff --git a/pkg/machine/hyperv/config.go b/pkg/machine/hyperv/config.go index 223241d9f4..a9da131cfc 100644 --- a/pkg/machine/hyperv/config.go +++ b/pkg/machine/hyperv/config.go @@ -126,11 +126,9 @@ func (v HyperVVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, m.ConfigPath = *configPath - ignitionPath, err := define.NewMachineFile(filepath.Join(configDir, m.Name)+".ign", nil) - if err != nil { + if err := machine.SetIgnitionFile(&m.IgnitionFile, vmtype, m.Name); err != nil { return nil, err } - m.IgnitionFile = *ignitionPath // Set creation time m.Created = time.Now() diff --git a/pkg/machine/hyperv/machine.go b/pkg/machine/hyperv/machine.go index ffacfa4ebb..cf526f68fb 100644 --- a/pkg/machine/hyperv/machine.go +++ b/pkg/machine/hyperv/machine.go @@ -43,6 +43,58 @@ const ( apiUpTimeout = 20 * time.Second ) +// hyperVReadyUnit is a unit file that sets up the virtual serial device +// where when the VM is done configuring, it will send an ack +// so a listening host knows it can begin interacting with it +// +// VSOCK-CONNECT:2 <- shortcut to connect to the hostvm +const hyperVReadyUnit = `[Unit] +After=remove-moby.service sshd.socket sshd.service +After=systemd-user-sessions.service +OnFailure=emergency.target +OnFailureJobMode=isolate +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/bin/sh -c '/usr/bin/echo Ready | socat - VSOCK-CONNECT:2:%d' +[Install] +RequiredBy=default.target +` + +// hyperVVsockNetUnit is a systemd unit file that calls the vm helper utility +// needed to take traffic from a network vsock0 device to the actual vsock +// and onto the host +const hyperVVsockNetUnit = ` +[Unit] +Description=vsock_network +After=NetworkManager.service + +[Service] +ExecStart=/usr/libexec/podman/gvforwarder -preexisting -iface vsock0 -url vsock://2:%d/connect +ExecStartPost=/usr/bin/nmcli c up vsock0 + +[Install] +WantedBy=multi-user.target +` + +const hyperVVsockNMConnection = ` +[connection] +id=vsock0 +type=tun +interface-name=vsock0 + +[tun] +mode=2 + +[802-3-ethernet] +cloned-mac-address=5A:94:EF:E4:0C:EE + +[ipv4] +method=auto + +[proxy] +` + type HyperVMachine struct { // ConfigPath is the fully qualified path to the configuration file ConfigPath define.VMFile @@ -93,104 +145,6 @@ func (m *HyperVMachine) addNetworkAndReadySocketsToRegistry() error { return nil } -// writeIgnitionConfigFile generates the ignition config and writes it to the -// filesystem -func (m *HyperVMachine) writeIgnitionConfigFile(opts machine.InitOptions, user, key string) error { - ign := machine.DynamicIgnition{ - Name: user, - Key: key, - VMName: m.Name, - VMType: machine.HyperVVirt, - TimeZone: opts.TimeZone, - WritePath: m.IgnitionFile.GetPath(), - UID: m.UID, - Rootful: m.Rootful, - } - - if err := ign.GenerateIgnitionConfig(); err != nil { - return err - } - - // ready is a unit file that sets up the virtual serial device - // where when the VM is done configuring, it will send an ack - // so a listening host knows it can being interacting with it - // - // VSOCK-CONNECT:2 <- shortcut to connect to the hostvm - ready := `[Unit] -After=remove-moby.service sshd.socket sshd.service -After=systemd-user-sessions.service -OnFailure=emergency.target -OnFailureJobMode=isolate -[Service] -Type=oneshot -RemainAfterExit=yes -ExecStart=/bin/sh -c '/usr/bin/echo Ready | socat - VSOCK-CONNECT:2:%d' -[Install] -RequiredBy=default.target -` - readyUnit := machine.Unit{ - Enabled: machine.BoolToPtr(true), - Name: "ready.service", - Contents: machine.StrToPtr(fmt.Sprintf(ready, m.ReadyHVSock.Port)), - } - - // userNetwork is a systemd unit file that calls the vm helpoer utility - // needed to take traffic from a network vsock0 device to the actual vsock - // and onto the host - userNetwork := ` -[Unit] -Description=vsock_network -After=NetworkManager.service - -[Service] -ExecStart=/usr/libexec/podman/gvforwarder -preexisting -iface vsock0 -url vsock://2:%d/connect -ExecStartPost=/usr/bin/nmcli c up vsock0 - -[Install] -WantedBy=multi-user.target -` - vsockNetUnit := machine.Unit{ - Contents: machine.StrToPtr(fmt.Sprintf(userNetwork, m.NetworkHVSock.Port)), - Enabled: machine.BoolToPtr(true), - Name: "vsock-network.service", - } - - ign.Cfg.Systemd.Units = append(ign.Cfg.Systemd.Units, readyUnit, vsockNetUnit) - - vSockNMConnection := ` -[connection] -id=vsock0 -type=tun -interface-name=vsock0 - -[tun] -mode=2 - -[802-3-ethernet] -cloned-mac-address=5A:94:EF:E4:0C:EE - -[ipv4] -method=auto - -[proxy] -` - - ign.Cfg.Storage.Files = append(ign.Cfg.Storage.Files, machine.File{ - Node: machine.Node{ - Path: "/etc/NetworkManager/system-connections/vsock0.nmconnection", - }, - FileEmbedded1: machine.FileEmbedded1{ - Append: nil, - Contents: machine.Resource{ - Source: machine.EncodeDataURLPtr(vSockNMConnection), - }, - Mode: machine.IntToPtr(0600), - }, - }) - - return ign.Write() -} - // readAndSplitIgnition reads the ignition file and splits it into key:value pairs func (m *HyperVMachine) readAndSplitIgnition() error { ignFile, err := m.IgnitionFile.Read() @@ -295,14 +249,21 @@ func (m *HyperVMachine) Init(opts machine.InitOptions) (bool, error) { } m.Rootful = opts.Rootful + builder := machine.NewIgnitionBuilder(machine.DynamicIgnition{ + Name: m.RemoteUsername, + Key: key, + VMName: m.Name, + VMType: machine.HyperVVirt, + TimeZone: opts.TimeZone, + WritePath: m.IgnitionFile.GetPath(), + UID: m.UID, + Rootful: m.Rootful, + }) + // If the user provides an ignition file, we need to // copy it into the conf dir if len(opts.IgnitionPath) > 0 { - inputIgnition, err := os.ReadFile(opts.IgnitionPath) - if err != nil { - return false, err - } - return false, os.WriteFile(m.IgnitionFile.GetPath(), inputIgnition, 0644) + return false, builder.BuildWithIgnitionFile(opts.IgnitionPath) } callbackFuncs.Add(m.IgnitionFile.Delete) @@ -310,8 +271,36 @@ func (m *HyperVMachine) Init(opts machine.InitOptions) (bool, error) { return false, err } - // Write the ignition file - if err := m.writeIgnitionConfigFile(opts, m.RemoteUsername, key); err != nil { + if err := builder.GenerateIgnitionConfig(); err != nil { + return false, err + } + + builder.WithUnit(machine.Unit{ + Enabled: machine.BoolToPtr(true), + Name: "ready.service", + Contents: machine.StrToPtr(fmt.Sprintf(hyperVReadyUnit, m.ReadyHVSock.Port)), + }) + + builder.WithUnit(machine.Unit{ + Contents: machine.StrToPtr(fmt.Sprintf(hyperVVsockNetUnit, m.NetworkHVSock.Port)), + Enabled: machine.BoolToPtr(true), + Name: "vsock-network.service", + }) + + builder.WithFile(machine.File{ + Node: machine.Node{ + Path: "/etc/NetworkManager/system-connections/vsock0.nmconnection", + }, + FileEmbedded1: machine.FileEmbedded1{ + Append: nil, + Contents: machine.Resource{ + Source: machine.EncodeDataURLPtr(hyperVVsockNMConnection), + }, + Mode: machine.IntToPtr(0600), + }, + }) + + if err := builder.Build(); err != nil { return false, err } diff --git a/pkg/machine/ignition.go b/pkg/machine/ignition.go index 7a6ca7a617..2444249af1 100644 --- a/pkg/machine/ignition.go +++ b/pkg/machine/ignition.go @@ -14,6 +14,7 @@ import ( "github.com/containers/common/libnetwork/etchosts" "github.com/containers/common/pkg/config" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/sirupsen/logrus" ) @@ -694,3 +695,65 @@ func GetPodmanDockerTmpConfig(uid int, rootful bool, newline bool) string { return fmt.Sprintf("L+ /run/docker.sock - - - - %s%s", podmanSock, suffix) } + +// SetIgnitionFile creates a new Machine File for the machine's ignition file +// and assignes the handle to `loc` +func SetIgnitionFile(loc *define.VMFile, vmtype VMType, vmName string) error { + vmConfigDir, err := GetConfDir(vmtype) + if err != nil { + return err + } + + ignitionFile, err := define.NewMachineFile(filepath.Join(vmConfigDir, vmName+".ign"), nil) + if err != nil { + return err + } + + *loc = *ignitionFile + return nil +} + +type IgnitionBuilder struct { + dynamicIgnition DynamicIgnition + units []Unit +} + +// NewIgnitionBuilder generates a new IgnitionBuilder type using the +// base `DynamicIgnition` object +func NewIgnitionBuilder(dynamicIgnition DynamicIgnition) IgnitionBuilder { + return IgnitionBuilder{ + dynamicIgnition, + []Unit{}, + } +} + +// GenerateIgnitionConfig generates the ignition config +func (i *IgnitionBuilder) GenerateIgnitionConfig() error { + return i.dynamicIgnition.GenerateIgnitionConfig() +} + +// WithUnit adds systemd units to the internal `DynamicIgnition` config +func (i *IgnitionBuilder) WithUnit(units ...Unit) { + i.dynamicIgnition.Cfg.Systemd.Units = append(i.dynamicIgnition.Cfg.Systemd.Units, units...) +} + +// WithFile adds storage files to the internal `DynamicIgnition` config +func (i *IgnitionBuilder) WithFile(files ...File) { + i.dynamicIgnition.Cfg.Storage.Files = append(i.dynamicIgnition.Cfg.Storage.Files, files...) +} + +// BuildWithIgnitionFile copies the provided ignition file into the internal +// `DynamicIgnition` write path +func (i *IgnitionBuilder) BuildWithIgnitionFile(ignPath string) error { + inputIgnition, err := os.ReadFile(ignPath) + if err != nil { + return err + } + + return os.WriteFile(i.dynamicIgnition.WritePath, inputIgnition, 0644) +} + +// Build writes the internal `DynamicIgnition` config to its write path +func (i *IgnitionBuilder) Build() error { + return i.dynamicIgnition.Write() +} diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go index eb776097f2..8492b5e964 100644 --- a/pkg/machine/qemu/config.go +++ b/pkg/machine/qemu/config.go @@ -64,10 +64,6 @@ func (v *MachineVM) setNewMachineCMD(qemuBinary string, cmdOpts *setNewMachineCM // NewMachine initializes an instance of a virtual machine based on the qemu // virtualization. func (p *QEMUVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, error) { - vmConfigDir, err := machine.GetConfDir(vmtype) - if err != nil { - return nil, err - } vm := new(MachineVM) if len(opts.Name) > 0 { vm.Name = opts.Name @@ -79,11 +75,9 @@ func (p *QEMUVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, e } // set VM ignition file - ignitionFile, err := define.NewMachineFile(filepath.Join(vmConfigDir, vm.Name+".ign"), nil) - if err != nil { + if err := machine.SetIgnitionFile(&vm.IgnitionFile, vmtype, vm.Name); err != nil { return nil, err } - vm.IgnitionFile = *ignitionFile // set VM image file imagePath, err := define.NewMachineFile(opts.ImagePath, nil) diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index f7fd7ef38b..dd999f5496 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -45,6 +45,23 @@ const ( dockerConnectTimeout = 5 * time.Second ) +// qemuReadyUnit is a unit file that sets up the virtual serial device +// where when the VM is done configuring, it will send an ack +// so a listening host tknows it can begin interacting with it +const qemuReadyUnit = `[Unit] +Requires=dev-virtio\\x2dports-%s.device +After=remove-moby.service sshd.socket sshd.service +After=systemd-user-sessions.service +OnFailure=emergency.target +OnFailureJobMode=isolate +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/bin/sh -c '/usr/bin/echo Ready >/dev/%s' +[Install] +RequiredBy=default.target +` + type MachineVM struct { // ConfigPath is the path to the configuration file ConfigPath define.VMFile @@ -202,50 +219,6 @@ func (v *MachineVM) addMountsToVM(opts machine.InitOptions) error { return nil } -// writeIgnitionConfigFile generates the ignition config and writes it to the -// filesystem -func (v *MachineVM) writeIgnitionConfigFile(opts machine.InitOptions, key string) error { - ign := &machine.DynamicIgnition{ - Name: opts.Username, - Key: key, - VMName: v.Name, - VMType: machine.QemuVirt, - TimeZone: opts.TimeZone, - WritePath: v.getIgnitionFile(), - UID: v.UID, - Rootful: v.Rootful, - } - - if err := ign.GenerateIgnitionConfig(); err != nil { - return err - } - - // ready is a unit file that sets up the virtual serial device - // where when the VM is done configuring, it will send an ack - // so a listening host knows it can being interacting with it - ready := `[Unit] -Requires=dev-virtio\\x2dports-%s.device -After=remove-moby.service sshd.socket sshd.service -After=systemd-user-sessions.service -OnFailure=emergency.target -OnFailureJobMode=isolate -[Service] -Type=oneshot -RemainAfterExit=yes -ExecStart=/bin/sh -c '/usr/bin/echo Ready >/dev/%s' -[Install] -RequiredBy=default.target -` - readyUnit := machine.Unit{ - Enabled: machine.BoolToPtr(true), - Name: "ready.service", - Contents: machine.StrToPtr(fmt.Sprintf(ready, "vport1p1", "vport1p1")), - } - ign.Cfg.Systemd.Units = append(ign.Cfg.Systemd.Units, readyUnit) - - return ign.Write() -} - // Init writes the json configuration file to the filesystem for // other verbs (start, stop) func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { @@ -327,17 +300,35 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { logrus.Warn("ignoring init option to disable user-mode networking: this mode is not supported by the QEMU backend") } + builder := machine.NewIgnitionBuilder(machine.DynamicIgnition{ + Name: opts.Username, + Key: key, + VMName: v.Name, + VMType: machine.QemuVirt, + TimeZone: opts.TimeZone, + WritePath: v.getIgnitionFile(), + UID: v.UID, + Rootful: v.Rootful, + }) + // If the user provides an ignition file, we need to // copy it into the conf dir if len(opts.IgnitionPath) > 0 { - inputIgnition, err := os.ReadFile(opts.IgnitionPath) - if err != nil { - return false, err - } - return false, os.WriteFile(v.getIgnitionFile(), inputIgnition, 0644) + return false, builder.BuildWithIgnitionFile(opts.IgnitionPath) + } + + if err := builder.GenerateIgnitionConfig(); err != nil { + return false, err + } + + readyUnit := machine.Unit{ + Enabled: machine.BoolToPtr(true), + Name: "ready.service", + Contents: machine.StrToPtr(fmt.Sprintf(qemuReadyUnit, "vport1p1", "vport1p1")), } + builder.WithUnit(readyUnit) - err = v.writeIgnitionConfigFile(opts, key) + err = builder.Build() callbackFuncs.Add(v.IgnitionFile.Delete) return err == nil, err