Skip to content

Commit ade327b

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 boot. Signed-off-by: Jake Correnti <[email protected]> Signed-off-by: Brent Baude <[email protected]>
1 parent a3bb0a1 commit ade327b

File tree

7 files changed

+144
-11
lines changed

7 files changed

+144
-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.AutocompleteDefault)
68+
6569
diskSizeFlagName := "disk-size"
6670
flags.Uint64Var(
6771
&initOpts.DiskSize,

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ is copied into the user's CONF_DIR and renamed. Additionally, no SSH keys are g
8080
Fully qualified registry, path, or URL to a VM image.
8181
Registry target must be in the form of `docker://registry/repo/image:version`.
8282

83+
Note: Only images provided by podman will be supported.
84+
8385
#### **--memory**, **-m**=*number*
8486

8587
Memory (in MiB). Note: 1024MiB = 1GiB.
@@ -96,6 +98,14 @@ if there is no existing remote connection configurations.
9698

9799
API forwarding, if available, follows this setting.
98100

101+
#### **--run-playbook**
102+
103+
Add the provided Ansible playbook to the machine and execute it after the first boot.
104+
105+
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.
106+
Use of the `--run-playbook` flag will require the image to include Ansible. The default image provided will have Ansible included.
107+
108+
99109
#### **--timezone**
100110

101111
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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,50 @@ var _ = Describe("podman machine init", func() {
9898
}
9999
})
100100

101+
It("run playbook", func() {
102+
str := randomString()
103+
104+
// ansible playbook file to create a text file containing a random string
105+
playbookContents := fmt.Sprintf(`- name: Simple podman machine example
106+
hosts: localhost
107+
tasks:
108+
- name: create a file
109+
ansible.builtin.copy:
110+
dest: /home/core/foobar.txt
111+
content: "%s\n"`, str)
112+
113+
playbookPath := filepath.Join(GinkgoT().TempDir(), "playbook.yaml")
114+
115+
// create the playbook file
116+
playbookFile, err := os.Create(playbookPath)
117+
Expect(err).ToNot(HaveOccurred())
118+
defer playbookFile.Close()
119+
120+
// write the desired contents into the file
121+
_, err = playbookFile.WriteString(playbookContents)
122+
Expect(err).To(Not(HaveOccurred()))
123+
124+
name := randomString()
125+
i := new(initMachine)
126+
session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withRunPlaybook(playbookPath).withNow()).run()
127+
Expect(err).ToNot(HaveOccurred())
128+
Expect(session).To(Exit(0))
129+
130+
// ensure the contents of the playbook file didn't change when getting copied
131+
ssh := new(sshMachine)
132+
sshSession, err := mb.setName(name).setCmd(ssh.withSSHCommand([]string{"cat", "playbook.yaml"})).run()
133+
Expect(err).ToNot(HaveOccurred())
134+
Expect(sshSession).To(Exit(0))
135+
Expect(sshSession.outputToStringSlice()).To(Equal(strings.Split(playbookContents, "\n")))
136+
137+
// output the contents of the file generated by the playbook
138+
sshSession, err = mb.setName(name).setCmd(ssh.withSSHCommand([]string{"cat", "foobar.txt"})).run()
139+
Expect(err).ToNot(HaveOccurred())
140+
141+
// check its the same as the random number or string that we generated
142+
Expect(sshSession.outputToString()).To(Equal(str))
143+
})
144+
101145
It("simple init with start", func() {
102146
i := initMachine{}
103147
session, err := mb.setCmd(i.withImage(mb.imagePath)).run()

pkg/machine/ignition/ignition.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package ignition
55
import (
66
"encoding/json"
77
"fmt"
8+
"io"
89
"io/fs"
910
"net/url"
1011
"os"
@@ -685,6 +686,56 @@ done
685686
`
686687
}
687688

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