diff --git a/ansible/group_vars/all/bifrost b/ansible/group_vars/all/bifrost
index 8f50d6fcf..720dc7e5f 100644
--- a/ansible/group_vars/all/bifrost
+++ b/ansible/group_vars/all/bifrost
@@ -55,21 +55,17 @@ kolla_bifrost_inspector_port_addition: "{{ inspector_port_addition }}"
 kolla_bifrost_inspector_extra_kernel_options: "{{ inspector_extra_kernel_options }}"
 
 # List of introspection rules for Bifrost's Ironic Inspector service.
-kolla_bifrost_inspector_rules:
-  - "{{ inspector_rule_ipmi_credentials }}"
-  - "{{ inspector_rule_deploy_kernel }}"
-  - "{{ inspector_rule_deploy_ramdisk }}"
-  - "{{ inspector_rule_local_boot }}"
-  - "{{ inspector_rule_root_hint_init }}"
-  - "{{ inspector_rule_root_hint_serial }}"
-  - "{{ inspector_rule_set_pxe_interface_mac }}"
-  - "{{ inspector_rule_eno3_lldp_switch_port_desc_to_name }}"
+kolla_bifrost_inspector_rules: "{{ inspector_rules }}"
 
 # Ironic inspector IPMI username to set.
-kolla_bifrost_inspector_ipmi_username:
+kolla_bifrost_inspector_ipmi_username: "{{ inspector_ipmi_username }}"
 
 # Ironic inspector IPMI password to set.
-kolla_bifrost_inspector_ipmi_password:
+kolla_bifrost_inspector_ipmi_password: "{{ inspector_ipmi_password }}"
+
+# Ironic inspector network interface name on which to check for an LLDP switch
+# port description to use as the node's name.
+kolla_bifrost_inspector_lldp_switch_port_interface: "{{ inspector_lldp_switch_port_interface }}"
 
 # Ironic inspector deployment kernel location.
 kolla_bifrost_inspector_deploy_kernel: "http://{{ provision_oc_net_name | net_ip }}:8080/ipa.vmlinuz"
diff --git a/ansible/group_vars/all/inspector b/ansible/group_vars/all/inspector
index 95e1e3117..bf51e2289 100644
--- a/ansible/group_vars/all/inspector
+++ b/ansible/group_vars/all/inspector
@@ -48,21 +48,13 @@ inspector_ipmi_username:
 # Ironic inspector IPMI password to set.
 inspector_ipmi_password:
 
-# Ironic inspector deployment kernel location.
-inspector_deploy_kernel:
-
-# Ironic inspector deployment ramdisk location.
-inspector_deploy_ramdisk:
+# Ironic inspector network interface name on which to check for an LLDP switch
+# port description to use as the node's name.
+inspector_lldp_switch_port_interface:
 
 ###############################################################################
 # Ironic inspector introspection rules configuration.
 
-# Inspector rules use python string formatting. To escape a curly bracket we
-# repeat it, but this also happens to be the Jinja2 variable start sequence.
-# These variables make escaping rules slightly less hairy.
-inspector_rule_escaped_left_curly: "{{ '{{' | replace('{{', '{{ \"{{\" }}') }}"
-inspector_rule_escaped_right_curly: "{{ '}}' | replace('}}', '{{ \"}}\" }}') }}"
-
 # Ironic inspector rule to set IPMI credentials.
 inspector_rule_ipmi_credentials:
   description: "Set IPMI driver_info if no credentials"
@@ -74,10 +66,10 @@ inspector_rule_ipmi_credentials:
   actions:
     - action: "set-attribute"
       path: "driver_info/ipmi_username"
-      value: "{{ inspector_ipmi_username }}"
+      value: "{{ inspector_rule_var_ipmi_username }}"
     - action: "set-attribute"
       path: "driver_info/ipmi_password"
-      value: "{{ inspector_ipmi_password }}"
+      value: "{{ inspector_rule_var_ipmi_password }}"
 
 # Ironic inspector rule to set deployment kernel.
 inspector_rule_deploy_kernel:
@@ -88,7 +80,7 @@ inspector_rule_deploy_kernel:
   actions:
     - action: "set-attribute"
       path: "driver_info/deploy_kernel"
-      value: "{{ inspector_deploy_kernel }}"
+      value: "{{ inspector_rule_var_deploy_kernel }}"
 
 # Ironic inspector rule to set deployment ramdisk.
 inspector_rule_deploy_ramdisk:
@@ -99,7 +91,7 @@ inspector_rule_deploy_ramdisk:
   actions:
     - action: "set-attribute"
       path: "driver_info/deploy_ramdisk"
-      value: "{{ inspector_deploy_ramdisk }}"
+      value: "{{ inspector_rule_var_deploy_ramdisk }}"
 
 # Ironic inspector rule to set local boot capability
 inspector_rule_local_boot:
@@ -149,18 +141,18 @@ inspector_rule_set_pxe_interface_mac:
       path: "extra/pxe_interface_mac"
       value: "{data[boot_interface]}"
 
-# Ironic inspector rule to set the node's name from eno3's LLDP switch port
-# description.
-inspector_rule_eno3_lldp_switch_port_desc_to_name:
-  description: "Set node name from LLDP switch port description"
+# Ironic inspector rule to set the node's name from an interface's LLDP switch
+# port description.
+inspector_rule_lldp_switch_port_desc_to_name:
+  description: "Set node name from {{ inspector_rule_var_lldp_switch_port_interface }} LLDP switch port description"
   conditions:
-    - field: "data://all_interfaces.eno3.lldp_processed.switch_port_description"
+    - field: "data://all_interfaces.{{ inspector_rule_var_lldp_switch_port_interface }}.lldp_processed.switch_port_description"
       op: "is-empty"
       invert: True
   actions:
     - action: "set-attribute"
       path: "name"
-      value: "{data[all_interfaces][eno3][lldp_processed][switch_port_description]}"
+      value: "{data[all_interfaces][{{ inspector_rule_var_lldp_switch_port_interface }}][lldp_processed][switch_port_description]}"
 
 # Ironic inspector rule to save introspection data to the node.
 inspector_rule_save_data:
@@ -170,3 +162,20 @@ inspector_rule_save_data:
     - action: "set-attribute"
       path: "extra/introspection_data"
       value: "{data}"
+
+# List of default ironic inspector rules.
+inspector_rules_default:
+  - "{{ inspector_rule_ipmi_credentials }}"
+  - "{{ inspector_rule_deploy_kernel }}"
+  - "{{ inspector_rule_deploy_ramdisk }}"
+  - "{{ inspector_rule_local_boot }}"
+  - "{{ inspector_rule_root_hint_init }}"
+  - "{{ inspector_rule_root_hint_serial }}"
+  - "{{ inspector_rule_set_pxe_interface_mac }}"
+  - "{{ inspector_rule_lldp_switch_port_desc_to_name }}"
+
+# List of additional ironic inspector rules.
+inspector_rules_extra: []
+
+# List of all ironic inspector rules.
+inspector_rules: "{{ inspector_rules_default + inspector_rules_extra }}"
diff --git a/ansible/overcloud-introspection-rules.yml b/ansible/overcloud-introspection-rules.yml
new file mode 100644
index 000000000..5cc1f1231
--- /dev/null
+++ b/ansible/overcloud-introspection-rules.yml
@@ -0,0 +1,44 @@
+---
+- name: Ensure openstackclient is installed
+  # Only required to run on a single host.
+  hosts: controllers[0]
+  vars:
+    venv: "{{ ansible_env.PWD }}/shade-venv"
+  roles:
+    - role: openstackclient
+      openstackclient_venv: "{{ venv }}"
+
+- name: Ensure introspection rules are registered in Ironic Inspector
+  # Only required to run on a single host.
+  hosts: controllers[0]
+  vars:
+    venv: "{{ ansible_env.PWD }}/shade-venv"
+  pre_tasks:
+    - name: Retrieve the IPA kernel Glance image UUID
+      shell: >
+        source {{ venv }}/bin/activate &&
+        openstack image show '{{ ipa_images_kernel_name }}' -f value -c id
+      changed_when: False
+      register: ipa_kernel_id
+      environment: "{{ openstack_auth_env }}"
+
+    - name: Retrieve the IPA ramdisk Glance image UUID
+      shell: >
+        source {{ venv }}/bin/activate &&
+        openstack image show '{{ ipa_images_ramdisk_name }}' -f value -c id
+      changed_when: False
+      register: ipa_ramdisk_id
+      environment: "{{ openstack_auth_env }}"
+
+  roles:
+    - role: ironic-inspector-rules
+      ironic_inspector_venv: "{{ venv }}"
+      ironic_inspector_auth_type: "{{ openstack_auth_type }}"
+      ironic_inspector_auth: "{{ openstack_auth }}"
+      ironic_inspector_rules: "{{ inspector_rules }}"
+      # These variables may be referenced in the introspection rules.
+      inspector_rule_var_ipmi_username: "{{ inspector_ipmi_username }}"
+      inspector_rule_var_ipmi_password: "{{ inspector_ipmi_password }}"
+      inspector_rule_var_lldp_switch_port_interface: "{{ inspector_lldp_switch_port_interface }}"
+      inspector_rule_var_deploy_kernel: "{{ ipa_kernel_id.stdout }}"
+      inspector_rule_var_deploy_ramdisk: "{{ ipa_ramdisk_id.stdout }}"
diff --git a/ansible/roles/openstackclient/defaults/main.yml b/ansible/roles/openstackclient/defaults/main.yml
new file mode 100644
index 000000000..0918fa36a
--- /dev/null
+++ b/ansible/roles/openstackclient/defaults/main.yml
@@ -0,0 +1,3 @@
+---
+# Path to a directory in which to create a virtualenv.
+openstackclient_venv:
diff --git a/ansible/roles/openstackclient/tasks/main.yml b/ansible/roles/openstackclient/tasks/main.yml
new file mode 100644
index 000000000..17f969d61
--- /dev/null
+++ b/ansible/roles/openstackclient/tasks/main.yml
@@ -0,0 +1,30 @@
+---
+- name: Ensure required packages are installed
+  yum:
+    name: "{{ item }}"
+    state: installed
+  become: True
+  with_items:
+    - gcc
+    - libffi-devel
+    - openssl-devel
+    - python-devel
+    - python-pip
+    - python-virtualenv
+
+- name: Ensure the latest version of pip is installed
+  pip:
+    name: "{{ item.name }}"
+    state: latest
+    virtualenv: "{{ openstackclient_venv }}"
+  with_items:
+    - { name: pip }
+
+- name: Ensure required Python packages are installed
+  pip:
+    name: "{{ item.name }}"
+    version: "{{ item.version | default(omit) }}"
+    state: present
+    virtualenv: "{{ openstackclient_venv }}"
+  with_items:
+    - name: python-openstackclient
diff --git a/ansible/seed-introspection-rules.yml b/ansible/seed-introspection-rules.yml
index 4d46b074d..bb5bf7d22 100644
--- a/ansible/seed-introspection-rules.yml
+++ b/ansible/seed-introspection-rules.yml
@@ -1,16 +1,19 @@
 ---
 - name: Ensure introspection rules are registered in Bifrost
   hosts: seed
+  vars:
+    venv: "{{ ansible_env.PWD }}/shade-venv"
   roles:
     - role: ironic-inspector-rules
-      ironic_inspector_venv: "{{ ansible_env.PWD }}/shade-venv"
+      ironic_inspector_venv: "{{ venv }}"
       # No auth required for Bifrost.
       ironic_inspector_auth_type: None
       ironic_inspector_auth: {}
       ironic_inspector_url: "http://localhost:5050"
       ironic_inspector_rules: "{{ kolla_bifrost_inspector_rules }}"
       # These variables may be referenced in the introspection rules.
-      inspector_ipmi_username: "{{ kolla_bifrost_inspector_ipmi_username }}"
-      inspector_ipmi_password: "{{ kolla_bifrost_inspector_ipmi_password }}"
-      inspector_deploy_kernel: "{{ kolla_bifrost_inspector_deploy_kernel }}"
-      inspector_deploy_ramdisk: "{{ kolla_bifrost_inspector_deploy_ramdisk }}"
+      inspector_rule_var_ipmi_username: "{{ kolla_bifrost_inspector_ipmi_username }}"
+      inspector_rule_var_ipmi_password: "{{ kolla_bifrost_inspector_ipmi_password }}"
+      inspector_rule_var_lldp_switch_port_interface: "{{ kolla_bifrost_inspector_lldp_switch_port_interface }}"
+      inspector_rule_var_deploy_kernel: "{{ kolla_bifrost_inspector_deploy_kernel }}"
+      inspector_rule_var_deploy_ramdisk: "{{ kolla_bifrost_inspector_deploy_ramdisk }}"
diff --git a/etc/kayobe/bifrost.yml b/etc/kayobe/bifrost.yml
index c7ac96526..9c590bcd3 100644
--- a/etc/kayobe/bifrost.yml
+++ b/etc/kayobe/bifrost.yml
@@ -59,6 +59,10 @@
 # Ironic inspector IPMI password to set.
 #kolla_bifrost_inspector_ipmi_password:
 
+# Ironic inspector network interface name on which to check for an LLDP switch
+# port description to use as the node's name.
+#kolla_bifrost_inspector_lldp_switch_port_interface:
+
 # Ironic inspector deployment kernel location.
 #kolla_bifrost_inspector_deploy_kernel:
 
diff --git a/etc/kayobe/inspector.yml b/etc/kayobe/inspector.yml
new file mode 100644
index 000000000..6a93d8600
--- /dev/null
+++ b/etc/kayobe/inspector.yml
@@ -0,0 +1,88 @@
+---
+###############################################################################
+# Ironic inspector PXE configuration.
+
+# List of extra kernel parameters for the inspector default PXE configuration.
+#inspector_extra_kernel_options:
+
+# URL of Ironic Python Agent (IPA) kernel image.
+#inspector_ipa_kernel_upstream_url:
+
+# URL of Ironic Python Agent (IPA) ramdisk image.
+#inspector_ipa_ramdisk_upstream_url:
+
+###############################################################################
+# Ironic inspector processing configuration.
+
+# Whether inspector should manage the firewall.
+#inspector_manage_firewall:
+
+# List of of inspector processing plugins.
+#inspector_processing_hooks:
+
+# Which MAC addresses to add as ports during introspection. One of 'all',
+# 'active' or 'pxe'.
+#inspector_port_addition:
+
+# Whether to enable discovery of nodes not managed by Ironic.
+#inspector_enable_discovery:
+
+# The Ironic driver with which to register newly discovered nodes.
+#inspector_discovery_enroll_node_driver:
+
+###############################################################################
+# Ironic inspector configuration.
+
+# Ironic inspector IPMI username to set.
+#inspector_ipmi_username:
+
+# Ironic inspector IPMI password to set.
+#inspector_ipmi_password:
+
+# Ironic inspector network interface name on which to check for an LLDP switch
+# port description to use as the node's name.
+#inspector_lldp_switch_port_interface:
+
+###############################################################################
+# Ironic inspector introspection rules configuration.
+
+# Ironic inspector rule to set IPMI credentials.
+#inspector_rule_ipmi_credentials:
+
+# Ironic inspector rule to set deployment kernel.
+#inspector_rule_deploy_kernel:
+
+# Ironic inspector rule to set deployment ramdisk.
+#inspector_rule_deploy_ramdisk:
+
+# Ironic inspector rule to set local boot capability
+#inspector_rule_local_boot:
+
+# Ironic inspector rule to initialise root device hints.
+#inspector_rule_root_hint_init:
+
+# Ironic inspector rule to set serial root device hint.
+#inspector_rule_root_hint_serial:
+
+# Ironic inspector rule to set the interface on which the node PXE booted.
+#inspector_rule_set_pxe_interface_mac:
+
+# Ironic inspector rule to set the node's name from an interface's LLDP switch
+# port description.
+#inspector_rule_lldp_switch_port_desc_to_name:
+
+# Ironic inspector rule to save introspection data to the node.
+#inspector_rule_save_data:
+
+# List of default ironic insepctor rules.
+#inspector_rules_default:
+
+# List of additional ironic inspector rules.
+#inspector_rules_extra:
+
+# List of all ironic inspector rules.
+#inspector_rules:
+
+###############################################################################
+# Dummy variable to allow Ansible to accept this file.
+workaround_ansible_issue_8743: yes