diff --git a/doc/source/python-roles.rst b/doc/source/python-roles.rst
index b7d871e26..8089c2b2d 100644
--- a/doc/source/python-roles.rst
+++ b/doc/source/python-roles.rst
@@ -10,6 +10,7 @@ Python Roles
 .. zuul:autorole:: ensure-sphinx
 .. zuul:autorole:: ensure-tox
 .. zuul:autorole:: ensure-twine
+.. zuul:autorole:: ensure-virtualenv
 .. zuul:autorole:: fetch-coverage-output
 .. zuul:autorole:: fetch-python-sdist-output
 .. zuul:autorole:: fetch-sphinx-output
diff --git a/roles/ensure-virtualenv/README.rst b/roles/ensure-virtualenv/README.rst
new file mode 100644
index 000000000..f3a64b0ae
--- /dev/null
+++ b/roles/ensure-virtualenv/README.rst
@@ -0,0 +1,28 @@
+Ensure virtualenv is available
+
+This role installs the requirements for the ``virtualenv`` command
+on the current distribution.
+
+Users should be aware of some portability issues when using
+``virtualenv``:
+
+* Distributions differ on the interpreter that ``virtualenv`` is
+  provided by, so by calling ``virtualenv`` with no other arguments
+  means that on some platforms you will get a Python 2 environment and
+  others a Python 3 environment.
+* If you wish to call ``virtualenv`` as a module (e.g. ``python -m
+  virtualenv``) you will need to know which interpreter owns the
+  ``virtualenv`` package for your distribution; e.g. on some, such as
+  Bionic, ``virtualenv`` is provided by ``python3-virtualenv`` but
+  ``python`` refers to Python 2, so ``python -m virtualenv`` is not a
+  portable way to call ``virtualenv``.
+* ``virtualenv -p python3`` is likely the most portable way to
+  consistently get a Python 3 environment.  ``virtualenv -p python2``
+  may not work on some platforms without Python 2.
+* If you use Python 3 and do not require the specific features of
+  ``virtualenv``, it is likely easier to use Python's inbuilt
+  ``python3 -m venv`` module to create an isolated environment.  If
+  you are using ``pip:`` in your Ansible roles and require an
+  environment, see the documentation for :zuul:role:`ensure-pip`.
+
+
diff --git a/roles/ensure-virtualenv/tasks/CentOS-7.yaml b/roles/ensure-virtualenv/tasks/CentOS-7.yaml
new file mode 100644
index 000000000..602c2ce25
--- /dev/null
+++ b/roles/ensure-virtualenv/tasks/CentOS-7.yaml
@@ -0,0 +1,6 @@
+- name: Install virtualenv
+  package:
+    name:
+      - python-virtualenv
+  become: yes
+
diff --git a/roles/ensure-virtualenv/tasks/Debian.yaml b/roles/ensure-virtualenv/tasks/Debian.yaml
new file mode 100644
index 000000000..b2d498b0b
--- /dev/null
+++ b/roles/ensure-virtualenv/tasks/Debian.yaml
@@ -0,0 +1,5 @@
+- name: Install virtualenv
+  package:
+    name:
+      - virtualenv
+  become: yes
diff --git a/roles/ensure-virtualenv/tasks/Gentoo.yaml b/roles/ensure-virtualenv/tasks/Gentoo.yaml
new file mode 100644
index 000000000..8ff8c8945
--- /dev/null
+++ b/roles/ensure-virtualenv/tasks/Gentoo.yaml
@@ -0,0 +1,5 @@
+- name: Install virtualenv
+  package:
+    name: dev-python/virtualenv
+  become: yes
+
diff --git a/roles/ensure-virtualenv/tasks/RedHat.yaml b/roles/ensure-virtualenv/tasks/RedHat.yaml
new file mode 100644
index 000000000..98f7a071b
--- /dev/null
+++ b/roles/ensure-virtualenv/tasks/RedHat.yaml
@@ -0,0 +1,6 @@
+- name: Install virtualenv
+  package:
+    name:
+      - python3-virtualenv
+  become: yes
+
diff --git a/roles/ensure-virtualenv/tasks/Suse.yaml b/roles/ensure-virtualenv/tasks/Suse.yaml
new file mode 100644
index 000000000..8e129ce2b
--- /dev/null
+++ b/roles/ensure-virtualenv/tasks/Suse.yaml
@@ -0,0 +1,4 @@
+- name: Install virtualenv
+  package:
+    name: python3-virtualenv
+  become: yes
diff --git a/roles/ensure-virtualenv/tasks/default.yaml b/roles/ensure-virtualenv/tasks/default.yaml
new file mode 100644
index 000000000..fd6bb8fbb
--- /dev/null
+++ b/roles/ensure-virtualenv/tasks/default.yaml
@@ -0,0 +1,3 @@
+- name: Unsupported platform
+  fail:
+    msg: 'This platform is currently unsupported'
diff --git a/roles/ensure-virtualenv/tasks/main.yaml b/roles/ensure-virtualenv/tasks/main.yaml
new file mode 100644
index 000000000..ede55216f
--- /dev/null
+++ b/roles/ensure-virtualenv/tasks/main.yaml
@@ -0,0 +1,18 @@
+- name: Check if virtualenv is installed
+  shell: |
+    command -v virtualenv || exit 1
+  args:
+    executable: /bin/bash
+  register: virtualenv_preinstalled
+  failed_when: false
+
+- name: Install virtualenv package
+  include: "{{ item }}"
+  with_first_found:
+    - "{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yaml"
+    - "{{ ansible_distribution_release }}.yaml"
+    - "{{ ansible_distribution }}.yaml"
+    - "{{ ansible_os_family }}.yaml"
+    - "default.yaml"
+  when:
+    - virtualenv_preinstalled.rc != 0
diff --git a/test-playbooks/ensure-pip.yaml b/test-playbooks/ensure-pip.yaml
index 8fb39a02c..498dbe4ab 100644
--- a/test-playbooks/ensure-pip.yaml
+++ b/test-playbooks/ensure-pip.yaml
@@ -1,7 +1,9 @@
 - hosts: all
-  roles:
-    - ensure-pip
   tasks:
+    - name: Include ensure-pip
+      include_role:
+        name: ensure-pip
+
     - name: Sanity check provided virtualenv command works
       shell: |
         tmp_venv=$(mktemp -d -t venv-XXXXXXXXXX)
@@ -11,12 +13,31 @@
       failed_when: false
       register: _venv_sanity
 
-    - name: Assert sanity check
+    - name: Assert pip venv sanity check
       fail:
         msg: 'The virtualenv_command: "{{ ensure_pip_virtualenv_command }}" does not appear to work!'
       when:
         - _venv_sanity.rc != 0
 
+    - name: Include ensure-virtualenv
+      include_role:
+        name: ensure-virtualenv
+
+    - name: Sanity check virtualenv command works
+      shell: |
+        tmp_venv=$(mktemp -d -t venv-XXXXXXXXXX)
+        trap "rm -rf $tmp_venv" EXIT
+        virtualenv $tmp_venv
+        $tmp_venv/bin/pip install tox
+      failed_when: false
+      register: _virtualenv_sanity
+
+    - name: Assert sanity check
+      fail:
+        msg: 'The virtualenv command does not appear to work!'
+      when:
+        - _virtualenv_sanity.rc != 0
+
 # NOTE(ianw) : this does not play nicely with pip-and-virtualenv which
 # has already installed from source.  We might be able to test this
 # once it's gone...
diff --git a/zuul-tests.d/python-jobs.yaml b/zuul-tests.d/python-jobs.yaml
index eb213c82e..79eb4d6eb 100644
--- a/zuul-tests.d/python-jobs.yaml
+++ b/zuul-tests.d/python-jobs.yaml
@@ -3,6 +3,7 @@
     description: Test the ensure-pip role
     files:
       - roles/ensure-pip/.*
+      - roles/ensure-virtualenv/.*
     run: test-playbooks/ensure-pip.yaml
     tags: all-platforms