diff --git a/cmd/podman/machine/platform.go b/cmd/podman/machine/platform.go index 67b5543065..0b08af843a 100644 --- a/cmd/podman/machine/platform.go +++ b/cmd/podman/machine/platform.go @@ -1,5 +1,4 @@ -//go:build (amd64 && !windows) || (arm64 && !windows) -// +build amd64,!windows arm64,!windows +//go:build (amd64 && !windows && amd64 && !darwin) || (arm64 && !windows && arm64 && !darwin) || (amd64 && darwin) package machine diff --git a/cmd/podman/machine/platform_darwin.go b/cmd/podman/machine/platform_darwin.go new file mode 100644 index 0000000000..f6b512a04e --- /dev/null +++ b/cmd/podman/machine/platform_darwin.go @@ -0,0 +1,36 @@ +//go:build darwin && arm64 + +package machine + +import ( + "fmt" + "os" + + "github.com/containers/common/pkg/config" + "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/qemu" + "github.com/sirupsen/logrus" +) + +func GetSystemProvider() (machine.VirtProvider, error) { + cfg, err := config.Default() + if err != nil { + return nil, err + } + provider := cfg.Machine.Provider + if providerOverride, found := os.LookupEnv("CONTAINERS_MACHINE_PROVIDER"); found { + provider = providerOverride + } + resolvedVMType, err := machine.ParseVMType(provider, machine.QemuVirt) + if err != nil { + return nil, err + } + + logrus.Debugf("Using Podman machine with `%s` virtualization provider", resolvedVMType.String()) + switch resolvedVMType { + case machine.QemuVirt: + return qemu.GetVirtualizationProvider(), nil + default: + return nil, fmt.Errorf("unsupported virtualization provider: `%s`", resolvedVMType.String()) + } +} diff --git a/go.mod b/go.mod index d110083e24..6b8e97eaaa 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/containers/storage v1.46.1 github.com/coreos/go-systemd/v22 v22.5.0 github.com/coreos/stream-metadata-go v0.4.1 + github.com/crc-org/vfkit v0.0.5-0.20230427143911-8117c28876bc github.com/cyphar/filepath-securejoin v0.2.3 github.com/digitalocean/go-qemu v0.0.0-20210326154740-ac9e0b687001 github.com/docker/docker v23.0.5+incompatible diff --git a/go.sum b/go.sum index 11024359e2..58aaa8dcdd 100644 --- a/go.sum +++ b/go.sum @@ -285,6 +285,8 @@ github.com/coreos/stream-metadata-go v0.4.1/go.mod h1:Lwjwqf1zwnVa7uy/v/KW28eUkd github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crc-org/vfkit v0.0.5-0.20230427143911-8117c28876bc h1:fhuTRmFWKxI59UGqN/y1Pq9+KRnNrZ5bztFT2rTQTGM= +github.com/crc-org/vfkit v0.0.5-0.20230427143911-8117c28876bc/go.mod h1:sluZ7Q1ZKWwI+PzGwwNy74pplP99gzRqmEWJ246denE= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -695,7 +697,7 @@ github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHef github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= diff --git a/pkg/machine/applehv/config.go b/pkg/machine/applehv/config.go index 6cab16b53f..bba953b048 100644 --- a/pkg/machine/applehv/config.go +++ b/pkg/machine/applehv/config.go @@ -1,9 +1,21 @@ //go:build arm64 && darwin -// +build arm64,darwin package applehv -import "github.com/containers/podman/v4/pkg/machine" +import ( + "errors" + "io/fs" + "path/filepath" + "time" + + "github.com/containers/podman/v4/pkg/machine" + "github.com/docker/go-units" + "golang.org/x/sys/unix" +) + +const ( + defaultVFKitEndpoint = "http://localhost:8081" +) type Virtualization struct { artifact machine.Artifact @@ -11,12 +23,29 @@ type Virtualization struct { format machine.ImageFormat } +type MMHardwareConfig struct { + CPUs uint16 + DiskPath string + DiskSize uint64 + Memory int32 +} + func (v Virtualization) Artifact() machine.Artifact { - return machine.None + return machine.Metal } func (v Virtualization) CheckExclusiveActiveVM() (bool, string, error) { - return false, "", machine.ErrNotImplemented + 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 Virtualization) Compression() machine.ImageCompression { @@ -28,25 +57,137 @@ func (v Virtualization) Format() machine.ImageFormat { } func (v Virtualization) IsValidVMName(name string) (bool, error) { - return false, machine.ErrNotImplemented + mm := MacMachine{Name: name} + configDir, err := machine.GetConfDir(machine.AppleHvVirt) + if err != nil { + return false, err + } + if err := loadMacMachineFromJSON(configDir, &mm); err != nil { + return false, err + } + return true, nil } func (v Virtualization) List(opts machine.ListOptions) ([]*machine.ListResponse, error) { - return nil, machine.ErrNotImplemented + var ( + response []*machine.ListResponse + ) + + mms, err := v.loadFromLocalJson() + if err != nil { + return nil, err + } + + for _, mm := range mms { + vmState, err := mm.state() + if err != nil { + if errors.Is(err, unix.ECONNREFUSED) { + vmState = machine.Stopped + } else { + return nil, err + } + } + + mlr := machine.ListResponse{ + Name: mm.Name, + CreatedAt: mm.Created, + LastUp: mm.LastUp, + Running: vmState == machine.Running, + Starting: vmState == machine.Starting, + Stream: mm.ImageStream, + VMType: machine.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 Virtualization) LoadVMByName(name string) (machine.VM, error) { - return nil, machine.ErrNotImplemented + m := MacMachine{Name: name} + return m.loadFromFile() } func (v Virtualization) NewMachine(opts machine.InitOptions) (machine.VM, error) { - return nil, machine.ErrNotImplemented + m := MacMachine{Name: opts.Name} + + configDir, err := machine.GetConfDir(machine.AppleHvVirt) + if err != nil { + return nil, err + } + + configPath, err := machine.NewMachineFile(getVMConfigPath(configDir, opts.Name), nil) + if err != nil { + return nil, err + } + m.ConfigPath = *configPath + + ignitionPath, err := machine.NewMachineFile(filepath.Join(configDir, m.Name)+".ign", nil) + if err != nil { + return nil, err + } + m.IgnitionFile = *ignitionPath + + // Set creation time + m.Created = time.Now() + + m.ResourceConfig = machine.ResourceConfig{ + CPUs: opts.CPUS, + DiskSize: opts.DiskSize, + // Diskpath will be needed + Memory: opts.Memory, + } + + if err := m.writeConfig(); err != nil { + return nil, err + } + return m.loadFromFile() } func (v Virtualization) RemoveAndCleanMachines() error { + // This can be implemented when host networking is completed. return machine.ErrNotImplemented } -func (v Virtualization) VMType() string { +func (v Virtualization) VMType() machine.VMType { return vmtype } + +func (v Virtualization) 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 := MacMachine{} + if err := loadMacMachineFromJSON(jsonFile, &mm); err != nil { + return nil, err + } + if err != nil { + return nil, err + } + mms = append(mms, &mm) + } + return mms, nil +} diff --git a/pkg/machine/applehv/machine.go b/pkg/machine/applehv/machine.go index 1edae28ed2..fe5f577f53 100644 --- a/pkg/machine/applehv/machine.go +++ b/pkg/machine/applehv/machine.go @@ -1,12 +1,25 @@ //go:build arm64 && darwin -// +build arm64,darwin package applehv import ( + "encoding/json" + "errors" + "fmt" + "io/fs" + "net/url" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" "time" + "github.com/containers/common/pkg/config" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/storage/pkg/homedir" + "github.com/docker/go-units" + "github.com/sirupsen/logrus" ) var ( @@ -18,28 +31,599 @@ func GetVirtualizationProvider() machine.VirtProvider { return &Virtualization{ artifact: machine.None, compression: machine.Xz, - format: machine.Qcow, + format: machine.Raw, } } -const ( - // Some of this will need to change when we are closer to having - // working code. - VolumeTypeVirtfs = "virtfs" - MountType9p = "9p" - 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 { + Bootloader string + Devices []string + LogLevel logrus.Level + PathToVfkitBinary string + Endpoint string +} -type apiForwardingState int +type MacMachine struct { + // ConfigPath is the fully qualified path to the configuration file + ConfigPath machine.VMFile + // HostUser contains info about host user + machine.HostUser + // ImageConfig describes the bootable image + machine.ImageConfig + // Mounts is the list of remote filesystems to mount + Mounts []machine.Mount + // Name of VM + Name string + // TODO We will need something like this for applehv but until host networking + // is worked out, we cannot be sure what it looks like. + /* + // NetworkVSock is for the user networking + NetworkHVSock machine.HVSockRegistryEntry + // ReadySocket tells host when vm is booted + ReadyHVSock HVSockRegistryEntry + // ResourceConfig is physical attrs of the VM + */ + machine.ResourceConfig + // SSHConfig for accessing the remote vm + machine.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 + VfkitHelper +} -const ( - noForwarding apiForwardingState = iota - claimUnsupported - notInstalled - machineLocal - dockerGlobal -) +func (m *MacMachine) Init(opts machine.InitOptions) (bool, error) { + var ( + key string + ) + dataDir, err := machine.GetDataDir(machine.AppleHvVirt) + if err != nil { + return false, err + } + // Acquire the image + switch opts.ImagePath { + case machine.Testing.String(), machine.Next.String(), machine.Stable.String(), "": + g, err := machine.NewGenericDownloader(machine.HyperVVirt, opts.Name, opts.ImagePath) + if err != nil { + return false, err + } + + imagePath, err := machine.NewMachineFile(g.Get().GetLocalUncompressedFile(dataDir), nil) + if err != nil { + return false, err + } + m.ImagePath = *imagePath + default: + // The user has provided an alternate image which can be a file path + // or URL. + m.ImageStream = "custom" + g, err := machine.NewGenericDownloader(vmtype, m.Name, opts.ImagePath) + if err != nil { + return false, err + } + imagePath, err := machine.NewMachineFile(g.Get().LocalUncompressedFile, nil) + if err != nil { + return false, err + } + m.ImagePath = *imagePath + if err := machine.DownloadImage(g); err != nil { + return false, err + } + } + + // Store VFKit stuffs + vfhelper, err := newVfkitHelper(m.Name, defaultVFKitEndpoint, m.ImagePath.GetPath()) + if err != nil { + return false, err + } + m.VfkitHelper = *vfhelper + + sshDir := filepath.Join(homedir.Get(), ".ssh") + m.IdentityPath = filepath.Join(sshDir, m.Name) + m.Rootful = opts.Rootful + m.RemoteUsername = opts.Username + + m.UID = os.Getuid() + + // TODO A final decision on networking implementation will need to be made + // prior to this working + //sshPort, err := utils.GetRandomPort() + //if err != nil { + // return false, err + //} + m.Port = 22 + + if len(opts.IgnitionPath) < 1 { + // TODO localhost needs to be restored here + uri := machine.SSHRemoteConnection.MakeSSHURL("192.168.64.2", fmt.Sprintf("/run/user/%d/podman/podman.sock", m.UID), strconv.Itoa(m.Port), m.RemoteUsername) + uriRoot := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/podman/podman.sock", strconv.Itoa(m.Port), "root") + identity := filepath.Join(sshDir, m.Name) + + uris := []url.URL{uri, uriRoot} + names := []string{m.Name, m.Name + "-root"} + + // The first connection defined when connections is empty will become the default + // regardless of IsDefault, so order according to rootful + if opts.Rootful { + uris[0], names[0], uris[1], names[1] = uris[1], names[1], uris[0], names[0] + } + + for i := 0; i < 2; i++ { + if err := machine.AddConnection(&uris[i], names[i], identity, opts.IsDefault && i == 0); err != nil { + return false, err + } + } + } else { + fmt.Println("An ignition path was provided. No SSH connection was added to Podman") + } + + // TODO resize disk + + if err := m.writeConfig(); err != nil { + return false, err + } + + if len(opts.IgnitionPath) < 1 { + var err error + key, err = machine.CreateSSHKeys(m.IdentityPath) + if err != nil { + return false, err + } + } + + 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) + } + // TODO Ignition stuff goes here + // 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, + } + + if err := ign.GenerateIgnitionConfig(); err != nil { + return false, err + } + if err := ign.Write(); err != nil { + return false, err + } + + return true, nil +} + +func (m *MacMachine) Inspect() (*machine.InspectInfo, error) { + vmState, err := m.state() + if err != nil { + return nil, err + } + ii := machine.InspectInfo{ + ConfigPath: m.ConfigPath, + ConnectionInfo: machine.ConnectionConfig{ + PodmanSocket: nil, + PodmanPipe: nil, + }, + Created: m.Created, + Image: machine.ImageConfig{ + IgnitionFile: m.IgnitionFile, + ImageStream: m.ImageStream, + ImagePath: m.ImagePath, + }, + LastUp: m.LastUp, + Name: m.Name, + Resources: machine.ResourceConfig{ + CPUs: m.CPUs, + DiskSize: m.DiskSize, + Memory: m.Memory, + }, + SSHConfig: m.SSHConfig, + State: vmState, + } + return &ii, nil +} + +func (m *MacMachine) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) { + var ( + files []string + ) + + vmState, err := m.state() + if err != nil { + return "", nil, err + } + if vmState == machine.Running { + if !opts.Force { + return "", nil, fmt.Errorf("invalid state: %s is running", m.Name) + } + if err := m.stop(true, true); err != nil { + return "", nil, err + } + } + + if !opts.SaveKeys { + files = append(files, m.IdentityPath, m.IdentityPath+".pub") + } + if !opts.SaveIgnition { + files = append(files, m.IgnitionFile.GetPath()) + } + + if !opts.SaveImage { + files = append(files, m.ImagePath.GetPath()) + } + + files = append(files, m.ConfigPath.GetPath()) + + confirmationMessage := "\nThe following files will be deleted:\n\n" + for _, msg := range files { + confirmationMessage += msg + "\n" + } + + confirmationMessage += "\n" + return confirmationMessage, func() error { + for _, f := range files { + if err := os.Remove(f); err != nil && !errors.Is(err, os.ErrNotExist) { + logrus.Error(err) + } + } + if err := machine.RemoveConnection(m.Name); err != nil { + logrus.Error(err) + } + if err := machine.RemoveConnection(m.Name + "-root"); err != nil { + logrus.Error(err) + } + + // 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) Set(name string, opts machine.SetOptions) ([]error, error) { + var setErrors []error + vmState, err := m.State(false) + if err != nil { + return nil, err + } + if vmState != machine.Stopped { + return nil, machine.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 + } + } + + // 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 != machine.Running { + return fmt.Errorf("vm %q is not running", m.Name) + } + username := opts.Username + if username == "" { + username = m.RemoteUsername + } + // TODO when host networking is figured out, we need to switch this back to + // machine.commonssh + return AppleHVSSH(username, m.IdentityPath, m.Name, m.Port, opts.Args) +} + +func (m *MacMachine) Start(name string, opts machine.StartOptions) error { + st, err := m.State(false) + if err != nil { + return err + } + if st == machine.Running { + return machine.ErrVMAlreadyRunning + } + // TODO Once we decide how to do networking, we can enable the following lines + // for API forwarding, etc + //_, _, err = m.startHostNetworking() + //if err != nil { + // return err + //} + // To start the VM, we need to call vfkit + // TODO need to hold the start command until fcos tells us it is started + return m.VfkitHelper.startVfkit(m) +} + +func (m *MacMachine) State(_ bool) (machine.Status, error) { + vmStatus, err := m.VfkitHelper.state() + if err != nil { + return "", err + } + return vmStatus, nil +} + +func (m *MacMachine) Stop(name string, opts machine.StopOptions) error { + vmState, err := m.State(false) + if err != nil { + return err + } + if vmState != machine.Running { + return machine.ErrWrongState + } + return m.VfkitHelper.stop(false, true) +} + +// 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 := MacMachine{} + + if err := loadMacMachineFromJSON(jsonPath, &mm); err != nil { + return nil, err + } + return &mm, nil } + +func loadMacMachineFromJSON(fqConfigPath string, macMachine *MacMachine) error { + b, err := os.ReadFile(fqConfigPath) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("%q: %w", fqConfigPath, machine.ErrNoSuchVM) + } + return err + } + return json.Unmarshal(b, macMachine) +} + +func (m *MacMachine) jsonConfigPath() (string, error) { + configDir, err := machine.GetConfDir(machine.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 = machine.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 == machine.Running + + if !vm.LastUp.IsZero() { // this means we have already written a time to the config + listEntry.LastUp = vm.LastUp + } else { // else we just created the machine AKA last up = created time + listEntry.LastUp = vm.Created + vm.LastUp = listEntry.LastUp + if err := vm.writeConfig(); err != nil { + return err + } + } + + listed = append(listed, listEntry) + } + return nil + }); err != nil { + return nil, err + } + return listed, err +} + +func (m *MacMachine) startHostNetworking() (string, machine.APIForwardingState, error) { + var ( + forwardSock string + state machine.APIForwardingState + ) + cfg, err := config.Default() + if err != nil { + return "", machine.NoForwarding, err + } + + attr := new(os.ProcAttr) + dnr, err := os.OpenFile(os.DevNull, os.O_RDONLY, 0755) + if err != nil { + return "", machine.NoForwarding, err + } + dnw, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0755) + if err != nil { + return "", machine.NoForwarding, err + } + + defer func() { + if err := dnr.Close(); err != nil { + logrus.Error(err) + } + }() + defer func() { + if err := dnw.Close(); err != nil { + logrus.Error(err) + } + }() + + gvproxy, err := cfg.FindHelperBinary("gvproxy", false) + if err != nil { + return "", 0, err + } + + attr.Files = []*os.File{dnr, dnw, dnw} + cmd := []string{gvproxy} + // Add the ssh port + cmd = append(cmd, []string{"-ssh-port", fmt.Sprintf("%d", m.Port)}...) + // TODO Fix when host networking is setup + //cmd = append(cmd, []string{"-listen", fmt.Sprintf("vsock://%s", m.NetworkHVSock.KeyName)}...) + + cmd, forwardSock, state = m.setupAPIForwarding(cmd) + if logrus.GetLevel() == logrus.DebugLevel { + cmd = append(cmd, "--debug") + fmt.Println(cmd) + } + _, err = os.StartProcess(cmd[0], cmd, attr) + if err != nil { + return "", 0, fmt.Errorf("unable to execute: %q: %w", cmd, err) + } + return forwardSock, state, nil +} + +// AppleHVSSH is a temporary function for applehv until we decide how the networking will work +// for certain. +func AppleHVSSH(username, identityPath, name string, sshPort int, inputArgs []string) error { + sshDestination := username + "@192.168.64.2" + port := strconv.Itoa(sshPort) + + args := []string{"-i", identityPath, "-p", port, sshDestination, + "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-o", "SetEnv=LC_ALL="} + if len(inputArgs) > 0 { + args = append(args, inputArgs...) + } else { + fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", name) + } + + cmd := exec.Command("ssh", args...) + logrus.Debugf("Executing: ssh %v\n", args) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + return cmd.Run() +} +func (m *MacMachine) setupAPIForwarding(cmd []string) ([]string, 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 := "core" + + if m.Rootful { + destSock = "/run/podman/podman.sock" + forwardUser = "root" + } + + cmd = append(cmd, []string{"-forward-sock", socket.GetPath()}...) + cmd = append(cmd, []string{"-forward-dest", destSock}...) + cmd = append(cmd, []string{"-forward-user", forwardUser}...) + cmd = append(cmd, []string{"-forward-identity", m.IdentityPath}...) + + return cmd, "", machine.MachineLocal +} + +func (m *MacMachine) dockerSock() (string, error) { + dd, err := machine.GetDataDir(machine.AppleHvVirt) + if err != nil { + return "", err + } + return filepath.Join(dd, "podman.sock"), nil +} + +func (m *MacMachine) forwardSocketPath() (*machine.VMFile, error) { + sockName := "podman.sock" + path, err := machine.GetDataDir(machine.AppleHvVirt) + if err != nil { + return nil, fmt.Errorf("Resolving data dir: %s", err.Error()) + } + return machine.NewMachineFile(filepath.Join(path, sockName), &sockName) +} diff --git a/pkg/machine/applehv/rest.go b/pkg/machine/applehv/rest.go new file mode 100644 index 0000000000..67fd49472b --- /dev/null +++ b/pkg/machine/applehv/rest.go @@ -0,0 +1,116 @@ +//go:build arm64 && darwin + +package applehv + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" + + "github.com/containers/podman/v4/pkg/machine" + "github.com/crc-org/vfkit/pkg/rest/define" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +type Endpoint string + +const ( + inspect = "/vm/inspect" + state = "/vm/state" + version = "/version" +) + +func (vf *VfkitHelper) get(endpoint string, payload io.Reader) (*http.Response, error) { + client := &http.Client{} + req, err := http.NewRequest(http.MethodGet, endpoint, payload) + if err != nil { + return nil, err + } + return client.Do(req) +} + +func (vf *VfkitHelper) post(endpoint string, payload io.Reader) (*http.Response, error) { + client := &http.Client{} + req, err := http.NewRequest(http.MethodPost, endpoint, payload) + if err != nil { + return nil, err + } + return client.Do(req) +} + +// getRawState asks vfkit for virtual machine state unmodified (see state()) +func (vf *VfkitHelper) getRawState() (machine.Status, error) { + var response define.VMState + endPoint := vf.Endpoint + state + serverResponse, err := vf.get(endPoint, nil) + if err != nil { + if errors.Is(err, unix.ECONNREFUSED) { + logrus.Debugf("connection refused: %s", endPoint) + } + return "", err + } + err = json.NewDecoder(serverResponse.Body).Decode(&response) + if err != nil { + return "", err + } + return ToMachineStatus(response.State) + +} + +// state asks vfkit for the virtual machine state. in case the vfkit +// service is not responding, we assume the service is not running +// and return a stopped status +func (vf *VfkitHelper) state() (machine.Status, error) { + vmState, err := vf.getRawState() + if err == nil { + return vmState, err + } + if errors.Is(err, unix.ECONNREFUSED) { + return machine.Stopped, nil + } + return "", err +} + +func (vf *VfkitHelper) stateChange(newState define.StateChange) error { + b, err := json.Marshal(define.VMState{State: string(newState)}) + if err != nil { + return err + } + payload := bytes.NewReader(b) + _, err = vf.post(vf.Endpoint+state, payload) + return err +} + +func (vf *VfkitHelper) stop(force, wait bool) error { + waitDuration := time.Millisecond * 10 + // TODO Add ability to wait until stopped + if force { + if err := vf.stateChange(define.HardStop); err != nil { + return err + } + } + if err := vf.stateChange(define.Stop); err != nil { + return err + } + if !wait { + return nil + } + waitErr := fmt.Errorf("failed waiting for vm to stop") + // Backoff to wait on the machine shutdown + for i := 0; i < 11; i++ { + _, err := vf.getRawState() + if err != nil || errors.Is(err, unix.ECONNREFUSED) { + waitErr = nil + break + } + waitDuration = waitDuration * 2 + logrus.Debugf("backoff wait time: %s", waitDuration.String()) + time.Sleep(waitDuration) + } + return waitErr +} diff --git a/pkg/machine/applehv/rest_config.go b/pkg/machine/applehv/rest_config.go new file mode 100644 index 0000000000..2308bad8e5 --- /dev/null +++ b/pkg/machine/applehv/rest_config.go @@ -0,0 +1,40 @@ +//go:build arm64 && darwin + +package applehv + +import ( + "errors" + "fmt" + + "github.com/containers/podman/v4/pkg/machine" +) + +// VZMachineState is what the restful service in vfkit will return +type VZMachineState string + +const ( + // Values that the machine can be in + // "VirtualMachineStateStoppedVirtualMachineStateRunningVirtualMachineStatePausedVirtualMachineStateErrorVirtualMachineStateStartingVirtualMachineStatePausingVirtualMachineStateResumingVirtualMachineStateStopping" + VZMachineStateStopped VZMachineState = "VirtualMachineStateStopped" + VZMachineStateRunning VZMachineState = "VirtualMachineStateRunning" + VZMachineStatePaused VZMachineState = "VirtualMachineStatePaused" + VZMachineStateError VZMachineState = "VirtualMachineStateError" + VZMachineStateStarting VZMachineState = "VirtualMachineStateStarting" + VZMachineStatePausing VZMachineState = "VirtualMachineStatePausing" + VZMachineStateResuming VZMachineState = "VirtualMachineStateResuming" + VZMachineStateStopping VZMachineState = "VirtualMachineStateStopping" +) + +func ToMachineStatus(val string) (machine.Status, error) { + switch val { + case string(VZMachineStateRunning), string(VZMachineStatePausing), string(VZMachineStateResuming), string(VZMachineStateStopping), string(VZMachineStatePaused): + return machine.Running, nil + case string(VZMachineStateStopped): + return machine.Stopped, nil + case string(VZMachineStateStarting): + return machine.Starting, nil + case string(VZMachineStateError): + return "", errors.New("machine is in error state") + } + return "", fmt.Errorf("unknown machine state: %s", val) +} diff --git a/pkg/machine/applehv/vfkit.go b/pkg/machine/applehv/vfkit.go new file mode 100644 index 0000000000..7f079f5efd --- /dev/null +++ b/pkg/machine/applehv/vfkit.go @@ -0,0 +1,119 @@ +//go:build arm64 && darwin + +package applehv + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/containers/common/pkg/config" + "github.com/containers/podman/v4/pkg/machine" + "github.com/sirupsen/logrus" +) + +// getDefaultDevices is a constructor for vfkit devices +func getDefaultDevices(imagePath, logPath string) []string { + defaultDevices := []string{ + "virtio-rng", + "virtio-net,nat,mac=72:20:43:d4:38:62", + fmt.Sprintf("virtio-blk,path=%s", imagePath), + fmt.Sprintf("virtio-serial,logFilePath=%s", logPath), + } + return defaultDevices +} + +func newVfkitHelper(name, endpoint, imagePath string) (*VfkitHelper, error) { + dataDir, err := machine.GetDataDir(machine.AppleHvVirt) + if err != nil { + return nil, err + } + logPath := filepath.Join(dataDir, fmt.Sprintf("%s.log", name)) + logLevel := logrus.GetLevel() + + cfg, err := config.Default() + if err != nil { + return nil, err + } + vfkitPath, err := cfg.FindHelperBinary("vfkit", false) + if err != nil { + return nil, err + } + return &VfkitHelper{ + Bootloader: fmt.Sprintf("efi,variable-store=%s/%s-efi-store,create", dataDir, name), + Devices: getDefaultDevices(imagePath, logPath), + LogLevel: logLevel, + PathToVfkitBinary: vfkitPath, + Endpoint: endpoint, + }, nil +} + +// toCmdLine creates the command line for calling vfkit. the vfkit binary is +// NOT part of the cmdline +func (vf *VfkitHelper) toCmdLine(cpus, memory string) []string { + endpoint := vf.Endpoint + if strings.HasPrefix(endpoint, "http") { + endpoint = strings.Replace(endpoint, "http", "tcp", 1) + } + cmd := []string{ + vf.PathToVfkitBinary, + "--cpus", cpus, + "--memory", memory, + "--bootloader", vf.Bootloader, + "--restful-uri", endpoint, + // this is on for development but can probably be disabled later, or + // we can leave it as optional. + //"--log-level", "debug", + } + for _, d := range vf.Devices { + cmd = append(cmd, "--device", d) + } + return cmd +} + +func (vf *VfkitHelper) startVfkit(m *MacMachine) error { + dnr, err := os.OpenFile(os.DevNull, os.O_RDONLY, 0755) + if err != nil { + return err + } + dnw, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0755) + if err != nil { + return err + } + + defer func() { + if err := dnr.Close(); err != nil { + logrus.Error(err) + } + }() + defer func() { + if err := dnw.Close(); err != nil { + logrus.Error(err) + } + }() + + attr := new(os.ProcAttr) + attr.Files = []*os.File{dnr, dnw} + cmdLine := vf.toCmdLine( + strconv.Itoa(int(m.ResourceConfig.CPUs)), + strconv.Itoa(int(m.ResourceConfig.Memory)), + ) + logrus.Debugf("vfkit cmd: %v", cmdLine) + //stderrBuf := &bytes.Buffer{} + cmd := &exec.Cmd{ + Args: cmdLine, + Path: vf.PathToVfkitBinary, + Stdin: dnr, + Stdout: dnw, + // this makes no sense to me ... this only works if I pipe stdout to stderr + Stderr: os.Stdout, + } + if err := cmd.Start(); err != nil { + return err + } + defer cmd.Process.Release() //nolint:errcheck + return nil +} diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 04ed792994..c6587b369f 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -66,11 +66,6 @@ type RemoteConnectionType string var ( SSHRemoteConnection RemoteConnectionType = "ssh" DefaultIgnitionUserName = "core" - ErrNoSuchVM = errors.New("VM does not exist") - ErrVMAlreadyExists = errors.New("VM already exists") - ErrVMAlreadyRunning = errors.New("VM already running or starting") - ErrMultipleActiveVM = errors.New("only one VM can be active at a time") - ErrNotImplemented = errors.New("functionality not implemented") ForwarderBinaryName = "gvproxy" ) diff --git a/pkg/machine/errors.go b/pkg/machine/errors.go new file mode 100644 index 0000000000..ca01f8325e --- /dev/null +++ b/pkg/machine/errors.go @@ -0,0 +1,12 @@ +package machine + +import "errors" + +var ( + ErrNoSuchVM = errors.New("VM does not exist") + ErrWrongState = errors.New("VM in wrong state to perform action") + ErrVMAlreadyExists = errors.New("VM already exists") + ErrVMAlreadyRunning = errors.New("VM already running or starting") + ErrMultipleActiveVM = errors.New("only one VM can be active at a time") + ErrNotImplemented = errors.New("functionality not implemented") +) diff --git a/pkg/machine/fcos.go b/pkg/machine/fcos.go index 8b9bae0f99..f945358a1a 100644 --- a/pkg/machine/fcos.go +++ b/pkg/machine/fcos.go @@ -40,11 +40,13 @@ const ( Qemu Artifact = iota HyperV + Metal None Qcow ImageFormat = iota Vhdx Tar + Raw ) // @@ -56,8 +58,11 @@ const ( // func (a Artifact) String() string { - if a == HyperV { + switch a { + case HyperV: return "hyperv" + case Metal: + return "metal" } return "qemu" } @@ -68,6 +73,8 @@ func (imf ImageFormat) String() string { return "vhdx.zip" case Tar: return "tar.xz" + case Raw: + return "raw.xz" } return "qcow2.xz" } diff --git a/pkg/machine/hyperv/machine.go b/pkg/machine/hyperv/machine.go index 687e854bf5..7eb737d5b3 100644 --- a/pkg/machine/hyperv/machine.go +++ b/pkg/machine/hyperv/machine.go @@ -341,7 +341,7 @@ func (m *HyperVMachine) Remove(_ string, opts machine.RemoveOptions) (string, fu } if !opts.SaveImage { - diskPath := m.ImagePath.GetPath() + diskPath = m.ImagePath.GetPath() files = append(files, diskPath) } diff --git a/vendor/github.com/crc-org/vfkit/LICENSE b/vendor/github.com/crc-org/vfkit/LICENSE new file mode 100644 index 0000000000..4782e63013 --- /dev/null +++ b/vendor/github.com/crc-org/vfkit/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021, 2022 Red Hat, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/crc-org/vfkit/pkg/rest/define/config.go b/vendor/github.com/crc-org/vfkit/pkg/rest/define/config.go new file mode 100644 index 0000000000..62f85cf1ed --- /dev/null +++ b/vendor/github.com/crc-org/vfkit/pkg/rest/define/config.go @@ -0,0 +1,24 @@ +package define + +// InspectResponse is used when responding to a request for +// information about the virtual machine +type InspectResponse struct { + CPUs uint `json:"cpus"` + Memory uint64 `json:"memory"` + // Devices []config.VirtioDevice `json:"devices"` +} + +// VMState can be used to describe the current state of a VM +// as well as used to request a state change +type VMState struct { + State string `json:"state"` +} + +type StateChange string + +const ( + Resume StateChange = "Resume" + Pause StateChange = "Pause" + Stop StateChange = "Stop" + HardStop StateChange = "HardStop" +) diff --git a/vendor/modules.txt b/vendor/modules.txt index db951e44bb..75fc8a1693 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -356,6 +356,9 @@ github.com/coreos/stream-metadata-go/release github.com/coreos/stream-metadata-go/release/rhcos github.com/coreos/stream-metadata-go/stream github.com/coreos/stream-metadata-go/stream/rhcos +# github.com/crc-org/vfkit v0.0.5-0.20230427143911-8117c28876bc +## explicit; go 1.17 +github.com/crc-org/vfkit/pkg/rest/define # github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 ## explicit github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer