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 20, 2025
1 parent a3bb0a1 commit fe5ddda
Show file tree
Hide file tree
Showing 7 changed files with 140 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.AutocompleteDefault)

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. The playbook provided cannot include other files from the host system, as they will not be copied.


#### **--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
43 changes: 43 additions & 0 deletions pkg/machine/e2e/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,49 @@ 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)

playbookPath := filepath.Join(GinkgoT().TempDir(), "playbook.yaml")

// create the playbook file
playbookFile, err := os.Create(playbookPath)
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(playbookPath).withNow()).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(0))

// ensure the contents of the playbook file didn't change when getting copied
ssh := new(sshMachine)
sshSession, err := mb.setName(name).setCmd(ssh.withSSHCommand([]string{"cat", "playbook.yaml"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(sshSession).To(Exit(0))
Expect(sshSession.outputToStringSlice()).To(Equal(strings.Split(playbookContents, "\n")))

// 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
51 changes: 51 additions & 0 deletions pkg/machine/ignition/ignition.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package ignition
import (
"encoding/json"
"fmt"
"io"
"io/fs"
"net/url"
"os"
Expand Down Expand Up @@ -685,6 +686,56 @@ done
`
}

func (i *IgnitionBuilder) AddPlaybook(input *os.File, destPath string, username string) error {
// read the config file to a string
s, err := io.ReadAll(input)
if err != nil {
return fmt.Errorf("read playbook: %w", 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", username)
unit.Add("Service", "Group", username)
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.yaml")
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 fe5ddda

Please sign in to comment.