summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-07-27 16:53:19 +0000
committerLars Wirzenius <liw@liw.fi>2021-07-27 16:53:19 +0000
commitda983483a853c672fc1238a36eafb66b79dcbe4a (patch)
treebfa88ff7aba022bf09ce4cae333fa36e7ba5e9ae
parent56671321dbd662fa3f2babdfdbf59f3287f5533b (diff)
parent91a5fef528e998939860f5bff93f18d4723bbdfd (diff)
downloadpuomi-da983483a853c672fc1238a36eafb66b79dcbe4a.tar.gz
Merge branch 'proto' into 'main'
feat: add scripts + infra for setting up nested VMs for routers See merge request larswirzenius/puomi!3
-rw-r--r--.gitignore5
-rw-r--r--README.md43
-rwxr-xr-xcheck11
-rw-r--r--env/env.yaml10
-rw-r--r--env/files/ca7
-rw-r--r--env/files/ca.pub1
-rwxr-xr-xenv/files/getip.py13
-rw-r--r--env/files/id_ed255197
-rw-r--r--env/files/id_ed25519.pub1
-rw-r--r--env/files/inner-hosts4
-rw-r--r--env/files/inner.yml12
-rw-r--r--env/files/puomi.yaml10
-rw-r--r--env/files/vmadm.yaml10
-rw-r--r--env/hosts1
-rw-r--r--env/playbook.yml215
-rwxr-xr-xenv/setup-inner.sh17
-rwxr-xr-xenv/setup.sh30
-rw-r--r--env/ssh/config8
-rw-r--r--env/ssh/ed255197
-rw-r--r--env/ssh/ed25519.pub1
-rw-r--r--puomi.md98
-rw-r--r--puomi.py24
-rw-r--r--puomi.yaml2
23 files changed, 537 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1477ce2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+*.html
+*.pdf
+*.qcow2
+test.py
+test.log
diff --git a/README.md b/README.md
index faae31e..dcd5863 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,49 @@ Puomi will be based on the Debian GNU/Linux operating system. It needs
to be run on a small PC or other suitable hardware. Puomi will have no
interactive user interface, and will be managed entirely via Ansible.
+## To build and test
+
+Create a VM, accessible with the name `puomienv`, then run this to
+provision and verify it works:
+
+~~~sh
+(cd env && ./setup.sh)
+./check
+~~~
+
+You need [Subplot](https://subplot.liw.fi/) installed for `./check`.
+
+If you use [vmadm](https://vmadm.liw.fi/ to create the outer VM:
+
+~~~sh
+(cd env && vmadm delete env.yaml && vmadm new env.yaml && ./setup.sh) && ./check
+~~~
+
+This will all take a while. On my laptop, it takes order of 10 to 15
+minutes. Changes making this faster would be welcome.
+
+## Walk through
+
+Some of the more important files in the source tree:
+
+* `env/` --- directory with Ansible, vmadm, and other files to set up
+ a virtual testing environment.
+ * `env/env.yaml` --- vmadm specification file for creating the outer
+ VM
+ * `env/playbook.yml` --- Ansible playbook for provisioning the outer
+ VM
+ * `env/setup.sh` --- shell script to provision the outer VM and
+ creating and provisioning the inner VMs
+ * `env/ssh` --- SSH keys and configuration for accessing the outer
+ VM, used by the acceptance test suite
+* `check` --- script that runs the acceptance tests
+* `puomi.md` --- Markdown file that describes Puomi, the test
+ environment, and how the environment is verified as working
+* `puomi.yaml` and `puomi.py` --- implementations of the scenario
+ steps used in the acceptance test in `puomi.md`
+* `test.log` --- log file from test program; created by `check`;
+ looking at the log may help debug any issues
+
## Hardware
We will be aiming Puomi at hardware like the following:
diff --git a/check b/check
new file mode 100755
index 0000000..254c08a
--- /dev/null
+++ b/check
@@ -0,0 +1,11 @@
+#!/bin/bash
+#
+# Typeset the subplot, and verify that a pre-existing test environment
+# functions.
+
+set -euo pipefail
+
+subplot docgen puomi.md -o puomi.pdf
+subplot docgen puomi.md -o puomi.html
+subplot codegen puomi.md -o test.py
+python3 test.py --log test.log
diff --git a/env/env.yaml b/env/env.yaml
new file mode 100644
index 0000000..b25eedb
--- /dev/null
+++ b/env/env.yaml
@@ -0,0 +1,10 @@
+# This is a vmadm spec file for a VM in which to test Puomi.
+
+puomienv:
+ cpus: 4
+ memory_mib: 8192
+ image_size_gib: 20
+ ssh_key_files:
+ - ssh/ed25519.pub
+ - ~/.ssh/liw-openpgp.pub
+
diff --git a/env/files/ca b/env/files/ca
new file mode 100644
index 0000000..560a62e
--- /dev/null
+++ b/env/files/ca
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBA4oQIXgis2NkrOTg/5LdccAd0iOM5H98hrjXFGGAuFgAAAJDqtZP16rWT
+9QAAAAtzc2gtZWQyNTUxOQAAACBA4oQIXgis2NkrOTg/5LdccAd0iOM5H98hrjXFGGAuFg
+AAAEDIagIVUs7Y4qitDfqu5LsebGP9GcbxzFbCwfBUTp6L5UDihAheCKzY2Ss5OD/kt1xw
+B3SI4zkf3yGuNcUYYC4WAAAADGxpd0BleG9sb2JlMQE=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/env/files/ca.pub b/env/files/ca.pub
new file mode 100644
index 0000000..8cd9f64
--- /dev/null
+++ b/env/files/ca.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEDihAheCKzY2Ss5OD/kt1xwB3SI4zkf3yGuNcUYYC4W liw@exolobe1
diff --git a/env/files/getip.py b/env/files/getip.py
new file mode 100755
index 0000000..a8b326d
--- /dev/null
+++ b/env/files/getip.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python3
+
+import json
+import sys
+
+
+leases = sys.argv[1]
+hostname = sys.argv[2]
+o = json.load(open(leases))
+
+for h in o:
+ if h["hostname"] == hostname:
+ print(h["ip-address"])
diff --git a/env/files/id_ed25519 b/env/files/id_ed25519
new file mode 100644
index 0000000..14601ed
--- /dev/null
+++ b/env/files/id_ed25519
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDcXZoUfCmGIMyBZzKWWbQlgZBbZ+Tr4EVBdy1UoRcaOAAAAJALYqDEC2Kg
+xAAAAAtzc2gtZWQyNTUxOQAAACDcXZoUfCmGIMyBZzKWWbQlgZBbZ+Tr4EVBdy1UoRcaOA
+AAAEB485JINzvdLZ/6EFMlmF6+aX4OF6G61N9yXtXASc7PY9xdmhR8KYYgzIFnMpZZtCWB
+kFtn5OvgRUF3LVShFxo4AAAADGxpd0BleG9sb2JlMQE=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/env/files/id_ed25519.pub b/env/files/id_ed25519.pub
new file mode 100644
index 0000000..6ad43c2
--- /dev/null
+++ b/env/files/id_ed25519.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINxdmhR8KYYgzIFnMpZZtCWBkFtn5OvgRUF3LVShFxo4 liw@exolobe1
diff --git a/env/files/inner-hosts b/env/files/inner-hosts
new file mode 100644
index 0000000..aa8d8c8
--- /dev/null
+++ b/env/files/inner-hosts
@@ -0,0 +1,4 @@
+[inner]
+puomi
+webby
+lappy \ No newline at end of file
diff --git a/env/files/inner.yml b/env/files/inner.yml
new file mode 100644
index 0000000..4841c78
--- /dev/null
+++ b/env/files/inner.yml
@@ -0,0 +1,12 @@
+- hosts: inner
+ remote_user: debian
+ become: yes
+ tasks:
+ - apt:
+ update_cache: yes
+ upgrade: dist
+ - apt:
+ name:
+ - traceroute
+ vars:
+ foo: bar
diff --git a/env/files/puomi.yaml b/env/files/puomi.yaml
new file mode 100644
index 0000000..65fba58
--- /dev/null
+++ b/env/files/puomi.yaml
@@ -0,0 +1,10 @@
+puomi:
+ networks:
+ - lan
+ - wan
+webby:
+ networks:
+ - wan
+lappy:
+ networks:
+ - lan
diff --git a/env/files/vmadm.yaml b/env/files/vmadm.yaml
new file mode 100644
index 0000000..73f83a1
--- /dev/null
+++ b/env/files/vmadm.yaml
@@ -0,0 +1,10 @@
+image_directory: "~"
+default_base_image: ~/debian-10-openstack-amd64.qcow2
+default_image_gib: 5
+default_memory_mib: 2048
+default_cpus: 1
+authorized_keys:
+ - ~/.ssh/id_ed25519.pub
+default_generate_host_certificate: true
+ca_key: ~/.ssh/ca
+default_networks: []
diff --git a/env/hosts b/env/hosts
new file mode 100644
index 0000000..77fec6a
--- /dev/null
+++ b/env/hosts
@@ -0,0 +1 @@
+puomienv
diff --git a/env/playbook.yml b/env/playbook.yml
new file mode 100644
index 0000000..7f34432
--- /dev/null
+++ b/env/playbook.yml
@@ -0,0 +1,215 @@
+- hosts: puomienv
+ remote_user: debian
+ become: yes
+ roles:
+ - sane_debian_system
+ - unix_users
+ tasks:
+ - name: "Install software"
+ apt:
+ name:
+ - qemu-system-x86
+ - virtinst
+ - virt-manager
+ - libvirt-daemon-system
+ - libvirt-clients
+ - libnss-libvirt
+ - python3-lxml
+ - vmadm
+ - jq
+ - libnss-libvirt
+ - ansible
+ - traceroute
+ - moreutils
+ - name: "configure nss to find VM names"
+ shell: |
+ if awk '$1 == "hosts:" && !/libvirt_guest/' /etc/nsswitch.conf | grep .
+ then
+ sed -i '/hosts:/s/files /files libvirt libvirt_guest /' /etc/nsswitch.conf
+ fi
+ - name: "put puomi into libvirt group"
+ user:
+ name: puomi
+ groups:
+ - libvirt
+ - name: "define libvirt network lan"
+ virt_net:
+ command: define
+ autostart: yes
+ name: lan
+ xml: |
+ <network>
+ <name>lan</name>
+ <bridge name='virbr1'/>
+ <forward/>
+ <ip address='192.168.40.1' netmask='255.255.255.0'>
+ <dhcp>
+ <range start='192.168.40.2' end='192.168.40.254'/>
+ </dhcp>
+ </ip>
+ </network>
+ - name: "autostart libvirt network lan"
+ virt_net:
+ autostart: yes
+ name: lan
+ - name: "start libvirt network lan"
+ virt_net:
+ command: start
+ name: lan
+ - name: "define libvirt network wan"
+ virt_net:
+ command: define
+ autostart: yes
+ name: wan
+ xml: |
+ <network>
+ <name>wan</name>
+ <bridge name='virbr2'/>
+ <forward/>
+ <ip address='192.168.50.1' netmask='255.255.255.0'>
+ <dhcp>
+ <range start='192.168.50.2' end='192.168.50.254'/>
+ </dhcp>
+ </ip>
+ </network>
+ - name: "autostart libvirt network wan"
+ virt_net:
+ autostart: yes
+ name: wan
+ - name: "start libvirt network wan"
+ virt_net:
+ command: start
+ name: wan
+ - name: "remove libvirt network default"
+ virt_net:
+ command: undefine
+ name: default
+ - name: "copy Debian 10 OpenStack image"
+ copy:
+ src: debian-10-openstack-amd64.qcow2
+ dest: /home/puomi/debian-10-openstack-amd64.qcow2
+ - name: "create ~puomi/.config/vmadm"
+ file:
+ state: directory
+ path: /home/puomi/.config/vmadm
+ owner: puomi
+ group: puomi
+ mode: 0755
+ - name: "configure vmadm"
+ copy:
+ src: vmadm.yaml
+ dest: /home/puomi/.config/vmadm/config.yaml
+ - name: "copy vmadm spec for VMs"
+ copy:
+ src: puomi.yaml
+ dest: /home/puomi/puomi.yaml
+ - name: "create ~puomi/.ssh"
+ file:
+ state: directory
+ path: /home/puomi/.ssh
+ owner: puomi
+ group: puomi
+ mode: 0700
+ - name: "copy SSH private key"
+ copy:
+ src: id_ed25519
+ dest: /home/puomi/.ssh/id_ed25519
+ owner: puomi
+ group: puomi
+ mode: 0600
+ - name: "copy SSH public key"
+ copy:
+ src: id_ed25519.pub
+ dest: /home/puomi/.ssh/id_ed25519.pub
+ owner: puomi
+ group: puomi
+ - name: "copy SSH CA private key"
+ copy:
+ src: ca
+ dest: /home/puomi/.ssh/ca
+ owner: puomi
+ group: puomi
+ mode: 0600
+ - name: "copy SSH CA public key"
+ copy:
+ src: ca.pub
+ dest: /home/puomi/.ssh/ca.pub
+ owner: puomi
+ group: puomi
+ - name: "configure SSH client to trust SSH CA host certificates"
+ shell: |
+ echo "@cert-authority * $(cat /home/puomi/.ssh/ca.pub)" | tee /home/puomi/.ssh/known_hosts
+ chown puomi:puomi /home/puomi/.ssh/known_hosts
+ - name: "copy files"
+ copy:
+ src: "{{ item }}"
+ dest: "/home/puomi/{{ item }}"
+ owner: puomi
+ group: puomi
+ mode: 0755
+ loop:
+ - inner.yml
+ - inner-hosts
+ - name: "copy scripts"
+ copy:
+ src: "{{ item }}"
+ dest: "/home/puomi/{{ item }}"
+ owner: puomi
+ group: puomi
+ mode: 0755
+ loop:
+ - getip.py
+ - setup-inner.sh
+ vars:
+ sane_debian_system_version: 2
+ unix_users_version: 2
+
+ sane_debian_system_hostname: puomienv
+ sane_debian_system_codename: buster
+ sane_debian_system_mirror: deb.debian.org
+
+ ansible_python_interpreter: /usr/bin/python3
+
+ unix_users:
+ - username: puomi
+ comment: Puomi for testing
+ authorized_keys: |
+ {{ ssh_pub }}
+
+ sane_debian_system_sources_lists:
+ - repo: deb http://ci-prod-controller.vm.liw.fi/debian unstable-ci main
+ signing_key: "{{ ci_prod_signing_key }}"
+
+ ssh_pub: |
+ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA4hKoygOkXNujMW40d2F93lIMbyu0ZwXSBQ2S17R6a8 liw@exolobe1
+
+ ci_prod_signing_key: |
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+ mQINBFrLO7kBEADdz6mHstYmKU5Dp6OSjxWtWaqTDOX1sJdmmaIK/9EKVIH0Maxp
+ 5kvVO5G6mULLAjv/kLG0MxasHPrq8I2A/y8AqKAGVL8QelwLjQMIFZ30/VbGQPHS
+ +T5TZXEnoQtNce1GUhFwJ38ZyjjwHBFV9tSec7rZ2Q3YeM3nNnGPf6DacXGfEOPO
+ HIN4sXAN2hzNXNjKRzTIvxQseb6nr7afUh/SlZ3yhQOCrIzmYlD7tP9WJe7ofL0p
+ JY4pDQYw8rT6nC2BE/ioemh84kERCT1vCe+OVFlSRuMlqfEv+ZpKQ+itOmPDQ/lM
+ jpUm1K2hrW/lWpxT/ZxHKo/w1K36J5WshgMZxfUu5BMCL9LMqMcrXNhNjDMfxDMM
+ 3yBPOvQ4ls6fecOZ/bsFo1p8VzMk/w/eG8vPs5yuNa5XxN95yFMXoOHGb5Xbu8D4
+ 6yiW+Af70LbiSNpGdmNdneiGB2fY38NxBukPw5u3S5qG8HedSmMr1RvSr5kHoAAe
+ UbOY+BYaaKsTAT7+1skUW1o3FJSqoRKCHAzTsMWC6zzhR8hRn7jVrrguH1hGbqq5
+ TZSCFQZExuTJ7uXrTLG0WoBXIjB5wWNcSeXn8myUWYB51nJNF4tJBouZOz9JwWGl
+ kiAQkrHnBttLQWdW9FyjbIoTZMtpvVx+m6ObGTGdGL1cNlLAvWprMXGc+QARAQAB
+ tDJJY2sgQVBUIHJlcG9zaXRvcnkgc2lnbmluZyBrZXkgKDIwMTgpIDxsaXdAbGl3
+ LmZpPokCTgQTAQgAOBYhBKL1uyDoXyxUH3O717Wr+TZVS6PGBQJayzu5AhsDBQsJ
+ CAcCBhUICQoLAgQWAgMBAh4BAheAAAoJELWr+TZVS6PGB5QQANTcikhRUHwt9N4h
+ dGc/Hp6CbqdshMoWlwpFskttoVDxQG5OAobuZl5XyzGcmja1lT85RGkZFfbca0IZ
+ LnXOLLSAu51QBkXNaj4OhjK/0uQ+ITrvL6RQSXNgHiUTR/W2XD1GIUq6nBqe2GSN
+ 31S1baYKKVj5QIMsi7Dq8ls3BBXuPCE+xTSaNmGWjes2t9pPidcRvxsksCLY1qgw
+ P1GFXBeMkBQ29kBP87SUL15SIk7OiQLlEURCy5iRls5rt/YEsdEpRWIb0Tm5Nrjv
+ 2M3VM+iBhfNXTwj0rJ34mlycF1qQmA7YcTEobT7z587GPY0VWzBpQUnEQj7rQWPM
+ cDYY0b+I6kQ8VKOaL4wVAtE98d7HzFIrIrwhTKufnrWrVDPYsmLZ+LPC1jiF7JBD
+ SR6Vftb+SdDR9xoE1yRuXbC6IfoW+5/qQNrdQ2mm9BFw5jOonBqchs18HTTf3441
+ 6SWwP9fY3Vi+IZphPPi0Gf85oMStgnv/Wnw6LacEL32ek39Desero/D8iGLZernK
+ Q2mC9mua5A/bYGVhsNWyURNFkKdbFa+/wW3NfdKYyZnsSfo+jJ2luNewrhAY7Kod
+ GWXTer9RxzTGA3EXFGvNr+BBOOxSj0SfWTl0Olo7J5dnxof+jLAUS1VHpceHGHps
+ GSJSdir7NkZidgwoCPA7BTqsb5LN
+ =dXB0
+ -----END PGP PUBLIC KEY BLOCK-----
diff --git a/env/setup-inner.sh b/env/setup-inner.sh
new file mode 100755
index 0000000..1d01c0b
--- /dev/null
+++ b/env/setup-inner.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+set -eu -o pipefail
+
+msg()
+{
+ printf ' %s\n' "$@"
+}
+
+msg "Delete any existing inner VMs"
+vmadm delete puomi.yaml
+
+msg "Create new inner VMs"
+vmadm new puomi.yaml
+
+msg "Provision inner VMs"
+chronic ansible-playbook -i inner-hosts inner.yml
diff --git a/env/setup.sh b/env/setup.sh
new file mode 100755
index 0000000..7071d6b
--- /dev/null
+++ b/env/setup.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+set -eu -o pipefail
+
+# Get the Debian 10 (buster) OpenStack cloud image. We use it as a
+# base image for creating the VMs we need.
+
+url="https://cloud.debian.org/images/cloud/OpenStack/current-10/debian-10-openstack-amd64.qcow2"
+image=debian-10-openstack-amd64.qcow2
+if [ ! -e "files/$image" ]
+then
+ echo "Download Debian cloud image (only happens on first run)"
+ wget -q -c -O "files/$image" "$url"
+fi
+
+# git does not preserve file modes properly, so set the permissions of
+# the SSH keys we're using so the the SSH client is happy.
+chmod 600 ssh/ed25519*
+
+echo "Provision outer VM"
+chronic ansible-playbook -i hosts playbook.yml
+
+echo "Create and provision the inner VMs"
+ssh -F ssh/config puomi@puomienv ./setup-inner.sh
+
+# Disable network forwarding in outer VM. This can't be done until the
+# inner VMs are provisioned, or they can't install anything.
+ssh debian@puomienv sudo sysctl -w net.ipv4.ip_forward=0
+
+echo "Finished; the router test environment is ready for use"
diff --git a/env/ssh/config b/env/ssh/config
new file mode 100644
index 0000000..69e34f3
--- /dev/null
+++ b/env/ssh/config
@@ -0,0 +1,8 @@
+Host *
+ ForwardAgent no
+ ControlMaster no
+ IdentitiesOnly yes
+ ServerAliveInterval 60
+ PasswordAuthentication no
+ KbdInteractiveAuthentication no
+ IdentityFile ssh/ed25519
diff --git a/env/ssh/ed25519 b/env/ssh/ed25519
new file mode 100644
index 0000000..8a59f0c
--- /dev/null
+++ b/env/ssh/ed25519
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAOISqMoDpFzbozFuNHdhfd5SDG8rtGcF0gUNkte0emvAAAAJAf032rH9N9
+qwAAAAtzc2gtZWQyNTUxOQAAACAOISqMoDpFzbozFuNHdhfd5SDG8rtGcF0gUNkte0emvA
+AAAECYIVFEDbQDeAv6UVxwrbW5D55Z6OFI2YxjXho4SvOdiA4hKoygOkXNujMW40d2F93l
+IMbyu0ZwXSBQ2S17R6a8AAAADGxpd0BleG9sb2JlMQE=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/env/ssh/ed25519.pub b/env/ssh/ed25519.pub
new file mode 100644
index 0000000..2f70c68
--- /dev/null
+++ b/env/ssh/ed25519.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA4hKoygOkXNujMW40d2F93lIMbyu0ZwXSBQ2S17R6a8 liw@exolobe1
diff --git a/puomi.md b/puomi.md
new file mode 100644
index 0000000..f710792
--- /dev/null
+++ b/puomi.md
@@ -0,0 +1,98 @@
+---
+title: "Puomi---a simple router"
+author: The Puomi project
+template: python
+bindings:
+- puomi.yaml
+- lib/runcmd.yaml
+functions:
+- puomi.py
+- lib/runcmd.py
+...
+
+# Introduction
+
+Puomi is, or will become, software for a simple Internet router and
+access point for home and small office use. A device running Puomi
+connects one or more machines via Ethernet and wifi to the Internet,
+while providing a firewall against outside intrusions.
+
+Puomi is based on the Debian GNU/Linux operating system. It needs to
+be run on a small PC or other suitable hardware. Puomi will have no
+interactive user interface, and will be managed entirely via Ansible.
+
+# Overview
+
+This chapter is sketches the overall shape of Puomi.
+
+* Puomi runs on a PC with at least two Ethernet ports.
+* Puomi is connected via a specific Ethernet port (named "eth0") to a
+ public Internet connection and to devices on the local network ("the
+ LAN") via other Ethernet ports.
+* Puomi routes traffic between the local network and the Internet and
+ between devices on the local network.
+* At this stage, there is no firewall. That will come later.
+
+# Testing
+
+The functionality of Puomi is verified in an environment consisting of
+nested virtual machines. The outer VM provides an isolated environment
+in which three inner VMs work. This isolation is primarily for
+networking purposes. The inner VMs are:
+
+* `puomi`---the router itself
+* `webby`---represents a web server on the public Internet
+* `lappy`---represents a host on the local network
+
+The outer VM uses [libvirt][] to manage the inner VMs, and the
+[vmadm][] tool to create VMs. vmadm is used for simplicity, since this
+is what it was written for. vmadm could be replaced with some shell or
+Python scripting, but life is too short. The vmadm tool is installed
+automatically into the outer VM. The outer VM can be created in
+whatever way is convenient: the acceptance test suite does not create
+it.
+
+[libvirt]: https://libvirt.org/
+[vmadm]: https://vmadm.liw.fi/
+
+~~~pikchr
+Webby: box "webby"
+
+arrow <->
+Internet: ellipse "Internet"
+
+arrow <-> "wan" aligned
+Puomi: box "puomi"
+
+arrow <-> "lan" aligned
+Laptop: box "laptop"
+~~~
+
+There are two libvirt virtual networks, `wan` and `lan`, configured in
+the outer VM. `webby` attached to `wan`, `lappy` is attached to `lan`,
+and `puomi` is attached to both. `webby` can't reach `lappy` directly,
+or vice versa, unless `puomi` routes the packets for them.
+
+The outer VM is further configured so that the inner VMs can't reach
+the network outside the outer VM.
+
+# Acceptance criteria
+
+This chapter documents and verifies the detailed acceptance criteria
+for the Puomi testing environment using [Subplot][] scenarios. It is
+meant to be used against a previously set up instance of the
+environment called `puomienv`.
+
+[Subplot]: https://subplot.liw.fi/
+
+## Smoke test
+
+This scenario verifies that the Puomi testing environment has all
+three virtual machines, and that it's possible to log into each of
+them from the test environment.
+
+~~~scenario
+given a router testing environment
+when I run ssh -F .ssh/config -v puomi@puomienv hostname
+then stdout is exactly "puomienv\n"
+~~~
diff --git a/puomi.py b/puomi.py
new file mode 100644
index 0000000..921ff27
--- /dev/null
+++ b/puomi.py
@@ -0,0 +1,24 @@
+import logging
+import os
+import shutil
+
+
+def env_setup(ctx):
+ logging.info("setting up test directory for router testing")
+
+ srcdir = globals()["srcdir"]
+ ssh = os.path.join(srcdir, "env", "ssh")
+ dst = os.path.join(os.getcwd(), ".ssh")
+
+ logging.debug(f"copy {ssh} to {dst}")
+ shutil.copytree(ssh, dst)
+
+ # The config refers to ssh/ed25519 as the key to use. However, the
+ # directory is actually .ssh here so that the SSH client finds it
+ # automatically. Create a symlink so both forms work.
+ os.symlink(".ssh", "ssh")
+
+ # Set permissions on the key files, so that the SSH client isn't upset if
+ # they're lax.
+ os.chmod(".ssh/ed25519", 0o600)
+ os.chmod(".ssh/ed25519.pub", 0o600)
diff --git a/puomi.yaml b/puomi.yaml
new file mode 100644
index 0000000..25c772a
--- /dev/null
+++ b/puomi.yaml
@@ -0,0 +1,2 @@
+- given: "a router testing environment"
+ function: env_setup