diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/api/handlers/decoder.go | 10 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/secrets.go | 8 | ||||
-rw-r--r-- | pkg/domain/infra/abi/containers.go | 6 | ||||
-rw-r--r-- | pkg/domain/infra/abi/parse/parse.go | 5 | ||||
-rw-r--r-- | pkg/machine/config.go | 141 | ||||
-rw-r--r-- | pkg/machine/e2e/config.go | 16 | ||||
-rw-r--r-- | pkg/machine/e2e/config_set.go | 43 | ||||
-rw-r--r-- | pkg/machine/e2e/init_test.go | 6 | ||||
-rw-r--r-- | pkg/machine/e2e/inspect_test.go | 9 | ||||
-rw-r--r-- | pkg/machine/e2e/list_test.go | 27 | ||||
-rw-r--r-- | pkg/machine/e2e/rm_test.go | 11 | ||||
-rw-r--r-- | pkg/machine/e2e/set_test.go | 134 | ||||
-rw-r--r-- | pkg/machine/e2e/ssh_test.go | 11 | ||||
-rw-r--r-- | pkg/machine/e2e/start_test.go | 5 | ||||
-rw-r--r-- | pkg/machine/e2e/stop_test.go | 9 | ||||
-rw-r--r-- | pkg/machine/qemu/config.go | 142 | ||||
-rw-r--r-- | pkg/machine/qemu/config_test.go | 34 | ||||
-rw-r--r-- | pkg/machine/qemu/machine.go | 221 | ||||
-rw-r--r-- | pkg/machine/qemu/machine_test.go | 17 | ||||
-rw-r--r-- | pkg/machine/wsl/machine.go | 258 |
20 files changed, 769 insertions, 344 deletions
diff --git a/pkg/api/handlers/decoder.go b/pkg/api/handlers/decoder.go index 5e8f12960..fbe03d97b 100644 --- a/pkg/api/handlers/decoder.go +++ b/pkg/api/handlers/decoder.go @@ -21,6 +21,7 @@ func NewAPIDecoder() *schema.Decoder { d.RegisterConverter(map[string][]string{}, convertURLValuesString) d.RegisterConverter(time.Time{}, convertTimeString) d.RegisterConverter(define.ContainerStatus(0), convertContainerStatusString) + d.RegisterConverter(map[string]string{}, convertStringMap) var Signal syscall.Signal d.RegisterConverter(Signal, convertSignal) @@ -48,6 +49,15 @@ func convertURLValuesString(query string) reflect.Value { return reflect.ValueOf(f) } +func convertStringMap(query string) reflect.Value { + res := make(map[string]string) + err := json.Unmarshal([]byte(query), &res) + if err != nil { + logrus.Infof("convertStringMap: Failed to Unmarshal %s: %s", query, err.Error()) + } + return reflect.ValueOf(res) +} + func convertContainerStatusString(query string) reflect.Value { result, err := define.StringToContainerStatus(query) if err != nil { diff --git a/pkg/api/handlers/libpod/secrets.go b/pkg/api/handlers/libpod/secrets.go index 8708e630c..3ea2c2ea8 100644 --- a/pkg/api/handlers/libpod/secrets.go +++ b/pkg/api/handlers/libpod/secrets.go @@ -1,9 +1,7 @@ package libpod import ( - "encoding/json" "net/http" - "reflect" "github.com/containers/podman/v4/libpod" "github.com/containers/podman/v4/pkg/api/handlers/utils" @@ -20,12 +18,6 @@ func CreateSecret(w http.ResponseWriter, r *http.Request) { decoder = r.Context().Value(api.DecoderKey).(*schema.Decoder) ) - decoder.RegisterConverter(map[string]string{}, func(str string) reflect.Value { - res := make(map[string]string) - json.Unmarshal([]byte(str), &res) - return reflect.ValueOf(res) - }) - query := struct { Name string `schema:"name"` Driver string `schema:"driver"` diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index b56c36015..89b09bb1d 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -641,7 +641,11 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st } restoreOptions.CheckpointImageID = img.ID() mountPoint, err := img.Mount(ctx, nil, "") - defer img.Unmount(true) + defer func() { + if err := img.Unmount(true); err != nil { + logrus.Errorf("Failed to unmount image: %v", err) + } + }() if err != nil { return nil, err } diff --git a/pkg/domain/infra/abi/parse/parse.go b/pkg/domain/infra/abi/parse/parse.go index 2d1adab74..3bac2ef99 100644 --- a/pkg/domain/infra/abi/parse/parse.go +++ b/pkg/domain/infra/abi/parse/parse.go @@ -73,6 +73,11 @@ func VolumeOptions(opts map[string]string) ([]libpod.VolumeCreateOption, error) finalVal = append(finalVal, o) // set option "GID": "$gid" volumeOptions["GID"] = splitO[1] + case "noquota": + logrus.Debugf("Removing noquota from options and adding WithVolumeDisableQuota") + libpodOptions = append(libpodOptions, libpod.WithVolumeDisableQuota()) + // set option "NOQUOTA": "true" + volumeOptions["NOQUOTA"] = "true" default: finalVal = append(finalVal, o) } diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 5bbaf8c51..9a0ce757a 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -4,6 +4,8 @@ package machine import ( + errors2 "errors" + "io/ioutil" "net" "net/url" "os" @@ -12,6 +14,7 @@ import ( "github.com/containers/storage/pkg/homedir" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) type InitOptions struct { @@ -68,7 +71,7 @@ type Download struct { Artifact string CompressionType string Format string - ImageName string `json:"image_name"` + ImageName string LocalPath string LocalUncompressedFile string Sha256sum string @@ -95,7 +98,10 @@ type ListResponse struct { } type SetOptions struct { - Rootful bool + CPUs *uint64 + DiskSize *uint64 + Memory *uint64 + Rootful *bool } type SSHOptions struct { @@ -117,8 +123,9 @@ type InspectOptions struct{} type VM interface { Init(opts InitOptions) (bool, error) + Inspect() (*InspectInfo, error) Remove(name string, opts RemoveOptions) (string, func() error, error) - Set(name string, opts SetOptions) error + Set(name string, opts SetOptions) ([]error, error) SSH(name string, opts SSHOptions) error Start(name string, opts StartOptions) error State(bypass bool) (Status, error) @@ -130,8 +137,14 @@ type DistributionDownload interface { Get() *Download } type InspectInfo struct { - State Status - VM + ConfigPath VMFile + Created time.Time + Image ImageConfig + LastUp time.Time + Name string + Resources ResourceConfig + SSHConfig SSHConfig + State Status } func (rc RemoteConnectionType) MakeSSHURL(host, path, port, userName string) url.URL { @@ -183,3 +196,121 @@ func GetConfDir(vmType string) (string, error) { mkdirErr := os.MkdirAll(confDir, 0755) return confDir, mkdirErr } + +// ResourceConfig describes physical attributes of the machine +type ResourceConfig struct { + // CPUs to be assigned to the VM + CPUs uint64 + // Disk size in gigabytes assigned to the vm + DiskSize uint64 + // Memory in megabytes assigned to the vm + Memory uint64 +} + +const maxSocketPathLength int = 103 + +type VMFile struct { + // Path is the fully qualified path to a file + Path string + // Symlink is a shortened version of Path by using + // a symlink + Symlink *string `json:"symlink,omitempty"` +} + +// GetPath returns the working path for a machinefile. it returns +// the symlink unless one does not exist +func (m *VMFile) GetPath() string { + if m.Symlink == nil { + return m.Path + } + return *m.Symlink +} + +// Delete removes the machinefile symlink (if it exists) and +// the actual path +func (m *VMFile) Delete() error { + if m.Symlink != nil { + if err := os.Remove(*m.Symlink); err != nil && !errors2.Is(err, os.ErrNotExist) { + logrus.Errorf("unable to remove symlink %q", *m.Symlink) + } + } + if err := os.Remove(m.Path); err != nil && !errors2.Is(err, os.ErrNotExist) { + return err + } + return nil +} + +// Read the contents of a given file and return in []bytes +func (m *VMFile) Read() ([]byte, error) { + return ioutil.ReadFile(m.GetPath()) +} + +// NewMachineFile is a constructor for VMFile +func NewMachineFile(path string, symlink *string) (*VMFile, error) { + if len(path) < 1 { + return nil, errors2.New("invalid machine file path") + } + if symlink != nil && len(*symlink) < 1 { + return nil, errors2.New("invalid symlink path") + } + mf := VMFile{Path: path} + if symlink != nil && len(path) > maxSocketPathLength { + if err := mf.makeSymlink(symlink); err != nil && !errors2.Is(err, os.ErrExist) { + return nil, err + } + } + return &mf, nil +} + +// makeSymlink for macOS creates a symlink in $HOME/.podman/ +// for a machinefile like a socket +func (m *VMFile) makeSymlink(symlink *string) error { + homedir, err := os.UserHomeDir() + if err != nil { + return err + } + sl := filepath.Join(homedir, ".podman", *symlink) + // make the symlink dir and throw away if it already exists + if err := os.MkdirAll(filepath.Dir(sl), 0700); err != nil && !errors2.Is(err, os.ErrNotExist) { + return err + } + m.Symlink = &sl + return os.Symlink(m.Path, sl) +} + +type Mount struct { + ReadOnly bool + Source string + Tag string + Target string + Type string +} + +// ImageConfig describes the bootable image for the VM +type ImageConfig struct { + // IgnitionFile is the path to the filesystem where the + // ignition file was written (if needs one) + IgnitionFile VMFile `json:"IgnitionFilePath"` + // ImageStream is the update stream for the image + ImageStream string + // ImageFile is the fq path to + ImagePath VMFile `json:"ImagePath"` +} + +// HostUser describes the host user +type HostUser struct { + // Whether this machine should run in a rootful or rootless manner + Rootful bool + // UID is the numerical id of the user that called machine + UID int +} + +// SSHConfig contains remote access information for SSH +type SSHConfig struct { + // IdentityPath is the fq path to the ssh priv key + IdentityPath string + // SSH port for user networking + Port int + // RemoteUsername of the vm user + RemoteUsername string +} diff --git a/pkg/machine/e2e/config.go b/pkg/machine/e2e/config.go index 7d75ca6bc..c17b840d3 100644 --- a/pkg/machine/e2e/config.go +++ b/pkg/machine/e2e/config.go @@ -131,7 +131,7 @@ func (m *machineTestBuilder) setTimeout(timeout time.Duration) *machineTestBuild func (mb *machineTestBuilder) toQemuInspectInfo() ([]qemuMachineInspectInfo, int, error) { args := []string{"machine", "inspect"} args = append(args, mb.names...) - session, err := runWrapper(mb.podmanBinary, args, defaultTimeout) + session, err := runWrapper(mb.podmanBinary, args, defaultTimeout, true) if err != nil { return nil, -1, err } @@ -140,11 +140,15 @@ func (mb *machineTestBuilder) toQemuInspectInfo() ([]qemuMachineInspectInfo, int return mii, session.ExitCode(), err } +func (m *machineTestBuilder) runWithoutWait() (*machineSession, error) { + return runWrapper(m.podmanBinary, m.cmd, m.timeout, false) +} + func (m *machineTestBuilder) run() (*machineSession, error) { - return runWrapper(m.podmanBinary, m.cmd, m.timeout) + return runWrapper(m.podmanBinary, m.cmd, m.timeout, true) } -func runWrapper(podmanBinary string, cmdArgs []string, timeout time.Duration) (*machineSession, error) { +func runWrapper(podmanBinary string, cmdArgs []string, timeout time.Duration, wait bool) (*machineSession, error) { if len(os.Getenv("DEBUG")) > 0 { cmdArgs = append([]string{"--log-level=debug"}, cmdArgs...) } @@ -156,8 +160,10 @@ func runWrapper(podmanBinary string, cmdArgs []string, timeout time.Duration) (* return nil, err } ms := machineSession{session} - ms.waitWithTimeout(timeout) - fmt.Println("output:", ms.outputToString()) + if wait { + ms.waitWithTimeout(timeout) + fmt.Println("output:", ms.outputToString()) + } return &ms, nil } diff --git a/pkg/machine/e2e/config_set.go b/pkg/machine/e2e/config_set.go new file mode 100644 index 000000000..b310ab1b9 --- /dev/null +++ b/pkg/machine/e2e/config_set.go @@ -0,0 +1,43 @@ +package e2e + +import ( + "strconv" +) + +type setMachine struct { + cpus *uint + diskSize *uint + memory *uint + + cmd []string +} + +func (i *setMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "set"} + if i.cpus != nil { + cmd = append(cmd, "--cpus", strconv.Itoa(int(*i.cpus))) + } + if i.diskSize != nil { + cmd = append(cmd, "--disk-size", strconv.Itoa(int(*i.diskSize))) + } + if i.memory != nil { + cmd = append(cmd, "--memory", strconv.Itoa(int(*i.memory))) + } + cmd = append(cmd, m.name) + i.cmd = cmd + return cmd +} + +func (i *setMachine) withCPUs(num uint) *setMachine { + i.cpus = &num + return i +} +func (i *setMachine) withDiskSize(size uint) *setMachine { + i.diskSize = &size + return i +} + +func (i *setMachine) withMemory(num uint) *setMachine { + i.memory = &num + return i +} diff --git a/pkg/machine/e2e/init_test.go b/pkg/machine/e2e/init_test.go index 309d460a9..304122738 100644 --- a/pkg/machine/e2e/init_test.go +++ b/pkg/machine/e2e/init_test.go @@ -28,13 +28,13 @@ var _ = Describe("podman machine init", func() { reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" session, err := mb.setName(reallyLongName).setCmd(&i).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(125)) + Expect(session).To(Exit(125)) }) It("simple init", func() { i := new(initMachine) session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).To(Exit(0)) inspectBefore, ec, err := mb.toQemuInspectInfo() Expect(err).To(BeNil()) @@ -52,7 +52,7 @@ var _ = Describe("podman machine init", func() { i := initMachine{} session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).To(Exit(0)) inspectBefore, ec, err := mb.toQemuInspectInfo() Expect(ec).To(BeZero()) diff --git a/pkg/machine/e2e/inspect_test.go b/pkg/machine/e2e/inspect_test.go index 30d810b8f..e282dd21d 100644 --- a/pkg/machine/e2e/inspect_test.go +++ b/pkg/machine/e2e/inspect_test.go @@ -7,6 +7,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" ) var _ = Describe("podman machine stop", func() { @@ -27,24 +28,24 @@ var _ = Describe("podman machine stop", func() { reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" session, err := mb.setName(reallyLongName).setCmd(&i).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(125)) + Expect(session).To(Exit(125)) }) It("inspect two machines", func() { i := new(initMachine) foo1, err := mb.setName("foo1").setCmd(i.withImagePath(mb.imagePath)).run() Expect(err).To(BeNil()) - Expect(foo1.ExitCode()).To(Equal(0)) + Expect(foo1).To(Exit(0)) ii := new(initMachine) foo2, err := mb.setName("foo2").setCmd(ii.withImagePath(mb.imagePath)).run() Expect(err).To(BeNil()) - Expect(foo2.ExitCode()).To(Equal(0)) + Expect(foo2).To(Exit(0)) inspect := new(inspectMachine) inspectSession, err := mb.setName("foo1").setCmd(inspect).run() Expect(err).To(BeNil()) - Expect(inspectSession.ExitCode()).To(Equal(0)) + Expect(inspectSession).To(Exit(0)) type fakeInfos struct { Status string diff --git a/pkg/machine/e2e/list_test.go b/pkg/machine/e2e/list_test.go index e7a439945..0ce9063f9 100644 --- a/pkg/machine/e2e/list_test.go +++ b/pkg/machine/e2e/list_test.go @@ -70,6 +70,33 @@ var _ = Describe("podman machine list", func() { Expect(util.StringInSlice(name1, listNames)).To(BeTrue()) Expect(util.StringInSlice(name2, listNames)).To(BeTrue()) }) + + It("list machine: check if running while starting", func() { + i := new(initMachine) + session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session).To(Exit(0)) + s := new(startMachine) + startSession, err := mb.setCmd(s).runWithoutWait() + Expect(err).To(BeNil()) + l := new(listMachine) + for { // needs to be infinite because we need to check if running when inspect returns to avoid race conditions. + listSession, err := mb.setCmd(l).run() + Expect(listSession).To(Exit(0)) + Expect(err).To(BeNil()) + if startSession.ExitCode() == -1 { + Expect(listSession.outputToString()).NotTo(ContainSubstring("Currently running")) + } else { + break + } + } + Expect(startSession).To(Exit(0)) + listSession, err := mb.setCmd(l).run() + Expect(listSession).To(Exit(0)) + Expect(err).To(BeNil()) + Expect(listSession.outputToString()).To(ContainSubstring("Currently running")) + Expect(listSession.outputToString()).NotTo(ContainSubstring("Less than a second ago")) // check to make sure time created is accurate + }) }) func stripAsterisk(sl []string) { diff --git a/pkg/machine/e2e/rm_test.go b/pkg/machine/e2e/rm_test.go index 011da5dde..43b8c594c 100644 --- a/pkg/machine/e2e/rm_test.go +++ b/pkg/machine/e2e/rm_test.go @@ -3,6 +3,7 @@ package e2e import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" ) var _ = Describe("podman machine rm", func() { @@ -23,14 +24,14 @@ var _ = Describe("podman machine rm", func() { reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" session, err := mb.setName(reallyLongName).setCmd(&i).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(125)) + Expect(session).To(Exit(125)) }) It("Remove machine", func() { i := new(initMachine) session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).To(Exit(0)) rm := rmMachine{} _, err = mb.setCmd(rm.withForce()).run() Expect(err).To(BeNil()) @@ -46,18 +47,18 @@ var _ = Describe("podman machine rm", func() { i := new(initMachine) session, err := mb.setCmd(i.withImagePath(mb.imagePath).withNow()).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).To(Exit(0)) rm := new(rmMachine) // Removing a running machine should fail stop, err := mb.setCmd(rm).run() Expect(err).To(BeNil()) - Expect(stop.ExitCode()).To(Equal(125)) + Expect(stop).To(Exit(125)) // Removing again with force stopAgain, err := mb.setCmd(rm.withForce()).run() Expect(err).To(BeNil()) - Expect(stopAgain.ExitCode()).To(BeZero()) + Expect(stopAgain).To(Exit(0)) // Inspect to be dead sure _, ec, err := mb.toQemuInspectInfo() diff --git a/pkg/machine/e2e/set_test.go b/pkg/machine/e2e/set_test.go new file mode 100644 index 000000000..9af29c560 --- /dev/null +++ b/pkg/machine/e2e/set_test.go @@ -0,0 +1,134 @@ +package e2e + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("podman machine set", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("set machine cpus", func() { + name := randomString(12) + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session).To(Exit(0)) + + set := setMachine{} + setSession, err := mb.setName(name).setCmd(set.withCPUs(2)).run() + Expect(err).To(BeNil()) + Expect(setSession).To(Exit(0)) + + s := new(startMachine) + startSession, err := mb.setCmd(s).run() + Expect(err).To(BeNil()) + Expect(startSession).To(Exit(0)) + + ssh2 := sshMachine{} + sshSession2, err := mb.setName(name).setCmd(ssh2.withSSHComand([]string{"lscpu", "|", "grep", "\"CPU(s):\"", "|", "head", "-1"})).run() + Expect(err).To(BeNil()) + Expect(sshSession2).To(Exit(0)) + Expect(sshSession2.outputToString()).To(ContainSubstring("2")) + + // Setting a running machine results in 125 + runner, err := mb.setName(name).setCmd(set.withCPUs(4)).run() + Expect(err).To(BeNil()) + Expect(runner).To(Exit(125)) + }) + + It("increase machine disk size", func() { + name := randomString(12) + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session).To(Exit(0)) + + set := setMachine{} + setSession, err := mb.setName(name).setCmd(set.withDiskSize(102)).run() + Expect(err).To(BeNil()) + Expect(setSession).To(Exit(0)) + + // shrinking disk size iss verboten + shrink, err := mb.setName(name).setCmd(set.withDiskSize(5)).run() + Expect(err).To(BeNil()) + Expect(shrink).To(Exit(125)) + + s := new(startMachine) + startSession, err := mb.setCmd(s).run() + Expect(err).To(BeNil()) + Expect(startSession).To(Exit(0)) + + ssh2 := sshMachine{} + sshSession2, err := mb.setName(name).setCmd(ssh2.withSSHComand([]string{"sudo", "fdisk", "-l", "|", "grep", "Disk"})).run() + Expect(err).To(BeNil()) + Expect(sshSession2).To(Exit(0)) + Expect(sshSession2.outputToString()).To(ContainSubstring("102 GiB")) + }) + + It("set machine ram", func() { + name := randomString(12) + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session).To(Exit(0)) + + set := setMachine{} + setSession, err := mb.setName(name).setCmd(set.withMemory(4000)).run() + Expect(err).To(BeNil()) + Expect(setSession).To(Exit(0)) + + s := new(startMachine) + startSession, err := mb.setCmd(s).run() + Expect(err).To(BeNil()) + Expect(startSession).To(Exit(0)) + + ssh2 := sshMachine{} + sshSession2, err := mb.setName(name).setCmd(ssh2.withSSHComand([]string{"cat", "/proc/meminfo", "|", "numfmt", "--field", "2", "--from-unit=Ki", "--to-unit=Mi", "|", "sed", "'s/ kB/M/g'", "|", "grep", "MemTotal"})).run() + Expect(err).To(BeNil()) + Expect(sshSession2).To(Exit(0)) + Expect(sshSession2.outputToString()).To(ContainSubstring("3824")) + }) + + It("no settings should change if no flags", func() { + name := randomString(12) + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session).To(Exit(0)) + + set := setMachine{} + setSession, err := mb.setName(name).setCmd(&set).run() + Expect(err).To(BeNil()) + Expect(setSession).To(Exit(0)) + + s := new(startMachine) + startSession, err := mb.setCmd(s).run() + Expect(err).To(BeNil()) + Expect(startSession).To(Exit(0)) + + ssh2 := sshMachine{} + sshSession2, err := mb.setName(name).setCmd(ssh2.withSSHComand([]string{"lscpu", "|", "grep", "\"CPU(s):\"", "|", "head", "-1"})).run() + Expect(err).To(BeNil()) + Expect(sshSession2).To(Exit(0)) + Expect(sshSession2.outputToString()).To(ContainSubstring("1")) + + ssh3 := sshMachine{} + sshSession3, err := mb.setName(name).setCmd(ssh3.withSSHComand([]string{"sudo", "fdisk", "-l", "|", "grep", "Disk"})).run() + Expect(err).To(BeNil()) + Expect(sshSession3).To(Exit(0)) + Expect(sshSession3.outputToString()).To(ContainSubstring("100 GiB")) + }) + +}) diff --git a/pkg/machine/e2e/ssh_test.go b/pkg/machine/e2e/ssh_test.go index 90296fa10..155d39a64 100644 --- a/pkg/machine/e2e/ssh_test.go +++ b/pkg/machine/e2e/ssh_test.go @@ -3,6 +3,7 @@ package e2e import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" ) var _ = Describe("podman machine ssh", func() { @@ -23,7 +24,7 @@ var _ = Describe("podman machine ssh", func() { ssh := sshMachine{} session, err := mb.setName(name).setCmd(ssh).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(125)) + Expect(session).To(Exit(125)) // TODO seems like stderr is not being returned; re-enabled when fixed //Expect(session.outputToString()).To(ContainSubstring("not exist")) }) @@ -33,14 +34,14 @@ var _ = Describe("podman machine ssh", func() { i := new(initMachine) session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath)).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).To(Exit(0)) ssh := sshMachine{} sshSession, err := mb.setName(name).setCmd(ssh).run() Expect(err).To(BeNil()) // TODO seems like stderr is not being returned; re-enabled when fixed //Expect(sshSession.outputToString()).To(ContainSubstring("is not running")) - Expect(sshSession.ExitCode()).To(Equal(125)) + Expect(sshSession).To(Exit(125)) }) It("ssh to running machine and check os-type", func() { @@ -48,12 +49,12 @@ var _ = Describe("podman machine ssh", func() { i := new(initMachine) session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath).withNow()).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).To(Exit(0)) ssh := sshMachine{} sshSession, err := mb.setName(name).setCmd(ssh.withSSHComand([]string{"cat", "/etc/os-release"})).run() Expect(err).To(BeNil()) - Expect(sshSession.ExitCode()).To(Equal(0)) + Expect(sshSession).To(Exit(0)) Expect(sshSession.outputToString()).To(ContainSubstring("Fedora CoreOS")) }) }) diff --git a/pkg/machine/e2e/start_test.go b/pkg/machine/e2e/start_test.go index 1cda0e8f1..1de66eb9a 100644 --- a/pkg/machine/e2e/start_test.go +++ b/pkg/machine/e2e/start_test.go @@ -4,6 +4,7 @@ import ( "github.com/containers/podman/v4/pkg/machine" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" ) var _ = Describe("podman machine start", func() { @@ -22,11 +23,11 @@ var _ = Describe("podman machine start", func() { i := new(initMachine) session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).To(Exit(0)) s := new(startMachine) startSession, err := mb.setCmd(s).run() Expect(err).To(BeNil()) - Expect(startSession.ExitCode()).To(Equal(0)) + Expect(startSession).To(Exit(0)) info, ec, err := mb.toQemuInspectInfo() Expect(err).To(BeNil()) diff --git a/pkg/machine/e2e/stop_test.go b/pkg/machine/e2e/stop_test.go index 5dee6a345..0c27045a6 100644 --- a/pkg/machine/e2e/stop_test.go +++ b/pkg/machine/e2e/stop_test.go @@ -3,6 +3,7 @@ package e2e import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" ) var _ = Describe("podman machine stop", func() { @@ -23,24 +24,24 @@ var _ = Describe("podman machine stop", func() { reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" session, err := mb.setName(reallyLongName).setCmd(&i).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(125)) + Expect(session).To(Exit(125)) }) It("Stop running machine", func() { i := new(initMachine) session, err := mb.setCmd(i.withImagePath(mb.imagePath).withNow()).run() Expect(err).To(BeNil()) - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).To(Exit(0)) stop := new(stopMachine) // Removing a running machine should fail stopSession, err := mb.setCmd(stop).run() Expect(err).To(BeNil()) - Expect(stopSession.ExitCode()).To(Equal(0)) + Expect(stopSession).To(Exit(0)) // Stopping it again should not result in an error stopAgain, err := mb.setCmd(stop).run() Expect(err).To(BeNil()) - Expect(stopAgain.ExitCode()).To(BeZero()) + Expect(stopAgain).To(Exit((0))) }) }) diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go index 9473eef6f..56c95e3b3 100644 --- a/pkg/machine/qemu/config.go +++ b/pkg/machine/qemu/config.go @@ -4,13 +4,9 @@ package qemu import ( - "errors" - "io/ioutil" - "os" - "path/filepath" "time" - "github.com/sirupsen/logrus" + "github.com/containers/podman/v4/pkg/machine" ) const ( @@ -23,7 +19,7 @@ const ( Stable string = "stable" // Max length of fully qualified socket path - maxSocketPathLength int = 103 + ) type Provider struct{} @@ -36,7 +32,7 @@ type MachineVMV1 struct { // The command line representation of the qemu command CmdLine []string // Mounts is the list of remote filesystems to mount - Mounts []Mount + Mounts []machine.Mount // IdentityPath is the fq path to the ssh priv key IdentityPath string // IgnitionFilePath is the fq path to the .ign file @@ -65,27 +61,27 @@ type MachineVMV1 struct { type MachineVM struct { // ConfigPath is the path to the configuration file - ConfigPath MachineFile + ConfigPath machine.VMFile // The command line representation of the qemu command CmdLine []string // HostUser contains info about host user - HostUser + machine.HostUser // ImageConfig describes the bootable image - ImageConfig + machine.ImageConfig // Mounts is the list of remote filesystems to mount - Mounts []Mount + Mounts []machine.Mount // Name of VM Name string // PidFilePath is the where the PID file lives - PidFilePath MachineFile + PidFilePath machine.VMFile // QMPMonitor is the qemu monitor object for sending commands QMPMonitor Monitor // ReadySocket tells host when vm is booted - ReadySocket MachineFile + ReadySocket machine.VMFile // ResourceConfig is physical attrs of the VM - ResourceConfig + machine.ResourceConfig // SSHConfig for accessing the remote vm - SSHConfig + 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 @@ -94,59 +90,6 @@ type MachineVM struct { LastUp time.Time } -// ImageConfig describes the bootable image for the VM -type ImageConfig struct { - IgnitionFilePath MachineFile - // ImageStream is the update stream for the image - ImageStream string - // ImagePath is the fq path to - ImagePath MachineFile -} - -// HostUser describes the host user -type HostUser struct { - // Whether this machine should run in a rootful or rootless manner - Rootful bool - // UID is the numerical id of the user that called machine - UID int -} - -// SSHConfig contains remote access information for SSH -type SSHConfig struct { - // IdentityPath is the fq path to the ssh priv key - IdentityPath string - // SSH port for user networking - Port int - // RemoteUsername of the vm user - RemoteUsername string -} - -// ResourceConfig describes physical attributes of the machine -type ResourceConfig struct { - // CPUs to be assigned to the VM - CPUs uint64 - // Memory in megabytes assigned to the vm - Memory uint64 - // Disk size in gigabytes assigned to the vm - DiskSize uint64 -} - -type MachineFile struct { - // Path is the fully qualified path to a file - Path string - // Symlink is a shortened version of Path by using - // a symlink - Symlink *string -} - -type Mount struct { - Type string - Tag string - Source string - Target string - ReadOnly bool -} - type Monitorv1 struct { // Address portion of the qmp monitor (/tmp/tmp.sock) Address string @@ -158,7 +101,7 @@ type Monitorv1 struct { type Monitor struct { // Address portion of the qmp monitor (/tmp/tmp.sock) - Address MachineFile + Address machine.VMFile // Network portion of the qmp monitor (unix) Network string // Timeout in seconds for qmp monitor transactions @@ -170,64 +113,3 @@ var ( // qmp monitor interactions. defaultQMPTimeout = 2 * time.Second ) - -// GetPath returns the working path for a machinefile. it returns -// the symlink unless one does not exist -func (m *MachineFile) GetPath() string { - if m.Symlink == nil { - return m.Path - } - return *m.Symlink -} - -// Delete removes the machinefile symlink (if it exists) and -// the actual path -func (m *MachineFile) Delete() error { - if m.Symlink != nil { - if err := os.Remove(*m.Symlink); err != nil && !errors.Is(err, os.ErrNotExist) { - logrus.Errorf("unable to remove symlink %q", *m.Symlink) - } - } - if err := os.Remove(m.Path); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - return nil -} - -// Read the contents of a given file and return in []bytes -func (m *MachineFile) Read() ([]byte, error) { - return ioutil.ReadFile(m.GetPath()) -} - -// NewMachineFile is a constructor for MachineFile -func NewMachineFile(path string, symlink *string) (*MachineFile, error) { - if len(path) < 1 { - return nil, errors.New("invalid machine file path") - } - if symlink != nil && len(*symlink) < 1 { - return nil, errors.New("invalid symlink path") - } - mf := MachineFile{Path: path} - if symlink != nil && len(path) > maxSocketPathLength { - if err := mf.makeSymlink(symlink); err != nil && !errors.Is(err, os.ErrExist) { - return nil, err - } - } - return &mf, nil -} - -// makeSymlink for macOS creates a symlink in $HOME/.podman/ -// for a machinefile like a socket -func (m *MachineFile) makeSymlink(symlink *string) error { - homedir, err := os.UserHomeDir() - if err != nil { - return err - } - sl := filepath.Join(homedir, ".podman", *symlink) - // make the symlink dir and throw away if it already exists - if err := os.MkdirAll(filepath.Dir(sl), 0700); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - m.Symlink = &sl - return os.Symlink(m.Path, sl) -} diff --git a/pkg/machine/qemu/config_test.go b/pkg/machine/qemu/config_test.go index 264de9ae8..3f92881fa 100644 --- a/pkg/machine/qemu/config_test.go +++ b/pkg/machine/qemu/config_test.go @@ -6,6 +6,7 @@ import ( "reflect" "testing" + "github.com/containers/podman/v4/pkg/machine" "github.com/containers/podman/v4/test/utils" ) @@ -37,7 +38,7 @@ func TestMachineFile_GetPath(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - m := &MachineFile{ + m := &machine.VMFile{ Path: tt.fields.Path, //nolint: scopelint Symlink: tt.fields.Symlink, //nolint: scopelint } @@ -67,13 +68,19 @@ func TestNewMachineFile(t *testing.T) { p := "/var/tmp/podman/my.sock" longp := filepath.Join(longTemp, utils.RandomString(100), "my.sock") - os.MkdirAll(filepath.Dir(longp), 0755) - f, _ := os.Create(longp) - f.Close() + err = os.MkdirAll(filepath.Dir(longp), 0755) + if err != nil { + panic(err) + } + f, err := os.Create(longp) + if err != nil { + panic(err) + } + _ = f.Close() sym := "my.sock" longSym := filepath.Join(homedir, ".podman", sym) - m := MachineFile{ + m := machine.VMFile{ Path: p, Symlink: nil, } @@ -84,7 +91,7 @@ func TestNewMachineFile(t *testing.T) { tests := []struct { name string args args - want *MachineFile + want *machine.VMFile wantErr bool }{ { @@ -96,7 +103,7 @@ func TestNewMachineFile(t *testing.T) { { name: "Good with short symlink", args: args{p, &sym}, - want: &MachineFile{p, nil}, + want: &machine.VMFile{Path: p}, wantErr: false, }, { @@ -114,19 +121,20 @@ func TestNewMachineFile(t *testing.T) { { name: "Good with long symlink", args: args{longp, &sym}, - want: &MachineFile{longp, &longSym}, + want: &machine.VMFile{Path: longp, Symlink: &longSym}, wantErr: false, }, } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { - got, err := NewMachineFile(tt.args.path, tt.args.symlink) //nolint: scopelint - if (err != nil) != tt.wantErr { //nolint: scopelint - t.Errorf("NewMachineFile() error = %v, wantErr %v", err, tt.wantErr) //nolint: scopelint + got, err := machine.NewMachineFile(tt.args.path, tt.args.symlink) + if (err != nil) != tt.wantErr { + t.Errorf("NewMachineFile() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { //nolint: scopelint - t.Errorf("NewMachineFile() got = %v, want %v", got, tt.want) //nolint: scopelint + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewMachineFile() got = %v, want %v", got, tt.want) } }) } diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index 5481bad29..91e15c2af 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -71,12 +71,12 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { if len(opts.Name) > 0 { vm.Name = opts.Name } - ignitionFile, err := NewMachineFile(filepath.Join(vmConfigDir, vm.Name+".ign"), nil) + ignitionFile, err := machine.NewMachineFile(filepath.Join(vmConfigDir, vm.Name+".ign"), nil) if err != nil { return nil, err } - vm.IgnitionFilePath = *ignitionFile - imagePath, err := NewMachineFile(opts.ImagePath, nil) + vm.IgnitionFile = *ignitionFile + imagePath, err := machine.NewMachineFile(opts.ImagePath, nil) if err != nil { return nil, err } @@ -105,14 +105,13 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { if err != nil { return nil, err } - cmd := []string{execPath} // Add memory cmd = append(cmd, []string{"-m", strconv.Itoa(int(vm.Memory))}...) // Add cpus cmd = append(cmd, []string{"-smp", strconv.Itoa(int(vm.CPUs))}...) // Add ignition file - cmd = append(cmd, []string{"-fw_cfg", "name=opt/com.coreos/config,file=" + vm.IgnitionFilePath.GetPath()}...) + cmd = append(cmd, []string{"-fw_cfg", "name=opt/com.coreos/config,file=" + vm.IgnitionFile.GetPath()}...) // Add qmp socket monitor, err := NewQMPMonitor("unix", vm.Name, defaultQMPTimeout) if err != nil { @@ -158,9 +157,9 @@ func migrateVM(configPath string, config []byte, vm *MachineVM) error { return err } - pidFilePath := MachineFile{Path: pidFile} + pidFilePath := machine.VMFile{Path: pidFile} qmpMonitor := Monitor{ - Address: MachineFile{Path: old.QMPMonitor.Address}, + Address: machine.VMFile{Path: old.QMPMonitor.Address}, Network: old.QMPMonitor.Network, Timeout: old.QMPMonitor.Timeout, } @@ -169,18 +168,18 @@ func migrateVM(configPath string, config []byte, vm *MachineVM) error { return err } virtualSocketPath := filepath.Join(socketPath, "podman", vm.Name+"_ready.sock") - readySocket := MachineFile{Path: virtualSocketPath} + readySocket := machine.VMFile{Path: virtualSocketPath} - vm.HostUser = HostUser{} - vm.ImageConfig = ImageConfig{} - vm.ResourceConfig = ResourceConfig{} - vm.SSHConfig = SSHConfig{} + vm.HostUser = machine.HostUser{} + vm.ImageConfig = machine.ImageConfig{} + vm.ResourceConfig = machine.ResourceConfig{} + vm.SSHConfig = machine.SSHConfig{} - ignitionFilePath, err := NewMachineFile(old.IgnitionFilePath, nil) + ignitionFilePath, err := machine.NewMachineFile(old.IgnitionFilePath, nil) if err != nil { return err } - imagePath, err := NewMachineFile(old.ImagePath, nil) + imagePath, err := machine.NewMachineFile(old.ImagePath, nil) if err != nil { return err } @@ -194,7 +193,7 @@ func migrateVM(configPath string, config []byte, vm *MachineVM) error { vm.CmdLine = old.CmdLine vm.DiskSize = old.DiskSize vm.IdentityPath = old.IdentityPath - vm.IgnitionFilePath = *ignitionFilePath + vm.IgnitionFile = *ignitionFilePath vm.ImagePath = *imagePath vm.ImageStream = old.ImageStream vm.Memory = old.Memory @@ -229,7 +228,7 @@ func migrateVM(configPath string, config []byte, vm *MachineVM) error { // and returns a vm instance func (p *Provider) LoadVMByName(name string) (machine.VM, error) { vm := &MachineVM{Name: name} - vm.HostUser = HostUser{UID: -1} // posix reserves -1, so use it to signify undefined + vm.HostUser = machine.HostUser{UID: -1} // posix reserves -1, so use it to signify undefined if err := vm.update(); err != nil { return nil, err } @@ -270,7 +269,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { if err != nil { return false, err } - uncompressedFile, err := NewMachineFile(dd.Get().LocalUncompressedFile, nil) + uncompressedFile, err := machine.NewMachineFile(dd.Get().LocalUncompressedFile, nil) if err != nil { return false, err } @@ -286,7 +285,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { if err != nil { return false, err } - imagePath, err := NewMachineFile(g.Get().LocalUncompressedFile, nil) + imagePath, err := machine.NewMachineFile(g.Get().LocalUncompressedFile, nil) if err != nil { return false, err } @@ -308,7 +307,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { return false, err } - mounts := []Mount{} + mounts := []machine.Mount{} for i, volume := range opts.Volumes { tag := fmt.Sprintf("vol%d", i) paths := strings.SplitN(volume, ":", 3) @@ -338,7 +337,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { virtfsOptions += ",readonly" } v.CmdLine = append(v.CmdLine, []string{"-virtfs", virtfsOptions}...) - mounts = append(mounts, Mount{Type: MountType9p, Tag: tag, Source: source, Target: target, ReadOnly: readonly}) + mounts = append(mounts, machine.Mount{Type: MountType9p, Tag: tag, Source: source, Target: target, ReadOnly: readonly}) } } v.Mounts = mounts @@ -390,25 +389,9 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { if err != nil { return false, err } - // Resize the disk image to input disk size - // only if the virtualdisk size is less than - // the given disk size - if opts.DiskSize<<(10*3) > originalDiskSize { - // Find the qemu executable - cfg, err := config.Default() - if err != nil { - return false, err - } - resizePath, err := cfg.FindHelperBinary("qemu-img", true) - if err != nil { - return false, err - } - resize := exec.Command(resizePath, []string{"resize", v.getImageFile(), strconv.Itoa(int(opts.DiskSize)) + "G"}...) - resize.Stdout = os.Stdout - resize.Stderr = os.Stderr - if err := resize.Run(); err != nil { - return false, errors.Errorf("resizing image: %q", err) - } + + if err := v.resizeDisk(opts.DiskSize, originalDiskSize>>(10*3)); err != nil { + return false, err } // If the user provides an ignition file, we need to // copy it into the conf dir @@ -432,14 +415,14 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { return err == nil, err } -func (v *MachineVM) Set(_ string, opts machine.SetOptions) error { - if v.Rootful == opts.Rootful { - return nil - } +func (v *MachineVM) Set(_ string, opts machine.SetOptions) ([]error, error) { + // If one setting fails to be applied, the others settings will not fail and still be applied. + // The setting(s) that failed to be applied will have its errors returned in setErrors + var setErrors []error state, err := v.State(false) if err != nil { - return err + return setErrors, err } if state == machine.Running { @@ -447,26 +430,45 @@ func (v *MachineVM) Set(_ string, opts machine.SetOptions) error { if v.Name != machine.DefaultMachineName { suffix = " " + v.Name } - return errors.Errorf("cannot change setting while the vm is running, run 'podman machine stop%s' first", suffix) + return setErrors, errors.Errorf("cannot change settings while the vm is running, run 'podman machine stop%s' first", suffix) } - changeCon, err := machine.AnyConnectionDefault(v.Name, v.Name+"-root") - if err != nil { - return err + if opts.Rootful != nil && v.Rootful != *opts.Rootful { + if err := v.setRootful(*opts.Rootful); err != nil { + setErrors = append(setErrors, errors.Wrapf(err, "failed to set rootful option")) + } else { + v.Rootful = *opts.Rootful + } } - if changeCon { - newDefault := v.Name - if opts.Rootful { - newDefault += "-root" - } - if err := machine.ChangeDefault(newDefault); err != nil { - return err + if opts.CPUs != nil && v.CPUs != *opts.CPUs { + v.CPUs = *opts.CPUs + v.editCmdLine("-smp", strconv.Itoa(int(v.CPUs))) + } + + if opts.Memory != nil && v.Memory != *opts.Memory { + v.Memory = *opts.Memory + v.editCmdLine("-m", strconv.Itoa(int(v.Memory))) + } + + if opts.DiskSize != nil && v.DiskSize != *opts.DiskSize { + if err := v.resizeDisk(*opts.DiskSize, v.DiskSize); err != nil { + setErrors = append(setErrors, errors.Wrapf(err, "failed to resize disk")) + } else { + v.DiskSize = *opts.DiskSize } } - v.Rootful = opts.Rootful - return v.writeConfig() + err = v.writeConfig() + if err != nil { + setErrors = append(setErrors, err) + } + + if len(setErrors) > 0 { + return setErrors, setErrors[0] + } + + return setErrors, nil } // Start executes the qemu command line and forks it @@ -482,12 +484,11 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { if err := v.writeConfig(); err != nil { return fmt.Errorf("writing JSON file: %w", err) } - defer func() error { + defer func() { v.Starting = false if err := v.writeConfig(); err != nil { - return fmt.Errorf("writing JSON file: %w", err) + logrus.Errorf("Writing JSON file: %v", err) } - return nil }() if v.isIncompatible() { logrus.Errorf("machine %q is incompatible with this release of podman and needs to be recreated, starting for recovery only", v.Name) @@ -806,7 +807,7 @@ func NewQMPMonitor(network, name string, timeout time.Duration) (Monitor, error) if timeout == 0 { timeout = defaultQMPTimeout } - address, err := NewMachineFile(filepath.Join(rtDir, "qmp_"+name+".sock"), nil) + address, err := machine.NewMachineFile(filepath.Join(rtDir, "qmp_"+name+".sock"), nil) if err != nil { return Monitor{}, err } @@ -1063,11 +1064,11 @@ func getVMInfos() ([]*machine.ListResponse, error) { return err } - if !vm.LastUp.IsZero() { + if !vm.LastUp.IsZero() { // this means we have already written a time to the config listEntry.LastUp = vm.LastUp - } else { + } else { // else we just created the machine AKA last up = created time listEntry.LastUp = vm.Created - vm.Created = time.Now() + vm.LastUp = listEntry.LastUp if err := vm.writeConfig(); err != nil { return err } @@ -1235,14 +1236,14 @@ func (v *MachineVM) userGlobalSocketLink() (string, error) { return filepath.Join(filepath.Dir(path), "podman.sock"), err } -func (v *MachineVM) forwardSocketPath() (*MachineFile, error) { +func (v *MachineVM) forwardSocketPath() (*machine.VMFile, error) { sockName := "podman.sock" path, err := machine.GetDataDir(v.Name) if err != nil { logrus.Errorf("Resolving data dir: %s", err.Error()) return nil, err } - return NewMachineFile(filepath.Join(path, sockName), &sockName) + return machine.NewMachineFile(filepath.Join(path, sockName), &sockName) } func (v *MachineVM) setConfigPath() error { @@ -1251,7 +1252,7 @@ func (v *MachineVM) setConfigPath() error { return err } - configPath, err := NewMachineFile(filepath.Join(vmConfigDir, v.Name)+".json", nil) + configPath, err := machine.NewMachineFile(filepath.Join(vmConfigDir, v.Name)+".json", nil) if err != nil { return err } @@ -1265,7 +1266,7 @@ func (v *MachineVM) setReadySocket() error { if err != nil { return err } - virtualSocketPath, err := NewMachineFile(filepath.Join(rtPath, "podman", readySocketName), &readySocketName) + virtualSocketPath, err := machine.NewMachineFile(filepath.Join(rtPath, "podman", readySocketName), &readySocketName) if err != nil { return err } @@ -1283,7 +1284,7 @@ func (v *MachineVM) setPIDSocket() error { } pidFileName := fmt.Sprintf("%s.pid", v.Name) socketDir := filepath.Join(rtPath, "podman") - pidFilePath, err := NewMachineFile(filepath.Join(socketDir, pidFileName), &pidFileName) + pidFilePath, err := machine.NewMachineFile(filepath.Join(socketDir, pidFileName), &pidFileName) if err != nil { return err } @@ -1460,5 +1461,85 @@ func (v *MachineVM) getImageFile() string { // getIgnitionFile wrapper returns the path to the ignition file func (v *MachineVM) getIgnitionFile() string { - return v.IgnitionFilePath.GetPath() + return v.IgnitionFile.GetPath() +} + +// Inspect returns verbose detail about the machine +func (v *MachineVM) Inspect() (*machine.InspectInfo, error) { + state, err := v.State(false) + if err != nil { + return nil, err + } + + return &machine.InspectInfo{ + ConfigPath: v.ConfigPath, + Created: v.Created, + Image: v.ImageConfig, + LastUp: v.LastUp, + Name: v.Name, + Resources: v.ResourceConfig, + SSHConfig: v.SSHConfig, + State: state, + }, nil +} + +// resizeDisk increases the size of the machine's disk in GB. +func (v *MachineVM) resizeDisk(diskSize uint64, oldSize uint64) error { + // Resize the disk image to input disk size + // only if the virtualdisk size is less than + // the given disk size + if diskSize < oldSize { + return errors.Errorf("new disk size must be larger than current disk size: %vGB", oldSize) + } + + // Find the qemu executable + cfg, err := config.Default() + if err != nil { + return err + } + resizePath, err := cfg.FindHelperBinary("qemu-img", true) + if err != nil { + return err + } + resize := exec.Command(resizePath, []string{"resize", v.getImageFile(), strconv.Itoa(int(diskSize)) + "G"}...) + resize.Stdout = os.Stdout + resize.Stderr = os.Stderr + if err := resize.Run(); err != nil { + return errors.Errorf("resizing image: %q", err) + } + + return nil +} + +func (v *MachineVM) setRootful(rootful bool) error { + changeCon, err := machine.AnyConnectionDefault(v.Name, v.Name+"-root") + if err != nil { + return err + } + + if changeCon { + newDefault := v.Name + if rootful { + newDefault += "-root" + } + err := machine.ChangeDefault(newDefault) + if err != nil { + return err + } + } + return nil +} + +func (v *MachineVM) editCmdLine(flag string, value string) { + found := false + for i, val := range v.CmdLine { + if val == flag { + found = true + v.CmdLine[i+1] = value + break + } + } + if !found { + v.CmdLine = append(v.CmdLine, []string{flag, value}...) + } } diff --git a/pkg/machine/qemu/machine_test.go b/pkg/machine/qemu/machine_test.go new file mode 100644 index 000000000..62ca6068a --- /dev/null +++ b/pkg/machine/qemu/machine_test.go @@ -0,0 +1,17 @@ +package qemu + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEditCmd(t *testing.T) { + vm := new(MachineVM) + vm.CmdLine = []string{"command", "-flag", "value"} + + vm.editCmdLine("-flag", "newvalue") + vm.editCmdLine("-anotherflag", "anothervalue") + + require.Equal(t, vm.CmdLine, []string{"command", "-flag", "newvalue", "-anotherflag", "anothervalue"}) +} diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index f57dbd299..bc2ac7864 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -18,7 +18,6 @@ import ( "strings" "time" - "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/pkg/machine" "github.com/containers/podman/v4/utils" "github.com/containers/storage/pkg/homedir" @@ -153,20 +152,22 @@ const ( type Provider struct{} type MachineVM struct { - // IdentityPath is the fq path to the ssh priv key - IdentityPath string - // IgnitionFilePath is the fq path to the .ign file + // ConfigPath is the path to the configuration file + ConfigPath string + // Created contains the original created time instead of querying the file mod time + Created time.Time + // ImageStream is the version of fcos being used ImageStream string // ImagePath is the fq path to ImagePath string + // LastUp contains the last recorded uptime + LastUp time.Time // Name of the vm Name string - // SSH port for user networking - Port int - // RemoteUsername of the vm user - RemoteUsername string // Whether this machine should run in a rootful or rootless manner Rootful bool + // SSH identity, username, etc + machine.SSHConfig } type ExitCodeError struct { @@ -181,16 +182,22 @@ func GetWSLProvider() machine.Provider { return wslProvider } -// NewMachine initializes an instance of a virtual machine based on the qemu -// virtualization. +// NewMachine initializes an instance of a wsl machine func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { vm := new(MachineVM) if len(opts.Name) > 0 { vm.Name = opts.Name } + configPath, err := getConfigPath(opts.Name) + if err != nil { + return vm, err + } + vm.ConfigPath = configPath vm.ImagePath = opts.ImagePath vm.RemoteUsername = opts.Username + vm.Created = time.Now() + vm.LastUp = vm.Created // Add a random port for ssh port, err := utils.GetRandomPort() @@ -202,25 +209,69 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { return vm, nil } +func getConfigPath(name string) (string, error) { + vmConfigDir, err := machine.GetConfDir(vmtype) + if err != nil { + return "", err + } + return filepath.Join(vmConfigDir, name+".json"), nil +} + // LoadByName reads a json file that describes a known qemu vm // and returns a vm instance func (p *Provider) LoadVMByName(name string) (machine.VM, error) { - vm := new(MachineVM) - vmConfigDir, err := machine.GetConfDir(vmtype) + configPath, err := getConfigPath(name) if err != nil { return nil, err } - b, err := ioutil.ReadFile(filepath.Join(vmConfigDir, name+".json")) - if os.IsNotExist(err) { - return nil, errors.Wrap(machine.ErrNoSuchVM, name) - } + + vm, err := readAndMigrate(configPath, name) + return vm, err +} + +// readAndMigrate returns the content of the VM's +// configuration file in json +func readAndMigrate(configPath string, name string) (*MachineVM, error) { + vm := new(MachineVM) + b, err := os.ReadFile(configPath) if err != nil { - return nil, err + if errors.Is(err, os.ErrNotExist) { + return nil, errors.Wrap(machine.ErrNoSuchVM, name) + } + return vm, err } err = json.Unmarshal(b, vm) + if err == nil && vm.Created.IsZero() { + err = vm.migrate40(configPath) + } return vm, err } +func (v *MachineVM) migrate40(configPath string) error { + v.ConfigPath = configPath + fi, err := os.Stat(configPath) + if err != nil { + return err + } + v.Created = fi.ModTime() + v.LastUp = getLegacyLastStart(v) + return v.writeConfig() +} + +func getLegacyLastStart(vm *MachineVM) time.Time { + vmDataDir, err := machine.GetDataDir(vmtype) + if err != nil { + return vm.Created + } + distDir := filepath.Join(vmDataDir, "wsldist") + start := filepath.Join(distDir, vm.Name, "laststart") + info, err := os.Stat(start) + if err != nil { + return vm.Created + } + return info.ModTime() +} + // Init writes the json configuration file to the filesystem for // other verbs (start, stop) func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { @@ -289,12 +340,7 @@ func downloadDistro(v *MachineVM, opts machine.InitOptions) error { } func (v *MachineVM) writeConfig() error { - vmConfigDir, err := machine.GetConfDir(vmtype) - if err != nil { - return err - } - - jsonFile := filepath.Join(vmConfigDir, v.Name) + ".json" + jsonFile := v.ConfigPath b, err := json.MarshalIndent(v, "", " ") if err != nil { @@ -736,28 +782,34 @@ func pipeCmdPassThrough(name string, input string, arg ...string) error { return cmd.Run() } -func (v *MachineVM) Set(name string, opts machine.SetOptions) error { - if v.Rootful == opts.Rootful { - return nil +func (v *MachineVM) Set(_ string, opts machine.SetOptions) ([]error, error) { + // If one setting fails to be applied, the others settings will not fail and still be applied. + // The setting(s) that failed to be applied will have its errors returned in setErrors + var setErrors []error + + if opts.Rootful != nil && v.Rootful != *opts.Rootful { + err := v.setRootful(*opts.Rootful) + if err != nil { + setErrors = append(setErrors, errors.Wrapf(err, "error setting rootful option")) + } else { + v.Rootful = *opts.Rootful + } } - changeCon, err := machine.AnyConnectionDefault(v.Name, v.Name+"-root") - if err != nil { - return err + if opts.CPUs != nil { + setErrors = append(setErrors, errors.Errorf("changing CPUs not suppored for WSL machines")) } - if changeCon { - newDefault := v.Name - if opts.Rootful { - newDefault += "-root" - } - if err := machine.ChangeDefault(newDefault); err != nil { - return err - } + if opts.Memory != nil { + setErrors = append(setErrors, errors.Errorf("changing memory not suppored for WSL machines")) + } - v.Rootful = opts.Rootful - return v.writeConfig() + if opts.DiskSize != nil { + setErrors = append(setErrors, errors.Errorf("changing Disk Size not suppored for WSL machines")) + } + + return setErrors, v.writeConfig() } func (v *MachineVM) Start(name string, _ machine.StartOptions) error { @@ -804,7 +856,8 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { } } - return markStart(name) + _, _, err = v.updateTimeStamps(true) + return err } func launchWinProxy(v *MachineVM) (bool, string, error) { @@ -999,6 +1052,8 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { return errors.Errorf("%q is not running", v.Name) } + _, _, _ = v.updateTimeStamps(true) + if err := stopWinProxy(v); err != nil { fmt.Fprintf(os.Stderr, "Could not stop API forwarding service (win-sshproxy.exe): %s\n", err.Error()) } @@ -1026,10 +1081,12 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { return nil } -// TODO: We need to rename isRunning to State(); I do not have a -// windows system to test this on. func (v *MachineVM) State(bypass bool) (machine.Status, error) { - return "", define.ErrNotImplemented + if v.isRunning() { + return machine.Running, nil + } + + return machine.Stopped, nil } func stopWinProxy(v *MachineVM) error { @@ -1204,14 +1261,9 @@ func GetVMInfos() ([]*machine.ListResponse, error) { var listed []*machine.ListResponse if err = filepath.WalkDir(vmConfigDir, func(path string, d fs.DirEntry, err error) error { - vm := new(MachineVM) if strings.HasSuffix(d.Name(), ".json") { - fullPath := filepath.Join(vmConfigDir, d.Name()) - b, err := ioutil.ReadFile(fullPath) - if err != nil { - return err - } - err = json.Unmarshal(b, vm) + path := filepath.Join(vmConfigDir, d.Name()) + vm, err := readAndMigrate(path, strings.TrimSuffix(d.Name(), ".json")) if err != nil { return err } @@ -1223,15 +1275,13 @@ func GetVMInfos() ([]*machine.ListResponse, error) { listEntry.CPUs, _ = getCPUs(vm) listEntry.Memory, _ = getMem(vm) listEntry.DiskSize = getDiskSize(vm) - fi, err := os.Stat(fullPath) - if err != nil { - return err - } - listEntry.CreatedAt = fi.ModTime() - listEntry.LastUp = getLastStart(vm, fi.ModTime()) - if vm.isRunning() { - listEntry.Running = true - } + listEntry.RemoteUsername = vm.RemoteUsername + listEntry.Port = vm.Port + listEntry.IdentityPath = vm.IdentityPath + + running := vm.isRunning() + listEntry.CreatedAt, listEntry.LastUp, _ = vm.updateTimeStamps(running) + listEntry.Running = running listed = append(listed, listEntry) } @@ -1242,6 +1292,16 @@ func GetVMInfos() ([]*machine.ListResponse, error) { return listed, err } +func (vm *MachineVM) updateTimeStamps(updateLast bool) (time.Time, time.Time, error) { + var err error + if updateLast { + vm.LastUp = time.Now() + err = vm.writeConfig() + } + + return vm.Created, vm.LastUp, err +} + func getDiskSize(vm *MachineVM) uint64 { vmDataDir, err := machine.GetDataDir(vmtype) if err != nil { @@ -1256,36 +1316,6 @@ func getDiskSize(vm *MachineVM) uint64 { return uint64(info.Size()) } -func markStart(name string) error { - vmDataDir, err := machine.GetDataDir(vmtype) - if err != nil { - return err - } - distDir := filepath.Join(vmDataDir, "wsldist") - start := filepath.Join(distDir, name, "laststart") - file, err := os.Create(start) - if err != nil { - return err - } - file.Close() - - return nil -} - -func getLastStart(vm *MachineVM, created time.Time) time.Time { - vmDataDir, err := machine.GetDataDir(vmtype) - if err != nil { - return created - } - distDir := filepath.Join(vmDataDir, "wsldist") - start := filepath.Join(distDir, vm.Name, "laststart") - info, err := os.Stat(start) - if err != nil { - return created - } - return info.ModTime() -} - func getCPUs(vm *MachineVM) (uint64, error) { dist := toDist(vm.Name) if run, _ := isWSLRunning(dist); !run { @@ -1362,3 +1392,53 @@ func (p *Provider) IsValidVMName(name string) (bool, error) { func (p *Provider) CheckExclusiveActiveVM() (bool, string, error) { return false, "", nil } + +func (v *MachineVM) setRootful(rootful bool) error { + changeCon, err := machine.AnyConnectionDefault(v.Name, v.Name+"-root") + if err != nil { + return err + } + + if changeCon { + newDefault := v.Name + if rootful { + newDefault += "-root" + } + err := machine.ChangeDefault(newDefault) + if err != nil { + return err + } + } + return nil +} + +// Inspect returns verbose detail about the machine +func (v *MachineVM) Inspect() (*machine.InspectInfo, error) { + state, err := v.State(false) + if err != nil { + return nil, err + } + + created, lastUp, _ := v.updateTimeStamps(state == machine.Running) + + return &machine.InspectInfo{ + ConfigPath: machine.VMFile{Path: v.ConfigPath}, + Created: created, + Image: machine.ImageConfig{ + ImagePath: machine.VMFile{Path: v.ImagePath}, + ImageStream: v.ImageStream, + }, + LastUp: lastUp, + Name: v.Name, + Resources: v.getResources(), + SSHConfig: v.SSHConfig, + State: state, + }, nil +} + +func (v *MachineVM) getResources() (resources machine.ResourceConfig) { + resources.CPUs, _ = getCPUs(v) + resources.Memory, _ = getMem(v) + resources.DiskSize = getDiskSize(v) + return +} |