Skip to content

Commit

Permalink
Add machine init --run-playbook
Browse files Browse the repository at this point in the history
Allow the user to provide an Ansible playbook file on init which will
then be run on first boot.

Signed-off-by: Jake Correnti <[email protected]>
Signed-off-by: Brent Baude <[email protected]>
  • Loading branch information
jakecorrenti committed Jan 17, 2025
1 parent a3bb0a1 commit d1e4b61
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 11 deletions.
4 changes: 4 additions & 0 deletions cmd/podman/machine/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions docs/source/markdown/podman-machine-init.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pkg/machine/define/initopts.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package define
import "net/url"

type InitOptions struct {
PlaybookPath string
CPUS uint64
DiskSize uint64
IgnitionPath string
Expand Down
32 changes: 21 additions & 11 deletions pkg/machine/e2e/config_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions pkg/machine/e2e/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package e2e_test
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
Expand Down Expand Up @@ -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()
Expand Down
50 changes: 50 additions & 0 deletions pkg/machine/ignition/ignition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
13 changes: 13 additions & 0 deletions pkg/machine/shim/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit d1e4b61

Please sign in to comment.