Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add machine init --playbook #25043

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 := "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
9 changes: 9 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 @@ -88,6 +90,13 @@ Memory (in MiB). Note: 1024MiB = 1GiB.

Start the virtual machine immediately after it has been initialized.

#### **--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 `--playbook` flag will require the image to include Ansible. The default image provided will have Ansible included.

#### **--rootful**

Whether this machine prefers rootful (`true`) or rootless (`false`)
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
--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, "--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
45 changes: 45 additions & 0 deletions pkg/machine/ignition/ignition.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,51 @@ 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("Unit", "ConditionFirstBoot", "yes")
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
37 changes: 37 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,16 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDe
}
}

isFirstBoot, err := mc.IsFirstBoot()
if err != nil {
logrus.Error(err)
}
if mp.VMType() == machineDefine.WSLVirt && mc.Ansible != nil && isFirstBoot {
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