Skip to content

Commit d1e4b61

Browse files
committed
Add machine init --run-playbook
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]>
1 parent a3bb0a1 commit d1e4b61

File tree

7 files changed

+150
-11
lines changed

7 files changed

+150
-11
lines changed

cmd/podman/machine/init.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ func init() {
6262
)
6363
_ = initCmd.RegisterFlagCompletionFunc(cpusFlagName, completion.AutocompleteNone)
6464

65+
runPlaybookFlagName := "run-playbook"
66+
flags.StringVar(&initOpts.PlaybookPath, runPlaybookFlagName, "", "Run an Ansible playbook after first boot")
67+
_ = initCmd.RegisterFlagCompletionFunc(runPlaybookFlagName, completion.AutocompleteNone)
68+
6569
diskSizeFlagName := "disk-size"
6670
flags.Uint64Var(
6771
&initOpts.DiskSize,

docs/source/markdown/podman-machine-init.1.md.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ if there is no existing remote connection configurations.
9696

9797
API forwarding, if available, follows this setting.
9898

99+
### **--run-playbook**
100+
101+
Add the provided Ansible playbook to the machine and execute it after the first boot.
102+
103+
Note: The playbook will be executed with the same privileges given to the user in the virtual machine
104+
105+
99106
#### **--timezone**
100107

101108
Set the timezone for the machine and containers. Valid values are `local` or

pkg/machine/define/initopts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package define
33
import "net/url"
44

55
type InitOptions struct {
6+
PlaybookPath string
67
CPUS uint64
78
DiskSize uint64
89
IgnitionPath string

pkg/machine/e2e/config_init_test.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,21 @@ import (
1111

1212
type initMachine struct {
1313
/*
14-
--cpus uint Number of CPUs (default 1)
15-
--disk-size uint Disk size in GiB (default 100)
16-
--ignition-path string Path to ignition file
17-
--username string Username of the remote user (default "core" for FCOS, "user" for Fedora)
18-
--image-path string Path to bootable image (default "testing")
19-
-m, --memory uint Memory in MiB (default 2048)
20-
--now Start machine now
21-
--rootful Whether this machine should prefer rootful container execution
22-
--timezone string Set timezone (default "local")
23-
-v, --volume stringArray Volumes to mount, source:target
24-
--volume-driver string Optional volume driver
14+
--cpus uint Number of CPUs (default 1)
15+
--disk-size uint Disk size in GiB (default 100)
16+
--ignition-path string Path to ignition file
17+
--username string Username of the remote user (default "core" for FCOS, "user" for Fedora)
18+
--image-path string Path to bootable image (default "testing")
19+
-m, --memory uint Memory in MiB (default 2048)
20+
--now Start machine now
21+
--rootful Whether this machine should prefer rootful container execution
22+
--run-playbook string Run an ansible playbook after first boot
23+
--timezone string Set timezone (default "local")
24+
-v, --volume stringArray Volumes to mount, source:target
25+
--volume-driver string Optional volume driver
2526
2627
*/
28+
playbook string
2729
cpus *uint
2830
diskSize *uint
2931
ignitionPath string
@@ -73,6 +75,9 @@ func (i *initMachine) buildCmd(m *machineTestBuilder) []string {
7375
if i.rootful {
7476
cmd = append(cmd, "--rootful")
7577
}
78+
if l := len(i.playbook); l > 0 {
79+
cmd = append(cmd, "--run-playbook", i.playbook)
80+
}
7681
if i.userModeNetworking {
7782
cmd = append(cmd, "--user-mode-networking")
7883
}
@@ -152,6 +157,11 @@ func (i *initMachine) withRootful(r bool) *initMachine {
152157
return i
153158
}
154159

160+
func (i *initMachine) withRunPlaybook(p string) *initMachine {
161+
i.playbook = p
162+
return i
163+
}
164+
155165
func (i *initMachine) withUserModeNetworking(r bool) *initMachine { //nolint:unused
156166
i.userModeNetworking = r
157167
return i

pkg/machine/e2e/init_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package e2e_test
33
import (
44
"fmt"
55
"os"
6+
"os/exec"
67
"path/filepath"
78
"runtime"
89
"strconv"
@@ -98,6 +99,59 @@ var _ = Describe("podman machine init", func() {
9899
}
99100
})
100101

102+
It("run playbook", func() {
103+
str := randomString()
104+
105+
// ansible playbook file to create a text file containing a random string
106+
playbookContents := fmt.Sprintf(`
107+
- name: Simple podman machine example
108+
hosts: localhost
109+
tasks:
110+
- name: create a file
111+
ansible.builtin.copy:
112+
dest: /home/core/foobar.txt
113+
content: "%s\n"
114+
`, str)
115+
116+
tmpDir, err := os.MkdirTemp("", "")
117+
defer func() { _ = utils.GuardedRemoveAll(tmpDir) }()
118+
Expect(err).ToNot(HaveOccurred())
119+
120+
// create the playbook file
121+
playbookFile, err := os.Create(filepath.Join(tmpDir, "playbook.yaml"))
122+
Expect(err).ToNot(HaveOccurred())
123+
124+
// write the desired contents into the file
125+
_, err = playbookFile.WriteString(playbookContents)
126+
Expect(err).To(Not(HaveOccurred()))
127+
128+
name := randomString()
129+
i := new(initMachine)
130+
session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withRunPlaybook(filepath.Join(tmpDir, "playbook.yaml")).withNow()).run()
131+
Expect(err).ToNot(HaveOccurred())
132+
Expect(session).To(Exit(0))
133+
134+
// calculate sha256sum of local playbook file
135+
cmd := exec.Command("sha256sum", filepath.Join(tmpDir, "playbook.yaml"))
136+
shasum, err := cmd.Output()
137+
Expect(err).ToNot(HaveOccurred())
138+
139+
// calculate the sha256sum of the playbook file on the guest
140+
ssh := &sshMachine{}
141+
sshSession, err := mb.setName(name).setCmd(ssh.withSSHCommand([]string{"sha256sum", "playbook"})).run()
142+
Expect(err).ToNot(HaveOccurred())
143+
144+
// compare the two and make sure they are the same
145+
Expect(strings.Split(string(shasum), " ")[0]).To(Equal(strings.Split(sshSession.outputToString(), " ")[0]))
146+
147+
// output the contents of the file generated by the playbook
148+
// sshSession, err = mb.setName(name).setCmd(ssh.withSSHCommand([]string{"cat", "foobar.txt"})).run()
149+
// Expect(err).ToNot(HaveOccurred())
150+
151+
// check its the same as the random number or string that we generated
152+
// Expect(sshSession.outputToString()).To(Equal(str))
153+
})
154+
101155
It("simple init with start", func() {
102156
i := initMachine{}
103157
session, err := mb.setCmd(i.withImage(mb.imagePath)).run()

pkg/machine/ignition/ignition.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,56 @@ done
685685
`
686686
}
687687

688+
func (i *IgnitionBuilder) AddPlaybook(input *os.File, destPath string, username string) error {
689+
// read the config file to a string
690+
s, err := os.ReadFile(input.Name())
691+
if err != nil {
692+
return err
693+
}
694+
695+
// create the ignition file object
696+
f := File{
697+
Node: Node{
698+
Group: GetNodeGrp(username),
699+
Path: destPath,
700+
User: GetNodeUsr(username),
701+
},
702+
FileEmbedded1: FileEmbedded1{
703+
Append: nil,
704+
Contents: Resource{
705+
Source: EncodeDataURLPtr(string(s)),
706+
},
707+
Mode: IntToPtr(0744),
708+
},
709+
}
710+
711+
// call ignitionBuilder.WithFile
712+
// add the config file to the ignition object
713+
i.WithFile(f)
714+
715+
unit := parser.NewUnitFile()
716+
unit.Add("Unit", "After", "ready.service")
717+
unit.Add("Service", "Type", "oneshot")
718+
unit.Add("Service", "User", *GetNodeUsr(username).Name)
719+
unit.Add("Service", "Group", *GetNodeGrp(username).Name)
720+
unit.Add("Service", "ExecStart", fmt.Sprintf("ansible-playbook %s", destPath))
721+
unit.Add("Install", "WantedBy", "default.target")
722+
unitContents, err := unit.ToString()
723+
if err != nil {
724+
return err
725+
}
726+
727+
// create a systemd service
728+
playbookUnit := Unit{
729+
Enabled: BoolToPtr(true),
730+
Name: "playbook.service",
731+
Contents: &unitContents,
732+
}
733+
i.WithUnit(playbookUnit)
734+
735+
return nil
736+
}
737+
688738
func GetNetRecoveryUnitFile() *parser.UnitFile {
689739
recoveryUnit := parser.NewUnitFile()
690740
recoveryUnit.Add("Unit", "Description", "Verifies health of network and recovers if necessary")

pkg/machine/shim/host.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,19 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
207207
}
208208
}
209209

210+
if len(opts.PlaybookPath) > 0 {
211+
f, err := os.Open(opts.PlaybookPath)
212+
if err != nil {
213+
return err
214+
}
215+
216+
playbookDest := fmt.Sprintf("/home/%s/%s", userName, "playbook")
217+
err = ignBuilder.AddPlaybook(f, playbookDest, userName)
218+
if err != nil {
219+
return err
220+
}
221+
}
222+
210223
readyIgnOpts, err := mp.PrepareIgnition(mc, &ignBuilder)
211224
if err != nil {
212225
return err

0 commit comments

Comments
 (0)