From 5a8acb4a9b34e6306e0eda3c1dafc60a02c3ab23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Mon, 15 Apr 2024 22:53:44 +0200 Subject: [PATCH 1/4] Support ansible provision mode for remote playbook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/cidata/cidata.go | 2 + pkg/limayaml/limayaml.go | 2 + pkg/limayaml/validate.go | 5 ++- pkg/start/ansible.go | 66 ++++++++++++++++++++++++++++++++ pkg/start/start.go | 4 ++ pkg/store/filenames/filenames.go | 1 + 6 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 pkg/start/ansible.go diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go index 99f2e569232..d7e70fc9eb6 100644 --- a/pkg/cidata/cidata.go +++ b/pkg/cidata/cidata.go @@ -334,6 +334,8 @@ func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort }) case limayaml.ProvisionModeBoot: continue + case limayaml.ProvisionModeAnsible: + continue default: return fmt.Errorf("unknown provision mode %q", f.Mode) } diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index f7bb6da5761..1a2e5ce5a00 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -180,12 +180,14 @@ const ( ProvisionModeUser ProvisionMode = "user" ProvisionModeBoot ProvisionMode = "boot" ProvisionModeDependency ProvisionMode = "dependency" + ProvisionModeAnsible ProvisionMode = "ansible" ) type Provision struct { Mode ProvisionMode `yaml:"mode" json:"mode"` // default: "system" SkipDefaultDependencyResolution *bool `yaml:"skipDefaultDependencyResolution,omitempty" json:"skipDefaultDependencyResolution,omitempty"` Script string `yaml:"script" json:"script"` + Playbook string `yaml:"playbook,omitempty" json:"playbook,omitempty"` } type Containerd struct { diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index f6efeb69e38..a9506a0d59f 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -183,9 +183,10 @@ func Validate(y LimaYAML, warn bool) error { i, ProvisionModeDependency) } case ProvisionModeDependency: + case ProvisionModeAnsible: default: - return fmt.Errorf("field `provision[%d].mode` must one of %q, %q, %q, or %q", - i, ProvisionModeSystem, ProvisionModeUser, ProvisionModeBoot, ProvisionModeDependency) + return fmt.Errorf("field `provision[%d].mode` must one of %q, %q, %q, %q, or %q", + i, ProvisionModeSystem, ProvisionModeUser, ProvisionModeBoot, ProvisionModeDependency, ProvisionModeAnsible) } if strings.Contains(p.Script, "LIMA_CIDATA") { logrus.Warn("provisioning scripts should not reference the LIMA_CIDATA variables") diff --git a/pkg/start/ansible.go b/pkg/start/ansible.go new file mode 100644 index 00000000000..7d4b0130ac7 --- /dev/null +++ b/pkg/start/ansible.go @@ -0,0 +1,66 @@ +package start + +import ( + "context" + "os" + "os/exec" + "path/filepath" + + "github.com/goccy/go-yaml" + "github.com/lima-vm/lima/pkg/limayaml" + "github.com/lima-vm/lima/pkg/store" + "github.com/lima-vm/lima/pkg/store/filenames" + "github.com/sirupsen/logrus" +) + +func runAnsibleProvision(ctx context.Context, inst *store.Instance) error { + y, err := inst.LoadYAML() + if err != nil { + return err + } + for _, f := range y.Provision { + if f.Mode == limayaml.ProvisionModeAnsible { + logrus.Infof("Waiting for ansible playbook %q", f.Playbook) + if err := runAnsiblePlaybook(ctx, inst, f.Playbook); err != nil { + return err + } + } + } + return nil +} + +func runAnsiblePlaybook(ctx context.Context, inst *store.Instance, playbook string) error { + inventory, err := createInventory(inst) + if err != nil { + return err + } + logrus.Debugf("ansible-playbook -i %q %q", inventory, playbook) + args := []string{"-i", inventory, playbook} + cmd := exec.CommandContext(ctx, "ansible-playbook", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func createInventory(inst *store.Instance) (string, error) { + vars := map[string]interface{}{ + "ansible_connection": "ssh", + "ansible_host": "lima-" + inst.Name, + "ansible_ssh_common_args": "-F " + inst.SSHConfigFile, + } + hosts := map[string]interface{}{ + inst.Name: vars, + } + group := "lima" + data := map[string]interface{}{ + group: map[string]interface{}{ + "hosts": hosts, + }, + } + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + inventory := filepath.Join(inst.Dir, filenames.InventoryYAML) + return inventory, os.WriteFile(inventory, bytes, 0o644) +} diff --git a/pkg/start/start.go b/pkg/start/start.go index 33c8a7c81e4..ffcb0193a28 100644 --- a/pkg/start/start.go +++ b/pkg/start/start.go @@ -291,6 +291,10 @@ func watchHostAgentEvents(ctx context.Context, inst *store.Instance, haStdoutPat return true } + if xerr := runAnsibleProvision(ctx, inst); xerr != nil { + err = xerr + return true + } if *inst.Config.Plain { logrus.Infof("READY. Run `ssh -F %q lima-%s` to open the shell.", inst.SSHConfigFile, inst.Name) } else { diff --git a/pkg/store/filenames/filenames.go b/pkg/store/filenames/filenames.go index 6615b410feb..b5044e43939 100644 --- a/pkg/store/filenames/filenames.go +++ b/pkg/store/filenames/filenames.go @@ -56,6 +56,7 @@ const ( VzIdentifier = "vz-identifier" VzEfi = "vz-efi" // efi variable store QemuEfiCodeFD = "qemu-efi-code.fd" // efi code; not always created + InventoryYAML = "inventory.yaml" // ansible inventory // SocketDir is the default location for forwarded sockets with a relative paths in HostSocket SocketDir = "sock" From a4479851f4f1fa97d494b0a10fdccbd198df9e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Mon, 22 Apr 2024 08:59:35 +0200 Subject: [PATCH 2/4] Add some documentation for ansible provision mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- examples/default.yaml | 6 ++++++ website/content/en/docs/dev/Internals/_index.md | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/examples/default.yaml b/examples/default.yaml index 45cf27b4b9e..09e02da02ec 100644 --- a/examples/default.yaml +++ b/examples/default.yaml @@ -227,6 +227,12 @@ containerd: # #!/bin/bash # dnf config-manager --add-repo ... # dnf install ... +# # `ansible` is executed after other scripts are complete +# # It requires `ansible-playbook` command to be installed. +# # Environment variables such as ANSIBLE_CONFIG can be used, to control the behavior of the playbook execution. +# # See ansible docs, and `ansible-config`, for more info https://docs.ansible.com/ansible/latest/playbook_guide/ +# - mode: ansible +# playbook: playbook.yaml # Probe scripts to check readiness. # 🟢 Builtin default: null diff --git a/website/content/en/docs/dev/Internals/_index.md b/website/content/en/docs/dev/Internals/_index.md index bb4e3e520a3..83227b84ef0 100644 --- a/website/content/en/docs/dev/Internals/_index.md +++ b/website/content/en/docs/dev/Internals/_index.md @@ -37,6 +37,9 @@ Metadata: cloud-init: - `cidata.iso`: cloud-init ISO9660 image. See [`cidata.iso`](#cidataiso). +Ansible: +- `inventory.yaml`: the Ansible node inventory. See [ansible](#ansible). + disk: - `basedisk`: the base image - `diffdisk`: the diff image (QCOW2) @@ -137,6 +140,10 @@ The directory contains the following files: - `$QEMU_SYSTEM_ARM`: path of `qemu-system-arm` - Default: `qemu-system-arm` in `$PATH` +## Ansible +The instance directory contains an inventory file, that might be used with Ansible playbooks and commands. +See [Building Ansible inventories](https://docs.ansible.com/ansible/latest/inventory_guide/) about dynamic inventories. + ## `cidata.iso` `cidata.iso` contains the following files: From 45ede8010f3fd2998f1ded791629cde1dca2820a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Wed, 8 May 2024 12:41:12 +0200 Subject: [PATCH 3/4] Rename inventory.yaml to ansible-inventory.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/start/ansible.go | 6 +- pkg/store/filenames/filenames.go | 62 +++++++++---------- .../content/en/docs/dev/Internals/_index.md | 2 +- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/pkg/start/ansible.go b/pkg/start/ansible.go index 7d4b0130ac7..2b4b4ab8330 100644 --- a/pkg/start/ansible.go +++ b/pkg/start/ansible.go @@ -30,7 +30,7 @@ func runAnsibleProvision(ctx context.Context, inst *store.Instance) error { } func runAnsiblePlaybook(ctx context.Context, inst *store.Instance, playbook string) error { - inventory, err := createInventory(inst) + inventory, err := createAnsibleInventory(inst) if err != nil { return err } @@ -42,7 +42,7 @@ func runAnsiblePlaybook(ctx context.Context, inst *store.Instance, playbook stri return cmd.Run() } -func createInventory(inst *store.Instance) (string, error) { +func createAnsibleInventory(inst *store.Instance) (string, error) { vars := map[string]interface{}{ "ansible_connection": "ssh", "ansible_host": "lima-" + inst.Name, @@ -61,6 +61,6 @@ func createInventory(inst *store.Instance) (string, error) { if err != nil { return "", err } - inventory := filepath.Join(inst.Dir, filenames.InventoryYAML) + inventory := filepath.Join(inst.Dir, filenames.AnsibleInventoryYAML) return inventory, os.WriteFile(inventory, bytes, 0o644) } diff --git a/pkg/store/filenames/filenames.go b/pkg/store/filenames/filenames.go index b5044e43939..e75c0a393a3 100644 --- a/pkg/store/filenames/filenames.go +++ b/pkg/store/filenames/filenames.go @@ -26,37 +26,37 @@ const ( // Filenames that may appear under an instance directory const ( - LimaYAML = "lima.yaml" - LimaVersion = "lima-version" // Lima version used to create instance - CIDataISO = "cidata.iso" - CIDataISODir = "cidata" - BaseDisk = "basedisk" - DiffDisk = "diffdisk" - Kernel = "kernel" - KernelCmdline = "kernel.cmdline" - Initrd = "initrd" - QMPSock = "qmp.sock" - SerialLog = "serial.log" // default serial (ttyS0, but ttyAMA0 on qemu-system-{arm,aarch64}) - SerialSock = "serial.sock" - SerialPCILog = "serialp.log" // pci serial (ttyS0 on qemu-system-{arm,aarch64}) - SerialPCISock = "serialp.sock" - SerialVirtioLog = "serialv.log" // virtio serial - SerialVirtioSock = "serialv.sock" - SSHSock = "ssh.sock" - SSHConfig = "ssh.config" - VhostSock = "virtiofsd-%d.sock" - VNCDisplayFile = "vncdisplay" - VNCPasswordFile = "vncpassword" - GuestAgentSock = "ga.sock" - VirtioPort = "io.lima-vm.guest_agent.0" - HostAgentPID = "ha.pid" - HostAgentSock = "ha.sock" - HostAgentStdoutLog = "ha.stdout.log" - HostAgentStderrLog = "ha.stderr.log" - VzIdentifier = "vz-identifier" - VzEfi = "vz-efi" // efi variable store - QemuEfiCodeFD = "qemu-efi-code.fd" // efi code; not always created - InventoryYAML = "inventory.yaml" // ansible inventory + LimaYAML = "lima.yaml" + LimaVersion = "lima-version" // Lima version used to create instance + CIDataISO = "cidata.iso" + CIDataISODir = "cidata" + BaseDisk = "basedisk" + DiffDisk = "diffdisk" + Kernel = "kernel" + KernelCmdline = "kernel.cmdline" + Initrd = "initrd" + QMPSock = "qmp.sock" + SerialLog = "serial.log" // default serial (ttyS0, but ttyAMA0 on qemu-system-{arm,aarch64}) + SerialSock = "serial.sock" + SerialPCILog = "serialp.log" // pci serial (ttyS0 on qemu-system-{arm,aarch64}) + SerialPCISock = "serialp.sock" + SerialVirtioLog = "serialv.log" // virtio serial + SerialVirtioSock = "serialv.sock" + SSHSock = "ssh.sock" + SSHConfig = "ssh.config" + VhostSock = "virtiofsd-%d.sock" + VNCDisplayFile = "vncdisplay" + VNCPasswordFile = "vncpassword" + GuestAgentSock = "ga.sock" + VirtioPort = "io.lima-vm.guest_agent.0" + HostAgentPID = "ha.pid" + HostAgentSock = "ha.sock" + HostAgentStdoutLog = "ha.stdout.log" + HostAgentStderrLog = "ha.stderr.log" + VzIdentifier = "vz-identifier" + VzEfi = "vz-efi" // efi variable store + QemuEfiCodeFD = "qemu-efi-code.fd" // efi code; not always created + AnsibleInventoryYAML = "ansible-inventory.yaml" // SocketDir is the default location for forwarded sockets with a relative paths in HostSocket SocketDir = "sock" diff --git a/website/content/en/docs/dev/Internals/_index.md b/website/content/en/docs/dev/Internals/_index.md index 83227b84ef0..2a10d26b4a3 100644 --- a/website/content/en/docs/dev/Internals/_index.md +++ b/website/content/en/docs/dev/Internals/_index.md @@ -38,7 +38,7 @@ cloud-init: - `cidata.iso`: cloud-init ISO9660 image. See [`cidata.iso`](#cidataiso). Ansible: -- `inventory.yaml`: the Ansible node inventory. See [ansible](#ansible). +- `ansible-inventory.yaml`: the Ansible node inventory. See [ansible](#ansible). disk: - `basedisk`: the base image From f59b361dec1cd699b88de90083eb8c80b57f4321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Wed, 8 May 2024 19:40:11 +0200 Subject: [PATCH 4/4] Add an integration test for ansible provisioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- .github/workflows/test.yml | 4 ++++ hack/ansible-test.yaml | 6 ++++++ hack/test-templates.sh | 7 +++++++ hack/test-templates/test-misc.yaml | 4 ++++ 4 files changed, 21 insertions(+) create mode 100644 hack/ansible-test.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96f2c4f5d2e..240f6af07ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -242,6 +242,10 @@ jobs: sudo modprobe kvm # `sudo usermod -aG kvm $(whoami)` does not take an effect on GHA sudo chown $(whoami) /dev/kvm + - name: Install ansible-playbook + run: | + sudo apt-get install -y --no-install-recommends ansible + if: matrix.template == '../hack/test-templates/test-misc.yaml' - name: "Show cache" run: ./hack/debug-cache.sh - name: "Test" diff --git a/hack/ansible-test.yaml b/hack/ansible-test.yaml new file mode 100644 index 00000000000..255c7eca551 --- /dev/null +++ b/hack/ansible-test.yaml @@ -0,0 +1,6 @@ +- hosts: all + tasks: + - name: Create test file + file: + path: /tmp/ansible + state: touch diff --git a/hack/test-templates.sh b/hack/test-templates.sh index b1eadfc4349..5f6732d1fea 100755 --- a/hack/test-templates.sh +++ b/hack/test-templates.sh @@ -35,6 +35,7 @@ declare -A CHECKS=( ["disk"]="" ["user-v2"]="" ["mount-path-with-spaces"]="" + ["provision-ansible"]="" ) case "$NAME" in @@ -62,6 +63,7 @@ case "$NAME" in CHECKS["snapshot-online"]="1" CHECKS["snapshot-offline"]="1" CHECKS["mount-path-with-spaces"]="1" + CHECKS["provision-ansible"]="1" ;; "net-user-v2") CHECKS["port-forwards"]="" @@ -143,6 +145,11 @@ if [[ -n ${CHECKS["mount-path-with-spaces"]} ]]; then [ "$(limactl shell "$NAME" cat "/tmp/lima test dir with spaces/test file")" = "test file content" ] fi +if [[ -n ${CHECKS["provision-ansible"]} ]]; then + INFO 'Testing that /tmp/ansible was created successfully on provision' + limactl shell "$NAME" test -e /tmp/ansible +fi + INFO "Testing proxy settings are imported" got=$(limactl shell "$NAME" env | grep FTP_PROXY) # Expected: FTP_PROXY is set in addition to ftp_proxy, localhost is replaced diff --git a/hack/test-templates/test-misc.yaml b/hack/test-templates/test-misc.yaml index 35f74c8e7ec..2bf6cc8bd1a 100644 --- a/hack/test-templates/test-misc.yaml +++ b/hack/test-templates/test-misc.yaml @@ -26,6 +26,10 @@ mounts: - location: "/tmp/lima" writable: true +provision: +- mode: ansible + playbook: ./hack/ansible-test.yaml + # in order to use this example, you must first create the disk "data". run: # $ limactl disk create data --size 10G additionalDisks: