diff --git a/dev/environment-setup.sh b/dev/environment-setup.sh
index eea18220e34d89c3d9a57801cc764b21eb9b6d7e..e639d5eeac641e36e061d614f1da9621a2581046 100755
--- a/dev/environment-setup.sh
+++ b/dev/environment-setup.sh
@@ -1,5 +1,8 @@
 #!/bin/bash
 
+# Save the current shell options.
+oldstate=$(set +o)
+
 set -eu
 set -o pipefail
 
@@ -19,3 +22,6 @@ function main {
 }
 
 main
+
+# Restore previous shell options.
+eval "$oldstate"
diff --git a/dev/functions b/dev/functions
index 60565bc49b209fe919b2936f2370b1b37bd5b4fe..3f246268677c22d30040c75aadb6c7c0c28f63a8 100644
--- a/dev/functions
+++ b/dev/functions
@@ -27,9 +27,6 @@ function config_defaults {
     # Path to the kayobe virtual environment.
     export KAYOBE_VENV_PATH="${KAYOBE_VENV_PATH:-${HOME}/kayobe-venv}"
 
-    # Path to the Tenks virtual environment.
-    export TENKS_VENV_PATH="${TENKS_VENV_PATH:-${HOME}/tenks-test-venv}"
-
     # Whether to provision a VM for the seed host.
     export KAYOBE_SEED_VM_PROVISION=${KAYOBE_SEED_VM_PROVISION:-1}
 
@@ -43,6 +40,14 @@ function config_defaults {
 
     # Additional arguments to pass to kayobe commands.
     export KAYOBE_EXTRA_ARGS=${KAYOBE_EXTRA_ARGS:-}
+
+    # Path to the Tenks virtual environment.
+    export TENKS_VENV_PATH="${TENKS_VENV_PATH:-${HOME}/tenks-test-venv}"
+
+    # Path to a Tenks YAML configuration file. If unset,
+    # tenks-deploy-config-overcloud.yml or tenks-deploy-config-compute.yml will
+    # be used.
+    export TENKS_CONFIG_PATH=${TENKS_CONFIG_PATH:-}
 }
 
 function config_set {
@@ -399,6 +404,50 @@ function overcloud_test {
     openstack server delete --wait "$name"
 }
 
+function write_bifrost_clouds_yaml {
+    # Generate a clouds.yaml for accessing the Ironic API in Bifrost.
+    if [[ ! -f ~/.config/openstack/clouds.yaml ]]; then
+        mkdir -p ~/.config/openstack
+        cat << EOF > ~/.config/openstack/clouds.yaml
+---
+clouds:
+  bifrost:
+    auth_type: "none"
+    endpoint: http://192.168.33.5:6385
+EOF
+    fi
+}
+
+function run_tenks_playbook {
+    # Run a Tenks playbook. Arguments:
+    # $1: The path to the Tenks repo.
+    # $2: The name of the playbook to run.
+    local tenks_path="$1"
+    local tenks_playbook="$2"
+    local parent="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+    if [[ -f "${KOLLA_CONFIG_PATH:-/etc/kolla}/admin-openrc.sh" ]]; then
+        # Overcloud
+        default_tenks_config=tenks-deploy-config-compute.yml
+        source "${KOLLA_CONFIG_PATH:-/etc/kolla}/admin-openrc.sh"
+    else
+        # Seed
+        default_tenks_config=tenks-deploy-config-overcloud.yml
+        write_bifrost_clouds_yaml
+        export OS_CLOUD=bifrost
+    fi
+
+    # Allow a specific Tenks config file to be specified via
+    # $TENKS_CONFIG_PATH.
+    tenks_config="${TENKS_CONFIG_PATH:-$parent/$default_tenks_config}"
+
+    ansible-playbook \
+        -vvv \
+        --inventory "$tenks_path/ansible/inventory" \
+        --extra-vars=@"$tenks_config" \
+        "$tenks_path/ansible/$tenks_playbook"
+}
+
 function tenks_deploy {
     set -eu
     # Create a simple test Tenks deployment. Assumes that a bridge named
@@ -427,15 +476,27 @@ function tenks_deploy {
     # vSwitch.
     sudo cp --no-clobber "$parent/ovs-vsctl" /usr/bin/ovs-vsctl
 
-    source "${KOLLA_CONFIG_PATH:-/etc/kolla}/admin-openrc.sh"
+    run_tenks_playbook "$tenks_path" deploy.yml
+}
 
-    pip install python-openstackclient
+function tenks_teardown {
+    set -eu
+    # Tear down a test Tenks deployment.
+    # Arguments:
+    # $1: The path to the Tenks repo.
+    local tenks_path="$1"
 
-    ansible-playbook \
-        -vvv \
-        --inventory "$tenks_path/ansible/inventory" \
-        --extra-vars=@"$parent/tenks-deploy-config.yml" \
-        "$tenks_path/ansible/deploy.yml"
+    echo "Tearing down Tenks"
+
+    environment_setup
+
+    # We don't want to use the Kayobe venv.
+    deactivate
+
+    # Source the Tenks venv.
+    source ${TENKS_VENV_PATH:-$HOME/tenks-test-venv}/bin/activate
+
+    run_tenks_playbook "$tenks_path" teardown.yml
 }
 
 # General purpose
diff --git a/dev/tenks-deploy-config.yml b/dev/tenks-deploy-config-compute.yml
similarity index 100%
rename from dev/tenks-deploy-config.yml
rename to dev/tenks-deploy-config-compute.yml
diff --git a/dev/tenks-deploy-config-overcloud.yml b/dev/tenks-deploy-config-overcloud.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b046f28b86f1531b91587ac07c7c1783381b64cd
--- /dev/null
+++ b/dev/tenks-deploy-config-overcloud.yml
@@ -0,0 +1,33 @@
+---
+# This file holds the config given to Tenks when running `tenks-deploy.sh`. It
+# assumes the existence of the bridge `breth1`.
+
+node_types:
+  type0:
+    memory_mb: 3072
+    vcpus: 1
+    volumes:
+      # There is a minimum disk space capacity requirement of 4GiB when using Ironic Python Agent:
+      # https://github.com/openstack/ironic-python-agent/blob/master/ironic_python_agent/utils.py#L290
+      # The CentOS7 cloud image seems to fill a 4GiB disk, so allow 6.
+      - capacity: 6GiB
+    physical_networks:
+      - physnet1
+    console_log_enabled: true
+
+specs:
+  - type: type0
+    count: 1
+    ironic_config:
+      resource_class: test-rc
+      network_interface: noop
+
+node_name_prefix: controller
+
+nova_flavors: []
+
+physnet_mappings:
+  physnet1: breth1
+
+# No placement service.
+wait_for_placement: false
diff --git a/dev/tenks-teardown.sh b/dev/tenks-teardown.sh
new file mode 100755
index 0000000000000000000000000000000000000000..12a377e80af257f799cef579f5ea1464b350a5ba
--- /dev/null
+++ b/dev/tenks-teardown.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+set -eu
+set -o pipefail
+
+# Simple script to teardown a Tenks cluster. This should be executed from
+# within the VM. Arguments:
+# $1: The path to the Tenks repo.
+
+PARENT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+source "${PARENT}/functions"
+
+
+function main {
+    if [ -z ${1+x} ]; then
+        echo "Usage: $0 <tenks repo path>"
+        return 1
+    fi
+    tenks_path="$1"
+
+    config_init
+    tenks_teardown "$tenks_path"
+}
+
+main "$@"
diff --git a/doc/source/development/automated.rst b/doc/source/development/automated.rst
index aa9b4e01c6034b4979bb0a4246db344a5c2e4f81..feb291ac54cea7fa54e5924a17d301ee64ffbfaf 100644
--- a/doc/source/development/automated.rst
+++ b/doc/source/development/automated.rst
@@ -105,6 +105,94 @@ script::
 
     ./dev/overcloud-upgrade.sh
 
+.. _development-automated-seed:
+
+Seed
+====
+
+These instructions cover deploying the seed services directly rather than in a
+VM. See :ref:`development-automated-seed-vm` for instructions covering
+deployment of the seed services in a VM.
+
+Preparation
+-----------
+
+Clone the kayobe repository::
+
+    git clone https://git.openstack.org/openstack/kayobe.git
+
+Change to the ``kayobe`` directory::
+
+    cd kayobe
+
+Clone the ``kayobe-config-dev`` repository to ``config/src/kayobe-config``::
+
+    mkdir -p config/src
+    git clone https://git.openstack.org/openstack/kayobe-config-dev.git config/src/kayobe-config
+
+Inspect the kayobe configuration and make any changes necessary for your
+environment.
+
+The default development configuration expects the presence of a bridge
+interface on the seed host to carry provisioning traffic.  The bridge should be
+named ``breth1`` with a single port ``eth1``, and an IP address of
+``192.168.33.5/24``.  This can be modified by editing
+``config/src/kayobe-config/etc/kayobe/inventory/group_vars/seed/network-interfaces``.
+Alternatively, this can be added using the following commands::
+
+    sudo ip l add breth1 type bridge
+    sudo ip l set breth1 up
+    sudo ip a add 192.168.33.5/24 dev breth1
+    sudo ip l add eth1 type dummy
+    sudo ip l set eth1 up
+    sudo ip l set eth1 master breth1
+
+Usage
+-----
+
+Run the ``dev/install.sh`` script to install kayobe and its dependencies in a
+virtual environment::
+
+    ./dev/install.sh
+
+Run the ``dev/seed-deploy.sh`` script to deploy the seed services::
+
+    ./dev/seed-deploy.sh
+
+Upon successful completion of this script, the seed will be active.
+
+Testing
+-------
+
+The seed services may be tested using the `Tenks
+<https://tenks.readthedocs.io/en/latest/>`__ project to create fake bare metal
+nodes.
+
+Clone the tenks repository::
+
+    git clone https://git.openstack.org/openstack/tenks.git
+
+Edit the Tenks configuration file, ``dev/tenks-deploy-config-seed.yml``.
+
+Run the ``dev/tenks-deploy.sh`` script to deploy Tenks::
+
+    ./dev/tenks-deploy.sh ./tenks
+
+Check that Tenks has created a VM called ``controller0``::
+
+    sudo virsh list --all
+
+Verify that VirtualBMC is running::
+
+    ~/tenks-venv/bin/vbmc list
+
+The machines and networking created by Tenks can be cleaned up via
+``dev/tenks-teardown.sh``::
+
+    ./dev/tenks-teardown.sh ./tenks
+
+.. _development-automated-seed-hypervisor:
+
 Seed Hypervisor
 ===============
 
@@ -149,18 +237,20 @@ hypervisor::
 
 Upon successful completion of this script, the seed hypervisor will be active.
 
+.. _development-automated-seed-vm:
+
 Seed VM
 =======
 
 The seed VM should be deployed on a system configured as a libvirt/KVM
-hypervisor, using the kayobe seed hypervisor support or otherwise.
+hypervisor, using :ref:`development-automated-seed-hypervisor` or otherwise.
 
 Preparation
 -----------
 
 The following commands should be executed on the seed hypervisor.
 
-Change the current directory to the kayobe repository::
+Clone the kayobe repository::
 
     git clone https://git.openstack.org/openstack/kayobe.git
 
@@ -178,7 +268,7 @@ Inspect the kayobe configuration and make any changes necessary for your
 environment.
 
 Usage
-=====
+-----
 
 Run the ``dev/install.sh`` script to install kayobe and its dependencies in a
 virtual environment::