summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README2
-rwxr-xr-xcheck51
-rw-r--r--roles/apache_server/tasks/main.yml7
-rw-r--r--roles/apache_server/templates/virtualhost.conf.tmpl10
-rw-r--r--roles/gitano_server/tasks/main.yml9
-rw-r--r--roles/radicle_node/README.md2
-rw-r--r--roles/radicle_node/files/rad-config-pin23
-rw-r--r--roles/radicle_node/files/rad-config-update34
-rw-r--r--roles/radicle_node/tasks/main.yml242
-rw-r--r--roles/radicle_node/templates/Caddyfile.j214
-rw-r--r--roles/radicle_node/templates/radicle-ci-broker.service.j218
-rw-r--r--roles/radicle_node/templates/radicle-httpd.service.j216
-rw-r--r--roles/radicle_node/templates/radicle-node.service.j216
-rw-r--r--roles/sane_debian_system/defaults/main.yml8
-rw-r--r--roles/sane_debian_system/subplot.md34
-rw-r--r--roles/sane_debian_system/subplot.yaml16
-rw-r--r--roles/sane_debian_system/tasks/apt.yml47
-rw-r--r--roles/sane_debian_system/tasks/env.yml25
-rw-r--r--roles/sane_debian_system/tasks/main.yml9
-rw-r--r--roles/sshd/README23
-rw-r--r--roles/sshd/defaults/main.yml9
-rw-r--r--roles/sshd/handlers/main.yml4
-rw-r--r--roles/sshd/subplot.md27
-rw-r--r--roles/sshd/tasks/main.yml112
-rw-r--r--roles/unix_users/subplot.md6
-rw-r--r--roles/unix_users/subplot.py13
-rw-r--r--roles/unix_users/subplot.yaml25
-rw-r--r--roles/unix_users/tasks/main.yml9
-rw-r--r--subplot.md11
-rw-r--r--subplot.subplot11
-rw-r--r--subplot/daemon.py84
-rwxr-xr-xsubplot/qemumgr.py6
-rw-r--r--subplot/runcmd.py247
-rw-r--r--subplot/subplot.yaml36
34 files changed, 771 insertions, 435 deletions
diff --git a/README b/README
index 8b9a929..551d0e1 100644
--- a/README
+++ b/README
@@ -2,7 +2,7 @@ README for debian-ansible
=============================================================================
This is a small collection of Ansible roles for Debian targets. These
-roles care chosen based on generic usefulness, and are meant to be
+roles are chosen based on generic usefulness, and are meant to be
highly cohesive, loosely coupled, and parameterised as needed.
Legalese
diff --git a/check b/check
index 795e2f7..225aa7e 100755
--- a/check
+++ b/check
@@ -4,26 +4,21 @@
set -eu -o pipefail
-cat_with_sep()
-{
- for x in "$@"
- do
- cat "$x"
- echo
- done
+cat_with_sep() {
+ for x in "$@"; do
+ cat "$x"
+ echo
+ done
}
-quiet=-q
hideok=chronic
-if [ "$#" -gt 0 ]
-then
- case "$1" in
+if [ "$#" -gt 0 ]; then
+ case "$1" in
verbose | -v | --verbose)
- quiet=
- hideok=
- shift 1
- ;;
- esac
+ hideok=
+ shift 1
+ ;;
+ esac
fi
dir="$(mktemp -d -p .)"
@@ -31,31 +26,31 @@ dir="$(mktemp -d -p .)"
trap 'rm -rf "$dir"' EXIT
rm -f test.log test.py
-cp subplot.md "$dir"
-cat_with_sep subplot.md roles/*/subplot.md > "$dir/subplot.md"
-cat_with_sep subplot/*.py roles/*/subplot.py > "$dir/subplot.py"
-cat_with_sep subplot/*.yaml roles/*/subplot.yaml > "$dir/subplot.yaml"
+cp subplot.subplot subplot.md "$dir"
+cat_with_sep subplot.md roles/*/subplot.md >"$dir/subplot.md"
+cat_with_sep subplot/*.py roles/*/subplot.py >"$dir/subplot.py"
+cat_with_sep 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
+ set -eu -o pipefail
+ cd "$dir"
+ subplot docgen subplot.subplot -o ../subplot.pdf
+ subplot docgen subplot.subplot -o ../subplot.html
+ subplot codegen subplot.subplot -o ../test.py
)
# Fix private key permissions. git doesn't preserve them.
chmod 0600 ssh/id
# Create configuration for the test VMs used by the test suite.
-cat > test.cfg <<EOF
+cat >test.cfg <<EOF
name: debian-ansible-test
-base_image: "$HOME/tmp/debian-10-openstack-amd64.qcow2"
+base_image: "$HOME/tmp/debian.qcow2"
username: debian
cpus: 2
memory: 1024
EOF
-$hideok python3 test.py --log test.log "$@"
+$hideok python3 test.py --log test.log --env ANSIBLE_STDOUT_CALLBACK=yaml "$@"
echo "Everything seems to be in order."
diff --git a/roles/apache_server/tasks/main.yml b/roles/apache_server/tasks/main.yml
index e01d01e..5509818 100644
--- a/roles/apache_server/tasks/main.yml
+++ b/roles/apache_server/tasks/main.yml
@@ -67,6 +67,7 @@
name: "{{ item }}"
with_items:
- ssl
+ - rewrite
- name: create dirs for static site contents
file:
@@ -101,6 +102,12 @@
notify:
- restart apache
+- name: set default charset to utf8
+ copy:
+ content: |
+ AddDefaultCharset UTF-8
+ dest: /etc/apache2/conf-available/charset.conf
+
- name: "install htpasswd files"
copy:
content: "{{ item.htpasswd }}"
diff --git a/roles/apache_server/templates/virtualhost.conf.tmpl b/roles/apache_server/templates/virtualhost.conf.tmpl
index 8d069ce..1e14db5 100644
--- a/roles/apache_server/templates/virtualhost.conf.tmpl
+++ b/roles/apache_server/templates/virtualhost.conf.tmpl
@@ -8,6 +8,10 @@
ErrorLog /var/log/apache2/{{ item.domain }}/error.log
CustomLog /var/log/apache2/{{ item.domain }}/access.log combined
<Directory /srv/http/{{ item.domain }}>
+{% if item.redirect|default(false) %}
+ Redirect permanent / "https://{{ item.redirect }}/"
+ Require all granted
+{% else %}
{% if item.letsencrypt|default(false) %}
Redirect permanent / "https://{{ item.domain }}/"
Require all granted
@@ -23,6 +27,7 @@
Require all granted
{% endif %}
{% endif %}
+{% endif %}
</Directory>
Alias /.well-known/ /srv/letsencrypt/{{ item.domain }}/
@@ -45,6 +50,10 @@
CustomLog /var/log/apache2/{{ item.domain }}/access.log combined
<Directory /srv/http/{{ item.domain }}>
Options +SymlinksIfOwnerMatch +Indexes +MultiViews
+{% if item.redirect|default(false) %}
+ Redirect permanent / "https://{{ item.redirect }}/"
+ Require all granted
+{% else %}
{% if item.htpasswd is defined %}
AuthType Basic
AuthName "{{ item.htpasswd_name }}"
@@ -54,6 +63,7 @@
AllowOverride AuthConfig
Require all granted
{% endif %}
+{% endif %}
</Directory>
SSLEngine on
diff --git a/roles/gitano_server/tasks/main.yml b/roles/gitano_server/tasks/main.yml
index 08486fa..b25d142 100644
--- a/roles/gitano_server/tasks/main.yml
+++ b/roles/gitano_server/tasks/main.yml
@@ -1,3 +1,6 @@
-- include: gitano.yml
-- include: git-daemon.yml
-- include: cgit.yml
+- ansible.builtin.import_tasks:
+ file: gitano.yml
+- ansible.builtin.import_tasks:
+ file: git-daemon.yml
+- ansible.builtin.import_tasks:
+ file: cgit.yml
diff --git a/roles/radicle_node/README.md b/roles/radicle_node/README.md
new file mode 100644
index 0000000..b653160
--- /dev/null
+++ b/roles/radicle_node/README.md
@@ -0,0 +1,2 @@
+This role, `radicle_node`, sets up a Debian system to be a
+[Radicle](https://radicle.xyz/) node.
diff --git a/roles/radicle_node/files/rad-config-pin b/roles/radicle_node/files/rad-config-pin
new file mode 100644
index 0000000..0e40f00
--- /dev/null
+++ b/roles/radicle_node/files/rad-config-pin
@@ -0,0 +1,23 @@
+#!/usr/bin/python3
+
+import json, os, subprocess, sys
+
+rid = sys.argv[1]
+
+p = subprocess.run(["rad", "config", "show"], check=True, capture_output=True)
+if p.returncode != 0:
+ sys.exit("rad config show failed")
+config = json.loads(p.stdout.decode())
+
+config["web"]["pinned"]["repositories"].append(rid)
+
+p = subprocess.run(["rad", "self", "--home"], check=True, capture_output=True)
+if p.returncode != 0:
+ sys.exit("rad self --home failed")
+
+home = p.stdout.decode().strip()
+filename = os.path.join(home, "config.json")
+if os.path.exists(filename):
+ os.rename(filename, filename + ".bak")
+with open(filename, "w") as f:
+ f.write(json.dumps(config, indent=4))
diff --git a/roles/radicle_node/files/rad-config-update b/roles/radicle_node/files/rad-config-update
new file mode 100644
index 0000000..40dd1a9
--- /dev/null
+++ b/roles/radicle_node/files/rad-config-update
@@ -0,0 +1,34 @@
+#!/usr/bin/python3
+
+import json, os, subprocess, sys
+
+alias = sys.argv[1]
+ext = sys.argv[2]
+policy = sys.argv[3]
+scope = sys.argv[4]
+peer = sys.argv[5]
+
+p = subprocess.run(["rad", "config", "show"], capture_output=True)
+if p.returncode != 0:
+ sys.exit("rad config show failed")
+config = json.loads(p.stdout.decode())
+
+config["node"]["alias"] = alias
+config["node"]["externalAddresses"] = [ext]
+config["node"]["policy"] = policy
+config["node"]["scope"] = scope
+
+nodes = config["node"]["connect"]
+if peer not in nodes:
+ nodes.append(peer)
+
+p = subprocess.run(["rad", "self", "--home"], check=True, capture_output=True)
+if p.returncode != 0:
+ sys.exit("rad self --home failed")
+
+home = p.stdout.decode().strip()
+filename = os.path.join(home, "config.json")
+if os.path.exists(filename):
+ os.rename(filename, filename + ".bak")
+with open(filename, "w") as f:
+ f.write(json.dumps(config, indent=4))
diff --git a/roles/radicle_node/tasks/main.yml b/roles/radicle_node/tasks/main.yml
new file mode 100644
index 0000000..8e04a83
--- /dev/null
+++ b/roles/radicle_node/tasks/main.yml
@@ -0,0 +1,242 @@
+- name: "check radicle_node_version"
+ shell: |
+ [ "{{ radicle_node_version }}" = "1" ] || \
+ (echo "Unexpected version {{ radicle_node_version }}" 1>&2; exit 1)
+
+- name: "check that radicle_node_key is set"
+ shell: |
+ echo radicle_node_key Ansible variable is not set
+ exit 1
+ when: radicle_node_key is not defined
+
+- name: "check that radicle_node_key_pub is set"
+ shell: |
+ echo radicle_node_key_pub Ansible variable is not set
+ exit 1
+ when: radicle_node_key_pub is not defined
+
+- name: "install important additional packages for Radicle"
+ apt:
+ name:
+ # For the Radicle installer
+ - curl
+
+ # Radicle is built on git.
+ - git
+
+ # Rsync for backups.
+ - rsync
+
+ # Web server for the web UI.
+ - caddy
+
+ # Radicle components.
+ - radicle
+ - radicle-ci-broker
+ - radicle-native-ci
+
+- name: "stop Radicle node if it's running"
+ shell: |
+ systemctl stop radicle-node || true
+
+- name: "stop Radicle CI broker if it's running"
+ shell: |
+ systemctl stop radicle-ci-broker || true
+
+- name: "configure git for _rad user"
+ shell: |
+ sudo -u _rad git config --global user.name "_rad"
+ sudo -u _rad git config --global user.email "liw@liw.fi"
+
+- name: "create directory for Radicle for the _rad user"
+ file:
+ state: directory
+ path: /home/_rad/.radicle
+ owner: _rad
+ group: _rad
+ mode: 0755
+
+- name: "create directory for web pages"
+ file:
+ state: directory
+ path: /srv/http
+ owner: _rad
+ group: _rad
+ mode: 0755
+
+- name: "create directory for Radicle backup"
+ when: radicle_node_backup is defined
+ file:
+ state: directory
+ path: radicle-backup
+ owner: root
+ group: root
+ mode: 0755
+
+- name: "restore from backup (step 1 or 2)"
+ when: radicle_node_backup is defined
+ synchronize:
+ src: "{{ radicle_node_backup }}/."
+ dest: radicle-backup/.
+ group: no
+ owner: no
+
+- name: "restore from backup (step 2 or 2)"
+ when: radicle_node_backup is defined
+ shell: |
+ find radicle-backup -name control.sock -delete
+ rsync -a --del radicle-backup/home/_rad/.radicle/. /home/_rad/.radicle/.
+ rsync -a --del radicle-backup/srv/http/. /srv/http/.
+ chown -R _rad:_rad /home/_rad/.radicle/. /srv/http/.
+
+- name: "create directory for Radicle keys"
+ file:
+ state: directory
+ path: /home/_rad/.radicle/keys
+ owner: _rad
+ group: _rad
+ mode: 0755
+
+- name: "install Radicle private key"
+ copy:
+ content: "{{ radicle_node_key }}"
+ dest: /home/_rad/.radicle/keys/radicle
+ owner: _rad
+ group: _rad
+ mode: 0600
+
+- name: "install Radicle public key"
+ copy:
+ content: "{{ radicle_node_key_pub }}"
+ dest: /home/_rad/.radicle/keys/radicle.pub
+ owner: _rad
+ group: _rad
+ mode: 0644
+
+- name: "install systemd unit for Radicle node"
+ template:
+ src: radicle-node.service.j2
+ dest: /lib/systemd/system/radicle-node.service
+
+- name: "init Radicle node config"
+ shell: |
+ if [ ! -e /home/_rad/.radicle/config.json ]; then
+ sudo -u _rad -i rad config init --alias "{{ radicle_node_domain_name }}"
+ fi
+
+- name: "(re)start systemd unit for Radicle node"
+ systemd:
+ name: radicle-node
+ state: restarted
+ masked: no
+ enabled: yes
+ daemon_reload: yes
+
+- name: "install script to add update Radicle config file"
+ when: radicle_node_connections is defined
+ copy:
+ src: rad-config-update
+ dest: /home/_rad/rad-config-update
+ owner: _rad
+ group: _rad
+ mode: 0755
+
+- name: "connect to other Radicle nodes"
+ when: radicle_node_connections is defined
+ with_items: "{{ radicle_node_connections }}"
+ shell: |
+ sudo -u _rad -i ./rad-config-update \
+ "{{ radicle_node_domain_name }}" \
+ "{{ radicle_node_domain_name }}:8776" \
+ "{{ radicle_node_policy }}" \
+ "{{ radicle_node_scope }}" \
+ "{{ item.nid }}@{{ item.host }}:{{ item.port }}"
+
+- name: "install script to add update Radicle repository pinning"
+ when: radicle_node_repositories is defined
+ copy:
+ src: rad-config-pin
+ dest: /home/_rad/rad-config-pin
+ owner: _rad
+ group: _rad
+ mode: 0755
+
+- name: "seed Radicle repositories"
+ when: radicle_node_repositories is defined
+ with_items: "{{ radicle_node_repositories }}"
+ shell: |
+ sudo -u _rad rad seed "{{ item.rid }}"
+ sudo -u _rad -i ./rad-config-pin "{{ item.rid }}"
+
+- name: "install Caddy configuation file"
+ template:
+ src: Caddyfile.j2
+ dest: /etc/caddy/Caddyfile
+
+- name: "create directory for CI logs"
+ file:
+ state: directory
+ path: /srv/http
+ owner: _rad
+ group: _rad
+
+- name: "restart Caddy"
+ systemd:
+ name: caddy
+ state: restarted
+ masked: no
+ enabled: yes
+ daemon_reload: yes
+
+- name: "install systemd unit for Radicle HTTPD"
+ template:
+ src: radicle-httpd.service.j2
+ dest: /lib/systemd/system/radicle-httpd.service
+
+- name: "enable systemd unit for Radicle HTTPD"
+ systemd:
+ name: radicle-httpd
+ state: restarted
+ masked: no
+ enabled: yes
+ daemon_reload: yes
+
+- name: "install Radicle CI broker config"
+ copy:
+ content: |
+ {{ radicle_node_ci_broker_config }}
+ dest: /home/_rad/ci-broker.yaml
+ owner: _rad
+ group: _rad
+ mode: 0644
+
+- name: "create state directory for Radicle native CI"
+ file:
+ state: directory
+ path: /home/_rad/native-ci.state
+ owner: _rad
+ group: _rad
+ mode: 0755
+
+- name: "install Radicle native CI config"
+ copy:
+ content: |
+ state: /srv/http
+ log: /home/_rad/native-ci.log
+ dest: /home/_rad/native-ci.yaml
+ owner: _rad
+ group: _rad
+ mode: 0644
+
+- name: "install systemd unit for Radicle CI broker"
+ template:
+ src: radicle-ci-broker.service.j2
+ dest: /lib/systemd/system/radicle-ci-broker.service
+
+- name: "enable systemd unit for Radicle CI broker"
+ systemd:
+ name: radicle-ci-broker
+ state: restarted
+ masked: no
+ enabled: yes
+ daemon_reload: yes
diff --git a/roles/radicle_node/templates/Caddyfile.j2 b/roles/radicle_node/templates/Caddyfile.j2
new file mode 100644
index 0000000..1954b4d
--- /dev/null
+++ b/roles/radicle_node/templates/Caddyfile.j2
@@ -0,0 +1,14 @@
+:80 {
+ root * /usr/share/caddy
+}
+{{ radicle_node_domain_name }}:443 {
+ reverse_proxy 127.0.0.1:8080
+}
+{{ radicle_node_ci_domain_name }}:443 {
+ root * /srv/http/
+ file_server browse
+}
+{{ radicle_node_wumpus_domain_name }}:443 {
+ root * /srv/wumpus/
+ file_server browse
+}
diff --git a/roles/radicle_node/templates/radicle-ci-broker.service.j2 b/roles/radicle_node/templates/radicle-ci-broker.service.j2
new file mode 100644
index 0000000..239ccc5
--- /dev/null
+++ b/roles/radicle_node/templates/radicle-ci-broker.service.j2
@@ -0,0 +1,18 @@
+[Unit]
+After=radicle-node.service
+Description=Radicle CI broker
+
+[Service]
+Type=simple
+Environment=RAD_HOME=/home/_rad/.radicle
+Environment=PATH=/home/_rad/.cargo/bin:/bin:/usr/bin:/sbin:/usr/sbin
+Environment=RUST_LOG=info
+ExecStart=/bin/ci-broker /home/_rad/ci-broker.yaml
+KillMode=control-group
+Restart=always
+RestartSec=1
+User=_rad
+Group=_rad
+
+[Install]
+WantedBy=default.target
diff --git a/roles/radicle_node/templates/radicle-httpd.service.j2 b/roles/radicle_node/templates/radicle-httpd.service.j2
new file mode 100644
index 0000000..32b2ecf
--- /dev/null
+++ b/roles/radicle_node/templates/radicle-httpd.service.j2
@@ -0,0 +1,16 @@
+[Unit]
+Description=Radicle HTTP Daemon
+After=network.target network-online.target
+Requires=network-online.target
+
+[Service]
+User=_rad
+Group=_rad
+ExecStart=/usr/bin/radicle-httpd --listen 127.0.0.1:8080
+Environment=RAD_HOME=/home/_rad/.radicle RUST_BACKTRACE=1 RUST_LOG=info
+KillMode=process
+Restart=always
+RestartSec=1
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/radicle_node/templates/radicle-node.service.j2 b/roles/radicle_node/templates/radicle-node.service.j2
new file mode 100644
index 0000000..ae2af8c
--- /dev/null
+++ b/roles/radicle_node/templates/radicle-node.service.j2
@@ -0,0 +1,16 @@
+[Unit]
+Description=Radicle Node
+After=network.target network-online.target
+Requires=network-online.target
+
+[Service]
+User=_rad
+Group=_rad
+ExecStart=/usr/bin/radicle-node --listen 0.0.0.0:8776 --force
+Environment=RAD_HOME=/home/_rad/.radicle RUST_BACKTRACE=1 RUST_LOG=info
+KillMode=process
+Restart=always
+RestartSec=3
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/sane_debian_system/defaults/main.yml b/roles/sane_debian_system/defaults/main.yml
index 43d0262..d896b75 100644
--- a/roles/sane_debian_system/defaults/main.yml
+++ b/roles/sane_debian_system/defaults/main.yml
@@ -1,12 +1,12 @@
# These are the variables expected by this role.
# Playbook should set this to the version of this role it expects to
-# use.
-sane_debian_system_version: null
+# use. Defaults to the inventory hostname.
+sane_debian_system_version: "{{ inventory_hostname }}"
-# The desired hostname. Default is empty, which means hostname won't
-# be set.
+# The desired hostname. There is no no default, which means hostname
+# won't be set.
sane_debian_system_hostname: ""
# The Debian release code name to use.
diff --git a/roles/sane_debian_system/subplot.md b/roles/sane_debian_system/subplot.md
index 087ae44..be05984 100644
--- a/roles/sane_debian_system/subplot.md
+++ b/roles/sane_debian_system/subplot.md
@@ -3,6 +3,13 @@
This role sets up a Debian system so that it can be managed with
Ansible in a reasonable way.
+## Version history
+
+### Version 2
+
+* `sane_debian_hostname` defaults to the inventory hostname. This
+ means it's not necessary to set it if the default is sufficient.
+
## Minimally sane Debian system
~~~scenario
@@ -13,19 +20,36 @@ and I run the playbook
then the host has the sudo package installed
and the host has the apt-transport-https package installed
and the host has the locales package installed
-and the host has the ntp package installed
+and the host has the systemd-timesyncd package installed
and the host has an empty /etc/apt/sources.list.d directory
and the host has hostname saneone
-and the host has saneone in /etc/hosts for 127.0.1.1
~~~
~~~{#sane1.yml .file .yaml}
-sane_debian_system_version: 1
+ansible_python_interpreter: /usr/bin/python3
-sane_debian_system_codename: buster
+sane_debian_system_version: 2
+sane_debian_system_codename: bullseye
sane_debian_system_hostname: saneone
~~~
+## Uses inventory hostname by default
+
+~~~scenario
+given a host running Debian
+when I use role sane_debian_system
+and I use variables from sane-without-hostname.yml
+and I run the playbook
+then the host has the sudo package installed
+and the host has hostname debian-ansible-test
+~~~
+
+~~~{#sane-without-hostname.yml .file .yaml}
+sane_debian_system_version: 2
+
+sane_debian_system_codename: bullseye
+~~~
+
## Checks that debian codename is set
~~~scenario
@@ -38,5 +62,5 @@ and stdout contains "sane_debian_system_codename"
~~~
~~~{#sane2.yml .file .yaml}
-sane_debian_system_version: 1
+sane_debian_system_version: 2
~~~
diff --git a/roles/sane_debian_system/subplot.yaml b/roles/sane_debian_system/subplot.yaml
index 9ac1ee3..b7f03cb 100644
--- a/roles/sane_debian_system/subplot.yaml
+++ b/roles/sane_debian_system/subplot.yaml
@@ -1,11 +1,19 @@
- then: the host has the {package} package installed
- function: host_has_package_installed
+ impl:
+ python:
+ function: host_has_package_installed
- then: the host has an empty {pathname} directory
- function: host_directory_is_empty
+ impl:
+ python:
+ function: host_directory_is_empty
- then: the host has hostname {hostname}
- function: host_hostname_is
+ impl:
+ python:
+ function: host_hostname_is
- then: the host has {hostname} in /etc/hosts for {addr}
- function: host_hostname_has_address
+ impl:
+ python:
+ function: host_hostname_has_address
diff --git a/roles/sane_debian_system/tasks/apt.yml b/roles/sane_debian_system/tasks/apt.yml
index 21eea70..0da3332 100644
--- a/roles/sane_debian_system/tasks/apt.yml
+++ b/roles/sane_debian_system/tasks/apt.yml
@@ -11,28 +11,24 @@
# First update package lists. The ones that come with the image may be
# badly out of date.
#
-# Ignore any error here so that later tasks can fix things such as a badly
-# formed sources.list.
+# Use shell to run apt-get, rather than the Ansible apt module, so
+# that we can pass in the --allow-releaseinfo--change option.
+- name: update package lists
+ shell: |
+ apt-get update --allow-releaseinfo-change
+
- name: update package lists
ignore_errors: yes
apt:
update_cache: yes
cache_valid_time: 0
-- name: install sudo
- apt:
- name: sudo
-
# Now install https transport for APT. This is installed before
# changing sources lists, so that if they happen to use https URLs apt
# will still work. apt-transport-https is in the main Debian archive,
# and we assume those are in the sources.list that come with the
# image.
-#
-# Ignore any error here so that later tasks can fix things such as a badly
-# formed sources.list.
- name: install apt-transport-https
- ignore_errors: yes
apt:
name: apt-transport-https
@@ -41,6 +37,22 @@
src: sources.list.j2
dest: /etc/apt/sources.list
+- name: "update package lists"
+ apt:
+ update_cache: yes
+
+- name: install necessary tools
+ apt:
+ name:
+ - sudo
+
+- name: "allow root to use sudo"
+ copy:
+ content: |
+ root ALL=(ALL:ALL) NOPASSWD: ALL
+ dest: /etc/sudoers.d/root
+ mode: 0600
+
- name: additional sources.list.d/*
with_items: "{{ sane_debian_system_sources_lists }}"
apt_repository:
@@ -49,15 +61,18 @@
- name: add archive signing keys
with_items: "{{ sane_debian_system_sources_lists }}"
- apt_key:
- data: "{{ item.signing_key }}"
- state: present
+ shell: |
+ key="{{ item.signing_key }}"
+ sum="$(echo -n "$key" | sha1sum | awk '{ print $1 }')"
+ echo "$key" > "/etc/apt/trusted.gpg.d/$sum.asc"
when: item.signing_key is defined
+# Use shell to run apt-get to update package lists so that we can pass
+# in the --allow-releaseinfo--change option.
- name: update package lists
- apt:
- update_cache: yes
- cache_valid_time: 0
+ shell: |
+ apt-get update --allow-releaseinfo-change
+
- name: add archive keyrings
with_items: "{{ sane_debian_system_sources_lists }}"
diff --git a/roles/sane_debian_system/tasks/env.yml b/roles/sane_debian_system/tasks/env.yml
index db8f5ba..eedd864 100644
--- a/roles/sane_debian_system/tasks/env.yml
+++ b/roles/sane_debian_system/tasks/env.yml
@@ -2,17 +2,17 @@
apt:
name: dbus
+- name: "start dbus"
+ systemd:
+ name: dbus
+ daemon_reload: yes
+ enabled: yes
+ state: started
+
- name: set /etc/hostname
hostname:
name: "{{ sane_debian_system_hostname }}"
- when: sane_debian_system_hostname is defined
-
-- name: add hostname to /etc/hosts
- lineinfile:
- dest: /etc/hosts
- regexp: '^127\.0\.1\.1 '
- line: "127.0.1.1 {{ sane_debian_system_hostname }}"
- when: sane_debian_system_hostname is defined
+ when: sane_debian_system_hostname != ""
- name: set timezone
timezone:
@@ -23,7 +23,14 @@
state: present
name:
- locales
- - ntp
+
+- name: install systemd-timesyncd or ntp
+ shell: |
+ if apt-cache show systemd-timesyncd > /dev/null; then
+ DEBIAN_FRONTEND=noninteractife apt-get install -y systemd-timesyncd
+ else
+ DEBIAN_FRONTEND=noninteractife apt-get install -y ntp
+ fi
- name: generate locales
locale_gen:
diff --git a/roles/sane_debian_system/tasks/main.yml b/roles/sane_debian_system/tasks/main.yml
index 7722f1c..bc8c6d3 100644
--- a/roles/sane_debian_system/tasks/main.yml
+++ b/roles/sane_debian_system/tasks/main.yml
@@ -1,7 +1,10 @@
- name: "sane_debian_system_version"
shell: |
- [ "{{ sane_debian_system_version }}" = "1" ] || \
+ [ "{{ sane_debian_system_version }}" = "2" ] || \
(echo "Unexpected version {{ sane_debian_system_version }}" 1>&2; exit 1)
-- include: apt.yml
-- include: env.yml
+- ansible.builtin.import_tasks:
+ file: apt.yml
+
+- ansible.builtin.import_tasks:
+ file: env.yml
diff --git a/roles/sshd/README b/roles/sshd/README
new file mode 100644
index 0000000..4b155c8
--- /dev/null
+++ b/roles/sshd/README
@@ -0,0 +1,23 @@
+This role, sshd, configures an SSH server on a Debian. Specifically
+may:
+
+- set host key and certificate
+- set user CA
+- set port on which server listens
+
+To use, define variables below:
+
+- `sshd_version`---must match the current version for the role
+- `sshd_host_key` and `sshd_host_cert`---the host key and
+ corresponding certificate
+ - note that you must define both for either to work
+ - rationale: there's little point in just setting the host key, as
+ it will still force people to accept it the first time; a host
+ certificate removes that need and allows the key to change at will
+- `sshd_port`---the port where the SSH server should listen
+ - rationale: on public-facing servers, the default port gets tons of
+ login attempts by attackers trying to guess passwords
+- `sshd_user_ca_pub`---the public keys of the SSH CAs trusted to
+ certify users
+ - rationale: using a user CA removes the need to maintain, or have,
+ `authorized_keys` files
diff --git a/roles/sshd/defaults/main.yml b/roles/sshd/defaults/main.yml
new file mode 100644
index 0000000..20c9563
--- /dev/null
+++ b/roles/sshd/defaults/main.yml
@@ -0,0 +1,9 @@
+# The user of the role MUST define the version they want to use. If
+# it's not what the version of unix_users being used actually
+# provides, the role will fail.
+sshd_version: null
+
+
+# Allow SSH server to use `authorized_keys` files?
+sshd_allow_authorized_keys: yes
+
diff --git a/roles/sshd/handlers/main.yml b/roles/sshd/handlers/main.yml
new file mode 100644
index 0000000..c4898c0
--- /dev/null
+++ b/roles/sshd/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: sshd_restart
+ systemd:
+ name: ssh
+ state: restarted
diff --git a/roles/sshd/subplot.md b/roles/sshd/subplot.md
new file mode 100644
index 0000000..e86e513
--- /dev/null
+++ b/roles/sshd/subplot.md
@@ -0,0 +1,27 @@
+# Role `sshd` &ndash; configure an SSH server
+
+This role sets up a Debian system so that it can be managed with
+Ansible in a reasonable way.
+
+## Version history
+
+### Version 1
+
+First version. Supports `sshd_version`, `sshd_port`, `sshd_host_key`,
+`sshd_host_cert`, and `sshd_user_ca_pub`.
+
+# Configure SSH
+
+~~~scenario
+given a host running Debian
+when I use role sshd
+and I use variables from sshd.yml
+and I run the playbook
+then stdout contains "sshd role version"
+~~~
+
+~~~{#sshd.yml .file .yaml}
+ansible_python_interpreter: /usr/bin/python3
+
+sshd_version: 1
+~~~
diff --git a/roles/sshd/tasks/main.yml b/roles/sshd/tasks/main.yml
new file mode 100644
index 0000000..ff77c40
--- /dev/null
+++ b/roles/sshd/tasks/main.yml
@@ -0,0 +1,112 @@
+- name: "sshd role version"
+ shell: |
+ [ "{{ sshd_version }}" = "1" ] || \
+ (echo "Unexpected version {{ sshd_version }}" 1>&2; exit 1)
+
+- name: "sshd role configuration sanity check"
+ when: not sshd_allow_authorized_keys and sshd_user_ca_pub is not defined
+ shell: |
+ echo "You MUST define sshd_allow_authorized_keys OR sshd_user_ca_pub"
+ exit 1
+
+- name: "Configure SSH server to read config files in sshd_config.d"
+ lineinfile:
+ path: /etc/ssh/sshd_config
+ regexp: "Include /etc/ssh/sshd_config.d"
+ line: "Include /etc/ssh/sshd_config.d/*.conf"
+ insertbefore: BOF
+ notify: sshd_restart
+
+- name: "Set SSH host identity"
+ when: sshd_host_key is defined and sshd_host_cert is defined
+ copy:
+ content: |
+ {{ sshd_host_key }}
+ dest: /etc/ssh/ssh_host_key
+ owner: root
+ group: root
+ mode: 0600
+ notify: sshd_restart
+
+- name: "Set SSH host certificate"
+ when: sshd_host_key is defined and sshd_host_cert is defined
+ copy:
+ content: |
+ {{ sshd_host_cert }}
+ dest: /etc/ssh/ssh_host_key-cert.pub
+ notify: sshd_restart
+
+- name: "Configure SSH server host key"
+ when: sshd_host_key is defined and sshd_host_cert is defined
+ copy:
+ content: |
+ HostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com
+ HostKey /etc/ssh/ssh_host_key
+ HostCertificate /etc/ssh/ssh_host_key-cert.pub
+ dest: /etc/ssh/sshd_config.d/host_id.conf
+ notify: sshd_restart
+
+- name: "Remove old host key settings from /etc/ssh/sshd_config"
+ when: sshd_host_key is defined and sshd_host_cert is defined
+ lineinfile:
+ path: /etc/ssh/sshd_config
+ state: absent
+ regex: "(?i)hostkey"
+ notify: sshd_restart
+
+- name: "Remove old host cert settings from /etc/ssh/sshd_config"
+ when: sshd_host_key is defined and sshd_host_cert is defined
+ lineinfile:
+ path: /etc/ssh/sshd_config
+ state: absent
+ regex: "(?i)hostcertificate"
+ notify: sshd_restart
+
+- name: "Remove old user CA settings from /etc/ssh/sshd_config"
+ when: sshd_host_key is defined and sshd_host_cert is defined
+ lineinfile:
+ path: /etc/ssh/sshd_config
+ state: absent
+ regex: "(?i)trustedusercakeys"
+ notify: sshd_restart
+
+- name: "Remove obsolete SSH host keys and certificates"
+ when: sshd_host_key is defined and sshd_host_cert is defined
+ shell: |
+ find /etc/ssh -maxdepth 1 -type f -name "ssh_host_*_key*" -delete
+ notify: sshd_restart
+
+- name: "Configure SSH server port"
+ when: sshd_port is defined
+ copy:
+ content: |
+ Port {{ sshd_port }}
+ dest: /etc/ssh/sshd_config.d/port.conf
+ notify: sshd_restart
+
+- name: "Configure user CA for SSH server"
+ when: sshd_user_ca_pub is defined
+ copy:
+ content: |
+ {{ sshd_user_ca_pub }}
+ dest: /etc/ssh/user_ca_pubs
+ notify: sshd_restart
+
+- name: "Configure SSH server to accept user CA"
+ when: sshd_user_ca_pub is defined
+ copy:
+ content: |
+ TrustedUserCAKeys /etc/ssh/user_ca_pubs
+ dest: /etc/ssh/sshd_config.d/user_ca.conf
+ notify: sshd_restart
+
+- name: "Configure SSH server to not use 'authorized_keys' files at all."
+ when: not sshd_allow_authorized_keys
+ copy:
+ content: |
+ AuthorizedKeysFile none
+ dest: /etc/ssh/sshd_config.d/authorized_keys.conf
+ notify: sshd_restart
+
+- name: "Run handlers"
+ meta: flush_handlers
diff --git a/roles/unix_users/subplot.md b/roles/unix_users/subplot.md
index 2fde3e7..c7929e9 100644
--- a/roles/unix_users/subplot.md
+++ b/roles/unix_users/subplot.md
@@ -24,6 +24,8 @@ This role makes use of the following variables:
* `authorized_keys` &ndash; OPTIONAL: text of contents of
`~/.ssh/authorized_keys`
* `password` &ndash; OPTIONAL: encrypted password
+ * `groups` &ndash; OPTIONAL: list of additional groups to which user
+ should be added
Create the encrypted password with something like:
@@ -43,10 +45,11 @@ then the host has user foo
and the user foo on host has encrypted password foopass
and the user foo on host has shell /bin/true
and the user foo on host has authorized_keys containing "ssh-rsa"
+and the user foo on host is in group operator
~~~
~~~{#foo.yml .file .yaml}
-unix_users_version: 1
+unix_users_version: 2
unix_users:
- username: foo
@@ -55,4 +58,5 @@ unix_users:
password: foopass
authorized_keys: |
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKVaQfxzzwpwk763IcPBs308TpYYp6+NTOMvYaj3j3ewz8feYQg3lOlKo/5xaPug2ZywG6v6tpn/p0drovT5YAIPJitP7yJAfEzJe/gO7c9uwx0uIpe6cc8bwRG0XFdUVK0EneB6LpIec+3juj4zitGBm0ffIoLDhJ7J0daTzQN62rZaw/2SjSvgbfnu3a2BYRPz1NGiXdvOCbytVSLlUAR6SxNPrFdh/BJnS4umyDaBL/1j2yaw/WlkfZPn5Ni3USZLRcbHnBUUbo64iwBwJabhdpeh0xLGTqDkaeudUgZjlrRHFyCbwJTPtDzJsPLb5HKGGzdXPHP7Lk6PM2CIOz liw@exolobe1
+ groups: [operator]
~~~
diff --git a/roles/unix_users/subplot.py b/roles/unix_users/subplot.py
index 7bf921d..05330fd 100644
--- a/roles/unix_users/subplot.py
+++ b/roles/unix_users/subplot.py
@@ -14,7 +14,7 @@ def host_has_user(ctx, username=None):
output, exit = qemu.ssh(["getent", "passwd", username])
assert_eq(exit, 0)
output = output.decode("UTF8")
- assert f"\n{username}:" in output
+ assert f"{username}:" in output
def host_user_has_shell(ctx, username=None, shell=None):
@@ -46,3 +46,14 @@ def host_user_has_authorized_keys_containing(ctx, username=None, substring=None)
assert_eq(exit, 0)
output = output.decode("UTF8")
assert substring in output
+
+
+def host_user_is_in_group(ctx, username=None, group=None):
+ assert_eq = globals()["assert_eq"]
+ qemu = ctx["qemu"]
+ output, exit = qemu.ssh(["sudo", "-u", username, "groups"])
+ assert_eq(exit, 0)
+ output = output.decode("UTF8")
+ groups = output.split()
+ logging.debug(f"host_user_is_in_group: groups={groups}")
+ assert group in groups
diff --git a/roles/unix_users/subplot.yaml b/roles/unix_users/subplot.yaml
index 10ac86c..e495602 100644
--- a/roles/unix_users/subplot.yaml
+++ b/roles/unix_users/subplot.yaml
@@ -1,14 +1,29 @@
- then: the host has no user {username}
- function: host_does_not_have_user
+ impl:
+ python:
+ function: host_does_not_have_user
- then: the host has user {username}
- function: host_has_user
+ impl:
+ python:
+ function: host_has_user
- then: the user {username} on host has encrypted password {password}
- function: host_user_has_password
+ impl:
+ python:
+ function: host_user_has_password
- then: the user {username} on host has shell {shell}
- function: host_user_has_shell
+ impl:
+ python:
+ function: host_user_has_shell
- then: the user {username} on host has authorized_keys containing "{substring}"
- function: host_user_has_authorized_keys_containing
+ impl:
+ python:
+ function: host_user_has_authorized_keys_containing
+
+- then: the user {username} on host is in group {group}
+ impl:
+ python:
+ function: host_user_is_in_group
diff --git a/roles/unix_users/tasks/main.yml b/roles/unix_users/tasks/main.yml
index cd6fb66..e181054 100644
--- a/roles/unix_users/tasks/main.yml
+++ b/roles/unix_users/tasks/main.yml
@@ -1,6 +1,6 @@
- name: "check unix_users_version"
shell: |
- [ "{{ unix_users_version }}" = "1" ] || \
+ [ "{{ unix_users_version }}" = "2" ] || \
(echo "Unexpected version {{ unix_users_version }}" 1>&2; exit 1)
- name: create system users
@@ -10,6 +10,13 @@
comment: "{{ item.comment|default('unnamed user') }}"
shell: "{{ item.shell|default('/bin/bash') }}"
system: "{{ item.system|default('no') }}"
+
+- name: add users to additional groups
+ with_items: "{{ unix_users }}"
+ when: item.groups is defined
+ user:
+ name: "{{ item.username }}"
+ groups: "{{ item.groups }}"
- name: set password for users
with_items: "{{ unix_users }}"
diff --git a/subplot.md b/subplot.md
index 4581174..a56e15b 100644
--- a/subplot.md
+++ b/subplot.md
@@ -62,14 +62,3 @@ Verify that everything looks OK and that other scenarios can be run.
given a host running Debian
then I can run /bin/true on the host
~~~
-
-
-
----
-title: "debian-ansible&mdash;Ansible roles for Debian systems"
-author: Lars Wirzenius
-bindings:
-- subplot.yaml
-functions:
-- subplot.py
-...
diff --git a/subplot.subplot b/subplot.subplot
new file mode 100644
index 0000000..5d8083f
--- /dev/null
+++ b/subplot.subplot
@@ -0,0 +1,11 @@
+title: "debian-ansible&mdash;Ansible roles for Debian systems"
+authors:
+ - Lars Wirzenius
+markdowns:
+ - subplot.md
+bindings:
+- subplot.yaml
+impls:
+ python:
+ - subplot.py
+ - lib/runcmd.py
diff --git a/subplot/daemon.py b/subplot/daemon.py
deleted file mode 100644
index e223505..0000000
--- a/subplot/daemon.py
+++ /dev/null
@@ -1,84 +0,0 @@
-#############################################################################
-# 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
index bfc3f24..c4fd385 100755
--- a/subplot/qemumgr.py
+++ b/subplot/qemumgr.py
@@ -107,7 +107,7 @@ class QemuSystem:
memory = int(self._memory / MiB)
argv = [
- "/usr/sbin/daemonize",
+ "/usr/bin/daemonize",
"-c",
self._dirname,
"-p",
@@ -140,7 +140,7 @@ class QemuSystem:
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("started qemu-system")
logging.debug(f" argv: {argv}")
logging.debug(f" pid: {self._pid}")
@@ -157,7 +157,7 @@ class QemuSystem:
return True
return None
- return wait_for(ssh_ok, 60, sleep=5)
+ return wait_for(ssh_ok, 300, sleep=5)
def ssh(self, argv):
assert self._username is not None
diff --git a/subplot/runcmd.py b/subplot/runcmd.py
deleted file mode 100644
index 2bf153c..0000000
--- a/subplot/runcmd.py
+++ /dev/null
@@ -1,247 +0,0 @@
-import logging
-import os
-import re
-import shlex
-import subprocess
-
-
-#
-# Helper functions.
-#
-
-# Get exit code or other stored data about the latest command run by
-# runcmd_run.
-
-
-def _runcmd_get(ctx, name):
- ns = ctx.declare("_runcmd")
- return ns[name]
-
-
-def runcmd_get_exit_code(ctx):
- return _runcmd_get(ctx, "exit")
-
-
-def runcmd_get_stdout(ctx):
- return _runcmd_get(ctx, "stdout")
-
-
-def runcmd_get_stdout_raw(ctx):
- return _runcmd_get(ctx, "stdout.raw")
-
-
-def runcmd_get_stderr(ctx):
- return _runcmd_get(ctx, "stderr")
-
-
-def runcmd_get_stderr_raw(ctx):
- return _runcmd_get(ctx, "stderr.raw")
-
-
-def runcmd_get_argv(ctx):
- return _runcmd_get(ctx, "argv")
-
-
-# Run a command, given an argv and other arguments for subprocess.Popen.
-#
-# This is meant to be a helper function, not bound directly to a step. The
-# stdout, stderr, and exit code are stored in the "_runcmd" namespace in the
-# ctx context.
-def runcmd_run(ctx, argv, **kwargs):
- ns = ctx.declare("_runcmd")
-
- env = dict(os.environ)
- for key, arg in kwargs.pop("env", {}).items():
- env[key] = arg
-
- pp = ns.get("path-prefix")
- if pp:
- env["PATH"] = pp + ":" + env["PATH"]
-
- logging.debug(f"runcmd_run")
- logging.debug(f" argv: {argv}")
- logging.debug(f" env: {env}")
- p = subprocess.Popen(
- argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, **kwargs
- )
- stdout, stderr = p.communicate("")
- ns["argv"] = argv
- ns["stdout.raw"] = stdout
- ns["stderr.raw"] = stderr
- ns["stdout"] = stdout.decode("utf-8")
- ns["stderr"] = stderr.decode("utf-8")
- ns["exit"] = p.returncode
- logging.debug(f" ctx: {ctx}")
- logging.debug(f" ns: {ns}")
-
-
-# Step: prepend srcdir to PATH whenever runcmd runs a command.
-def runcmd_helper_srcdir_path(ctx):
- srcdir = globals()["srcdir"]
- runcmd_prepend_to_path(ctx, srcdir)
-
-
-# Step: This creates a helper script.
-def runcmd_helper_script(ctx, filename=None):
- get_file = globals()["get_file"]
- with open(filename, "wb") as f:
- f.write(get_file(filename))
-
-
-#
-# Step functions for running commands.
-#
-
-
-def runcmd_prepend_to_path(ctx, dirname=None):
- ns = ctx.declare("_runcmd")
- pp = ns.get("path-prefix", "")
- if pp:
- pp = f"{pp}:{dirname}"
- else:
- pp = dirname
- ns["path-prefix"] = pp
-
-
-def runcmd_step(ctx, argv0=None, args=None):
- runcmd_try_to_run(ctx, argv0=argv0, args=args)
- runcmd_exit_code_is_zero(ctx)
-
-
-def runcmd_try_to_run(ctx, argv0=None, args=None):
- argv = [shlex.quote(argv0)] + shlex.split(args)
- runcmd_run(ctx, argv)
-
-
-#
-# Step functions for examining exit codes.
-#
-
-
-def runcmd_exit_code_is_zero(ctx):
- runcmd_exit_code_is(ctx, exit=0)
-
-
-def runcmd_exit_code_is(ctx, exit=None):
- assert_eq = globals()["assert_eq"]
- assert_eq(runcmd_get_exit_code(ctx), int(exit))
-
-
-def runcmd_exit_code_is_nonzero(ctx):
- runcmd_exit_code_is_not(ctx, exit=0)
-
-
-def runcmd_exit_code_is_not(ctx, exit=None):
- assert_ne = globals()["assert_ne"]
- assert_ne(runcmd_get_exit_code(ctx), int(exit))
-
-
-#
-# Step functions and helpers for examining output in various ways.
-#
-
-
-def runcmd_stdout_is(ctx, text=None):
- _runcmd_output_is(runcmd_get_stdout(ctx), text)
-
-
-def runcmd_stdout_isnt(ctx, text=None):
- _runcmd_output_isnt(runcmd_get_stdout(ctx), text)
-
-
-def runcmd_stderr_is(ctx, text=None):
- _runcmd_output_is(runcmd_get_stderr(ctx), text)
-
-
-def runcmd_stderr_isnt(ctx, text=None):
- _runcmd_output_isnt(runcmd_get_stderr(ctx), text)
-
-
-def _runcmd_output_is(actual, wanted):
- assert_eq = globals()["assert_eq"]
- wanted = bytes(wanted, "utf8").decode("unicode_escape")
- logging.debug("_runcmd_output_is:")
- logging.debug(f" actual: {actual!r}")
- logging.debug(f" wanted: {wanted!r}")
- assert_eq(actual, wanted)
-
-
-def _runcmd_output_isnt(actual, wanted):
- assert_ne = globals()["assert_ne"]
- wanted = bytes(wanted, "utf8").decode("unicode_escape")
- logging.debug("_runcmd_output_isnt:")
- logging.debug(f" actual: {actual!r}")
- logging.debug(f" wanted: {wanted!r}")
- assert_ne(actual, wanted)
-
-
-def runcmd_stdout_contains(ctx, text=None):
- _runcmd_output_contains(runcmd_get_stdout(ctx), text)
-
-
-def runcmd_stdout_doesnt_contain(ctx, text=None):
- _runcmd_output_doesnt_contain(runcmd_get_stdout(ctx), text)
-
-
-def runcmd_stderr_contains(ctx, text=None):
- _runcmd_output_contains(runcmd_get_stderr(ctx), text)
-
-
-def runcmd_stderr_doesnt_contain(ctx, text=None):
- _runcmd_output_doesnt_contain(runcmd_get_stderr(ctx), text)
-
-
-def _runcmd_output_contains(actual, wanted):
- assert_eq = globals()["assert_eq"]
- wanted = bytes(wanted, "utf8").decode("unicode_escape")
- logging.debug("_runcmd_output_contains:")
- logging.debug(f" actual: {actual!r}")
- logging.debug(f" wanted: {wanted!r}")
- assert_eq(wanted in actual, True)
-
-
-def _runcmd_output_doesnt_contain(actual, wanted):
- assert_ne = globals()["assert_ne"]
- wanted = bytes(wanted, "utf8").decode("unicode_escape")
- logging.debug("_runcmd_output_doesnt_contain:")
- logging.debug(f" actual: {actual!r}")
- logging.debug(f" wanted: {wanted!r}")
- assert_ne(wanted in actual, True)
-
-
-def runcmd_stdout_matches_regex(ctx, regex=None):
- _runcmd_output_matches_regex(runcmd_get_stdout(ctx), regex)
-
-
-def runcmd_stdout_doesnt_match_regex(ctx, regex=None):
- _runcmd_output_doesnt_match_regex(runcmd_get_stdout(ctx), regex)
-
-
-def runcmd_stderr_matches_regex(ctx, regex=None):
- _runcmd_output_matches_regex(runcmd_get_stderr(ctx), regex)
-
-
-def runcmd_stderr_doesnt_match_regex(ctx, regex=None):
- _runcmd_output_doesnt_match_regex(runcmd_get_stderr(ctx), regex)
-
-
-def _runcmd_output_matches_regex(actual, regex):
- assert_ne = globals()["assert_ne"]
- r = re.compile(regex)
- m = r.search(actual)
- logging.debug("_runcmd_output_matches_regex:")
- logging.debug(f" actual: {actual!r}")
- logging.debug(f" regex: {regex!r}")
- logging.debug(f" match: {m}")
- assert_ne(m, None)
-
-
-def _runcmd_output_doesnt_match_regex(actual, regex):
- assert_eq = globals()["assert_eq"]
- r = re.compile(regex)
- m = r.search(actual)
- logging.debug("_runcmd_output_doesnt_match_regex:")
- logging.debug(f" actual: {actual!r}")
- logging.debug(f" regex: {regex!r}")
- logging.debug(f" match: {m}")
- assert_eq(m, None)
diff --git a/subplot/subplot.yaml b/subplot/subplot.yaml
index 5993feb..286e878 100644
--- a/subplot/subplot.yaml
+++ b/subplot/subplot.yaml
@@ -1,24 +1,42 @@
- given: a host running Debian
- function: create_vm
- cleanup: destroy_vm
+ impl:
+ python:
+ function: create_vm
+ cleanup: destroy_vm
- then: I can run /bin/true on the host
- function: run_true_on_host
+ impl:
+ python:
+ function: run_true_on_host
- when: I use role {role}
- function: use_role_in_playbook
+ impl:
+ python:
+ function: use_role_in_playbook
- when: I use variables from {filename}
- function: set_vars_file
+ impl:
+ python:
+ function: set_vars_file
+ types:
+ filename: file
- when: I run the playbook
- function: run_playbook
+ impl:
+ python:
+ function: run_playbook
- when: I try to run the playbook
- function: try_playbook
+ impl:
+ python:
+ function: try_playbook
- then: the command fails
- function: command_fails
+ impl:
+ python:
+ function: command_fails
- then: stdout contains "{text:text}"
- function: xstdout_contains
+ impl:
+ python:
+ function: xstdout_contains