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 boot.

Signed-off-by: Jake Correnti <[email protected]>
Signed-off-by: Brent Baude <[email protected]>
  • Loading branch information
jakecorrenti committed Jan 30, 2025
1 parent be5d807 commit 10753fa
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 13 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
10 changes: 10 additions & 0 deletions docs/source/markdown/podman-machine-init.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ is copied into the user's CONF_DIR and renamed. Additionally, no SSH keys are g
Fully qualified registry, path, or URL to a VM image.
Registry target must be in the form of `docker://registry/repo/image:version`.

Note: Only images provided by podman will be supported.

#### **--memory**, **-m**=*number*

Memory (in MiB). Note: 1024MiB = 1GiB.
Expand All @@ -96,6 +98,14 @@ 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.
Use of the `--run-playbook` flag will require the image to include Ansible. The default image provided will have Ansible included.


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

// 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")))

// wait until the playbook.service is done before checking to make sure the playbook was a success
playbookFinished := false
for range 900 {
sshSession, err = mb.setName(name).setCmd(ssh.withSSHCommand([]string{"systemctl", "is-active", "playbook.service"})).run()
Expect(err).ToNot(HaveOccurred())

if sshSession.outputToString() == "inactive" {
playbookFinished = true
break
}

time.Sleep(10 * time.Millisecond)
}

if !playbookFinished {
Fail("playbook.service did not finish")
}

// output the contents of the file generated by the playbook
guestUser := "core"
if isWSL() {
guestUser = "user"
}
sshSession, err = mb.setName(name).setCmd(ssh.withSSHCommand([]string{"cat", fmt.Sprintf("/home/%s/foobar.txt", guestUser)})).run()
Expect(err).ToNot(HaveOccurred())
Expect(sshSession).To(Exit(0))

// 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
44 changes: 44 additions & 0 deletions pkg/machine/ignition/ignition.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,50 @@ done
`
}

func (i *IgnitionBuilder) AddPlaybook(contents string, destPath string, username string) error {
// 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(contents),
},
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
33 changes: 33 additions & 0 deletions pkg/machine/shim/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -207,6 +208,32 @@ 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
}
s, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("read playbook: %w", err)
}

playbookDest := fmt.Sprintf("/home/%s/%s", userName, "playbook.yaml")

if mp.VMType() != machineDefine.WSLVirt {
err = ignBuilder.AddPlaybook(string(s), playbookDest, userName)
if err != nil {
return err
}
}

mc.Ansible = &vmconfigs.AnsibleConfig{
PlaybookPath: playbookDest,
Contents: string(s),
User: userName,
}
}

readyIgnOpts, err := mp.PrepareIgnition(mc, &ignBuilder)
if err != nil {
return err
Expand Down Expand Up @@ -543,6 +570,12 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDe
}
}

if mp.VMType() == machineDefine.WSLVirt && mc.Ansible != nil {
if err := machine.CommonSSHSilent(mc.Ansible.User, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"ansible-playbook", mc.Ansible.PlaybookPath}); err != nil {
logrus.Error(err)
}
}

// Provider is responsible for waiting
if mp.UseProviderNetworkSetup() {
return nil
Expand Down
8 changes: 8 additions & 0 deletions pkg/machine/vmconfigs/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type MachineConfig struct {
Starting bool

Rosetta bool

Ansible *AnsibleConfig
}

type machineImage interface { //nolint:unused
Expand Down Expand Up @@ -148,3 +150,9 @@ type VMStats struct {
// LastUp contains the last recorded uptime
LastUp time.Time
}

type AnsibleConfig struct {
PlaybookPath string
Contents string
User string
}
8 changes: 7 additions & 1 deletion pkg/machine/wsl/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func createKeys(mc *vmconfigs.MachineConfig, dist string) error {
return nil
}

func configureSystem(mc *vmconfigs.MachineConfig, dist string) error {
func configureSystem(mc *vmconfigs.MachineConfig, dist string, ansibleConfig *vmconfigs.AnsibleConfig) error {
user := mc.SSH.RemoteUsername
if err := wslInvoke(dist, "sh", "-c", fmt.Sprintf(appendPort, mc.SSH.Port, mc.SSH.Port)); err != nil {
return fmt.Errorf("could not configure SSH port for guest OS: %w", err)
Expand All @@ -167,6 +167,12 @@ func configureSystem(mc *vmconfigs.MachineConfig, dist string) error {
return fmt.Errorf("could not generate systemd-sysusers override for guest OS: %w", err)
}

if ansibleConfig != nil {
if err := wslPipe(ansibleConfig.Contents, dist, "sh", "-c", fmt.Sprintf("cat > %s", ansibleConfig.PlaybookPath)); err != nil {
return fmt.Errorf("could not generate playbook file for guest os: %w", err)
}
}

lingerCmd := withUser("cat > /home/[USER]/.config/systemd/[USER]/linger-example.service", user)
if err := wslPipe(lingerService, dist, "sh", "-c", lingerCmd); err != nil {
return fmt.Errorf("could not generate linger service for guest OS: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion pkg/machine/wsl/stubber.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (w WSLStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConf
}

fmt.Println("Configuring system...")
if err = configureSystem(mc, dist); err != nil {
if err = configureSystem(mc, dist, mc.Ansible); err != nil {
return err
}

Expand Down

0 comments on commit 10753fa

Please sign in to comment.