Skip to content

Support ansible provision mode for remote playbook #2283

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

Merged
merged 4 commits into from
Jun 19, 2024
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 .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions examples/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions hack/ansible-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- hosts: all
tasks:
- name: Create test file
file:
path: /tmp/ansible
state: touch
7 changes: 7 additions & 0 deletions hack/test-templates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ declare -A CHECKS=(
["disk"]=""
["user-v2"]=""
["mount-path-with-spaces"]=""
["provision-ansible"]=""
)

case "$NAME" in
Expand Down Expand Up @@ -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"]=""
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions hack/test-templates/test-misc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions pkg/cidata/cidata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/limayaml/limayaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions pkg/limayaml/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
66 changes: 66 additions & 0 deletions pkg/start/ansible.go
Original file line number Diff line number Diff line change
@@ -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 := createAnsibleInventory(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 createAnsibleInventory(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.AnsibleInventoryYAML)
return inventory, os.WriteFile(inventory, bytes, 0o644)
}
4 changes: 4 additions & 0 deletions pkg/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ func watchHostAgentEvents(ctx context.Context, inst *store.Instance, haStdoutPat
return true
}

if xerr := runAnsibleProvision(ctx, inst); xerr != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Expand Down
61 changes: 31 additions & 30 deletions pkg/store/filenames/filenames.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,36 +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
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"
Expand Down
7 changes: 7 additions & 0 deletions website/content/en/docs/dev/Internals/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ Metadata:
cloud-init:
- `cidata.iso`: cloud-init ISO9660 image. See [`cidata.iso`](#cidataiso).

Ansible:
- `ansible-inventory.yaml`: the Ansible node inventory. See [ansible](#ansible).

disk:
- `basedisk`: the base image
- `diffdisk`: the diff image (QCOW2)
Expand Down Expand Up @@ -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:

Expand Down