diff --git a/ansible/disable-selinux.yml b/ansible/disable-selinux.yml
index 0124879f46d83a209552410e7ffd519f5354fcff..760743435b2d0e40d314fa174ef66e69a07b430b 100644
--- a/ansible/disable-selinux.yml
+++ b/ansible/disable-selinux.yml
@@ -6,3 +6,4 @@
   roles:
     - role: disable-selinux
       disable_selinux_reboot_timeout: "{{ 600 if ansible_virtualization_role == 'host' else 300 }}"
+      when: ansible_os_family == 'RedHat'
diff --git a/ansible/group_vars/all/overcloud b/ansible/group_vars/all/overcloud
index 90ed6a2b8f8f40386df97a03c1ab72f97411d66b..e01e1ed49617787be35c1cb74d5d50d24179fd2f 100644
--- a/ansible/group_vars/all/overcloud
+++ b/ansible/group_vars/all/overcloud
@@ -8,7 +8,7 @@ overcloud_group_default: controllers
 
 # List of names of Ansible groups for overcloud hosts.
 overcloud_groups: >
-  {{ (overcloud_group_hosts_map.keys() +
+  {{ (list(overcloud_group_hosts_map) +
       [overcloud_group_default]) | reject('equalto', 'ignore') | unique | sort | list }}
 
 # Dict mapping overcloud Ansible group names to lists of hosts in the group.
diff --git a/ansible/kayobe-ansible-user.yml b/ansible/kayobe-ansible-user.yml
index 956d7f750ff3360ee4e012aca099f2ed36122820..75ab7630635708671ca647aa22626f918261287a 100644
--- a/ansible/kayobe-ansible-user.yml
+++ b/ansible/kayobe-ansible-user.yml
@@ -42,7 +42,7 @@
     ansible_user: "{{ bootstrap_user }}"
     # We can't assume that a virtualenv exists at this point, so use the system
     # python interpreter.
-    ansible_python_interpreter: /usr/bin/python
+    ansible_python_interpreter: /usr/libexec/platform-python
   roles:
     - role: singleplatform-eng.users
       users:
@@ -69,7 +69,7 @@
   vars:
     # We can't assume that a virtualenv exists at this point, so use the system
     # python interpreter.
-    ansible_python_interpreter: /usr/bin/python
+    ansible_python_interpreter: /usr/libexec/platform-python
   tasks:
     - name: Verify that a command can be executed
       command: hostname
diff --git a/ansible/kayobe-target-venv.yml b/ansible/kayobe-target-venv.yml
index dbbc536e713476cf165ccbc2f3291366466afed6..b58ed63438feb8743bbd3b23207e31b12fd6eb13 100644
--- a/ansible/kayobe-target-venv.yml
+++ b/ansible/kayobe-target-venv.yml
@@ -20,9 +20,9 @@
         - name: Gather facts
           setup:
 
-        - name: Ensure the python-virtualenv package is installed
+        - name: Ensure the Python virtualenv package is installed
           package:
-            name: python-virtualenv
+            name: python{{ ansible_python.version.major }}-virtualenv
             state: present
           become: True
 
@@ -52,6 +52,9 @@
             name: pip
             virtualenv: "{{ virtualenv }}"
             virtualenv_site_packages: True
+          when:
+            - ansible_os_family == 'RedHat'
+            - ansible_distribution_major_version | int == 7
 
         - name: Ensure kayobe virtualenv has the latest version of pip installed
           pip:
@@ -61,6 +64,18 @@
             # Site packages are required for using the yum and selinux python
             # modules, which are not available via PyPI.
             virtualenv_site_packages: True
+            virtualenv_python: "python{{ ansible_python.version.major }}.{{ ansible_python.version.minor }}"
+
+        # NOTE(mgoddard): SELinux python bindings available on PyPI only work
+        # with Python 3 on CentOS 8.
+        - name: Ensure kayobe virtualenv has SELinux bindings installed
+          pip:
+            name: selinux
+            state: latest
+            virtualenv: "{{ virtualenv }}"
+          when:
+            - ansible_os_family == 'RedHat'
+            - ansible_distribution_major_version | int >= 8
       vars:
         # Use the system python interpreter since the virtualenv might not
         # exist.
@@ -68,9 +83,13 @@
       when: virtualenv is defined
 
     - block:
-        - name: Ensure the python-setuptools package is installed
+        - name: Ensure Python setuptools and pip packages are installed
+          vars:
+            packages:
+              - python{{ ansible_python.version.major }}-setuptools
+              - "{% if ansible_distribution_major_version | int >= 8 %}python3-pip{% endif %}"
           package:
-            name: python-setuptools
+            name: "{{ packages | select }}"
             state: present
           become: True
 
@@ -78,4 +97,7 @@
           easy_install:
             name: pip
           become: True
+          when:
+            - ansible_os_family == 'RedHat'
+            - ansible_distribution_major_version | int == 7
       when: virtualenv is not defined
diff --git a/ansible/kolla-target-venv.yml b/ansible/kolla-target-venv.yml
index e4f58753c954aa6851ba08c545b61ddba28b4ae1..e5d36742b7c7f3d40cc365c3fd089da54e8e6bbe 100644
--- a/ansible/kolla-target-venv.yml
+++ b/ansible/kolla-target-venv.yml
@@ -19,9 +19,13 @@
     - kolla-target-venv
   tasks:
     - block:
-        - name: Ensure the python-virtualenv package is installed
+        - name: Gather facts
+          setup:
+          when: ansible_python is not defined
+
+        - name: Ensure the Python virtualenv package is installed
           package:
-            name: python-virtualenv
+            name: python{{ ansible_python.version.major }}-virtualenv
             state: present
           become: True
 
@@ -33,6 +37,7 @@
             # Site packages are required for using the yum and selinux python
             # modules, which are not available via PyPI.
             virtualenv_site_packages: True
+            virtualenv_python: "python{{ ansible_python.version.major }}.{{ ansible_python.version.minor }}"
           become: True
 
         - name: Ensure kolla-ansible virtualenv has docker SDK for python installed
@@ -43,6 +48,18 @@
             extra_args: "{% if kolla_upper_constraints_file %}-c {{ kolla_upper_constraints_file }}{% endif %}"
           become: True
 
+        # NOTE(mgoddard): SELinux python bindings available on PyPI only work
+        # with Python 3 on CentOS 8.
+        - name: Ensure kolla-ansible virtualenv has SELinux bindings installed
+          pip:
+            name: selinux
+            state: latest
+            virtualenv: "{{ kolla_ansible_target_venv }}"
+          become: True
+          when:
+            - ansible_os_family == 'RedHat'
+            - ansible_distribution_major_version | int >= 8
+
         - name: Ensure kolla-ansible virtualenv has correct ownership
           file:
             path: "{{ kolla_ansible_target_venv }}"
diff --git a/ansible/network.yml b/ansible/network.yml
index 2de5070b3a2f2c4d994878d7b2b9ef9842dbd7ca..2db2ffe10f5d91c01ac0a964c62a2680f90a8fb8 100644
--- a/ansible/network.yml
+++ b/ansible/network.yml
@@ -96,7 +96,8 @@
         # interfaces with an explicit MTU set will be taken account of. If no
         # interface has an explicit MTU set, then the corresponding veth will
         # not either.
-        mtu: "{{ [veth_bridge_mtu_map.get(interface), item | net_mtu] | max }}"
+        mtu_list: "{{ [veth_bridge_mtu_map.get(interface), item | net_mtu] | reject('none') | list }}"
+        mtu: "{{ mtu_list | max if mtu_list | length > 0 else None }}"
 
     - name: Update a fact containing veth interfaces
       set_fact:
diff --git a/ansible/roles/console-allocation/library/console_allocation.py b/ansible/roles/console-allocation/library/console_allocation.py
index 2797a85a3b0a211e35ec8114f292462184bda4ab..ee652d0ee5e60bb550e8902ba13e2f440ff2a8b7 100644
--- a/ansible/roles/console-allocation/library/console_allocation.py
+++ b/ansible/roles/console-allocation/library/console_allocation.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3
 
 # Copyright (c) 2017 StackHPC Ltd.
 #
diff --git a/ansible/roles/disable-selinux/tasks/main.yml b/ansible/roles/disable-selinux/tasks/main.yml
index f9b06a9db302fd711d572081b8239080bde183be..44b1540e930c96b8df513cd1e690869f6a6302b5 100644
--- a/ansible/roles/disable-selinux/tasks/main.yml
+++ b/ansible/roles/disable-selinux/tasks/main.yml
@@ -1,8 +1,7 @@
 ---
 - name: Ensure required packages are installed
   package:
-    name:
-      - libselinux-python
+    name: "{% if ansible_distribution_major_version | int == 7 %}libselinux-python{% else %}python3-libselinux{% endif %}"
     state: present
   become: True
 
diff --git a/ansible/roles/ip-allocation/library/ip_allocation.py b/ansible/roles/ip-allocation/library/ip_allocation.py
index 9f30bad06af265596841f427f579316ebf56337d..1a135518d03ecddb286022ca535691105a7e5e6a 100644
--- a/ansible/roles/ip-allocation/library/ip_allocation.py
+++ b/ansible/roles/ip-allocation/library/ip_allocation.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3
 
 # Copyright (c) 2017 StackHPC Ltd.
 #
diff --git a/ansible/roles/ironic-inspector-rules/library/os_ironic_inspector_rule.py b/ansible/roles/ironic-inspector-rules/library/os_ironic_inspector_rule.py
index c97f625f2e935925d034e8280e026772233e3d43..cb58ad600d3b0adf933f20f8131c4c29c0bad862 100644
--- a/ansible/roles/ironic-inspector-rules/library/os_ironic_inspector_rule.py
+++ b/ansible/roles/ironic-inspector-rules/library/os_ironic_inspector_rule.py
@@ -1,3 +1,5 @@
+#!/usr/bin/python3
+
 # Copyright (c) 2017 StackHPC Ltd.
 #
 # Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -12,8 +14,6 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-#!/usr/bin/python
-
 from ansible.module_utils.basic import *
 from ansible.module_utils.openstack import *
 
diff --git a/ansible/roles/kolla-ansible/library/kolla_passwords.py b/ansible/roles/kolla-ansible/library/kolla_passwords.py
index be461a92b398b976844da5fdfdcb99e1b8e1da06..d7bc6314caadf15bad786c6f683fccd4c6fb6a52 100644
--- a/ansible/roles/kolla-ansible/library/kolla_passwords.py
+++ b/ansible/roles/kolla-ansible/library/kolla_passwords.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3
 
 # Copyright (c) 2017 StackHPC Ltd.
 #
@@ -127,7 +127,9 @@ def kolla_passwords(module):
         # Merge in overrides.
         if module.params['overrides']:
             with tempfile.NamedTemporaryFile(delete=False) as f:
-                yaml.dump(module.params['overrides'], f)
+                # NOTE(mgoddard): Temporary files are opened in binary mode, so
+                # specify an encoding.
+                yaml.dump(module.params['overrides'], f, encoding='utf-8')
                 overrides_path = f.name
             try:
                 kolla_mergepwd(module, overrides_path, temp_file_path, temp_file_path)
diff --git a/ansible/roles/kolla-ansible/tasks/install.yml b/ansible/roles/kolla-ansible/tasks/install.yml
index 0a3724dc2df115dcf76459be99859f4321673751..24def5d0243a4b61598c9d1041cb4f22e9c65ef2 100644
--- a/ansible/roles/kolla-ansible/tasks/install.yml
+++ b/ansible/roles/kolla-ansible/tasks/install.yml
@@ -80,6 +80,7 @@
     state: latest
     extra_args: "{% if kolla_upper_constraints_file %}-c {{ kolla_upper_constraints_file }}{% endif %}"
     virtualenv: "{{ kolla_ansible_venv }}"
+    virtualenv_python: "{{ kolla_ansible_venv_python }}"
 
 # This is a workaround for the lack of a python package for libselinux-python
 # on PyPI. Without using --system-site-packages to create the virtualenv, it
@@ -94,6 +95,7 @@
     state: link
   when:
     - ansible_os_family == 'RedHat'
+    - ansible_distribution_major_version | int == 7
     - ansible_selinux != False
     - ansible_selinux.status != 'disabled'
     - kolla_ansible_venv_python_major_version | int == 2
diff --git a/ansible/roles/kolla-ansible/templates/requirements.txt.j2 b/ansible/roles/kolla-ansible/templates/requirements.txt.j2
index dd71d6676413b410ada3536a18d894fc10827d24..ff323513fde01111973e1061f1309c23d5a9bcd8 100644
--- a/ansible/roles/kolla-ansible/templates/requirements.txt.j2
+++ b/ansible/roles/kolla-ansible/templates/requirements.txt.j2
@@ -8,6 +8,9 @@ kolla-ansible=={{ kolla_openstack_release }}
 # Limit the version of ansible used by kolla-ansible to avoid new releases from
 # breaking tested code. Changes to this limit should be tested.
 ansible>=2.6,<2.9
+{% if ansible_os_family == 'RedHat' and ansible_distribution_major_version | int >= 8 %}
+selinux
+{% endif %}
 {% if kolla_ansible_venv_extra_requirements is defined %}
 {% for item in kolla_ansible_venv_extra_requirements %}
 {{ item }}
diff --git a/ansible/roles/kolla/tasks/install.yml b/ansible/roles/kolla/tasks/install.yml
index 69ac843c88ecf138abc0913bf2c8b04f9d05ae0d..b27bbb4bdff390e46f9f6a857e27dd5c9351cfe0 100644
--- a/ansible/roles/kolla/tasks/install.yml
+++ b/ansible/roles/kolla/tasks/install.yml
@@ -14,9 +14,9 @@
       - gcc
       - libffi-devel
       - openssl-devel
-      - python-devel
-      - python-pip
-      - python-virtualenv
+      - python{{ ansible_python.version.major }}-devel
+      - python{{ ansible_python.version.major }}-pip
+      - python{{ ansible_python.version.major }}-virtualenv
     state: present
   become: True
 
@@ -50,6 +50,7 @@
     name: "{{ item.name }}"
     state: latest
     virtualenv: "{{ kolla_venv }}"
+    virtualenv_python: "python{{ ansible_python.version.major }}.{{ ansible_python.version.minor }}"
   with_items:
     - { name: pip }
 
diff --git a/ansible/roles/swift-rings/files/swift-ring-builder.py b/ansible/roles/swift-rings/files/swift-ring-builder.py
index bf2ae655a4af0637fd84652ba260f2fc96276fc3..bf6d8eb1a77ca903dc55eadd16570a4c70b66984 100644
--- a/ansible/roles/swift-rings/files/swift-ring-builder.py
+++ b/ansible/roles/swift-rings/files/swift-ring-builder.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 """
 Script to build a Swift ring from a declarative YAML configuration. This has
diff --git a/dev/functions b/dev/functions
index 496a694acf3e350b21123935b1c2207f5f75b172..b9b7f154fe51ec6301e9f0e972f691a93e232ae9 100644
--- a/dev/functions
+++ b/dev/functions
@@ -85,12 +85,41 @@ function config_init {
 
 # Installation
 
+function is_dnf {
+    if [[ -e /etc/centos-release ]]; then
+        which dnf >/dev/null 2>&1
+    else
+        return 1
+    fi
+}
+
+function is_yum {
+    if [[ -e /etc/centos-release ]]; then
+        which yum >/dev/null 2>&1
+    else
+        return 1
+    fi
+}
+
+function python_version {
+    # Echo python major version.
+    if is_dnf; then
+        echo 3
+    elif is_yum; then
+        echo 2
+    else
+        echo 3
+    fi
+}
+
 function install_dependencies {
     echo "Installing package dependencies for kayobe"
-    if [[ -e /etc/centos-release ]]; then
-        sudo yum -y install gcc git vim python-virtualenv libffi-devel
+    if is_dnf; then
+        sudo dnf -y install gcc git vim python3-pyyaml python3-virtualenv libffi-devel
+    elif is_yum; then
+        sudo yum -y install gcc git vim python2-virtualenv libffi-devel
     else
-        sudo apt install -y python-dev python-virtualenv gcc git libffi-dev
+        sudo apt install -y python-dev python3-virtualenv gcc git libffi-dev
     fi
 }
 
@@ -106,7 +135,7 @@ function install_venv {
     fi
     if [[ ! -f "${venv_path}/bin/activate" ]]; then
         echo "Creating virtual environment in ${venv_path}"
-        virtualenv "${venv_path}"
+        virtualenv -p python$(python_version) "${venv_path}"
         # NOTE: Virtualenv's activate and deactivate scripts reference an
         # unbound variable.
         set +u
@@ -132,7 +161,7 @@ function install_kayobe_dev_venv {
 
 function upgrade_kayobe_venv {
     echo "Upgrading kayobe virtual environment in ${KAYOBE_VENV_PATH}"
-    virtualenv "${KAYOBE_VENV_PATH}"
+    virtualenv -p python$(python_version) "${KAYOBE_VENV_PATH}"
     # NOTE: Virtualenv's activate and deactivate scripts reference an
     # unbound variable.
     set +u