From 746ddbc6e17d9ab8238cc4a8e3a348ccb44878b5 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Tue, 8 Sep 2020 12:23:48 +0300 Subject: test: add a subplot to verify the roles work --- .gitignore | 7 ++ check | 43 +++++++++ roles/unix_users/subplot.md | 19 ++++ roles/unix_users/subplot.py | 7 ++ roles/unix_users/subplot.yaml | 2 + ssh.config.in | 10 ++ ssh/id | 27 ++++++ ssh/id.pub | 1 + subplot.md | 53 +++++++++++ subplot/daemon.py | 84 +++++++++++++++++ subplot/qemumgr.py | 210 ++++++++++++++++++++++++++++++++++++++++++ subplot/runcmd.py | 77 ++++++++++++++++ subplot/subplot.py | 120 ++++++++++++++++++++++++ subplot/subplot.yaml | 15 +++ test.config | 1 + test.key | 27 ++++++ 16 files changed, 703 insertions(+) create mode 100644 .gitignore create mode 100755 check create mode 100644 roles/unix_users/subplot.md create mode 100644 roles/unix_users/subplot.py create mode 100644 roles/unix_users/subplot.yaml create mode 100644 ssh.config.in create mode 100644 ssh/id create mode 100644 ssh/id.pub create mode 100644 subplot.md create mode 100644 subplot/daemon.py create mode 100755 subplot/qemumgr.py create mode 100644 subplot/runcmd.py create mode 100644 subplot/subplot.py create mode 100644 subplot/subplot.yaml create mode 100644 test.config create mode 100644 test.key diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b36d6fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +subplot.html +subplot.pdf +test.log +test.md +test.py +qemu.out +ssh.config diff --git a/check b/check new file mode 100755 index 0000000..4d36e49 --- /dev/null +++ b/check @@ -0,0 +1,43 @@ + #!/bin/bash +# +# Run the automated tests for the project. + +set -eu -o pipefail + +quiet=-q +hideok=chronic +if [ "$#" -gt 0 ] +then + case "$1" in + verbose | -v | --verbose) + quiet= + hideok= + shift 1 + ;; + esac +fi + +dir="$(mktemp -d -p .)" + +trap 'rm -rf "$dir"' EXIT + +rm -f test.log test.py +cp subplot.md "$dir" +cat subplot.md roles/*/subplot.md > "$dir/subplot.md" +cat subplot/*.py roles/*/subplot.py > "$dir/subplot.py" +cat subplot/*.yaml roles/*/subplot.yaml > "$dir/subplot.yaml" + +( + set -eu -o pipefail + cd "$dir" + sp-docgen subplot.md -o ../subplot.pdf + sp-docgen subplot.md -o ../subplot.html + sp-codegen subplot.md -o ../test.py +) + +# Fix private key permissions. git doesn't preserve them. +chmod 0600 ssh/id + +$hideok python3 test.py --log test.log "$@" + +echo "Everything seems to be in order." diff --git a/roles/unix_users/subplot.md b/roles/unix_users/subplot.md new file mode 100644 index 0000000..049bfc2 --- /dev/null +++ b/roles/unix_users/subplot.md @@ -0,0 +1,19 @@ +# unix_users – manage Unix users + +This role creates or updates Unix users. + +## Create user with unix_users + +~~~scenario +given a host running Debian +when I use role unix_users +and I use variables from foo.yml +and I run the playbook +then the host has user foo +~~~ + +~~~{#foo.yml .file .yaml} +unix_users: +- username: foo + comment: Foo Bar +~~~ diff --git a/roles/unix_users/subplot.py b/roles/unix_users/subplot.py new file mode 100644 index 0000000..118d007 --- /dev/null +++ b/roles/unix_users/subplot.py @@ -0,0 +1,7 @@ +def host_has_user(ctx, username=None): + assert_eq = globals()["assert_eq"] + qemu = ctx["qemu"] + output, exit = qemu.ssh(["getent", "passwd", username]) + assert_eq(exit, 0) + output = output.decode("UTF8") + assert f"\n{username}:" in output diff --git a/roles/unix_users/subplot.yaml b/roles/unix_users/subplot.yaml new file mode 100644 index 0000000..9fcc961 --- /dev/null +++ b/roles/unix_users/subplot.yaml @@ -0,0 +1,2 @@ +- then: the host has user {username} + function: host_has_user diff --git a/ssh.config.in b/ssh.config.in new file mode 100644 index 0000000..3f1e3f6 --- /dev/null +++ b/ssh.config.in @@ -0,0 +1,10 @@ +host qemu +hostname localhost +user debian +port +userknownhostsfile /dev/null +passwordauthentication no +stricthostkeychecking accept-new +identityfile ssh/test.key +controlmaster auto +controlpersist 60s diff --git a/ssh/id b/ssh/id new file mode 100644 index 0000000..4f65d74 --- /dev/null +++ b/ssh/id @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAylWkH8c88KcJO+tyHDwbN9PE6WGKevjUzjL2Go9493sM/H3mEIN5 +TpSqP+cWj7oNmcsBur+raZ/6dHa6L0+WACDyYrT+8iQHxMyXv4Du3PbsMdLiKXunHPG8ER +tFxXVFStBJ3gei6SHnPt47o+M4rRgZtH3yKCw4SeydHWk80Detq2WsP9ko0r4G357t2tgW +ET89TRol3bzgm8rVUi5VAEeksTT6xXYfwSZ0uLpsg2gS/9Y9smsP1pZH2T5+TYt1EmS0XG +x5wVFG6OuIsAcCWm4XaXodMSxk6g5GnrnVIGY5a0Rxcgm8CUz7Q8ybDy2+Ryhhs3Vzxz+y +5OjzNgiDswAAA8i+UR6evlEengAAAAdzc2gtcnNhAAABAQDKVaQfxzzwpwk763IcPBs308 +TpYYp6+NTOMvYaj3j3ewz8feYQg3lOlKo/5xaPug2ZywG6v6tpn/p0drovT5YAIPJitP7y +JAfEzJe/gO7c9uwx0uIpe6cc8bwRG0XFdUVK0EneB6LpIec+3juj4zitGBm0ffIoLDhJ7J +0daTzQN62rZaw/2SjSvgbfnu3a2BYRPz1NGiXdvOCbytVSLlUAR6SxNPrFdh/BJnS4umyD +aBL/1j2yaw/WlkfZPn5Ni3USZLRcbHnBUUbo64iwBwJabhdpeh0xLGTqDkaeudUgZjlrRH +FyCbwJTPtDzJsPLb5HKGGzdXPHP7Lk6PM2CIOzAAAAAwEAAQAAAQEAh8izbPQbTHD8fG7E +VHht16hRdEGWWnJU9dAzYp24E3VLwMKIu7pPlVGlc18Uv/2fFP+suHPah/bpcHEg/5EMXC +fAIkfO9BcD86lNiSHwqu82kTUxu58VBhKgIGbKCvppNwzTFaLQTF4JPyKKqbBaH6eV0I/Z +C+apG8sjoVI3ko8oKjjwTQ/oHHC71APXmLznhupxF2ohHf0jZW3g2Ktc86AiL0JUwmE6nb +FrZxCTBaYSaa7pQSlgJOjR0+xqLcf7a/rechXlAknKnFCBU2j2rlBMFy8QZUS0ADY1sWcq +jMRksBVSdPgfC5Ki/a8JWERhX6QoSnvnNR2RJxWpcPs1eQAAAIACOBS8WM26pw5OhDorV2 +NJIqwkzT8fMjjCuTACsa75wtqO4c61rOYy9K0C0/ukTmGHwi9HhjLTC9Ndhi9cWMKOyQL+ +UMKaurmnvtu39odLAG3CGE/SIM6Z6WyyGVpy/FJweTbDKAP7ldr1J49W+dIaCSPyHyatYT +Fwa7OsJRga5gAAAIEA+mvBnY9U9dj6NVZZAWHF4umSI+VuVmZl8NI0DMTG2hIAPbqi2Eez +zxp3195mc2Tf9YM81YqC1MVn3nqYJ5Kx+kibbTBphAWcbQe6dhn6zR8jKc8FNA8kdTJi4i +lvQ2wTEmrJtv2dEMkSNEZOrVw0eAZ9AuDBtcCvmzeRTAt0eQUAAACBAM7XoVeGHIaVA/GL +skuyNb7J56rCnMXAMJ9jgoaZxx3ON9DDfz8YiAnL1drzdkAtUNL0QlvVQuFlzAUlCPzCHO +HSEUEW7VFoejy+WSxoP6+F1qVQNnr38OQEswkZSko/+yiDMdgiAAUPaE8v0UZerJg8quHB +gDal1friWML9x0dXAAAADGxpd0BleG9sb2JlMQECAwQFBg== +-----END OPENSSH PRIVATE KEY----- diff --git a/ssh/id.pub b/ssh/id.pub new file mode 100644 index 0000000..714c5cc --- /dev/null +++ b/ssh/id.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKVaQfxzzwpwk763IcPBs308TpYYp6+NTOMvYaj3j3ewz8feYQg3lOlKo/5xaPug2ZywG6v6tpn/p0drovT5YAIPJitP7yJAfEzJe/gO7c9uwx0uIpe6cc8bwRG0XFdUVK0EneB6LpIec+3juj4zitGBm0ffIoLDhJ7J0daTzQN62rZaw/2SjSvgbfnu3a2BYRPz1NGiXdvOCbytVSLlUAR6SxNPrFdh/BJnS4umyDaBL/1j2yaw/WlkfZPn5Ni3USZLRcbHnBUUbo64iwBwJabhdpeh0xLGTqDkaeudUgZjlrRHFyCbwJTPtDzJsPLb5HKGGzdXPHP7Lk6PM2CIOz liw@exolobe1 diff --git a/subplot.md b/subplot.md new file mode 100644 index 0000000..ae7fbb0 --- /dev/null +++ b/subplot.md @@ -0,0 +1,53 @@ +# Introduction + +`debian-ansible` is a collection of Ansible roles for managing Debian +systems. The roles are re-usable and parameterised so that they can be +adapted to some variations. + +This document describes the roles and also acts as an acceptance test +suite for them. The [Subplot][] program can generate a test program +based on this document, and the test program tests that the roles work +as intended. + +[Subplot]: https://subplot.liw.fi/ + +## Source files + +At the root of the `debian-ansible` source tree is a `subplot.md`, and +the subdirectory `subplot` with some other files used by the main +file. Each role directory should have a `subplot.md`, and may also +have `subplot.yaml` and `subplot.py`. The files in role directories +get combined with the main ones the the `./check` script. + +## Scenario structure + +Each `subplot.md` file is meant to contain at least one scenario. All +scenarios have the same structure: + +* create a new VM +* construct an Ansible playbook that uses the role in a specific way +* run the playbook against the VM +* examine the VM to verify it has been changed in the expected way + +The VM is created using a base image, which the user must specify to +the test program by setting the `BASEIMAGE` environment variable. + +## Sanity check + +Verify that everything looks OK and that other scenarios can be run. + +~~~scenario +given a host running Debian +then I can run /bin/true on the host +~~~ + + + +--- +title: "debian-ansible—Ansible roles for Debian systems" +author: Lars Wirzenius +bindings: +- subplot.yaml +functions: +- subplot.py +... diff --git a/subplot/daemon.py b/subplot/daemon.py new file mode 100644 index 0000000..e223505 --- /dev/null +++ b/subplot/daemon.py @@ -0,0 +1,84 @@ +############################################################################# +# Start and stop daemons, or background processes. + + +import logging +import os +import signal +import time + + +# Start a process in the background. +def start_daemon(ctx, name, argv): + runcmd = globals()["runcmd"] + exit_code_is = globals()["exit_code_is"] + + logging.debug(f"Starting daemon {name}") + logging.debug(f" ctx={ctx.as_dict()}") + logging.debug(f" name={name}") + logging.debug(f" argv={argv}") + + if "daemon" not in ctx.as_dict(): + ctx["daemon"] = {} + assert name not in ctx["daemon"] + this = ctx["daemon"][name] = { + "pid-file": f"{name}.pid", + "stderr": f"{name}.stderr", + "stdout": f"{name}.stdout", + } + runcmd( + ctx, + [ + "/usr/sbin/daemonize", + "-c", + os.getcwd(), + "-p", + this["pid-file"], + "-e", + this["stderr"], + "-o", + this["stdout"], + ] + + argv, + ) + + # Wait for a bit for daemon to start and maybe find a problem and die. + time.sleep(3) + if ctx["exit"] != 0: + logging.error(f"obnam-server stderr: {ctx['stderr']}") + + exit_code_is(ctx, 0) + this["pid"] = int(open(this["pid-file"]).read().strip()) + assert process_exists(this["pid"]) + + logging.debug(f"Started daemon {name}") + logging.debug(f" ctx={ctx.as_dict()}") + + +# Stop a daemon. +def stop_daemon(ctx, name): + logging.debug(f"Stopping daemon {name}") + logging.debug(f" ctx={ctx.as_dict()}") + logging.debug(f" ctx['daemon']={ctx.as_dict()['daemon']}") + + this = ctx["daemon"][name] + terminate_process(this["pid"], signal.SIGKILL) + + +# Does a process exist? +def process_exists(pid): + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + return True + + +# Terminate process. +def terminate_process(pid, signalno): + logging.debug(f"Terminating process {pid} with signal {signalno}") + try: + os.kill(pid, signalno) + except ProcessLookupError: + logging.debug("Process did not actually exist (anymore?)") + pass diff --git a/subplot/qemumgr.py b/subplot/qemumgr.py new file mode 100755 index 0000000..8951122 --- /dev/null +++ b/subplot/qemumgr.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 + + +import logging +import os +import shlex +import shutil +import signal +import subprocess + +# import tempfile +import time + + +CLOUD_INIT_META_TEMPLATE = """\ +# Amazon EC2 style metadata +local-hostname: {hostname} +""" + +CLOUD_INIT_USER_TEMPLATE = """\ +#cloud-config +ssh_authorized_keys: +- {pubkey} +""" + + +class QemuSystem: + def __init__(self, name, base_image, disk_size, pubkey): + self._name = name + self._dirname = "." + self._image = self._copy_image(base_image, disk_size) + self._pubkey = pubkey + self._memory = 512 + self._vcpus = 1 + self._username = None + self._port = None + self._pid = None + + def _join(self, *names): + return os.path.join(self._dirname, *names) + + def set_disk_size(self, size): + self._disk_size = str(size * 1024 * 1024) + + def set_memory(self, memory): + self._memory = memory + + def set_vcpus(self, vcpus): + self._vcpus = vcpus + + def set_username(self, username): + self._username = username + + def _copy_image(self, base_image, size): + image = self._join("qemu.img") + logging.debug(f"QemuSystem: actual disk image: {image}") + shutil.copy(base_image, image) + if size is not None: + subprocess.check_call(["qemu-img", "resize", "-q", image, str(size)]) + return image + + def _cloud_init_iso(self): + iso = self._join("cloud-init.iso") + + config = self._join("cloud-init") + os.mkdir(config) + + meta = os.path.join(config, "meta-data") + write_file(meta, CLOUD_INIT_META_TEMPLATE.format(hostname=self._name)) + + user = os.path.join(config, "user-data") + write_file(user, CLOUD_INIT_USER_TEMPLATE.format(pubkey=self._pubkey)) + + if os.path.exists(iso): + os.remove(iso) + subprocess.check_call( + [ + "genisoimage", + "-quiet", + "-joliet", + "-rock", + "-output", + iso, + "-volid", + "cidata", + config, + ] + ) + + return iso + + def start(self): + iso = self._cloud_init_iso() + + self._port = 2222 # FIXME + pid_file = self._join("qemu.pid") + out_file = self._join("qemu.out") + err_file = self._join("qemu.err") + + MiB = 1024 ** 2 + memory = int(self._memory / MiB) + + argv = [ + "/usr/sbin/daemonize", + "-c", + self._dirname, + "-p", + pid_file, + "-e", + err_file, + "-o", + out_file, + "/usr/bin/qemu-system-x86_64", + "-name", + self._name, + "-m", + str(memory), + "-cpu", + "host", + "-smp", + f"cpus={self._vcpus}", + "-drive", + f"file={self._image},format=qcow2,cache=none,if=virtio", + "-drive", + f"file={iso},if=ide,media=cdrom", + "-device", + "virtio-net,netdev=user.0", + "-netdev", + f"user,id=user.0,hostfwd=tcp::{self._port}-:22", + "-nographic", + "-enable-kvm", + ] + + subprocess.check_call(argv, stdout=None, stderr=None) + self._pid = int(wait_for(lambda: got_pid(pid_file), 1)) + + logging.debug(f"started qemu-system") + logging.debug(f" argv: {argv}") + logging.debug(f" pid: {self._pid}") + + def stop(self): + if self._pid is not None: + logging.debug(f"killing qemu-system process {self._pid}") + os.kill(self._pid, signal.SIGTERM) + + def wait_for_ssh(self): + def ssh_ok(): + output, exit = self.ssh(["true"]) + logging.debug(f"tried ssh, exit={exit}") + if exit == 0: + return True + return None + + return wait_for(ssh_ok, 60, sleep=5) + + def ssh(self, argv): + assert self._username is not None + + srcdir = globals()["srcdir"] + + ssh_opts = [ + "-i", + os.path.join(srcdir, "ssh", "id"), + "-p", + str(self._port), + "-l", + self._username, + "-ouserknownhostsfile=/dev/null", + "-ostricthostkeychecking=accept-new", + "-opasswordauthentication=no", + ] + + real_argv = ( + ["ssh"] + ssh_opts + ["localhost", "--"] + [shlex.quote(x) for x in argv] + ) + + logging.debug(f"running on VM: {real_argv}") + p = subprocess.Popen( + real_argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + output, _ = p.communicate() + logging.debug(f"exit code: {p.returncode}") + logging.debug(f"output: {output}") + return output, p.returncode + + +def read_file(filename): + with open(filename, "r") as f: + return f.read() + + +def write_file(filename, content): + with open(filename, "w") as f: + f.write(content) + + +def wait_for(func, timeout, sleep=0.1): + start = time.time() + while time.time() < start + timeout: + val = func() + if val is not None: + return val + time.sleep(sleep) + + +def got_pid(pid_file): + if os.path.exists(pid_file): + pid = read_file(pid_file) + if pid: + return pid.strip() diff --git a/subplot/runcmd.py b/subplot/runcmd.py new file mode 100644 index 0000000..7193c15 --- /dev/null +++ b/subplot/runcmd.py @@ -0,0 +1,77 @@ +# Some step implementations for running commands and capturing the result. + +import subprocess + + +# Run a command, capture its stdout, stderr, and exit code in context. +def runcmd(ctx, argv, **kwargs): + p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) + stdout, stderr = p.communicate("") + ctx["argv"] = argv + ctx["stdout"] = stdout.decode("utf-8") + ctx["stderr"] = stderr.decode("utf-8") + ctx["exit"] = p.returncode + + +# Check that latest exit code captured by runcmd was a specific one. +def exit_code_is(ctx, wanted): + if ctx.get("exit") != wanted: + print("context:", ctx.as_dict()) + assert_eq(ctx.get("exit"), wanted) + + +# Check that latest exit code captured by runcmd was not a specific one. +def exit_code_is_not(ctx, unwanted): + if ctx.get("exit") == unwanted: + print("context:", ctx.as_dict()) + assert_ne(ctx.get("exit"), unwanted) + + +# Check that latest exit code captured by runcmd was zero. +def exit_code_zero(ctx): + exit_code_is(ctx, 0) + + +# Check that latest exit code captured by runcmd was not zero. +def exit_code_nonzero(ctx): + exit_code_is_not(ctx, 0) + + +# Check that stdout of latest runcmd contains a specific string. +def stdout_contains(ctx, pattern=None): + stdout = ctx.get("stdout", "") + if pattern not in stdout: + print("pattern:", repr(pattern)) + print("stdout:", repr(stdout)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern in stdout, True) + + +# Check that stdout of latest runcmd does not contain a specific string. +def stdout_does_not_contain(ctx, pattern=None): + stdout = ctx.get("stdout", "") + if pattern in stdout: + print("pattern:", repr(pattern)) + print("stdout:", repr(stdout)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern not in stdout, True) + + +# Check that stderr of latest runcmd does contains a specific string. +def stderr_contains(ctx, pattern=None): + stderr = ctx.get("stderr", "") + if pattern not in stderr: + print("pattern:", repr(pattern)) + print("stderr:", repr(stderr)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern in stderr, True) + + +# Check that stderr of latest runcmd does not contain a specific string. +def stderr_does_not_contain(ctx, pattern=None): + stderr = ctx.get("stderr", "") + if pattern not in stderr: + print("pattern:", repr(pattern)) + print("stderr:", repr(stderr)) + print("ctx:", ctx.as_dict()) + assert_eq(pattern not in stderr, True) diff --git a/subplot/subplot.py b/subplot/subplot.py new file mode 100644 index 0000000..4434f24 --- /dev/null +++ b/subplot/subplot.py @@ -0,0 +1,120 @@ +import logging +import os +import yaml + + +def fixme(ctx, **kwargs): + assert 0 + + +def create_vm(ctx): + QemuSystem = globals()["QemuSystem"] + srcdir = globals()["srcdir"] + + name = "debian-ansible-test" + base_image = "/home/liw/tmp/debian-10-openstack-amd64.qcow2" + GiB = 1024 ** 3 + disk_size = 10 * GiB + pubkey = open(os.path.join(srcdir, "ssh", "id.pub")).read().strip() + memory = 1 * GiB + cpus = 2 + username = "debian" + + logging.info("starting a VM using qemu-system") + logging.info(f" name : {name}") + logging.info(f" image : {base_image}") + logging.info(f" disk : {disk_size}") + logging.info(f" pubkey : {pubkey}") + logging.info(f" memory : {memory}") + logging.info(f" cpus : {cpus}") + logging.info(f" username: {username}") + + qemu = QemuSystem(name, base_image, disk_size, pubkey) + qemu.set_memory(memory) + qemu.set_vcpus(cpus) + qemu.set_username(username) + qemu.start() + + logging.debug("waiting for SSH to be ready") + if qemu.wait_for_ssh(): + logging.debug("SSH is ready") + else: + logging.error("SSH did not get ready") + assert 0 + logging.info("a qemu-system VM is up and running and accessible over SSH") + ctx["qemu"] = qemu + + +def destroy_vm(ctx): + logging.debug(f"destroying qemu running") + qemu = ctx["qemu"] + qemu.stop() + + +def run_true_on_host(ctx): + qemu = ctx["qemu"] + qemu.ssh(["/bin/true"]) + + +def use_role_in_playbook(ctx, role=None): + empty_playbook = { + "hosts": "test-host", + "remote_user": "debian", # FIXME: don't hardcode this + "become": True, + "roles": [], + } + playbook = ctx.get("playbook", dict(empty_playbook)) + playbook["roles"].append(role) + ctx["playbook"] = playbook + + +def set_vars_file(ctx, filename=None): + get_file = globals()["get_file"] + data = get_file(filename) + with open("vars.yml", "wb") as f: + f.write(data) + + +def run_playbook(ctx): + runcmd = globals()["runcmd"] + exit_code_zero = globals()["exit_code_zero"] + assert_ne = globals()["assert_ne"] + srcdir = globals()["srcdir"] + + with open("hosts", "w") as f: + f.write("test-host\n") + + if not os.path.exists("vars.yml"): + with open("vars.yml", "w") as f: + yaml.safe_dump([], stream=f) + + playbook = [ctx["playbook"]] + assert_ne(playbook, None) + with open("playbook.yml", "w") as f: + yaml.safe_dump(playbook, stream=f) + + ssh_opts = [ + "-ouserknownhostsfile=/dev/null", + "-ostricthostkeychecking=accept-new", + "-i", + os.path.join(srcdir, "ssh", "id"), + "-p2222", + ] + + env = dict(os.environ) + env["ANSIBLE_SSH_ARGS"] = " ".join(ssh_opts) + env["ANSIBLE_LOG"] = "ansible.log" + env["ANSIBLE_ROLES_PATH"] = os.path.join(srcdir, "roles") + + argv = [ + "ansible-playbook", + "-i", + "hosts", + f"-e@vars.yml", + "-eansible_ssh_host=localhost", + "-eansible_ssh_port=2222", + "playbook.yml", + ] + + runcmd(ctx, argv, env=env) + exit_code_zero(ctx) diff --git a/subplot/subplot.yaml b/subplot/subplot.yaml new file mode 100644 index 0000000..01c85f5 --- /dev/null +++ b/subplot/subplot.yaml @@ -0,0 +1,15 @@ +- given: a host running Debian + function: create_vm + cleanup: destroy_vm + +- then: I can run /bin/true on the host + function: run_true_on_host + +- when: I use role {role} + function: use_role_in_playbook + +- when: I use variables from {filename} + function: set_vars_file + +- when: I run the playbook + function: run_playbook diff --git a/test.config b/test.config new file mode 100644 index 0000000..e1a73bb --- /dev/null +++ b/test.config @@ -0,0 +1 @@ +image: /home/liw/tmp/debian-10-openstack-amd64.qcow2 diff --git a/test.key b/test.key new file mode 100644 index 0000000..347f335 --- /dev/null +++ b/test.key @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAozhFGOsofrh7iVBOGqFOevAZ6UADRK9J78HhbZ3XGxjwDJ8/f3Ov +XIELh+hhjWM1DlK+0qxEpEXJwoTiLlzHuA/HEqX8J5yApXkCvWNDamZNVqSDKpsP5gcaix +KXFz2VaL3IGGumDzWuf8qtcJ3TAvVcpWAahLNEvV4HdQZBXL0bnnTYhc9cVSkWYGAKtwdm +CBX5MkpTTFQpkHV300jF1s6Vbf6upDSP7xp2WrIzZ4CAKVV1mjpIrZx+zvszsv1Xfm+Zg4 +nekRFbmfehreiXHylzM5h4zkNhUBL+z/lr40/J1VeTzZKhyrwjzDgyd1CAI2mfU+Jgybax +HQZ+Ke2ZTQAAA8iH8gOIh/IDiAAAAAdzc2gtcnNhAAABAQCjOEUY6yh+uHuJUE4aoU568B +npQANEr0nvweFtndcbGPAMnz9/c69cgQuH6GGNYzUOUr7SrESkRcnChOIuXMe4D8cSpfwn +nICleQK9Y0NqZk1WpIMqmw/mBxqLEpcXPZVovcgYa6YPNa5/yq1wndMC9VylYBqEs0S9Xg +d1BkFcvRuedNiFz1xVKRZgYAq3B2YIFfkySlNMVCmQdXfTSMXWzpVt/q6kNI/vGnZasjNn +gIApVXWaOkitnH7O+zOy/Vd+b5mDid6REVuZ96Gt6JcfKXMzmHjOQ2FQEv7P+WvjT8nVV5 +PNkqHKvCPMODJ3UIAjaZ9T4mDJtrEdBn4p7ZlNAAAAAwEAAQAAAQEAkRQ6Lu0PgJvgiuxL +kFYjGRmEHpUckpewg6F1C+dJxTdEYCPI4DPnArVdl39R/sfY6BNatI4VMWMq9HEDTqx/hb +bYf1X0rQzqOKs4aTjrOo8WXVY7lMphtlhIGqf6jtjZjlFqo3JEF67inYp84eYXIsEPiZvD +1oI2LpB+1mEqBhBSHHxl76X6OHeMINFYylQwTg+0DUIriv4qBOQ0iLIfyM8ERlGWigWe5M +hR9hReX1o+adDfmRQyF3wPyeYpuv/RGZIGN8xpHr/CK8arUGtV8QrJAXdt0o0jLdX/Ulv7 +kwOZFXN2e5xA6HvvigdgNkGtXscPy9ELHtcnEVD++ihFgQAAAIB3c54FGhbxOTpy758TWv +Zrx3XKEpgWr981ym0H9ponp2qC0lnpEQFcHCz1gmjVOnrTWgJYlAfa/cp++LqHOsGtyD/F +OafdI+RsByiQHDcBAZjceIySnw2XEzNyAC/kKEhS91/UsB9xnAx49A9eBkEyq3tSr6DU6d +BrUey3Oc1pXQAAAIEA0MS7tlxQahMzXkgid39Xun4bG3lhzSsLwB8vaInz2BQW+wj8yFyV +A8UTgSMDAZfgq9IaVtunlYuItMxVa3f5xtCX7lA3QNEmQuTPDUy6DA4MJMsA21/qTNDnjA +wl+PTiZ2r8O2yCfNaJ2oct8d+q5WHvC+oJQZPt13xFK7nbAKEAAACBAMglfRedYwqBVxIS +sCLGEnVbSFsi3/futfhEenEW1+rMhuEqApyxiq/Fo82ZJZjQPcOz0wt4Re3nYK0dqVtKnw +iHDPIZhyU/l2s5IBfbidgmunrS7DJva6GGVQnPnGt15AsRVSCLjktMUo9qtOzc/FoEPpLS +G1zC/u0llRa+tF0tAAAADGxpd0BleG9sb2JlMQECAwQFBg== +-----END OPENSSH PRIVATE KEY----- -- cgit v1.2.1