From 833456e079c31111a15fedaa3ccd7f852e89e508 Mon Sep 17 00:00:00 2001 From: Brent Baude Date: Sat, 2 Apr 2022 15:25:19 -0500 Subject: Add podman machine test suite This PR introduces a test suite for podman machine. It can currently be run on developers' local machines and is not part of the official CI testing; however, the expectation is that any work on machine should come with an accompanying test. At present, the test must be run on Linux. It is untested on Darwin. There is no Makefile target for the test. It can be run like `ginkgo -v pkg/machine/test/.`. It should be run as a unprivileged user. Signed-off-by: Brent Baude --- Makefile | 2 +- cmd/podman/machine/init.go | 2 - hack/golangci-lint.sh | 4 +- pkg/machine/e2e/basic_test.go | 50 +++++++++++ pkg/machine/e2e/config.go | 174 ++++++++++++++++++++++++++++++++++++++ pkg/machine/e2e/config_basic.go | 19 +++++ pkg/machine/e2e/config_init.go | 101 ++++++++++++++++++++++ pkg/machine/e2e/config_inspect.go | 24 ++++++ pkg/machine/e2e/config_list.go | 45 ++++++++++ pkg/machine/e2e/config_rm.go | 56 ++++++++++++ pkg/machine/e2e/config_ssh.go | 33 ++++++++ pkg/machine/e2e/config_start.go | 16 ++++ pkg/machine/e2e/config_stop.go | 16 ++++ pkg/machine/e2e/init_test.go | 77 +++++++++++++++++ pkg/machine/e2e/inspect_test.go | 67 +++++++++++++++ pkg/machine/e2e/list_test.go | 79 +++++++++++++++++ pkg/machine/e2e/machine_test.go | 129 ++++++++++++++++++++++++++++ pkg/machine/e2e/rm_test.go | 67 +++++++++++++++ pkg/machine/e2e/ssh_test.go | 59 +++++++++++++ pkg/machine/e2e/start_test.go | 36 ++++++++ pkg/machine/e2e/stop_test.go | 46 ++++++++++ pkg/machine/fcos.go | 14 +-- pkg/machine/qemu/machine.go | 42 ++++++--- 23 files changed, 1134 insertions(+), 24 deletions(-) create mode 100644 pkg/machine/e2e/basic_test.go create mode 100644 pkg/machine/e2e/config.go create mode 100644 pkg/machine/e2e/config_basic.go create mode 100644 pkg/machine/e2e/config_init.go create mode 100644 pkg/machine/e2e/config_inspect.go create mode 100644 pkg/machine/e2e/config_list.go create mode 100644 pkg/machine/e2e/config_rm.go create mode 100644 pkg/machine/e2e/config_ssh.go create mode 100644 pkg/machine/e2e/config_start.go create mode 100644 pkg/machine/e2e/config_stop.go create mode 100644 pkg/machine/e2e/init_test.go create mode 100644 pkg/machine/e2e/inspect_test.go create mode 100644 pkg/machine/e2e/list_test.go create mode 100644 pkg/machine/e2e/machine_test.go create mode 100644 pkg/machine/e2e/rm_test.go create mode 100644 pkg/machine/e2e/ssh_test.go create mode 100644 pkg/machine/e2e/start_test.go create mode 100644 pkg/machine/e2e/stop_test.go diff --git a/Makefile b/Makefile index 982d7b1e5..c14e58f66 100644 --- a/Makefile +++ b/Makefile @@ -537,7 +537,7 @@ localunit: test/goecho/goecho test/version/version UNIT=1 $(GOBIN)/ginkgo \ -r \ $(TESTFLAGS) \ - --skipPackage test/e2e,pkg/apparmor,pkg/bindings,hack \ + --skipPackage test/e2e,pkg/apparmor,pkg/bindings,hack,pkg/machine/e2e \ --cover \ --covermode atomic \ --coverprofile coverprofile \ diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index 2d0afbf05..4991c6aa3 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -135,7 +135,6 @@ func initMachine(cmd *cobra.Command, args []string) error { if err != nil { return err } - if finished, err := vm.Init(initOpts); err != nil || !finished { // Finished = true, err = nil - Success! Log a message with further instructions // Finished = false, err = nil - The installation is partially complete and podman should @@ -144,7 +143,6 @@ func initMachine(cmd *cobra.Command, args []string) error { // - a user has chosen to perform their own reboot // - reexec for limited admin operations, returning to parent // Finished = *, err != nil - Exit with an error message - return err } fmt.Println("Machine init complete") diff --git a/hack/golangci-lint.sh b/hack/golangci-lint.sh index bcb83a2fd..8b80bd9c9 100755 --- a/hack/golangci-lint.sh +++ b/hack/golangci-lint.sh @@ -10,9 +10,9 @@ BUILD_TAGS[abi]="${BUILD_TAGS[default]},!remoteclient" BUILD_TAGS[tunnel]="${BUILD_TAGS[default]},remote,remoteclient" declare -A SKIP_DIRS -SKIP_DIRS[abi]="" +SKIP_DIRS[abi]="pkg/machine/e2e" # TODO: add "remote" build tag to pkg/api -SKIP_DIRS[tunnel]="pkg/api" +SKIP_DIRS[tunnel]="pkg/api,pkg/machine/e2e" [[ $1 == run ]] && shift diff --git a/pkg/machine/e2e/basic_test.go b/pkg/machine/e2e/basic_test.go new file mode 100644 index 000000000..f67fb4c67 --- /dev/null +++ b/pkg/machine/e2e/basic_test.go @@ -0,0 +1,50 @@ +package e2e + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("run basic podman commands", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("Basic ops", func() { + name := randomString(12) + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath).withNow()).run() + Expect(err).To(BeNil()) + Expect(session).To(Exit(0)) + + bm := basicMachine{} + imgs, err := mb.setCmd(bm.withPodmanCommand([]string{"images", "-q"})).run() + Expect(err).To(BeNil()) + Expect(imgs).To(Exit(0)) + Expect(len(imgs.outputToStringSlice())).To(Equal(0)) + + newImgs, err := mb.setCmd(bm.withPodmanCommand([]string{"pull", "quay.io/libpod/alpine_nginx"})).run() + Expect(err).To(BeNil()) + Expect(newImgs).To(Exit(0)) + Expect(len(newImgs.outputToStringSlice())).To(Equal(1)) + + runAlp, err := mb.setCmd(bm.withPodmanCommand([]string{"run", "quay.io/libpod/alpine_nginx", "cat", "/etc/os-release"})).run() + Expect(err).To(BeNil()) + Expect(runAlp).To(Exit(0)) + Expect(runAlp.outputToString()).To(ContainSubstring("Alpine Linux")) + + rmCon, err := mb.setCmd(bm.withPodmanCommand([]string{"rm", "-a"})).run() + Expect(err).To(BeNil()) + Expect(rmCon).To(Exit(0)) + }) + +}) diff --git a/pkg/machine/e2e/config.go b/pkg/machine/e2e/config.go new file mode 100644 index 000000000..7d75ca6bc --- /dev/null +++ b/pkg/machine/e2e/config.go @@ -0,0 +1,174 @@ +package e2e + +import ( + "encoding/json" + "fmt" + "math/rand" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/qemu" + "github.com/containers/podman/v4/pkg/util" + . "github.com/onsi/ginkgo" //nolint:golint,stylecheck + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + . "github.com/onsi/gomega/gexec" //nolint:golint,stylecheck +) + +var originalHomeDir = os.Getenv("HOME") + +const ( + defaultTimeout time.Duration = 90 * time.Second +) + +type machineCommand interface { + buildCmd(m *machineTestBuilder) []string +} + +type MachineTestBuilder interface { + setName(string) *MachineTestBuilder + setCmd(mc machineCommand) *MachineTestBuilder + setTimeout(duration time.Duration) *MachineTestBuilder + run() (*machineSession, error) +} +type machineSession struct { + *gexec.Session +} + +type machineTestBuilder struct { + cmd []string + imagePath string + name string + names []string + podmanBinary string + timeout time.Duration +} +type qemuMachineInspectInfo struct { + State machine.Status + VM qemu.MachineVM +} + +// waitWithTimeout waits for a command to complete for a given +// number of seconds +func (ms *machineSession) waitWithTimeout(timeout time.Duration) { + Eventually(ms, timeout).Should(Exit()) + os.Stdout.Sync() + os.Stderr.Sync() +} + +func (ms *machineSession) Bytes() []byte { + return []byte(ms.outputToString()) +} + +func (ms *machineSession) outputToStringSlice() []string { + var results []string + output := string(ms.Out.Contents()) + for _, line := range strings.Split(output, "\n") { + if line != "" { + results = append(results, line) + } + } + return results +} + +// outputToString returns the output from a session in string form +func (ms *machineSession) outputToString() string { + if ms == nil || ms.Out == nil || ms.Out.Contents() == nil { + return "" + } + + fields := strings.Fields(string(ms.Out.Contents())) + return strings.Join(fields, " ") +} + +// newMB constructor for machine test builders +func newMB() (*machineTestBuilder, error) { + mb := machineTestBuilder{ + timeout: defaultTimeout, + } + cwd, err := os.Getwd() + if err != nil { + return nil, err + } + mb.podmanBinary = filepath.Join(cwd, "../../../bin/podman-remote") + if os.Getenv("PODMAN_BINARY") != "" { + mb.podmanBinary = os.Getenv("PODMAN_BINARY") + } + return &mb, nil +} + +// setName sets the name of the virtuaql machine for the command +func (m *machineTestBuilder) setName(name string) *machineTestBuilder { + m.name = name + return m +} + +// setCmd takes a machineCommand struct and assembles a cmd line +// representation of the podman machine command +func (m *machineTestBuilder) setCmd(mc machineCommand) *machineTestBuilder { + // If no name for the machine exists, we set a random name. + if !util.StringInSlice(m.name, m.names) { + if len(m.name) < 1 { + m.name = randomString(12) + } + m.names = append(m.names, m.name) + } + m.cmd = mc.buildCmd(m) + return m +} + +func (m *machineTestBuilder) setTimeout(timeout time.Duration) *machineTestBuilder { + m.timeout = timeout + return m +} + +// toQemuInspectInfo is only for inspecting qemu machines. Other providers will need +// to make their own. +func (mb *machineTestBuilder) toQemuInspectInfo() ([]qemuMachineInspectInfo, int, error) { + args := []string{"machine", "inspect"} + args = append(args, mb.names...) + session, err := runWrapper(mb.podmanBinary, args, defaultTimeout) + if err != nil { + return nil, -1, err + } + mii := []qemuMachineInspectInfo{} + err = json.Unmarshal(session.Bytes(), &mii) + return mii, session.ExitCode(), err +} + +func (m *machineTestBuilder) run() (*machineSession, error) { + return runWrapper(m.podmanBinary, m.cmd, m.timeout) +} + +func runWrapper(podmanBinary string, cmdArgs []string, timeout time.Duration) (*machineSession, error) { + if len(os.Getenv("DEBUG")) > 0 { + cmdArgs = append([]string{"--log-level=debug"}, cmdArgs...) + } + fmt.Println(podmanBinary + " " + strings.Join(cmdArgs, " ")) + c := exec.Command(podmanBinary, cmdArgs...) + session, err := Start(c, GinkgoWriter, GinkgoWriter) + if err != nil { + Fail(fmt.Sprintf("Unable to start session: %q", err)) + return nil, err + } + ms := machineSession{session} + ms.waitWithTimeout(timeout) + fmt.Println("output:", ms.outputToString()) + return &ms, nil +} + +func (m *machineTestBuilder) init() {} + +// randomString returns a string of given length composed of random characters +func randomString(n int) string { + var randomLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, n) + for i := range b { + b[i] = randomLetters[rand.Intn(len(randomLetters))] + } + return string(b) +} diff --git a/pkg/machine/e2e/config_basic.go b/pkg/machine/e2e/config_basic.go new file mode 100644 index 000000000..be0896156 --- /dev/null +++ b/pkg/machine/e2e/config_basic.go @@ -0,0 +1,19 @@ +package e2e + +type basicMachine struct { + args []string + cmd []string +} + +func (s basicMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"-r"} + if len(s.args) > 0 { + cmd = append(cmd, s.args...) + } + return cmd +} + +func (s *basicMachine) withPodmanCommand(args []string) *basicMachine { + s.args = args + return s +} diff --git a/pkg/machine/e2e/config_init.go b/pkg/machine/e2e/config_init.go new file mode 100644 index 000000000..55218221d --- /dev/null +++ b/pkg/machine/e2e/config_init.go @@ -0,0 +1,101 @@ +package e2e + +import ( + "strconv" +) + +type initMachine struct { + /* + --cpus uint Number of CPUs (default 1) + --disk-size uint Disk size in GB (default 100) + --ignition-path string Path to ignition file + --image-path string Path to qcow image (default "testing") + -m, --memory uint Memory in MB (default 2048) + --now Start machine now + --rootful Whether this machine should prefer rootful container exectution + --timezone string Set timezone (default "local") + -v, --volume stringArray Volumes to mount, source:target + --volume-driver string Optional volume driver + + */ + cpus *uint + diskSize *uint + ignitionPath string + imagePath string + memory *uint + now bool + timezone string + volumes []string + + cmd []string +} + +func (i *initMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "init"} + 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 l := len(i.ignitionPath); l > 0 { + cmd = append(cmd, "--ignition-path", i.ignitionPath) + } + if l := len(i.imagePath); l > 0 { + cmd = append(cmd, "--image-path", i.imagePath) + } + if i.memory != nil { + cmd = append(cmd, "--memory", strconv.Itoa(int(*i.memory))) + } + if l := len(i.timezone); l > 0 { + cmd = append(cmd, "--timezone", i.timezone) + } + for _, v := range i.volumes { + cmd = append(cmd, "--volume", v) + } + if i.now { + cmd = append(cmd, "--now") + } + cmd = append(cmd, m.name) + i.cmd = cmd + return cmd +} + +func (i *initMachine) withCPUs(num uint) *initMachine { + i.cpus = &num + return i +} +func (i *initMachine) withDiskSize(size uint) *initMachine { + i.diskSize = &size + return i +} + +func (i *initMachine) withIgnitionPath(path string) *initMachine { + i.ignitionPath = path + return i +} + +func (i *initMachine) withImagePath(path string) *initMachine { + i.imagePath = path + return i +} + +func (i *initMachine) withMemory(num uint) *initMachine { + i.memory = &num + return i +} + +func (i *initMachine) withNow() *initMachine { + i.now = true + return i +} + +func (i *initMachine) withTimezone(tz string) *initMachine { + i.timezone = tz + return i +} + +func (i *initMachine) withVolume(v string) *initMachine { + i.volumes = append(i.volumes, v) + return i +} diff --git a/pkg/machine/e2e/config_inspect.go b/pkg/machine/e2e/config_inspect.go new file mode 100644 index 000000000..74c9a5d9c --- /dev/null +++ b/pkg/machine/e2e/config_inspect.go @@ -0,0 +1,24 @@ +package e2e + +type inspectMachine struct { + /* + --format string Format volume output using JSON or a Go template + */ + cmd []string + format string +} + +func (i *inspectMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "inspect"} + if len(i.format) > 0 { + cmd = append(cmd, "--format", i.format) + } + cmd = append(cmd, m.names...) + i.cmd = cmd + return cmd +} + +func (i *inspectMachine) withFormat(format string) *inspectMachine { + i.format = format + return i +} diff --git a/pkg/machine/e2e/config_list.go b/pkg/machine/e2e/config_list.go new file mode 100644 index 000000000..150f984bc --- /dev/null +++ b/pkg/machine/e2e/config_list.go @@ -0,0 +1,45 @@ +package e2e + +type listMachine struct { + /* + --format string Format volume output using JSON or a Go template (default "{{.Name}}\t{{.VMType}}\t{{.Created}}\t{{.LastUp}}\t{{.CPUs}}\t{{.Memory}}\t{{.DiskSize}}\n") + --noheading Do not print headers + -q, --quiet Show only machine names + */ + + format string + noHeading bool + quiet bool + + cmd []string +} + +func (i *listMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "list"} + if len(i.format) > 0 { + cmd = append(cmd, "--format", i.format) + } + if i.noHeading { + cmd = append(cmd, "--noheading") + } + if i.quiet { + cmd = append(cmd, "--quiet") + } + i.cmd = cmd + return cmd +} + +func (i *listMachine) withNoHeading() *listMachine { + i.noHeading = true + return i +} + +func (i *listMachine) withQuiet() *listMachine { + i.quiet = true + return i +} + +func (i *listMachine) withFormat(format string) *listMachine { + i.format = format + return i +} diff --git a/pkg/machine/e2e/config_rm.go b/pkg/machine/e2e/config_rm.go new file mode 100644 index 000000000..6cf262a22 --- /dev/null +++ b/pkg/machine/e2e/config_rm.go @@ -0,0 +1,56 @@ +package e2e + +type rmMachine struct { + /* + -f, --force Stop and do not prompt before rming + --save-ignition Do not delete ignition file + --save-image Do not delete the image file + --save-keys Do not delete SSH keys + + */ + force bool + saveIgnition bool + saveImage bool + saveKeys bool + + cmd []string +} + +func (i *rmMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "rm"} + if i.force { + cmd = append(cmd, "--force") + } + if i.saveIgnition { + cmd = append(cmd, "--save-ignition") + } + if i.saveImage { + cmd = append(cmd, "--save-image") + } + if i.saveKeys { + cmd = append(cmd, "--save-keys") + } + cmd = append(cmd, m.name) + i.cmd = cmd + return cmd +} + +func (i *rmMachine) withForce() *rmMachine { + i.force = true + return i +} + +func (i *rmMachine) withSaveIgnition() *rmMachine { + i.saveIgnition = true + return i +} + +func (i *rmMachine) withSaveImage() *rmMachine { + i.saveImage = true + return i +} + +func (i *rmMachine) withSaveKeys() *rmMachine { + i.saveKeys = true + return i +} diff --git a/pkg/machine/e2e/config_ssh.go b/pkg/machine/e2e/config_ssh.go new file mode 100644 index 000000000..b09eed47d --- /dev/null +++ b/pkg/machine/e2e/config_ssh.go @@ -0,0 +1,33 @@ +package e2e + +type sshMachine struct { + /* + --username string Username to use when ssh-ing into the VM. + */ + + username string + sshCommand []string + + cmd []string +} + +func (s sshMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "ssh"} + if len(m.name) > 0 { + cmd = append(cmd, m.name) + } + if len(s.sshCommand) > 0 { + cmd = append(cmd, s.sshCommand...) + } + return cmd +} + +func (s *sshMachine) withUsername(name string) *sshMachine { + s.username = name + return s +} + +func (s *sshMachine) withSSHComand(sshCommand []string) *sshMachine { + s.sshCommand = sshCommand + return s +} diff --git a/pkg/machine/e2e/config_start.go b/pkg/machine/e2e/config_start.go new file mode 100644 index 000000000..86b1721f8 --- /dev/null +++ b/pkg/machine/e2e/config_start.go @@ -0,0 +1,16 @@ +package e2e + +type startMachine struct { + /* + No command line args other than a machine vm name (also not required) + */ + cmd []string +} + +func (s startMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "start"} + if len(m.name) > 0 { + cmd = append(cmd, m.name) + } + return cmd +} diff --git a/pkg/machine/e2e/config_stop.go b/pkg/machine/e2e/config_stop.go new file mode 100644 index 000000000..04dcfb524 --- /dev/null +++ b/pkg/machine/e2e/config_stop.go @@ -0,0 +1,16 @@ +package e2e + +type stopMachine struct { + /* + No command line args other than a machine vm name (also not required) + */ + cmd []string +} + +func (s stopMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "stop"} + if len(m.name) > 0 { + cmd = append(cmd, m.name) + } + return cmd +} diff --git a/pkg/machine/e2e/init_test.go b/pkg/machine/e2e/init_test.go new file mode 100644 index 000000000..309d460a9 --- /dev/null +++ b/pkg/machine/e2e/init_test.go @@ -0,0 +1,77 @@ +package e2e + +import ( + "time" + + "github.com/containers/podman/v4/pkg/machine" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("podman machine init", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("bad init name", func() { + i := initMachine{} + reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + session, err := mb.setName(reallyLongName).setCmd(&i).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(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)) + + inspectBefore, ec, err := mb.toQemuInspectInfo() + Expect(err).To(BeNil()) + Expect(ec).To(BeZero()) + + Expect(len(inspectBefore)).To(BeNumerically(">", 0)) + testMachine := inspectBefore[0] + Expect(testMachine.VM.Name).To(Equal(mb.names[0])) + Expect(testMachine.VM.CPUs).To(Equal(uint64(1))) + Expect(testMachine.VM.Memory).To(Equal(uint64(2048))) + + }) + + It("simple init with start", func() { + i := initMachine{} + session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(0)) + + inspectBefore, ec, err := mb.toQemuInspectInfo() + Expect(ec).To(BeZero()) + Expect(len(inspectBefore)).To(BeNumerically(">", 0)) + Expect(err).To(BeNil()) + Expect(len(inspectBefore)).To(BeNumerically(">", 0)) + Expect(inspectBefore[0].VM.Name).To(Equal(mb.names[0])) + + s := startMachine{} + ssession, err := mb.setCmd(s).setTimeout(time.Minute * 10).run() + Expect(err).To(BeNil()) + Expect(ssession).Should(Exit(0)) + + inspectAfter, ec, err := mb.toQemuInspectInfo() + Expect(err).To(BeNil()) + Expect(ec).To(BeZero()) + Expect(len(inspectBefore)).To(BeNumerically(">", 0)) + Expect(len(inspectAfter)).To(BeNumerically(">", 0)) + Expect(inspectAfter[0].State).To(Equal(machine.Running)) + }) + +}) diff --git a/pkg/machine/e2e/inspect_test.go b/pkg/machine/e2e/inspect_test.go new file mode 100644 index 000000000..30d810b8f --- /dev/null +++ b/pkg/machine/e2e/inspect_test.go @@ -0,0 +1,67 @@ +package e2e + +import ( + "encoding/json" + + "github.com/containers/podman/v4/pkg/machine/qemu" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("podman machine stop", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("inspect bad name", func() { + i := inspectMachine{} + reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + session, err := mb.setName(reallyLongName).setCmd(&i).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(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)) + + ii := new(initMachine) + foo2, err := mb.setName("foo2").setCmd(ii.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(foo2.ExitCode()).To(Equal(0)) + + inspect := new(inspectMachine) + inspectSession, err := mb.setName("foo1").setCmd(inspect).run() + Expect(err).To(BeNil()) + Expect(inspectSession.ExitCode()).To(Equal(0)) + + type fakeInfos struct { + Status string + VM qemu.MachineVM + } + infos := make([]fakeInfos, 0, 2) + err = json.Unmarshal(inspectSession.Bytes(), &infos) + Expect(err).ToNot(HaveOccurred()) + Expect(len(infos)).To(Equal(2)) + + //rm := new(rmMachine) + //// Must manually clean up due to multiple names + //for _, name := range []string{"foo1", "foo2"} { + // mb.setName(name).setCmd(rm.withForce()).run() + // mb.names = []string{} + //} + //mb.names = []string{} + + }) +}) diff --git a/pkg/machine/e2e/list_test.go b/pkg/machine/e2e/list_test.go new file mode 100644 index 000000000..e7a439945 --- /dev/null +++ b/pkg/machine/e2e/list_test.go @@ -0,0 +1,79 @@ +package e2e + +import ( + "strings" + + "github.com/containers/buildah/util" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("podman machine list", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("list machine", func() { + list := new(listMachine) + firstList, err := mb.setCmd(list).run() + Expect(err).NotTo(HaveOccurred()) + Expect(firstList).Should(Exit(0)) + Expect(len(firstList.outputToStringSlice())).To(Equal(1)) // just the header + + i := new(initMachine) + session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session).To(Exit(0)) + + secondList, err := mb.setCmd(list).run() + Expect(err).NotTo(HaveOccurred()) + Expect(secondList).To(Exit(0)) + Expect(len(secondList.outputToStringSlice())).To(Equal(2)) // one machine and the header + }) + + It("list machines with quiet", func() { + // Random names for machines to test list + name1 := randomString(12) + name2 := randomString(12) + + list := new(listMachine) + firstList, err := mb.setCmd(list.withQuiet()).run() + Expect(err).NotTo(HaveOccurred()) + Expect(firstList).Should(Exit(0)) + Expect(len(firstList.outputToStringSlice())).To(Equal(0)) // No header with quiet + + i := new(initMachine) + session, err := mb.setName(name1).setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session).To(Exit(0)) + + session2, err := mb.setName(name2).setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session2).To(Exit(0)) + + secondList, err := mb.setCmd(list.withQuiet()).run() + Expect(err).NotTo(HaveOccurred()) + Expect(secondList).To(Exit(0)) + Expect(len(secondList.outputToStringSlice())).To(Equal(2)) // two machines, no header + + listNames := secondList.outputToStringSlice() + stripAsterisk(listNames) + Expect(util.StringInSlice(name1, listNames)).To(BeTrue()) + Expect(util.StringInSlice(name2, listNames)).To(BeTrue()) + }) +}) + +func stripAsterisk(sl []string) { + for idx, val := range sl { + sl[idx] = strings.TrimRight(val, "*") + } +} diff --git a/pkg/machine/e2e/machine_test.go b/pkg/machine/e2e/machine_test.go new file mode 100644 index 000000000..46fe18069 --- /dev/null +++ b/pkg/machine/e2e/machine_test.go @@ -0,0 +1,129 @@ +package e2e + +import ( + "fmt" + "io" + "io/ioutil" + url2 "net/url" + "os" + "path" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/containers/podman/v4/pkg/machine" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} + +const ( + defaultStream string = "podman-testing" + tmpDir string = "/var/tmp" +) + +var ( + fqImageName string + suiteImageName string +) + +// TestLibpod ginkgo master function +func TestMachine(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Podman Machine tests") +} + +var _ = BeforeSuite(func() { + fcd, err := machine.GetFCOSDownload(defaultStream) + if err != nil { + Fail("unable to get virtual machine image") + } + suiteImageName = strings.TrimSuffix(path.Base(fcd.Location), ".xz") + fqImageName = filepath.Join(tmpDir, suiteImageName) + if _, err := os.Stat(fqImageName); err != nil { + if os.IsNotExist(err) { + getMe, err := url2.Parse(fcd.Location) + if err != nil { + Fail(fmt.Sprintf("unable to create url for download: %q", err)) + } + now := time.Now() + if err := machine.DownloadVMImage(getMe, fqImageName+".xz"); err != nil { + Fail(fmt.Sprintf("unable to download machine image: %q", err)) + } + fmt.Println("Download took: ", time.Since(now).String()) + if err := machine.Decompress(fqImageName+".xz", fqImageName); err != nil { + Fail(fmt.Sprintf("unable to decompress image file: %q", err)) + } + } else { + Fail(fmt.Sprintf("unable to check for cache image: %q", err)) + } + } +}) + +var _ = SynchronizedAfterSuite(func() {}, + func() { + fmt.Println("After") + }) + +func setup() (string, *machineTestBuilder) { + homeDir, err := ioutil.TempDir("/var/tmp", "podman_test") + if err != nil { + Fail(fmt.Sprintf("failed to create home directory: %q", err)) + } + if err := os.MkdirAll(filepath.Join(homeDir, ".ssh"), 0700); err != nil { + Fail(fmt.Sprintf("failed to create ssh dir: %q", err)) + } + sshConfig, err := os.Create(filepath.Join(homeDir, ".ssh", "config")) + if err != nil { + Fail(fmt.Sprintf("failed to create ssh config: %q", err)) + } + if _, err := sshConfig.WriteString("IdentitiesOnly=yes"); err != nil { + Fail(fmt.Sprintf("failed to write ssh config: %q", err)) + } + if err := sshConfig.Close(); err != nil { + Fail(fmt.Sprintf("unable to close ssh config file descriptor: %q", err)) + } + if err := os.Setenv("HOME", homeDir); err != nil { + Fail("failed to set home dir") + } + if err := os.Unsetenv("SSH_AUTH_SOCK"); err != nil { + Fail("unable to unset SSH_AUTH_SOCK") + } + mb, err := newMB() + if err != nil { + Fail(fmt.Sprintf("failed to create machine test: %q", err)) + } + f, err := os.Open(fqImageName) + if err != nil { + Fail(fmt.Sprintf("failed to open file %s: %q", fqImageName, err)) + } + mb.imagePath = filepath.Join(homeDir, suiteImageName) + n, err := os.Create(mb.imagePath) + if err != nil { + Fail(fmt.Sprintf("failed to create file %s: %q", mb.imagePath, err)) + } + if _, err := io.Copy(n, f); err != nil { + Fail(fmt.Sprintf("failed to copy %ss to %s: %q", fqImageName, mb.imagePath, err)) + } + return homeDir, mb +} + +func teardown(origHomeDir string, testDir string, mb *machineTestBuilder) { + s := new(stopMachine) + for _, name := range mb.names { + if _, err := mb.setName(name).setCmd(s).run(); err != nil { + fmt.Printf("error occured rm'ing machine: %q\n", err) + } + } + if err := os.RemoveAll(testDir); err != nil { + Fail(fmt.Sprintf("failed to remove test dir: %q", err)) + } + // this needs to be last in teardown + if err := os.Setenv("HOME", origHomeDir); err != nil { + Fail("failed to set home dir") + } +} diff --git a/pkg/machine/e2e/rm_test.go b/pkg/machine/e2e/rm_test.go new file mode 100644 index 000000000..011da5dde --- /dev/null +++ b/pkg/machine/e2e/rm_test.go @@ -0,0 +1,67 @@ +package e2e + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("podman machine rm", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("bad init name", func() { + i := rmMachine{} + reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + session, err := mb.setName(reallyLongName).setCmd(&i).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(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)) + rm := rmMachine{} + _, err = mb.setCmd(rm.withForce()).run() + Expect(err).To(BeNil()) + + // Inspecting a non-existent machine should fail + // which means it is gone + _, ec, err := mb.toQemuInspectInfo() + Expect(err).To(BeNil()) + Expect(ec).To(Equal(125)) + }) + + It("Remove 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)) + 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)) + + // Removing again with force + stopAgain, err := mb.setCmd(rm.withForce()).run() + Expect(err).To(BeNil()) + Expect(stopAgain.ExitCode()).To(BeZero()) + + // Inspect to be dead sure + _, ec, err := mb.toQemuInspectInfo() + Expect(err).To(BeNil()) + Expect(ec).To(Equal(125)) + }) +}) diff --git a/pkg/machine/e2e/ssh_test.go b/pkg/machine/e2e/ssh_test.go new file mode 100644 index 000000000..90296fa10 --- /dev/null +++ b/pkg/machine/e2e/ssh_test.go @@ -0,0 +1,59 @@ +package e2e + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("podman machine ssh", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("bad machine name", func() { + name := randomString(12) + ssh := sshMachine{} + session, err := mb.setName(name).setCmd(ssh).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(125)) + // TODO seems like stderr is not being returned; re-enabled when fixed + //Expect(session.outputToString()).To(ContainSubstring("not exist")) + }) + + It("ssh to non-running machine", func() { + name := randomString(12) + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(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)) + }) + + It("ssh to running machine and check os-type", func() { + name := randomString(12) + 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)) + + 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.outputToString()).To(ContainSubstring("Fedora CoreOS")) + }) +}) diff --git a/pkg/machine/e2e/start_test.go b/pkg/machine/e2e/start_test.go new file mode 100644 index 000000000..1cda0e8f1 --- /dev/null +++ b/pkg/machine/e2e/start_test.go @@ -0,0 +1,36 @@ +package e2e + +import ( + "github.com/containers/podman/v4/pkg/machine" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("podman machine start", func() { + var ( + mb *machineTestBuilder + testDir string + ) + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("start simple machine", func() { + i := new(initMachine) + session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(0)) + s := new(startMachine) + startSession, err := mb.setCmd(s).run() + Expect(err).To(BeNil()) + Expect(startSession.ExitCode()).To(Equal(0)) + + info, ec, err := mb.toQemuInspectInfo() + Expect(err).To(BeNil()) + Expect(ec).To(BeZero()) + Expect(info[0].State).To(Equal(machine.Running)) + }) +}) diff --git a/pkg/machine/e2e/stop_test.go b/pkg/machine/e2e/stop_test.go new file mode 100644 index 000000000..5dee6a345 --- /dev/null +++ b/pkg/machine/e2e/stop_test.go @@ -0,0 +1,46 @@ +package e2e + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("podman machine stop", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("stop bad name", func() { + i := stopMachine{} + reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + session, err := mb.setName(reallyLongName).setCmd(&i).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(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)) + + 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)) + + // Stopping it again should not result in an error + stopAgain, err := mb.setCmd(stop).run() + Expect(err).To(BeNil()) + Expect(stopAgain.ExitCode()).To(BeZero()) + }) +}) diff --git a/pkg/machine/fcos.go b/pkg/machine/fcos.go index 872ca889e..df58b8a1e 100644 --- a/pkg/machine/fcos.go +++ b/pkg/machine/fcos.go @@ -43,7 +43,7 @@ type FcosDownload struct { } func NewFcosDownloader(vmType, vmName, imageStream string) (DistributionDownload, error) { - info, err := getFCOSDownload(imageStream) + info, err := GetFCOSDownload(imageStream) if err != nil { return nil, err } @@ -79,7 +79,7 @@ func (f FcosDownload) Get() *Download { return &f.Download } -type fcosDownloadInfo struct { +type FcosDownloadInfo struct { CompressionType string Location string Release string @@ -139,7 +139,7 @@ func getStreamURL(streamType string) url2.URL { // This should get Exported and stay put as it will apply to all fcos downloads // getFCOS parses fedoraCoreOS's stream and returns the image download URL and the release version -func getFCOSDownload(imageStream string) (*fcosDownloadInfo, error) { // nolint:staticcheck,unparam +func GetFCOSDownload(imageStream string) (*FcosDownloadInfo, error) { //nolint:staticcheck var ( fcosstable stream.Stream altMeta release.Release @@ -150,8 +150,8 @@ func getFCOSDownload(imageStream string) (*fcosDownloadInfo, error) { // nolint: // fcos trees, we should remove it and re-release at least on // macs. // TODO: remove when podman4.0 is in coreos - // nolint:staticcheck - imageStream = "podman-testing" + + imageStream = "podman-testing" //nolint:staticcheck switch imageStream { case "podman-testing": @@ -194,7 +194,7 @@ func getFCOSDownload(imageStream string) (*fcosDownloadInfo, error) { // nolint: } disk := qcow2.Disk - return &fcosDownloadInfo{ + return &FcosDownloadInfo{ Location: disk.Location, Sha256Sum: disk.Sha256, CompressionType: "xz", @@ -228,7 +228,7 @@ func getFCOSDownload(imageStream string) (*fcosDownloadInfo, error) { // nolint: if disk == nil { return nil, fmt.Errorf("unable to pull VM image: no disk in stream") } - return &fcosDownloadInfo{ + return &FcosDownloadInfo{ Location: disk.Location, Release: qemu.Release, Sha256Sum: disk.Sha256, diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index c57fa32fb..69a986102 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -76,7 +76,6 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { return nil, err } vm.IgnitionFilePath = *ignitionFile - imagePath, err := NewMachineFile(opts.ImagePath, nil) if err != nil { return nil, err @@ -373,7 +372,6 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { if err := v.writeConfig(); err != nil { return false, fmt.Errorf("writing JSON file: %w", err) } - // User has provided ignition file so keygen // will be skipped. if len(opts.IgnitionPath) < 1 { @@ -387,7 +385,6 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { if err := v.prepare(); err != nil { return false, err } - originalDiskSize, err := getDiskSize(v.getImageFile()) if err != nil { return false, err @@ -514,17 +511,28 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { time.Sleep(wait) wait++ } + defer qemuSocketConn.Close() if err != nil { return err } - fd, err := qemuSocketConn.(*net.UnixConn).File() if err != nil { return err } + defer fd.Close() + dnr, err := os.OpenFile("/dev/null", os.O_RDONLY, 0755) + if err != nil { + return err + } + defer dnr.Close() + dnw, err := os.OpenFile("/dev/null", os.O_WRONLY, 0755) + if err != nil { + return err + } + defer dnw.Close() attr := new(os.ProcAttr) - files := []*os.File{os.Stdin, os.Stdout, os.Stderr, fd} + files := []*os.File{dnr, dnw, dnw, fd} attr.Files = files logrus.Debug(v.CmdLine) cmd := v.CmdLine @@ -552,7 +560,7 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { } _, err = os.StartProcess(cmd[0], cmd, attr) if err != nil { - return err + return errors.Wrapf(err, "unable to execute %q", cmd) } } fmt.Println("Waiting for VM ...") @@ -575,11 +583,11 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { if err != nil { return err } + defer conn.Close() _, err = bufio.NewReader(conn).ReadString('\n') if err != nil { return err } - if len(v.Mounts) > 0 { state, err := v.State() if err != nil { @@ -918,7 +926,7 @@ func (v *MachineVM) SSH(_ string, opts machine.SSHOptions) error { sshDestination := username + "@localhost" port := strconv.Itoa(v.Port) - args := []string{"-i", v.IdentityPath, "-p", port, sshDestination, "-o", "UserKnownHostsFile /dev/null", "-o", "StrictHostKeyChecking no"} + args := []string{"-i", v.IdentityPath, "-p", port, sshDestination, "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no"} if len(opts.Args) > 0 { args = append(args, opts.Args...) } else { @@ -1085,9 +1093,19 @@ func (v *MachineVM) startHostNetworking() (string, apiForwardingState, error) { } attr := new(os.ProcAttr) - // Pass on stdin, stdout, stderr - files := []*os.File{os.Stdin, os.Stdout, os.Stderr} - attr.Files = files + dnr, err := os.OpenFile("/dev/null", os.O_RDONLY, 0755) + if err != nil { + return "", noForwarding, err + } + dnw, err := os.OpenFile("/dev/null", os.O_WRONLY, 0755) + if err != nil { + return "", noForwarding, err + } + + defer dnr.Close() + defer dnw.Close() + + attr.Files = []*os.File{dnr, dnw, dnw} cmd := []string{binary} cmd = append(cmd, []string{"-listen-qemu", fmt.Sprintf("unix://%s", v.QMPMonitor.Address.GetPath()), "-pid-file", v.PidFilePath.GetPath()}...) // Add the ssh port @@ -1104,7 +1122,7 @@ func (v *MachineVM) startHostNetworking() (string, apiForwardingState, error) { fmt.Println(cmd) } _, err = os.StartProcess(cmd[0], cmd, attr) - return forwardSock, state, err + return forwardSock, state, errors.Wrapf(err, "unable to execute: %q", cmd) } func (v *MachineVM) setupAPIForwarding(cmd []string) ([]string, string, apiForwardingState) { -- cgit v1.2.3-54-g00ecf