diff --git a/pkg/machine/applehv/claim.go b/pkg/machine/applehv/claim.go deleted file mode 100644 index c02f15d839..0000000000 --- a/pkg/machine/applehv/claim.go +++ /dev/null @@ -1,83 +0,0 @@ -//go:build darwin - -package applehv - -import ( - "fmt" - "io" - "io/fs" - "net" - "os" - "os/user" - "path/filepath" - "time" -) - -// TODO the following functions were taken from pkg/qemu/claim_darwin.go and -// should be refactored. I'm thinking even something in pkg/machine/ - -func dockerClaimSupported() bool { - return true -} - -func dockerClaimHelperInstalled() bool { - u, err := user.Current() - if err != nil { - return false - } - - labelName := fmt.Sprintf("com.github.containers.podman.helper-%s", u.Username) - fileName := filepath.Join("/Library", "LaunchDaemons", labelName+".plist") - info, err := os.Stat(fileName) - return err == nil && info.Mode().IsRegular() -} - -func claimDockerSock() bool { - u, err := user.Current() - if err != nil { - return false - } - - helperSock := fmt.Sprintf("/var/run/podman-helper-%s.socket", u.Username) - con, err := net.DialTimeout("unix", helperSock, time.Second*5) - if err != nil { - return false - } - _ = con.SetWriteDeadline(time.Now().Add(time.Second * 5)) - _, err = fmt.Fprintln(con, "GO") - if err != nil { - return false - } - _ = con.SetReadDeadline(time.Now().Add(time.Second * 5)) - read, err := io.ReadAll(con) - - return err == nil && string(read) == "OK" -} - -func findClaimHelper() string { - exe, err := os.Executable() - if err != nil { - return "" - } - - exe, err = filepath.EvalSymlinks(exe) - if err != nil { - return "" - } - - return filepath.Join(filepath.Dir(exe), "podman-mac-helper") -} - -func checkSockInUse(sock string) bool { - if info, err := os.Stat(sock); err == nil && info.Mode()&fs.ModeSocket == fs.ModeSocket { - _, err = net.DialTimeout("unix", dockerSock, dockerConnectTimeout) - return err == nil - } - - return false -} - -func alreadyLinked(target string, link string) bool { - read, err := os.Readlink(link) - return err == nil && read == target -} diff --git a/pkg/machine/applehv/config.go b/pkg/machine/applehv/config.go index 60149c34df..f5d509598a 100644 --- a/pkg/machine/applehv/config.go +++ b/pkg/machine/applehv/config.go @@ -2,200 +2,7 @@ package applehv -import ( - "errors" - "fmt" - "io/fs" - "path/filepath" - "time" - - "github.com/containers/podman/v4/pkg/machine" - "github.com/containers/podman/v4/pkg/machine/compression" - "github.com/containers/podman/v4/pkg/machine/define" - "github.com/containers/podman/v4/pkg/machine/ignition" - "github.com/containers/podman/v4/pkg/machine/vmconfigs" - vfConfig "github.com/crc-org/vfkit/pkg/config" - "github.com/docker/go-units" - "golang.org/x/sys/unix" -) - const ( localhostURI = "http://localhost" ignitionSocketName = "ignition.sock" ) - -type AppleHVVirtualization struct { - machine.Virtualization -} - -type MMHardwareConfig struct { - CPUs uint16 - DiskPath string - DiskSize uint64 - Memory int32 -} - -func VirtualizationProvider() machine.VirtProvider { - return &AppleHVVirtualization{ - machine.NewVirtualization(define.AppleHV, compression.Xz, define.Raw, vmtype), - } -} - -func (v AppleHVVirtualization) CheckExclusiveActiveVM() (bool, string, error) { - fsVms, err := getVMInfos() - if err != nil { - return false, "", err - } - for _, vm := range fsVms { - if vm.Running || vm.Starting { - return true, vm.Name, nil - } - } - - return false, "", nil -} - -func (v AppleHVVirtualization) IsValidVMName(name string) (bool, error) { - configDir, err := machine.GetConfDir(define.AppleHvVirt) - if err != nil { - return false, err - } - fqName := filepath.Join(configDir, fmt.Sprintf("%s.json", name)) - if _, err := loadMacMachineFromJSON(fqName); err != nil { - return false, err - } - return true, nil -} - -func (v AppleHVVirtualization) List(opts machine.ListOptions) ([]*machine.ListResponse, error) { - var ( - response []*machine.ListResponse - ) - - mms, err := v.loadFromLocalJson() - if err != nil { - return nil, err - } - - for _, mm := range mms { - vmState, err := mm.Vfkit.State() - if err != nil { - if errors.Is(err, unix.ECONNREFUSED) { - vmState = define.Stopped - } else { - return nil, err - } - } - - mlr := machine.ListResponse{ - Name: mm.Name, - CreatedAt: mm.Created, - LastUp: mm.LastUp, - Running: vmState == define.Running, - Starting: vmState == define.Starting, - Stream: mm.ImageStream, - VMType: define.AppleHvVirt.String(), - CPUs: mm.CPUs, - Memory: mm.Memory * units.MiB, - DiskSize: mm.DiskSize * units.GiB, - Port: mm.Port, - RemoteUsername: mm.RemoteUsername, - IdentityPath: mm.IdentityPath, - } - response = append(response, &mlr) - } - return response, nil -} - -func (v AppleHVVirtualization) LoadVMByName(name string) (machine.VM, error) { - m := MacMachine{Name: name} - return m.loadFromFile() -} - -func (v AppleHVVirtualization) NewMachine(opts define.InitOptions) (machine.VM, error) { - m := MacMachine{Name: opts.Name} - - if len(opts.USBs) > 0 { - return nil, fmt.Errorf("USB host passthrough is not supported for applehv machines") - } - - configDir, err := machine.GetConfDir(define.AppleHvVirt) - if err != nil { - return nil, err - } - - configPath, err := define.NewMachineFile(getVMConfigPath(configDir, opts.Name), nil) - if err != nil { - return nil, err - } - m.ConfigPath = *configPath - - dataDir, err := machine.GetDataDir(define.AppleHvVirt) - if err != nil { - return nil, err - } - - if err := ignition.SetIgnitionFile(&m.IgnitionFile, vmtype, m.Name, configDir); err != nil { - return nil, err - } - - // Set creation time - m.Created = time.Now() - - m.ResourceConfig = vmconfigs.ResourceConfig{ - CPUs: opts.CPUS, - DiskSize: opts.DiskSize, - // Diskpath will be needed - Memory: opts.Memory, - } - bl := vfConfig.NewEFIBootloader(fmt.Sprintf("%s/%ss", dataDir, opts.Name), true) - m.Vfkit.VirtualMachine = vfConfig.NewVirtualMachine(uint(opts.CPUS), opts.Memory, bl) - - if err := m.writeConfig(); err != nil { - return nil, err - } - return m.loadFromFile() -} - -func (v AppleHVVirtualization) RemoveAndCleanMachines() error { - // This can be implemented when host networking is completed. - return define.ErrNotImplemented -} - -func (v AppleHVVirtualization) VMType() define.VMType { - return vmtype -} - -func (v AppleHVVirtualization) loadFromLocalJson() ([]*MacMachine, error) { - var ( - jsonFiles []string - mms []*MacMachine - ) - configDir, err := machine.GetConfDir(v.VMType()) - if err != nil { - return nil, err - } - if err := filepath.WalkDir(configDir, func(input string, d fs.DirEntry, e error) error { - if e != nil { - return e - } - if filepath.Ext(d.Name()) == ".json" { - jsonFiles = append(jsonFiles, input) - } - return nil - }); err != nil { - return nil, err - } - - for _, jsonFile := range jsonFiles { - mm, err := loadMacMachineFromJSON(jsonFile) - if err != nil { - return nil, err - } - if err != nil { - return nil, err - } - mms = append(mms, mm) - } - return mms, nil -} diff --git a/pkg/machine/applehv/ignition.go b/pkg/machine/applehv/ignition.go index 678468e62e..0e7581c77e 100644 --- a/pkg/machine/applehv/ignition.go +++ b/pkg/machine/applehv/ignition.go @@ -7,14 +7,20 @@ import ( "net/http" "github.com/containers/podman/v4/pkg/machine/define" + "github.com/containers/podman/v4/pkg/machine/vmconfigs" "github.com/sirupsen/logrus" ) // serveIgnitionOverSock allows podman to open a small httpd instance on the vsock between the host // and guest to inject the ignitionfile into fcos -func (m *MacMachine) serveIgnitionOverSock(ignitionSocket *define.VMFile) error { - logrus.Debugf("reading ignition file: %s", m.IgnitionFile.GetPath()) - ignFile, err := m.IgnitionFile.Read() +func serveIgnitionOverSock(ignitionSocket *define.VMFile, mc *vmconfigs.MachineConfig) error { + ignitionFile, err := mc.IgnitionFile() + if err != nil { + return err + } + + logrus.Debugf("reading ignition file: %s", ignitionFile.GetPath()) + ignFile, err := ignitionFile.Read() if err != nil { return err } @@ -22,7 +28,7 @@ func (m *MacMachine) serveIgnitionOverSock(ignitionSocket *define.VMFile) error mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, err := w.Write(ignFile) if err != nil { - logrus.Error("failed to serve ignition file: %v", err) + logrus.Errorf("failed to serve ignition file: %v", err) } }) listener, err := net.Listen("unix", ignitionSocket.GetPath()) diff --git a/pkg/machine/applehv/machine.go b/pkg/machine/applehv/machine.go index 1a13435b07..5eab08ea31 100644 --- a/pkg/machine/applehv/machine.go +++ b/pkg/machine/applehv/machine.go @@ -3,505 +3,27 @@ package applehv import ( - "context" - "encoding/json" - "errors" "fmt" - "io/fs" - "net" "os" "os/exec" - "path/filepath" - "strconv" - "strings" "syscall" - "time" - "github.com/containers/common/pkg/config" "github.com/containers/common/pkg/strongunits" - gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" "github.com/containers/podman/v4/pkg/machine" - "github.com/containers/podman/v4/pkg/machine/applehv/vfkit" - "github.com/containers/podman/v4/pkg/machine/connection" "github.com/containers/podman/v4/pkg/machine/define" "github.com/containers/podman/v4/pkg/machine/ignition" - "github.com/containers/podman/v4/pkg/machine/sockets" "github.com/containers/podman/v4/pkg/machine/vmconfigs" "github.com/containers/podman/v4/pkg/systemd/parser" - "github.com/containers/podman/v4/utils" - "github.com/containers/storage/pkg/lockfile" - vfConfig "github.com/crc-org/vfkit/pkg/config" vfRest "github.com/crc-org/vfkit/pkg/rest" - "github.com/docker/go-units" "github.com/sirupsen/logrus" ) -var ( - // vmtype refers to qemu (vs libvirt, krun, etc). - vmtype = define.AppleHvVirt -) +func (a *AppleHVStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() error, error) { + mc.Lock() + defer mc.Unlock() -const ( - dockerSock = "/var/run/docker.sock" - dockerConnectTimeout = 5 * time.Second - apiUpTimeout = 20 * time.Second -) - -// VfkitHelper describes the use of vfkit: cmdline and endpoint -type VfkitHelper struct { - LogLevel logrus.Level - Endpoint string - VfkitBinaryPath *define.VMFile - VirtualMachine *vfConfig.VirtualMachine -} - -type MacMachine struct { - // ConfigPath is the fully qualified path to the configuration file - ConfigPath define.VMFile - // HostUser contains info about host user - vmconfigs.HostUser - // ImageConfig describes the bootable image - machine.ImageConfig - // Mounts is the list of remote filesystems to mount - Mounts []vmconfigs.Mount - // Name of VM - Name string - // ReadySocket tells host when vm is booted - ReadySocket define.VMFile - // ResourceConfig is physical attrs of the VM - vmconfigs.ResourceConfig - // SSHConfig for accessing the remote vm - vmconfigs.SSHConfig - // Starting tells us whether the machine is running or if we have just dialed it to start it - Starting bool - // Created contains the original created time instead of querying the file mod time - Created time.Time - // LastUp contains the last recorded uptime - LastUp time.Time - // The VFKit endpoint where we can interact with the VM - Vfkit vfkit.VfkitHelper - LogPath define.VMFile - GvProxyPid define.VMFile - GvProxySock define.VMFile - - // Used at runtime for serializing write operations - lock *lockfile.LockFile -} - -// setGVProxyInfo sets the VM's gvproxy pid and socket files -func (m *MacMachine) setGVProxyInfo(runtimeDir string) error { - gvProxyPid, err := define.NewMachineFile(filepath.Join(runtimeDir, "gvproxy.pid"), nil) - if err != nil { - return err - } - m.GvProxyPid = *gvProxyPid - - return sockets.SetSocket(&m.GvProxySock, filepath.Join(runtimeDir, "gvproxy.sock"), nil) -} - -// setVfkitInfo stores the default devices, sets the vfkit endpoint, and -// locates/stores the path to the binary -func (m *MacMachine) setVfkitInfo(cfg *config.Config, readySocket define.VMFile) error { - defaultDevices, err := getDefaultDevices(m.ImagePath.GetPath(), m.LogPath.GetPath(), readySocket.GetPath()) - if err != nil { - return err - } - // Store VFKit stuffs - vfkitPath, err := cfg.FindHelperBinary("vfkit", false) - if err != nil { - return err - } - vfkitBinaryPath, err := define.NewMachineFile(vfkitPath, nil) - if err != nil { - return err - } - - m.Vfkit.VirtualMachine.Devices = defaultDevices - randPort, err := utils.GetRandomPort() - if err != nil { - return err - } - - m.Vfkit.Endpoint = localhostURI + ":" + strconv.Itoa(randPort) - m.Vfkit.VfkitBinaryPath = vfkitBinaryPath - - return nil -} - -// addMountsToVM converts the volumes passed through the CLI to virtio-fs mounts -// and adds them to the machine -func (m *MacMachine) addMountsToVM(opts define.InitOptions, virtiofsMnts *[]machine.VirtIoFs) error { - var mounts []vmconfigs.Mount - for _, volume := range opts.Volumes { - source, target, _, readOnly, err := machine.ParseVolumeFromPath(volume) - if err != nil { - return err - } - mnt := machine.NewVirtIoFsMount(source, target, readOnly) - *virtiofsMnts = append(*virtiofsMnts, mnt) - mounts = append(mounts, mnt.ToMount()) - } - m.Mounts = mounts - - return nil -} - -func (m *MacMachine) Init(opts define.InitOptions) (bool, error) { - var ( - key string - virtiofsMnts []machine.VirtIoFs - err error - ) - - // cleanup half-baked files if init fails at any point - callbackFuncs := machine.InitCleanup() - defer callbackFuncs.CleanIfErr(&err) - go callbackFuncs.CleanOnSignal() - - callbackFuncs.Add(m.ConfigPath.Delete) - dataDir, err := machine.GetDataDir(define.AppleHvVirt) - if err != nil { - return false, err - } - cfg, err := config.Default() - if err != nil { - return false, err - } - - dl, err := VirtualizationProvider().NewDownload(m.Name) - if err != nil { - return false, err - } - - imagePath, strm, err := dl.AcquireVMImage(opts.ImagePath) - if err != nil { - return false, err - } - callbackFuncs.Add(imagePath.Delete) - - // Set the values for imagePath and strm - m.ImagePath = *imagePath - m.ImageStream = strm.String() - - logPath, err := define.NewMachineFile(filepath.Join(dataDir, fmt.Sprintf("%s.log", m.Name)), nil) - if err != nil { - return false, err - } - callbackFuncs.Add(logPath.Delete) - - m.LogPath = *logPath - runtimeDir, err := m.getRuntimeDir() - if err != nil { - return false, err - } - - if err := sockets.SetSocket(&m.ReadySocket, sockets.ReadySocketPath(runtimeDir, m.Name), nil); err != nil { - return false, err - } - - if err = m.setGVProxyInfo(runtimeDir); err != nil { - return false, err - } - - if err = m.setVfkitInfo(cfg, m.ReadySocket); err != nil { - return false, err - } - - m.IdentityPath, err = machine.GetSSHIdentityPath(define.DefaultIdentityName) - if err != nil { - return false, err - } - m.Rootful = opts.Rootful - m.RemoteUsername = opts.Username - - m.UID = os.Getuid() - - sshPort, err := utils.GetRandomPort() - if err != nil { - return false, err - } - m.Port = sshPort - - if err = m.addMountsToVM(opts, &virtiofsMnts); err != nil { - return false, err - } - - err = connection.AddSSHConnectionsToPodmanSocket( - m.UID, - m.Port, - m.IdentityPath, - m.Name, - m.RemoteUsername, - opts, - ) - if err != nil { - return false, err - } - callbackFuncs.Add(m.removeSystemConnections) - - logrus.Debugf("resizing disk to %d GiB", opts.DiskSize) - if err = m.resizeDisk(strongunits.GiB(opts.DiskSize)); err != nil { - return false, err - } - - if err = m.writeConfig(); err != nil { - return false, err - } - - if len(opts.IgnitionPath) < 1 { - key, err = machine.GetSSHKeys(m.IdentityPath) - if err != nil { - return false, err - } - } - - builder := ignition.NewIgnitionBuilder(ignition.DynamicIgnition{ - Name: opts.Username, - Key: key, - VMName: m.Name, - VMType: define.AppleHvVirt, - TimeZone: opts.TimeZone, - WritePath: m.IgnitionFile.GetPath(), - UID: m.UID, - Rootful: m.Rootful, - }) - - if len(opts.IgnitionPath) > 0 { - err = builder.BuildWithIgnitionFile(opts.IgnitionPath) - return false, err - } - - if err := builder.GenerateIgnitionConfig(); err != nil { - return false, err - } - - readyUnitFile, err := ignition.CreateReadyUnitFile(define.AppleHvVirt, nil) - if err != nil { - return false, err - } - - builder.WithUnit(ignition.Unit{ - Enabled: ignition.BoolToPtr(true), - Name: "ready.service", - Contents: ignition.StrToPtr(readyUnitFile), - }) - builder.WithUnit(generateSystemDFilesForVirtiofsMounts(virtiofsMnts)...) - - // TODO Ignition stuff goes here - err = builder.Build() - callbackFuncs.Add(m.IgnitionFile.Delete) - - return err == nil, err -} - -func (m *MacMachine) removeSystemConnections() error { - return connection.RemoveConnections(m.Name, fmt.Sprintf("%s-root", m.Name)) -} - -func (m *MacMachine) Inspect() (*machine.InspectInfo, error) { - vmState, err := m.Vfkit.State() - if err != nil { - return nil, err - } - - podmanSocket, err := m.forwardSocketPath() - if err != nil { - return nil, err - } - - ii := machine.InspectInfo{ - ConfigPath: m.ConfigPath, - ConnectionInfo: machine.ConnectionConfig{ - PodmanSocket: podmanSocket, - PodmanPipe: nil, - }, - Created: m.Created, - Image: machine.ImageConfig{ - IgnitionFile: m.IgnitionFile, - ImageStream: m.ImageStream, - ImagePath: m.ImagePath, - }, - LastUp: m.LastUp, - Name: m.Name, - Resources: vmconfigs.ResourceConfig{ - CPUs: m.CPUs, - DiskSize: m.DiskSize, - Memory: m.Memory, - }, - SSHConfig: m.SSHConfig, - State: vmState, - Rootful: m.Rootful, - } - return &ii, nil -} - -// collectFilesToDestroy retrieves the files that will be destroyed by `Remove` -func (m *MacMachine) collectFilesToDestroy(opts machine.RemoveOptions) []string { - files := []string{} - if !opts.SaveIgnition { - files = append(files, m.IgnitionFile.GetPath()) - } - - if !opts.SaveImage { - files = append(files, m.ImagePath.GetPath()) - } - - files = append(files, m.ConfigPath.GetPath()) - return files -} - -func (m *MacMachine) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) { - var ( - files []string - ) - - m.lock.Lock() - defer m.lock.Unlock() - - vmState, err := m.Vfkit.State() - if err != nil { - return "", nil, err - } - - if vmState == define.Running { - if !opts.Force { - return "", nil, &define.ErrVMRunningCannotDestroyed{Name: m.Name} - } - if err := m.Vfkit.Stop(true, true); err != nil { - return "", nil, err - } - defer func() { - if err := machine.CleanupGVProxy(m.GvProxyPid); err != nil { - logrus.Error(err) - } - }() - } - - files = m.collectFilesToDestroy(opts) - - confirmationMessage := "\nThe following files will be deleted:\n\n" - for _, msg := range files { - confirmationMessage += msg + "\n" - } - - confirmationMessage += "\n" - return confirmationMessage, func() error { - connection.RemoveFilesAndConnections(files, m.Name, m.Name+"-root") - // TODO We will need something like this for applehv too i think - /* - // Remove the HVSOCK for networking - if err := m.NetworkHVSock.Remove(); err != nil { - logrus.Errorf("unable to remove registry entry for %s: %q", m.NetworkHVSock.KeyName, err) - } - - // Remove the HVSOCK for events - if err := m.ReadyHVSock.Remove(); err != nil { - logrus.Errorf("unable to remove registry entry for %s: %q", m.NetworkHVSock.KeyName, err) - } - */ - return nil - }, nil -} - -func (m *MacMachine) writeConfig() error { - b, err := json.MarshalIndent(m, "", " ") - if err != nil { - return err - } - return os.WriteFile(m.ConfigPath.Path, b, 0644) -} - -func (m *MacMachine) setRootful(rootful bool) error { - if err := machine.SetRootful(rootful, m.Name, m.Name+"-root"); err != nil { - return err - } - - m.HostUser.Modified = true - return nil -} - -func (m *MacMachine) Set(name string, opts machine.SetOptions) ([]error, error) { - var setErrors []error - - m.lock.Lock() - defer m.lock.Unlock() - - vmState, err := m.State(false) - if err != nil { - return nil, err - } - if vmState != define.Stopped { - return nil, define.ErrWrongState - } - if cpus := opts.CPUs; cpus != nil { - m.CPUs = *cpus - } - if mem := opts.Memory; mem != nil { - m.Memory = *mem - } - if newSize := opts.DiskSize; newSize != nil { - if *newSize < m.DiskSize { - setErrors = append(setErrors, errors.New("new disk size smaller than existing disk size: cannot shrink disk size")) - } else { - m.DiskSize = *newSize - if err := m.resizeDisk(strongunits.GiB(*opts.DiskSize)); err != nil { - setErrors = append(setErrors, err) - } - } - } - if opts.USBs != nil { - setErrors = append(setErrors, errors.New("changing USBs not supported for applehv machines")) - } - - if opts.Rootful != nil && m.Rootful != *opts.Rootful { - if err := m.setRootful(*opts.Rootful); err != nil { - setErrors = append(setErrors, fmt.Errorf("failed to set rootful option: %w", err)) - } else { - m.Rootful = *opts.Rootful - } - } - - // Write the machine config to the filesystem - err = m.writeConfig() - setErrors = append(setErrors, err) - switch len(setErrors) { - case 0: - return setErrors, nil - case 1: - return nil, setErrors[0] - default: - // Number of errors is 2 or more - lastErr := setErrors[len(setErrors)-1] - return setErrors[:len(setErrors)-1], lastErr - } -} - -func (m *MacMachine) SSH(name string, opts machine.SSHOptions) error { - st, err := m.State(false) - if err != nil { - return err - } - if st != define.Running { - return fmt.Errorf("vm %q is not running", m.Name) - } - username := opts.Username - if username == "" { - username = m.RemoteUsername - } - return machine.CommonSSH(username, m.IdentityPath, m.Name, m.Port, opts.Args) -} - -// deleteIgnitionSocket retrieves the ignition socket, deletes it, and returns a -// pointer to the `VMFile` -func (m *MacMachine) deleteIgnitionSocket() (*define.VMFile, error) { - ignitionSocket, err := m.getIgnitionSock() - if err != nil { - return nil, err - } - if err := ignitionSocket.Delete(); err != nil { - return nil, err - } - return ignitionSocket, nil + // TODO we could delete the vfkit pid/log files if we wanted to be thorough + return []string{}, func() error { return nil }, nil } // getIgnitionVsockDeviceAsCLI retrieves the ignition vsock device and converts @@ -546,424 +68,18 @@ func getVfKitEndpointCMDArgs(endpoint string) ([]string, error) { return restEndpoint.ToCmdLine() } -// addVolumesToVfKit adds the VM's mounts to vfkit's devices -func (m *MacMachine) addVolumesToVfKit() error { - for _, vol := range m.Mounts { - virtfsDevice, err := vfConfig.VirtioFsNew(vol.Source, vol.Tag) - if err != nil { - return err - } - m.Vfkit.VirtualMachine.Devices = append(m.Vfkit.VirtualMachine.Devices, virtfsDevice) - } - return nil -} - -func (m *MacMachine) Start(name string, opts machine.StartOptions) error { - var ignitionSocket *define.VMFile - - m.lock.Lock() - defer m.lock.Unlock() - - st, err := m.State(false) - if err != nil { - return err - } - - if st == define.Running { - return define.ErrVMAlreadyRunning - } - - if _, err := m.getRuntimeDir(); err != nil { - return err - } - - // TODO handle returns from startHostNetworking - forwardSock, forwardState, err := m.startHostNetworking() - if err != nil { - return err - } - - // Add networking - netDevice, err := vfConfig.VirtioNetNew("5a:94:ef:e4:0c:ee") - if err != nil { - return err - } - // Set user networking with gvproxy - netDevice.SetUnixSocketPath(m.GvProxySock.GetPath()) - - m.Vfkit.VirtualMachine.Devices = append(m.Vfkit.VirtualMachine.Devices, netDevice) - - if err := m.addVolumesToVfKit(); err != nil { - return err - } - - // To start the VM, we need to call vfkit - - logrus.Debugf("vfkit path is: %s", m.Vfkit.VfkitBinaryPath.Path) - cmd, err := m.Vfkit.VirtualMachine.Cmd(m.Vfkit.VfkitBinaryPath.Path) - if err != nil { - return err - } - - vfkitEndpointArgs, err := getVfKitEndpointCMDArgs(m.Vfkit.Endpoint) - if err != nil { - return err - } - cmd.Args = append(cmd.Args, vfkitEndpointArgs...) - - firstBoot, err := m.isFirstBoot() - if err != nil { - return err - } - - if firstBoot { - // If this is the first boot of the vm, we need to add the vsock - // device to vfkit so we can inject the ignition file - ignitionSocket, err = m.deleteIgnitionSocket() - if err != nil { - return err - } - - ignitionVsockDeviceCLI, err := getIgnitionVsockDeviceAsCLI(ignitionSocket.GetPath()) - if err != nil { - return err - } - cmd.Args = append(cmd.Args, ignitionVsockDeviceCLI...) - } - - if logrus.IsLevelEnabled(logrus.DebugLevel) { - debugDevArgs, err := getDebugDevicesCMDArgs() - if err != nil { - return err - } - cmd.Args = append(cmd.Args, debugDevArgs...) - cmd.Args = append(cmd.Args, "--gui") // add command line switch to pop the gui open - } - - readSocketBaseDir := filepath.Dir(m.ReadySocket.GetPath()) - if err := os.MkdirAll(readSocketBaseDir, 0755); err != nil { - return err - } - - if firstBoot { - logrus.Debug("first boot detected") - logrus.Debugf("serving ignition file over %s", ignitionSocket.GetPath()) - go func() { - if err := m.serveIgnitionOverSock(ignitionSocket); err != nil { - logrus.Error(err) - } - logrus.Debug("ignition vsock server exited") - }() - } - - if err := m.ReadySocket.Delete(); err != nil { - return err - } - - logrus.Debugf("listening for ready on: %s", m.ReadySocket.GetPath()) - readyListen, err := net.Listen("unix", m.ReadySocket.GetPath()) - if err != nil { - return err - } - - logrus.Debug("waiting for ready notification") - readyChan := make(chan error) - go sockets.ListenAndWaitOnSocket(readyChan, readyListen) - - if err := cmd.Start(); err != nil { - return err - } - - processErrChan := make(chan error) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - go func() { - defer close(processErrChan) - for { - select { - case <-ctx.Done(): - return - default: - } - if err := checkProcessRunning("vfkit", cmd.Process.Pid); err != nil { - processErrChan <- err - return - } - // lets poll status every half second - time.Sleep(500 * time.Millisecond) - } - }() - - // wait for either socket or to be ready or process to have exited - select { - case err := <-processErrChan: - if err != nil { - return err - } - case err := <-readyChan: - if err != nil { - return err - } - } - - logrus.Debug("ready notification received") - machine.WaitAPIAndPrintInfo( - forwardState, - m.Name, - findClaimHelper(), - forwardSock, - opts.NoInfo, - m.isIncompatible(), - m.Rootful, - ) - - // update the podman/docker socket service if the host user has been modified at all (UID or Rootful) - if m.HostUser.Modified { - if machine.UpdatePodmanDockerSockService(m, name, m.UID, m.Rootful) == nil { - // Reset modification state if there are no errors, otherwise ignore errors - // which are already logged - m.HostUser.Modified = false - _ = m.writeConfig() - } - } - - return nil -} - -func (m *MacMachine) State(_ bool) (define.Status, error) { - vmStatus, err := m.Vfkit.State() +func (a *AppleHVStubber) State(mc *vmconfigs.MachineConfig, _ bool) (define.Status, error) { + vmStatus, err := mc.AppleHypervisor.Vfkit.State() if err != nil { return "", err } return vmStatus, nil } -func (m *MacMachine) Stop(name string, opts machine.StopOptions) error { - m.lock.Lock() - defer m.lock.Unlock() - - vmState, err := m.State(false) - if err != nil { - return err - } - - if vmState != define.Running { - return nil - } - - defer func() { - if err := machine.CleanupGVProxy(m.GvProxyPid); err != nil { - logrus.Error(err) - } - }() - if err := m.Vfkit.Stop(false, true); err != nil { - return err - } - - // keep track of last up - m.LastUp = time.Now() - return m.writeConfig() -} - -// getVMConfigPath is a simple wrapper for getting the fully-qualified -// path of the vm json config file. It should be used to get conformity -func getVMConfigPath(configDir, vmName string) string { - return filepath.Join(configDir, fmt.Sprintf("%s.json", vmName)) -} - -func (m *MacMachine) loadFromFile() (*MacMachine, error) { - if len(m.Name) < 1 { - return nil, errors.New("encountered machine with no name") - } - - jsonPath, err := m.jsonConfigPath() - if err != nil { - return nil, err - } - - mm, err := loadMacMachineFromJSON(jsonPath) - if err != nil { - return nil, err - } - - lock, err := machine.GetLock(mm.Name, vmtype) - if err != nil { - return nil, err - } - mm.lock = lock - - return mm, nil -} - -func loadMacMachineFromJSON(fqConfigPath string) (*MacMachine, error) { - b, err := os.ReadFile(fqConfigPath) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - name := strings.TrimSuffix(filepath.Base(fqConfigPath), ".json") - return nil, fmt.Errorf("%s: %w", name, define.ErrNoSuchVM) - } - return nil, err - } - mm := new(MacMachine) - if err := json.Unmarshal(b, mm); err != nil { - return nil, err - } - return mm, nil -} - -func (m *MacMachine) jsonConfigPath() (string, error) { - configDir, err := machine.GetConfDir(define.AppleHvVirt) - if err != nil { - return "", err - } - return getVMConfigPath(configDir, m.Name), nil -} - -func getVMInfos() ([]*machine.ListResponse, error) { - vmConfigDir, err := machine.GetConfDir(vmtype) - if err != nil { - return nil, err - } - - var listed []*machine.ListResponse - - if err = filepath.WalkDir(vmConfigDir, func(path string, d fs.DirEntry, err error) error { - vm := new(MacMachine) - if strings.HasSuffix(d.Name(), ".json") { - fullPath := filepath.Join(vmConfigDir, d.Name()) - b, err := os.ReadFile(fullPath) - if err != nil { - return err - } - err = json.Unmarshal(b, vm) - if err != nil { - return err - } - listEntry := new(machine.ListResponse) - - listEntry.Name = vm.Name - listEntry.Stream = vm.ImageStream - listEntry.VMType = define.AppleHvVirt.String() - listEntry.CPUs = vm.CPUs - listEntry.Memory = vm.Memory * units.MiB - listEntry.DiskSize = vm.DiskSize * units.GiB - listEntry.Port = vm.Port - listEntry.RemoteUsername = vm.RemoteUsername - listEntry.IdentityPath = vm.IdentityPath - listEntry.CreatedAt = vm.Created - listEntry.Starting = vm.Starting - - if listEntry.CreatedAt.IsZero() { - listEntry.CreatedAt = time.Now() - vm.Created = time.Now() - if err := vm.writeConfig(); err != nil { - return err - } - } - - vmState, err := vm.State(false) - if err != nil { - return err - } - listEntry.Running = vmState == define.Running - listEntry.LastUp = vm.LastUp - - listed = append(listed, listEntry) - } - return nil - }); err != nil { - return nil, err - } - return listed, err -} - -// setupStartHostNetworkingCmd generates the cmd that will be used to start the -// host networking. Includes the ssh port, gvproxy pid file, gvproxy socket, and -// a debug flag depending on the logrus log level -func (m *MacMachine) setupStartHostNetworkingCmd() (gvproxy.GvproxyCommand, string, machine.APIForwardingState) { - cmd := gvproxy.NewGvproxyCommand() - cmd.SSHPort = m.Port - cmd.PidFile = m.GvProxyPid.GetPath() - cmd.AddVfkitSocket(fmt.Sprintf("unixgram://%s", m.GvProxySock.GetPath())) - cmd.Debug = logrus.IsLevelEnabled(logrus.DebugLevel) - - cmd, forwardSock, state := m.setupAPIForwarding(cmd) - if cmd.Debug { - logrus.Debug(cmd.ToCmdline()) - } - - return cmd, forwardSock, state -} - -func (m *MacMachine) startHostNetworking() (string, machine.APIForwardingState, error) { - var ( - forwardSock string - state machine.APIForwardingState - ) - - // TODO This should probably be added to startHostNetworking everywhere - // GvProxy does not clean up after itself - if err := m.GvProxySock.Delete(); err != nil { - b, err := m.GvProxyPid.Read() - if err != nil { - return "", machine.NoForwarding, err - } - pid, err := strconv.Atoi(string(b)) - if err != nil { - return "", 0, err - } - gvProcess, err := os.FindProcess(pid) - if err != nil { - return "", 0, err - } - // shoot it with a signal 0 and see if it is active - err = gvProcess.Signal(syscall.Signal(0)) - if err == nil { - return "", 0, fmt.Errorf("gvproxy process %s already running", string(b)) - } - if err := m.GvProxySock.Delete(); err != nil { - return "", 0, err - } - } - cfg, err := config.Default() - if err != nil { - return "", machine.NoForwarding, err - } - - gvproxyBinary, err := cfg.FindHelperBinary("gvproxy", false) - if err != nil { - return "", 0, err - } - - logrus.Debugf("gvproxy binary being used: %s", gvproxyBinary) - - cmd, forwardSock, state := m.setupStartHostNetworkingCmd() - c := cmd.Cmd(gvproxyBinary) - if err := c.Start(); err != nil { - return "", 0, fmt.Errorf("unable to execute: %q: %w", cmd.ToCmdline(), err) - } - - // We need to wait and make sure gvproxy is in fact running - // before continuing - for i := 0; i < 10; i++ { - _, err := os.Stat(m.GvProxySock.GetPath()) - if err == nil { - break - } - if err := checkProcessRunning("gvproxy", c.Process.Pid); err != nil { - // gvproxy is no longer running - return "", 0, err - } - logrus.Debugf("gvproxy unixgram socket %q not found: %v", m.GvProxySock.GetPath(), err) - // Sleep for 1/2 second - time.Sleep(500 * time.Millisecond) - } - if err != nil { - // I guess we would also check the pidfile and look to see if it is running - // to? - return "", 0, fmt.Errorf("unable to verify gvproxy is running") - } - return forwardSock, state, nil +func (a *AppleHVStubber) StopVM(mc *vmconfigs.MachineConfig, _ bool) error { + mc.Lock() + defer mc.Unlock() + return mc.AppleHypervisor.Vfkit.Stop(false, true) } // checkProcessRunning checks non blocking if the pid exited @@ -981,95 +97,14 @@ func checkProcessRunning(processName string, pid int) error { return nil } -func (m *MacMachine) setupAPIForwarding(cmd gvproxy.GvproxyCommand) (gvproxy.GvproxyCommand, string, machine.APIForwardingState) { - socket, err := m.forwardSocketPath() - if err != nil { - return cmd, "", machine.NoForwarding - } - - destSock := fmt.Sprintf("/run/user/%d/podman/podman.sock", m.UID) - forwardUser := m.RemoteUsername - - if m.Rootful { - destSock = "/run/podman/podman.sock" - forwardUser = "root" - } - - cmd.AddForwardSock(socket.GetPath()) - cmd.AddForwardDest(destSock) - cmd.AddForwardUser(forwardUser) - cmd.AddForwardIdentity(m.IdentityPath) - - link, err := m.userGlobalSocketLink() - if err != nil { - return cmd, socket.GetPath(), machine.MachineLocal - } - - if !dockerClaimSupported() { - return cmd, socket.GetPath(), machine.ClaimUnsupported - } - - if !dockerClaimHelperInstalled() { - return cmd, socket.GetPath(), machine.NotInstalled - } - - if !alreadyLinked(socket.GetPath(), link) { - if checkSockInUse(link) { - return cmd, socket.GetPath(), machine.MachineLocal - } - - _ = os.Remove(link) - if err = os.Symlink(socket.GetPath(), link); err != nil { - logrus.Warnf("could not create user global API forwarding link: %s", err.Error()) - return cmd, socket.GetPath(), machine.MachineLocal - } - } - - if !alreadyLinked(link, dockerSock) { - if checkSockInUse(dockerSock) { - return cmd, socket.GetPath(), machine.MachineLocal - } - - if !claimDockerSock() { - logrus.Warn("podman helper is installed, but was not able to claim the global docker sock") - return cmd, socket.GetPath(), machine.MachineLocal - } - } - - return cmd, socket.GetPath(), machine.MachineLocal - -} - -func (m *MacMachine) dockerSock() (string, error) { - dd, err := machine.GetDataDir(define.AppleHvVirt) - if err != nil { - return "", err - } - return filepath.Join(dd, "podman.sock"), nil -} - -func (m *MacMachine) forwardSocketPath() (*define.VMFile, error) { - sockName := "podman.sock" - path, err := machine.GetDataDir(define.AppleHvVirt) - if err != nil { - return nil, fmt.Errorf("Resolving data dir: %s", err.Error()) - } - return define.NewMachineFile(filepath.Join(path, sockName), &sockName) -} - // resizeDisk uses os truncate to resize (only larger) a raw disk. the input size // is assumed GiB -func (m *MacMachine) resizeDisk(newSize strongunits.GiB) error { - if uint64(newSize) < m.DiskSize { - // TODO this error needs to be changed to the common error. would do now but the PR for the common - // error has not merged - return fmt.Errorf("invalid disk size %d: new disk must be larger than %dGB", newSize, m.DiskSize) - } - logrus.Debugf("resizing %s to %d bytes", m.ImagePath.GetPath(), newSize.ToBytes()) +func resizeDisk(mc *vmconfigs.MachineConfig, newSize strongunits.GiB) error { + logrus.Debugf("resizing %s to %d bytes", mc.ImagePath.GetPath(), newSize.ToBytes()) // seems like os.truncate() is not very performant with really large files // so exec'ing out to the command truncate size := fmt.Sprintf("%dG", newSize) - c := exec.Command("truncate", "-s", size, m.ImagePath.GetPath()) + c := exec.Command("truncate", "-s", size, mc.ImagePath.GetPath()) if logrus.IsLevelEnabled(logrus.DebugLevel) { c.Stderr = os.Stderr c.Stdout = os.Stdout @@ -1077,56 +112,6 @@ func (m *MacMachine) resizeDisk(newSize strongunits.GiB) error { return c.Run() } -// isFirstBoot returns a bool reflecting if the machine has been booted before -func (m *MacMachine) isFirstBoot() (bool, error) { - never, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - if err != nil { - return false, err - } - return m.LastUp == never, nil -} - -func (m *MacMachine) getIgnitionSock() (*define.VMFile, error) { - dataDir, err := machine.GetDataDir(define.AppleHvVirt) - if err != nil { - return nil, err - } - if err := os.MkdirAll(dataDir, 0755); err != nil { - if !errors.Is(err, os.ErrExist) { - return nil, err - } - } - return define.NewMachineFile(filepath.Join(dataDir, ignitionSocketName), nil) -} - -func (m *MacMachine) getRuntimeDir() (string, error) { - tmpDir, ok := os.LookupEnv("TMPDIR") - if !ok { - tmpDir = "/tmp" - } - rtd := filepath.Join(tmpDir, "podman") - logrus.Debugf("creating runtimeDir: %s", rtd) - if err := os.MkdirAll(rtd, 0755); err != nil { - return "", err - } - - return rtd, nil -} - -func (m *MacMachine) userGlobalSocketLink() (string, error) { - path, err := machine.GetDataDir(define.AppleHvVirt) - if err != nil { - logrus.Errorf("Resolving data dir: %s", err.Error()) - return "", err - } - // User global socket is located in parent directory of machine dirs (one per user) - return filepath.Join(filepath.Dir(path), "podman.sock"), err -} - -func (m *MacMachine) isIncompatible() bool { - return m.UID == -1 -} - func generateSystemDFilesForVirtiofsMounts(mounts []machine.VirtIoFs) []ignition.Unit { // mounting in fcos with virtiofs is a bit of a dance. we need a unit file for the mount, a unit file // for automatic mounting on boot, and a "preparatory" service file that disables FCOS security, performs diff --git a/pkg/machine/applehv/stubber.go b/pkg/machine/applehv/stubber.go new file mode 100644 index 0000000000..062137942d --- /dev/null +++ b/pkg/machine/applehv/stubber.go @@ -0,0 +1,311 @@ +//go:build darwin + +package applehv + +import ( + "context" + "fmt" + "net" + "strconv" + "time" + + "github.com/containers/common/pkg/config" + gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/applehv/vfkit" + "github.com/containers/podman/v4/pkg/machine/define" + "github.com/containers/podman/v4/pkg/machine/ignition" + "github.com/containers/podman/v4/pkg/machine/sockets" + "github.com/containers/podman/v4/pkg/machine/vmconfigs" + "github.com/containers/podman/v4/pkg/strongunits" + "github.com/containers/podman/v4/utils" + vfConfig "github.com/crc-org/vfkit/pkg/config" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +// applehcMACAddress is a pre-defined mac address that vfkit recognizes +// and is required for network flow +const applehvMACAddress = "5a:94:ef:e4:0c:ee" + +var ( + vfkitCommand = "vfkit" + gvProxyWaitBackoff = 500 * time.Millisecond + gvProxyMaxBackoffAttempts = 6 +) + +type AppleHVStubber struct { + vmconfigs.AppleHVConfig +} + +func (a AppleHVStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig, ignBuilder *ignition.IgnitionBuilder) error { + mc.AppleHypervisor = new(vmconfigs.AppleHVConfig) + mc.AppleHypervisor.Vfkit = vfkit.VfkitHelper{} + bl := vfConfig.NewEFIBootloader(fmt.Sprintf("%s/efi-bl-%s", opts.Dirs.DataDir.GetPath(), opts.Name), true) + mc.AppleHypervisor.Vfkit.VirtualMachine = vfConfig.NewVirtualMachine(uint(mc.Resources.CPUs), mc.Resources.Memory, bl) + + randPort, err := utils.GetRandomPort() + if err != nil { + return err + } + mc.AppleHypervisor.Vfkit.Endpoint = localhostURI + ":" + strconv.Itoa(randPort) + + var virtiofsMounts []machine.VirtIoFs + for _, mnt := range mc.Mounts { + virtiofsMounts = append(virtiofsMounts, machine.MountToVirtIOFs(mnt)) + } + + // Populate the ignition file with virtiofs stuff + ignBuilder.WithUnit(generateSystemDFilesForVirtiofsMounts(virtiofsMounts)...) + + return resizeDisk(mc, strongunits.GiB(mc.Resources.DiskSize)) +} + +func (a AppleHVStubber) GetHyperVisorVMs() ([]string, error) { + // not applicable for applehv + return nil, nil +} + +func (a AppleHVStubber) MountType() vmconfigs.VolumeMountType { + return vmconfigs.VirtIOFS +} + +func (a AppleHVStubber) MountVolumesToVM(_ *vmconfigs.MachineConfig, _ bool) error { + // virtiofs: nothing to do here + return nil +} + +func (a AppleHVStubber) RemoveAndCleanMachines(_ *define.MachineDirs) error { + return nil +} + +func (a AppleHVStubber) SetProviderAttrs(mc *vmconfigs.MachineConfig, cpus, memory *uint64, newDiskSize *strongunits.GiB) error { + if newDiskSize != nil { + if err := resizeDisk(mc, *newDiskSize); err != nil { + return err + } + } + // VFKit does not require saving memory, disk, or cpu + return nil +} + +func (a AppleHVStubber) StartNetworking(mc *vmconfigs.MachineConfig, cmd *gvproxy.GvproxyCommand) error { + gvProxySock, err := mc.GVProxySocket() + if err != nil { + return err + } + // make sure it does not exist before gvproxy is called + if err := gvProxySock.Delete(); err != nil { + logrus.Error(err) + } + cmd.AddVfkitSocket(fmt.Sprintf("unixgram://%s", gvProxySock.GetPath())) + return nil +} + +func (a AppleHVStubber) StartVM(mc *vmconfigs.MachineConfig) (func() error, func() error, error) { + var ( + ignitionSocket *define.VMFile + ) + + if bl := mc.AppleHypervisor.Vfkit.VirtualMachine.Bootloader; bl == nil { + return nil, nil, fmt.Errorf("unable to determine boot loader for this machine") + } + + // Add networking + netDevice, err := vfConfig.VirtioNetNew(applehvMACAddress) + if err != nil { + return nil, nil, err + } + // Set user networking with gvproxy + + gvproxySocket, err := mc.GVProxySocket() + if err != nil { + return nil, nil, err + } + + // Wait on gvproxy to be running and aware + if err := waitForGvProxy(gvproxySocket); err != nil { + return nil, nil, err + } + + netDevice.SetUnixSocketPath(gvproxySocket.GetPath()) + + readySocket, err := mc.ReadySocket() + if err != nil { + return nil, nil, err + } + + logfile, err := mc.LogFile() + if err != nil { + return nil, nil, err + } + + // create a one-time virtual machine for starting because we dont want all this information in the + // machineconfig if possible. the preference was to derive this stuff + vm := vfConfig.NewVirtualMachine(uint(mc.Resources.CPUs), mc.Resources.Memory, mc.AppleHypervisor.Vfkit.VirtualMachine.Bootloader) + + defaultDevices, err := getDefaultDevices(mc.ImagePath.GetPath(), logfile.GetPath(), readySocket.GetPath()) + if err != nil { + return nil, nil, err + } + + vm.Devices = append(vm.Devices, defaultDevices...) + vm.Devices = append(vm.Devices, netDevice) + + mounts, err := virtIOFsToVFKitVirtIODevice(mc.Mounts) + if err != nil { + return nil, nil, err + } + vm.Devices = append(vm.Devices, mounts...) + + // To start the VM, we need to call vfkit + cfg, err := config.Default() + if err != nil { + return nil, nil, err + } + + vfkitBinaryPath, err := cfg.FindHelperBinary(vfkitCommand, true) + if err != nil { + return nil, nil, err + } + + logrus.Debugf("vfkit path is: %s", vfkitBinaryPath) + + cmd, err := vm.Cmd(vfkitBinaryPath) + if err != nil { + return nil, nil, err + } + + vfkitEndpointArgs, err := getVfKitEndpointCMDArgs(mc.AppleHypervisor.Vfkit.Endpoint) + if err != nil { + return nil, nil, err + } + + machineDataDir, err := mc.DataDir() + if err != nil { + return nil, nil, err + } + + cmd.Args = append(cmd.Args, vfkitEndpointArgs...) + + firstBoot, err := mc.IsFirstBoot() + if err != nil { + return nil, nil, err + } + + if logrus.IsLevelEnabled(logrus.DebugLevel) { + debugDevArgs, err := getDebugDevicesCMDArgs() + if err != nil { + return nil, nil, err + } + cmd.Args = append(cmd.Args, debugDevArgs...) + cmd.Args = append(cmd.Args, "--gui") // add command line switch to pop the gui open + } + + if firstBoot { + // If this is the first boot of the vm, we need to add the vsock + // device to vfkit so we can inject the ignition file + socketName := fmt.Sprintf("%s-%s", mc.Name, ignitionSocketName) + ignitionSocket, err = machineDataDir.AppendToNewVMFile(socketName, &socketName) + if err != nil { + return nil, nil, err + } + if err := ignitionSocket.Delete(); err != nil { + logrus.Errorf("unable to delete ignition socket: %q", err) + } + + ignitionVsockDeviceCLI, err := getIgnitionVsockDeviceAsCLI(ignitionSocket.GetPath()) + if err != nil { + return nil, nil, err + } + cmd.Args = append(cmd.Args, ignitionVsockDeviceCLI...) + + logrus.Debug("first boot detected") + logrus.Debugf("serving ignition file over %s", ignitionSocket.GetPath()) + go func() { + if err := serveIgnitionOverSock(ignitionSocket, mc); err != nil { + logrus.Error(err) + } + logrus.Debug("ignition vsock server exited") + }() + } + + logrus.Debugf("listening for ready on: %s", readySocket.GetPath()) + if err := readySocket.Delete(); err != nil { + logrus.Warnf("unable to delete previous ready socket: %q", err) + } + readyListen, err := net.Listen("unix", readySocket.GetPath()) + if err != nil { + return nil, nil, err + } + + logrus.Debug("waiting for ready notification") + readyChan := make(chan error) + go sockets.ListenAndWaitOnSocket(readyChan, readyListen) + + logrus.Debugf("vfkit command-line: %v", cmd.Args) + + if err := cmd.Start(); err != nil { + return nil, nil, err + } + + returnFunc := func() error { + processErrChan := make(chan error) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + defer close(processErrChan) + for { + select { + case <-ctx.Done(): + return + default: + } + if err := checkProcessRunning("vfkit", cmd.Process.Pid); err != nil { + processErrChan <- err + return + } + // lets poll status every half second + time.Sleep(500 * time.Millisecond) + } + }() + + // wait for either socket or to be ready or process to have exited + select { + case err := <-processErrChan: + if err != nil { + return err + } + case err := <-readyChan: + if err != nil { + return err + } + logrus.Debug("ready notification received") + } + return nil + } + return cmd.Process.Release, returnFunc, nil +} + +func (a AppleHVStubber) StopHostNetworking() error { + // TODO implement me + panic("implement me") +} + +func (a AppleHVStubber) VMType() define.VMType { + return define.AppleHvVirt +} + +func waitForGvProxy(gvproxySocket *define.VMFile) error { + backoffWait := gvProxyWaitBackoff + logrus.Debug("checking that gvproxy is running") + for i := 0; i < gvProxyMaxBackoffAttempts; i++ { + err := unix.Access(gvproxySocket.GetPath(), unix.W_OK) + if err == nil { + return nil + } + time.Sleep(backoffWait) + backoffWait *= 2 + } + return fmt.Errorf("unable to connect to gvproxy %q", gvproxySocket.GetPath()) +} diff --git a/pkg/machine/applehv/vfkit.go b/pkg/machine/applehv/vfkit.go index 7014d3555b..1c38b1eeb4 100644 --- a/pkg/machine/applehv/vfkit.go +++ b/pkg/machine/applehv/vfkit.go @@ -3,10 +3,11 @@ package applehv import ( - "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/vmconfigs" vfConfig "github.com/crc-org/vfkit/pkg/config" ) +// TODO this signature could be an machineconfig func getDefaultDevices(imagePath, logPath, readyPath string) ([]vfConfig.VirtioDevice, error) { var devices []vfConfig.VirtioDevice @@ -53,11 +54,14 @@ func getIgnitionVsockDevice(path string) (vfConfig.VirtioDevice, error) { return vfConfig.VirtioVsockNew(1024, path, true) } -func VirtIOFsToVFKitVirtIODevice(fs machine.VirtIoFs) vfConfig.VirtioFs { - return vfConfig.VirtioFs{ - DirectorySharingConfig: vfConfig.DirectorySharingConfig{ - MountTag: fs.Tag, - }, - SharedDir: fs.Source, +func virtIOFsToVFKitVirtIODevice(mounts []vmconfigs.Mount) ([]vfConfig.VirtioDevice, error) { + var virtioDevices []vfConfig.VirtioDevice + for _, vol := range mounts { + virtfsDevice, err := vfConfig.VirtioFsNew(vol.Source, vol.Tag) + if err != nil { + return nil, err + } + virtioDevices = append(virtioDevices, virtfsDevice) } + return virtioDevices, nil } diff --git a/pkg/machine/applehv/vfkit/config.go b/pkg/machine/applehv/vfkit/config.go index 80be55c864..ca24bfcd90 100644 --- a/pkg/machine/applehv/vfkit/config.go +++ b/pkg/machine/applehv/vfkit/config.go @@ -57,6 +57,9 @@ func (vf *VfkitHelper) getRawState() (define.Status, error) { if err != nil { return "", err } + if err := serverResponse.Body.Close(); err != nil { + logrus.Error(err) + } return ToMachineStatus(response.State) } @@ -66,7 +69,7 @@ func (vf *VfkitHelper) getRawState() (define.Status, error) { func (vf *VfkitHelper) State() (define.Status, error) { vmState, err := vf.getRawState() if err == nil { - return vmState, err + return vmState, nil } if errors.Is(err, unix.ECONNREFUSED) { return define.Stopped, nil @@ -107,7 +110,7 @@ func (vf *VfkitHelper) Stop(force, wait bool) error { waitErr = nil break } - waitDuration = waitDuration * 2 + waitDuration *= 2 logrus.Debugf("backoff wait time: %s", waitDuration.String()) time.Sleep(waitDuration) } diff --git a/pkg/machine/define/vmfile.go b/pkg/machine/define/vmfile.go index 16516e956b..1795a4dc5a 100644 --- a/pkg/machine/define/vmfile.go +++ b/pkg/machine/define/vmfile.go @@ -73,6 +73,7 @@ func NewMachineFile(path string, symlink *string) (*VMFile, error) { return nil, errors.New("invalid symlink path") } mf := VMFile{Path: path} + logrus.Debugf("socket length for %s is %d", path, len(path)) if symlink != nil && len(path) > MaxSocketPathLength { if err := mf.makeSymlink(symlink); err != nil && !errors.Is(err, os.ErrExist) { return nil, err @@ -100,5 +101,5 @@ func (m *VMFile) makeSymlink(symlink *string) error { // AppendToNewVMFile takes a given path and appends it to the existing vmfile path. The new // VMFile is returned func (m *VMFile) AppendToNewVMFile(additionalPath string, symlink *string) (*VMFile, error) { - return NewMachineFile(filepath.Join(m.GetPath(), additionalPath), symlink) + return NewMachineFile(filepath.Join(m.Path, additionalPath), symlink) } diff --git a/pkg/machine/e2e/machine_test.go b/pkg/machine/e2e/machine_test.go index 3da976811a..789eb43e9c 100644 --- a/pkg/machine/e2e/machine_test.go +++ b/pkg/machine/e2e/machine_test.go @@ -59,7 +59,7 @@ var _ = BeforeSuite(func() { downloadLocation := os.Getenv("MACHINE_IMAGE") if downloadLocation == "" { - downloadLocation, err = GetDownload() + downloadLocation, err = GetDownload(testProvider.VMType()) if err != nil { Fail("unable to derive download disk from fedora coreos") } @@ -69,9 +69,15 @@ var _ = BeforeSuite(func() { Fail("machine tests require a file reference to a disk image right now") } - // TODO Fix or remove - this only works for qemu rn - // compressionExtension := fmt.Sprintf(".%s", testProvider.Compression().String()) - compressionExtension := ".xz" + var compressionExtension string + switch testProvider.VMType() { + case define.AppleHvVirt: + compressionExtension = ".gz" + case define.HyperVVirt: + compressionExtension = ".zip" + default: + compressionExtension = ".xz" + } suiteImageName = strings.TrimSuffix(path.Base(downloadLocation), compressionExtension) fqImageName = filepath.Join(tmpDir, suiteImageName) diff --git a/pkg/machine/e2e/pull_test.go b/pkg/machine/e2e/pull_test.go index 80b62b6f4b..b590def06e 100644 --- a/pkg/machine/e2e/pull_test.go +++ b/pkg/machine/e2e/pull_test.go @@ -7,14 +7,16 @@ import ( "net/http" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/coreos/stream-metadata-go/fedoracoreos" "github.com/coreos/stream-metadata-go/stream" "github.com/sirupsen/logrus" ) -func GetDownload() (string, error) { +func GetDownload(vmType define.VMType) (string, error) { var ( - fcosstable stream.Stream + fcosstable stream.Stream + artifactType, format string ) url := fedoracoreos.GetStreamURL("testing") resp, err := http.Get(url.String()) @@ -34,6 +36,19 @@ func GetDownload() (string, error) { if err := json.Unmarshal(body, &fcosstable); err != nil { return "", err } + + switch vmType { + case define.AppleHvVirt: + artifactType = "applehv" + format = "raw.gz" + case define.HyperVVirt: + artifactType = "hyperv" + format = "vhdx.zip" + default: + artifactType = "qemu" + format = "qcow2.xz" + } + arch, ok := fcosstable.Architectures[machine.GetFcosArch()] if !ok { return "", fmt.Errorf("unable to pull VM image: no targetArch in stream") @@ -42,17 +57,17 @@ func GetDownload() (string, error) { if upstreamArtifacts == nil { return "", fmt.Errorf("unable to pull VM image: no artifact in stream") } - upstreamArtifact, ok := upstreamArtifacts["qemu"] + upstreamArtifact, ok := upstreamArtifacts[artifactType] if !ok { - return "", fmt.Errorf("unable to pull VM image: no %s artifact in stream", "qemu") + return "", fmt.Errorf("unable to pull VM image: no %s artifact in stream", artifactType) } formats := upstreamArtifact.Formats if formats == nil { return "", fmt.Errorf("unable to pull VM image: no formats in stream") } - formatType, ok := formats["qcow2.xz"] + formatType, ok := formats[format] if !ok { - return "", fmt.Errorf("unable to pull VM image: no %s format in stream", "qcow2.xz") + return "", fmt.Errorf("unable to pull VM image: no %s format in stream", format) } disk := formatType.Disk return disk.Location, nil diff --git a/pkg/machine/provider/platform_darwin.go b/pkg/machine/provider/platform_darwin.go index ffe8dc2377..e7934f0a88 100644 --- a/pkg/machine/provider/platform_darwin.go +++ b/pkg/machine/provider/platform_darwin.go @@ -5,14 +5,13 @@ import ( "os" "github.com/containers/common/pkg/config" - "github.com/containers/podman/v4/pkg/machine" "github.com/containers/podman/v4/pkg/machine/applehv" "github.com/containers/podman/v4/pkg/machine/define" - "github.com/containers/podman/v4/pkg/machine/qemu" + "github.com/containers/podman/v4/pkg/machine/vmconfigs" "github.com/sirupsen/logrus" ) -func Get() (machine.VirtProvider, error) { +func Get() (vmconfigs.VMProvider, error) { cfg, err := config.Default() if err != nil { return nil, err @@ -28,10 +27,8 @@ func Get() (machine.VirtProvider, error) { logrus.Debugf("Using Podman machine with `%s` virtualization provider", resolvedVMType.String()) switch resolvedVMType { - case define.QemuVirt: - return qemu.VirtualizationProvider(), nil case define.AppleHvVirt: - return applehv.VirtualizationProvider(), nil + return new(applehv.AppleHVStubber), nil default: return nil, fmt.Errorf("unsupported virtualization provider: `%s`", resolvedVMType.String()) } diff --git a/pkg/machine/qemu/stubber.go b/pkg/machine/qemu/stubber.go index 4d10d3fd76..06f3b19286 100644 --- a/pkg/machine/qemu/stubber.go +++ b/pkg/machine/qemu/stubber.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/containers/podman/v4/pkg/machine/ignition" + "github.com/containers/common/pkg/config" "github.com/containers/common/pkg/strongunits" gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" @@ -68,7 +70,7 @@ func (q *QEMUStubber) setQEMUCommandLine(mc *vmconfigs.MachineConfig) error { return nil } -func (q *QEMUStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig) error { +func (q *QEMUStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig, _ *ignition.IgnitionBuilder) error { monitor, err := command.NewQMPMonitor(opts.Name, opts.Dirs.RuntimeDir) if err != nil { return err diff --git a/pkg/machine/shim/claim_unsupported.go b/pkg/machine/shim/claim_unsupported.go index 3e3ce835db..0fc9403002 100644 --- a/pkg/machine/shim/claim_unsupported.go +++ b/pkg/machine/shim/claim_unsupported.go @@ -1,4 +1,4 @@ -//build: !darwin +//go:build !darwin package shim diff --git a/pkg/machine/shim/host.go b/pkg/machine/shim/host.go index 1fe1759709..0add3644db 100644 --- a/pkg/machine/shim/host.go +++ b/pkg/machine/shim/host.go @@ -182,7 +182,7 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) (*vmconfigs.M return nil, err } - readyUnitFile, err := ignition.CreateReadyUnitFile(machineDefine.QemuVirt, nil) + readyUnitFile, err := ignition.CreateReadyUnitFile(mp.VMType(), nil) if err != nil { return nil, err } @@ -194,12 +194,8 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) (*vmconfigs.M } ignBuilder.WithUnit(readyUnit) - if err := ignBuilder.Build(); err != nil { - return nil, err - } - // Mounts - mc.Mounts = vmconfigs.CmdLineVolumesToMounts(opts.Volumes, mp.MountType()) + mc.Mounts = CmdLineVolumesToMounts(opts.Volumes, mp.MountType()) // TODO AddSSHConnectionToPodmanSocket could take an machineconfig instead if err := connection.AddSSHConnectionsToPodmanSocket(mc.HostUser.UID, mc.SSH.Port, mc.SSH.IdentityPath, mc.Name, mc.SSH.RemoteUsername, opts); err != nil { @@ -211,7 +207,11 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) (*vmconfigs.M } callbackFuncs.Add(cleanup) - if err := mp.CreateVM(createOpts, mc); err != nil { + if err := mp.CreateVM(createOpts, mc, &ignBuilder); err != nil { + return nil, err + } + + if err := ignBuilder.Build(); err != nil { return nil, err } @@ -327,7 +327,6 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDe if err != nil { return err } - // if there are generic things that need to be done, a preStart function could be added here // should it be extensive diff --git a/pkg/machine/shim/networking.go b/pkg/machine/shim/networking.go index bae3d23cd1..21277936ad 100644 --- a/pkg/machine/shim/networking.go +++ b/pkg/machine/shim/networking.go @@ -6,6 +6,7 @@ import ( "net" "os" "path/filepath" + "strings" "time" "github.com/containers/common/pkg/config" @@ -101,6 +102,8 @@ func startNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider) } c := cmd.Cmd(binary) + + logrus.Debugf("gvproxy command-line: %s %s", binary, strings.Join(cmd.ToCmdline(), " ")) if err := c.Start(); err != nil { return forwardSock, 0, fmt.Errorf("unable to execute: %q: %w", cmd.ToCmdline(), err) } diff --git a/pkg/machine/shim/volume.go b/pkg/machine/shim/volume.go new file mode 100644 index 0000000000..fe570b4342 --- /dev/null +++ b/pkg/machine/shim/volume.go @@ -0,0 +1,30 @@ +package shim + +import ( + "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/vmconfigs" +) + +func CmdLineVolumesToMounts(volumes []string, volumeType vmconfigs.VolumeMountType) []vmconfigs.Mount { + mounts := []vmconfigs.Mount{} + for i, volume := range volumes { + var mount vmconfigs.Mount + tag, source, target, readOnly, _ := vmconfigs.SplitVolume(i, volume) + switch volumeType { + case vmconfigs.VirtIOFS: + virtioMount := machine.NewVirtIoFsMount(source, target, readOnly) + mount = virtioMount.ToMount() + default: + mount = vmconfigs.Mount{ + Type: volumeType.String(), + Tag: tag, + Source: source, + Target: target, + ReadOnly: readOnly, + OriginalInput: volume, + } + } + mounts = append(mounts, mount) + } + return mounts +} diff --git a/pkg/machine/sockets/sockets.go b/pkg/machine/sockets/sockets.go index 6d966dbfb3..1c0c56698f 100644 --- a/pkg/machine/sockets/sockets.go +++ b/pkg/machine/sockets/sockets.go @@ -9,6 +9,7 @@ import ( "time" "github.com/containers/podman/v4/pkg/machine/define" + "github.com/sirupsen/logrus" ) // SetSocket creates a new machine file for the socket and assigns it to @@ -33,10 +34,12 @@ func ReadySocketPath(runtimeDir, machineName string) string { func ListenAndWaitOnSocket(errChan chan<- error, listener net.Listener) { conn, err := listener.Accept() if err != nil { + logrus.Debug("failed to connect to ready socket") errChan <- err return } _, err = bufio.NewReader(conn).ReadString('\n') + logrus.Debug("ready ack received") if closeErr := conn.Close(); closeErr != nil { errChan <- closeErr diff --git a/pkg/machine/vmconfigs/config.go b/pkg/machine/vmconfigs/config.go index aec81d07a9..890db29c78 100644 --- a/pkg/machine/vmconfigs/config.go +++ b/pkg/machine/vmconfigs/config.go @@ -8,6 +8,7 @@ import ( "github.com/containers/common/pkg/strongunits" gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" "github.com/containers/podman/v4/pkg/machine/define" + "github.com/containers/podman/v4/pkg/machine/ignition" "github.com/containers/podman/v4/pkg/machine/qemu/command" "github.com/containers/storage/pkg/lockfile" ) @@ -106,7 +107,7 @@ func (f fcosMachineImage) path() string { } type VMProvider interface { //nolint:interfacebloat - CreateVM(opts define.CreateVMOpts, mc *MachineConfig) error + CreateVM(opts define.CreateVMOpts, mc *MachineConfig, builder *ignition.IgnitionBuilder) error GetHyperVisorVMs() ([]string, error) MountType() VolumeMountType MountVolumesToVM(mc *MachineConfig, quiet bool) error diff --git a/pkg/machine/vmconfigs/machine.go b/pkg/machine/vmconfigs/machine.go index c5fb05ce48..0b1575e571 100644 --- a/pkg/machine/vmconfigs/machine.go +++ b/pkg/machine/vmconfigs/machine.go @@ -10,14 +10,12 @@ import ( "strings" "time" - "github.com/containers/podman/v4/pkg/machine/connection" - - "github.com/sirupsen/logrus" - define2 "github.com/containers/podman/v4/libpod/define" + "github.com/containers/podman/v4/pkg/machine/connection" "github.com/containers/podman/v4/pkg/machine/define" "github.com/containers/podman/v4/pkg/machine/lock" "github.com/containers/podman/v4/utils" + "github.com/sirupsen/logrus" ) /* @@ -235,7 +233,15 @@ func (mc *MachineConfig) ReadySocket() (*define.VMFile, error) { if err != nil { return nil, err } - return rtDir.AppendToNewVMFile(mc.Name+".sock", nil) + return readySocket(mc.Name, rtDir) +} + +func (mc *MachineConfig) GVProxySocket() (*define.VMFile, error) { + machineRuntimeDir, err := mc.RuntimeDir() + if err != nil { + return nil, err + } + return gvProxySocket(mc.Name, machineRuntimeDir) } func (mc *MachineConfig) LogFile() (*define.VMFile, error) { @@ -264,6 +270,14 @@ func (mc *MachineConfig) Kind() (define.VMType, error) { return define.UnknownVirt, nil } +func (mc *MachineConfig) IsFirstBoot() (bool, error) { + never, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") + if err != nil { + return false, err + } + return mc.LastUp == never, nil +} + // LoadMachineByName returns a machine config based on the vm name and provider func LoadMachineByName(name string, dirs *define.MachineDirs) (*MachineConfig, error) { fullPath, err := dirs.ConfigDir.AppendToNewVMFile(name+".json", nil) diff --git a/pkg/machine/vmconfigs/sockets.go b/pkg/machine/vmconfigs/sockets.go new file mode 100644 index 0000000000..b33bb34ae8 --- /dev/null +++ b/pkg/machine/vmconfigs/sockets.go @@ -0,0 +1,17 @@ +//go:build !darwin + +package vmconfigs + +import ( + "fmt" + + "github.com/containers/podman/v4/pkg/machine/define" +) + +func gvProxySocket(name string, machineRuntimeDir *define.VMFile) (*define.VMFile, error) { + return machineRuntimeDir.AppendToNewVMFile(fmt.Sprintf("%s-gvproxy.sock", name), nil) +} + +func readySocket(name string, machineRuntimeDir *define.VMFile) (*define.VMFile, error) { + return machineRuntimeDir.AppendToNewVMFile(name+".sock", nil) +} diff --git a/pkg/machine/vmconfigs/sockets_darwin.go b/pkg/machine/vmconfigs/sockets_darwin.go new file mode 100644 index 0000000000..83e4cceec7 --- /dev/null +++ b/pkg/machine/vmconfigs/sockets_darwin.go @@ -0,0 +1,17 @@ +package vmconfigs + +import ( + "fmt" + + "github.com/containers/podman/v4/pkg/machine/define" +) + +func gvProxySocket(name string, machineRuntimeDir *define.VMFile) (*define.VMFile, error) { + socketName := fmt.Sprintf("%s-gvproxy.sock", name) + return machineRuntimeDir.AppendToNewVMFile(socketName, &socketName) +} + +func readySocket(name string, machineRuntimeDir *define.VMFile) (*define.VMFile, error) { + socketName := name + ".sock" + return machineRuntimeDir.AppendToNewVMFile(socketName, &socketName) +} diff --git a/pkg/machine/vmconfigs/volumes.go b/pkg/machine/vmconfigs/volumes.go index edfc3abb9d..8ee2093fd1 100644 --- a/pkg/machine/vmconfigs/volumes.go +++ b/pkg/machine/vmconfigs/volumes.go @@ -58,20 +58,3 @@ func SplitVolume(idx int, volume string) (string, string, string, bool, string) readonly, securityModel := extractMountOptions(paths) return tag, source, target, readonly, securityModel } - -func CmdLineVolumesToMounts(volumes []string, volumeType VolumeMountType) []Mount { - mounts := []Mount{} - for i, volume := range volumes { - tag, source, target, readOnly, _ := SplitVolume(i, volume) - mount := Mount{ - Type: volumeType.String(), - Tag: tag, - Source: source, - Target: target, - ReadOnly: readOnly, - OriginalInput: volume, - } - mounts = append(mounts, mount) - } - return mounts -} diff --git a/pkg/machine/volumes.go b/pkg/machine/volumes.go index b2a9d9de41..a2e71f3d5c 100644 --- a/pkg/machine/volumes.go +++ b/pkg/machine/volumes.go @@ -61,3 +61,13 @@ func NewVirtIoFsMount(src, target string, readOnly bool) VirtIoFs { vfs.Tag = vfs.unitName() return vfs } + +func MountToVirtIOFs(mnt vmconfigs.Mount) VirtIoFs { + return VirtIoFs{ + VolumeKind: VirtIOFsVk, + ReadOnly: mnt.ReadOnly, + Source: mnt.Source, + Tag: mnt.Tag, + Target: mnt.Target, + } +}