Skip to content

Commit e1a5c0c

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 be5d807 commit e1a5c0c

File tree

10 files changed

+201
-13
lines changed

10 files changed

+201
-13
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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,73 @@ 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: ~/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+
// wait until the playbook.service is done before checking to make sure the playbook was a success
138+
playbookFinished := false
139+
for range 900 {
140+
sshSession, err = mb.setName(name).setCmd(ssh.withSSHCommand([]string{"systemctl", "is-active", "playbook.service"})).run()
141+
Expect(err).ToNot(HaveOccurred())
142+
143+
if sshSession.outputToString() == "inactive" {
144+
playbookFinished = true
145+
break
146+
}
147+
148+
time.Sleep(10 * time.Millisecond)
149+
}
150+
151+
if !playbookFinished {
152+
Fail("playbook.service did not finish")
153+
}
154+
155+
// output the contents of the file generated by the playbook
156+
guestUser := "core"
157+
if isWSL() {
158+
guestUser = "user"
159+
}
160+
sshSession, err = mb.setName(name).setCmd(ssh.withSSHCommand([]string{"cat", fmt.Sprintf("/home/%s/foobar.txt", guestUser)})).run()
161+
Expect(err).ToNot(HaveOccurred())
162+
Expect(sshSession).To(Exit(0))
163+
164+
// check its the same as the random number or string that we generated
165+
Expect(sshSession.outputToString()).To(Equal(str))
166+
})
167+
101168
It("simple init with start", func() {
102169
i := initMachine{}
103170
session, err := mb.setCmd(i.withImage(mb.imagePath)).run()

pkg/machine/ignition/ignition.go

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

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

pkg/machine/shim/host.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"errors"
66
"fmt"
7+
"io"
78
"os"
89
"path/filepath"
910
"runtime"
@@ -207,6 +208,32 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
207208
}
208209
}
209210

211+
if len(opts.PlaybookPath) > 0 {
212+
f, err := os.Open(opts.PlaybookPath)
213+
if err != nil {
214+
return err
215+
}
216+
s, err := io.ReadAll(f)
217+
if err != nil {
218+
return fmt.Errorf("read playbook: %w", err)
219+
}
220+
221+
playbookDest := fmt.Sprintf("/home/%s/%s", userName, "playbook.yaml")
222+
223+
if mp.VMType() != machineDefine.WSLVirt {
224+
err = ignBuilder.AddPlaybook(string(s), playbookDest, userName)
225+
if err != nil {
226+
return err
227+
}
228+
}
229+
230+
mc.Ansible = &vmconfigs.AnsibleConfig{
231+
PlaybookPath: playbookDest,
232+
Contents: string(s),
233+
User: userName,
234+
}
235+
}
236+
210237
readyIgnOpts, err := mp.PrepareIgnition(mc, &ignBuilder)
211238
if err != nil {
212239
return err
@@ -543,6 +570,16 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDe
543570
}
544571
}
545572

573+
isFirstBoot, err := mc.IsFirstBoot()
574+
if err != nil {
575+
logrus.Error(err)
576+
}
577+
if mp.VMType() == machineDefine.WSLVirt && mc.Ansible != nil && isFirstBoot {
578+
if err := machine.CommonSSHSilent(mc.Ansible.User, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"ansible-playbook", mc.Ansible.PlaybookPath}); err != nil {
579+
logrus.Error(err)
580+
}
581+
}
582+
546583
// Provider is responsible for waiting
547584
if mp.UseProviderNetworkSetup() {
548585
return nil

pkg/machine/vmconfigs/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ type MachineConfig struct {
5353
Starting bool
5454

5555
Rosetta bool
56+
57+
Ansible *AnsibleConfig
5658
}
5759

5860
type machineImage interface { //nolint:unused
@@ -148,3 +150,9 @@ type VMStats struct {
148150
// LastUp contains the last recorded uptime
149151
LastUp time.Time
150152
}
153+
154+
type AnsibleConfig struct {
155+
PlaybookPath string
156+
Contents string
157+
User string
158+
}

pkg/machine/wsl/machine.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func createKeys(mc *vmconfigs.MachineConfig, dist string) error {
148148
return nil
149149
}
150150

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

170+
if ansibleConfig != nil {
171+
if err := wslPipe(ansibleConfig.Contents, dist, "sh", "-c", fmt.Sprintf("cat > %s", ansibleConfig.PlaybookPath)); err != nil {
172+
return fmt.Errorf("could not generate playbook file for guest os: %w", err)
173+
}
174+
}
175+
170176
lingerCmd := withUser("cat > /home/[USER]/.config/systemd/[USER]/linger-example.service", user)
171177
if err := wslPipe(lingerService, dist, "sh", "-c", lingerCmd); err != nil {
172178
return fmt.Errorf("could not generate linger service for guest OS: %w", err)

pkg/machine/wsl/stubber.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (w WSLStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConf
6868
}
6969

7070
fmt.Println("Configuring system...")
71-
if err = configureSystem(mc, dist); err != nil {
71+
if err = configureSystem(mc, dist, mc.Ansible); err != nil {
7272
return err
7373
}
7474

0 commit comments

Comments
 (0)