diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index 69afdc02c6..a8f89e348e 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -62,6 +62,10 @@ func init() { ) _ = initCmd.RegisterFlagCompletionFunc(cpusFlagName, completion.AutocompleteNone) + runPlaybookFlagName := "run-playbook" + flags.StringVar(&initOpts.PlaybookPath, runPlaybookFlagName, "", "Run an Ansible playbook after first boot") + _ = initCmd.RegisterFlagCompletionFunc(runPlaybookFlagName, completion.AutocompleteNone) + diskSizeFlagName := "disk-size" flags.Uint64Var( &initOpts.DiskSize, diff --git a/docs/source/markdown/podman-machine-init.1.md.in b/docs/source/markdown/podman-machine-init.1.md.in index af603e0d4e..472549b83a 100644 --- a/docs/source/markdown/podman-machine-init.1.md.in +++ b/docs/source/markdown/podman-machine-init.1.md.in @@ -96,6 +96,13 @@ if there is no existing remote connection configurations. API forwarding, if available, follows this setting. +### **--run-playbook** + +Add the provided Ansible playbook to the machine and execute it after the first boot. + +Note: The playbook will be executed with the same privileges given to the user in the virtual machine + + #### **--timezone** Set the timezone for the machine and containers. Valid values are `local` or diff --git a/pkg/machine/define/initopts.go b/pkg/machine/define/initopts.go index 4ddf87ca19..b1ccaaa6b7 100644 --- a/pkg/machine/define/initopts.go +++ b/pkg/machine/define/initopts.go @@ -3,6 +3,7 @@ package define import "net/url" type InitOptions struct { + PlaybookPath string CPUS uint64 DiskSize uint64 IgnitionPath string diff --git a/pkg/machine/e2e/config_init_test.go b/pkg/machine/e2e/config_init_test.go index 0423010477..0c600c5d46 100644 --- a/pkg/machine/e2e/config_init_test.go +++ b/pkg/machine/e2e/config_init_test.go @@ -11,19 +11,21 @@ import ( type initMachine struct { /* - --cpus uint Number of CPUs (default 1) - --disk-size uint Disk size in GiB (default 100) - --ignition-path string Path to ignition file - --username string Username of the remote user (default "core" for FCOS, "user" for Fedora) - --image-path string Path to bootable image (default "testing") - -m, --memory uint Memory in MiB (default 2048) - --now Start machine now - --rootful Whether this machine should prefer rootful container execution - --timezone string Set timezone (default "local") - -v, --volume stringArray Volumes to mount, source:target - --volume-driver string Optional volume driver + --cpus uint Number of CPUs (default 1) + --disk-size uint Disk size in GiB (default 100) + --ignition-path string Path to ignition file + --username string Username of the remote user (default "core" for FCOS, "user" for Fedora) + --image-path string Path to bootable image (default "testing") + -m, --memory uint Memory in MiB (default 2048) + --now Start machine now + --rootful Whether this machine should prefer rootful container execution + --run-playbook string Run an ansible playbook after first boot + --timezone string Set timezone (default "local") + -v, --volume stringArray Volumes to mount, source:target + --volume-driver string Optional volume driver */ + playbook string cpus *uint diskSize *uint ignitionPath string @@ -73,6 +75,9 @@ func (i *initMachine) buildCmd(m *machineTestBuilder) []string { if i.rootful { cmd = append(cmd, "--rootful") } + if l := len(i.playbook); l > 0 { + cmd = append(cmd, "--run-playbook", i.playbook) + } if i.userModeNetworking { cmd = append(cmd, "--user-mode-networking") } @@ -152,6 +157,11 @@ func (i *initMachine) withRootful(r bool) *initMachine { return i } +func (i *initMachine) withRunPlaybook(p string) *initMachine { + i.playbook = p + return i +} + func (i *initMachine) withUserModeNetworking(r bool) *initMachine { //nolint:unused i.userModeNetworking = r return i diff --git a/pkg/machine/e2e/init_test.go b/pkg/machine/e2e/init_test.go index bd5fe683c5..6071d60422 100644 --- a/pkg/machine/e2e/init_test.go +++ b/pkg/machine/e2e/init_test.go @@ -3,6 +3,7 @@ package e2e_test import ( "fmt" "os" + "os/exec" "path/filepath" "runtime" "strconv" @@ -98,6 +99,59 @@ var _ = Describe("podman machine init", func() { } }) + It("run playbook", func() { + str := randomString() + + // ansible playbook file to create a text file containing a random string + playbookContents := fmt.Sprintf(` +- name: Simple podman machine example + hosts: localhost + tasks: + - name: create a file + ansible.builtin.copy: + dest: /home/core/foobar.txt + content: "%s\n" +`, str) + + tmpDir, err := os.MkdirTemp("", "") + defer func() { _ = utils.GuardedRemoveAll(tmpDir) }() + Expect(err).ToNot(HaveOccurred()) + + // create the playbook file + playbookFile, err := os.Create(filepath.Join(tmpDir, "playbook.yaml")) + Expect(err).ToNot(HaveOccurred()) + + // write the desired contents into the file + _, err = playbookFile.WriteString(playbookContents) + Expect(err).To(Not(HaveOccurred())) + + name := randomString() + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withRunPlaybook(filepath.Join(tmpDir, "playbook.yaml")).withNow()).run() + Expect(err).ToNot(HaveOccurred()) + Expect(session).To(Exit(0)) + + // calculate sha256sum of local playbook file + cmd := exec.Command("sha256sum", filepath.Join(tmpDir, "playbook.yaml")) + shasum, err := cmd.Output() + Expect(err).ToNot(HaveOccurred()) + + // calculate the sha256sum of the playbook file on the guest + ssh := &sshMachine{} + sshSession, err := mb.setName(name).setCmd(ssh.withSSHCommand([]string{"sha256sum", "playbook"})).run() + Expect(err).ToNot(HaveOccurred()) + + // compare the two and make sure they are the same + Expect(strings.Split(string(shasum), " ")[0]).To(Equal(strings.Split(sshSession.outputToString(), " ")[0])) + + // output the contents of the file generated by the playbook + // sshSession, err = mb.setName(name).setCmd(ssh.withSSHCommand([]string{"cat", "foobar.txt"})).run() + // Expect(err).ToNot(HaveOccurred()) + + // check its the same as the random number or string that we generated + // Expect(sshSession.outputToString()).To(Equal(str)) + }) + It("simple init with start", func() { i := initMachine{} session, err := mb.setCmd(i.withImage(mb.imagePath)).run() diff --git a/pkg/machine/ignition/ignition.go b/pkg/machine/ignition/ignition.go index 62bf7a872f..00a794eba0 100644 --- a/pkg/machine/ignition/ignition.go +++ b/pkg/machine/ignition/ignition.go @@ -685,6 +685,56 @@ done ` } +func (i *IgnitionBuilder) AddPlaybook(input *os.File, destPath string, username string) error { + // read the config file to a string + s, err := os.ReadFile(input.Name()) + if err != nil { + return err + } + + // create the ignition file object + f := File{ + Node: Node{ + Group: GetNodeGrp(username), + Path: destPath, + User: GetNodeUsr(username), + }, + FileEmbedded1: FileEmbedded1{ + Append: nil, + Contents: Resource{ + Source: EncodeDataURLPtr(string(s)), + }, + Mode: IntToPtr(0744), + }, + } + + // call ignitionBuilder.WithFile + // add the config file to the ignition object + i.WithFile(f) + + unit := parser.NewUnitFile() + unit.Add("Unit", "After", "ready.service") + unit.Add("Service", "Type", "oneshot") + unit.Add("Service", "User", *GetNodeUsr(username).Name) + unit.Add("Service", "Group", *GetNodeGrp(username).Name) + unit.Add("Service", "ExecStart", fmt.Sprintf("ansible-playbook %s", destPath)) + unit.Add("Install", "WantedBy", "default.target") + unitContents, err := unit.ToString() + if err != nil { + return err + } + + // create a systemd service + playbookUnit := Unit{ + Enabled: BoolToPtr(true), + Name: "playbook.service", + Contents: &unitContents, + } + i.WithUnit(playbookUnit) + + return nil +} + func GetNetRecoveryUnitFile() *parser.UnitFile { recoveryUnit := parser.NewUnitFile() recoveryUnit.Add("Unit", "Description", "Verifies health of network and recovers if necessary") diff --git a/pkg/machine/shim/host.go b/pkg/machine/shim/host.go index b0daa19ba9..9126db48fe 100644 --- a/pkg/machine/shim/host.go +++ b/pkg/machine/shim/host.go @@ -207,6 +207,19 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error { } } + if len(opts.PlaybookPath) > 0 { + f, err := os.Open(opts.PlaybookPath) + if err != nil { + return err + } + + playbookDest := fmt.Sprintf("/home/%s/%s", userName, "playbook") + err = ignBuilder.AddPlaybook(f, playbookDest, userName) + if err != nil { + return err + } + } + readyIgnOpts, err := mp.PrepareIgnition(mc, &ignBuilder) if err != nil { return err