diff --git a/ansible/deploy.yml b/ansible/deploy.yml
index 0197bc3..f5a78ff 100644
--- a/ansible/deploy.yml
+++ b/ansible/deploy.yml
@@ -7,3 +7,6 @@
 
 - name: Enrol nodes in Ironic
   import_playbook: enrol_nodes.yml
+
+- name: Register flavors in Nova
+  import_playbook: register_flavors.yml
diff --git a/ansible/host_vars/localhost b/ansible/host_vars/localhost
index dc100a4..bd4dc62 100644
--- a/ansible/host_vars/localhost
+++ b/ansible/host_vars/localhost
@@ -51,20 +51,24 @@ node_types: {}
 specs: []
 
 # nova_flavors is a list of Nova flavors to be created. Each flavor must
-# specify the resource class it is associated with. This resource class must
-# be referenced in `specs`.
-#
+# specify the resource class it is associated with, as well as the Tenks node
+# type whose hardware specs should be used.
 # For example:
 #
 # nova_flavors:
 #     # Required.
 #   - resource_class: my_rc
+#     # Required.
+#     node_type: type0
 #     # Defaults to `resource_class`.
 #     name: my_flavor
 #     # Optional, defaults to [].
 #     required_traits: []
 #     # Optional, defaults to [].
 #     forbidden_traits: []
+#     # Extra key-value pairs to add to the flavor's specs. Optional, defaults
+#     # to {}.
+#     custom_specs: {}
 nova_flavors: []
 
 # The Glance UUID of the image to use for the deployment kernel.
diff --git a/ansible/register_flavors.yml b/ansible/register_flavors.yml
new file mode 100644
index 0000000..f7a7314
--- /dev/null
+++ b/ansible/register_flavors.yml
@@ -0,0 +1,11 @@
+---
+- hosts: localhost
+  tasks:
+    - name: Register Nova flavors
+      include_role:
+        name: nova-flavors
+      vars:
+        flavors_virtualenv_path: "{{ virtualenv_path }}"
+        flavors_python_upper_constraints_url: >-
+          {{ python_upper_constraints_url }}
+        flavors: "{{ nova_flavors }}"
diff --git a/ansible/roles/nova-flavors/README.md b/ansible/roles/nova-flavors/README.md
new file mode 100644
index 0000000..88cb76e
--- /dev/null
+++ b/ansible/roles/nova-flavors/README.md
@@ -0,0 +1,21 @@
+Nova Flavors
+============
+
+This role creates flavors in Nova.
+
+Requirements
+------------
+
+- *OS_\** environment variables for the OpenStack cloud in question present in
+  the shell environment. These can be sourced from an OpenStack RC file, for
+  example.
+
+Role Variables
+--------------
+
+- `flavors`: A list of dicts of details for flavors that are to be created. The
+  format for this is detailed in `defaults/main.yml`.
+- `flavors_virtualenv_path`: The path to the virtualenv in which to install the
+  OpenStack clients.
+- `flavors_python_upper_constraints_url`: The URL of the upper constraints file
+  to pass to pip when installing Python packages.
diff --git a/ansible/roles/nova-flavors/defaults/main.yml b/ansible/roles/nova-flavors/defaults/main.yml
new file mode 100644
index 0000000..7c6aeb5
--- /dev/null
+++ b/ansible/roles/nova-flavors/defaults/main.yml
@@ -0,0 +1,25 @@
+---
+# A list of Nova flavors to create.
+# For example:
+#
+# flavors:
+#     # Required.
+#   - resource_class: my_rc
+#     # Required.
+#     node_type: type0
+#     # Defaults to `resource_class`.
+#     name: my_flavor
+#     # Optional, defaults to [].
+#     required_traits: []
+#     # Optional, defaults to [].
+#     forbidden_traits: []
+#     # Extra key-value pairs to add to the flavor's specs. Optional, defaults
+#     # to {}.
+#     custom_specs: {}
+flavors: []
+
+# The path to the virtualenv in which to install the OpenStack clients.
+flavors_virtualenv_path:
+# The URL of the upper constraints file to pass to pip when installing Python
+# packages.
+flavors_python_upper_constraints_url:
diff --git a/ansible/roles/nova-flavors/files/requirements.txt b/ansible/roles/nova-flavors/files/requirements.txt
new file mode 100644
index 0000000..34eb0da
--- /dev/null
+++ b/ansible/roles/nova-flavors/files/requirements.txt
@@ -0,0 +1,4 @@
+# This file contains the Python packages that are needed in the Tenks virtual
+# env.
+
+openstacksdk>=0.17.2 # Apache
diff --git a/ansible/roles/nova-flavors/tasks/main.yml b/ansible/roles/nova-flavors/tasks/main.yml
new file mode 100644
index 0000000..aad5134
--- /dev/null
+++ b/ansible/roles/nova-flavors/tasks/main.yml
@@ -0,0 +1,44 @@
+---
+    - name: Ensure Python requirements are installed
+      pip:
+        requirements: "{{ '/'.join([role_path, 'files', 'requirements.txt']) }}"
+        extra_args: >-
+          -c {{ flavors_python_upper_constraints_url }}
+        virtualenv: "{{ flavors_virtualenv_path }}"
+
+    - name: Register Nova flavors
+      os_nova_flavor:
+        auth_type: password
+        name: "{{ item.name | default(item.resource_class) }}"
+        # FIXME(w-miller): don't necessarily assume the first disk?
+        disk: "{{ node_types[item.node_type].volumes.0.capacity | default('0')
+                  | size_string_to_gb }}"
+        ram: "{{ node_types[item.node_type].memory_mb }}"
+        vcpus: "{{ node_types[item.node_type].vcpus }}"
+        # NOTE(w-miller): I'm not quite sure whether this is janky or beautiful.
+        #
+        #   * Set hardware specs to zero here for scheduling purposes.
+        #   * Add the resource class name.
+        #   * Add required and forbidden traits.
+        #   * Add any custom specs from the user.
+        extra_specs: >-
+          {{ {
+            "resources:DISK_GB": 0,
+            "resources:MEMORY_MB": 0,
+            "resources:VCPU": 0,
+            "resources:CUSTOM_" ~ (
+              item.resource_class | upper
+                | regex_replace('[^A-Za-z0-9]', '_')): 1
+             }
+             | combine(dict(item.required_traits | default([])
+                       | map('regex_replace', '(.*)', 'trait:\1')
+                       | zip_longest([], fillvalue='required')))
+             | combine(dict(item.forbidden_traits | default([])
+                       | map('regex_replace', '(.*)', 'trait:\1')
+                       | zip_longest([], fillvalue='forbidden')))
+             | combine(item.custom_specs | default({}))
+          }}
+      vars:
+        ansible_python_interpreter: >-
+          {{ '/'.join([flavors_virtualenv_path, 'bin', 'python']) }}
+      loop: "{{ flavors }}"