diff --git a/roles/openstack-clients/README.rst b/roles/openstack-clients/README.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c8348ee0a6ae3fe30119abcf84b8fbfd5c39fc80
--- /dev/null
+++ b/roles/openstack-clients/README.rst
@@ -0,0 +1,18 @@
+Install openstack clients required for Kolla-Ansible CI scripts.
+The defaults are suitable for CI environment.
+
+**Role Variables**
+
+.. zuul:rolevar:: openstack_clients_pip_packages
+
+   List of dictionaries, with package and enabled keys, e.g.:
+   - package: python-barbicanclient
+     enabled: true
+
+.. zuul:rolevar:: openstack_clients_venv_base
+
+   Directory used as base for python venv
+
+.. zuul:rolevar:: openstack_clients_venv_name
+
+   Name of python venv to use for package installations
diff --git a/roles/openstack-clients/defaults/main.yml b/roles/openstack-clients/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8935f3c54a8f1fe3ad6956502e16bf56d57c860b
--- /dev/null
+++ b/roles/openstack-clients/defaults/main.yml
@@ -0,0 +1,32 @@
+---
+openstack_clients_pip_packages:
+  - package: python-barbicanclient
+    enabled: "{{ scenario == 'scenario_nfv' }}"
+  - package: python-designateclient
+    enabled: "{{ scenario == 'magnum' }}"
+  - package: python-heatclient
+    enabled: true
+  - package: python-ironicclient
+    enabled: "{{ scenario == 'ironic' }}"
+  - package: python-ironic-inspector-client
+    enabled: "{{ scenario == 'ironic' }}"
+  - package: python-magnumclient
+    enabled: "{{ scenario == 'magnum' }}"
+  - package: python-masakariclient
+    enabled: "{{ scenario == 'masakari' }}"
+  - package: python-mistralclient
+    enabled: "{{ scenario == 'scenario_nfv' }}"
+  - package: python-octaviaclient
+    enabled: "{{ scenario in ['octavia', 'ovn'] }}"
+  - package: python-openstackclient
+    enabled: true
+  - package: python-tackerclient
+    enabled: "{{ scenario == 'scenario_nfv' }}"
+  - package: python-troveclient
+    enabled: "{{ scenario == 'magnum' }}"
+  - package: python-zunclient
+    enabled: "{{ scenario == 'zun' }}"
+
+openstack_clients_venv_base: "{{ ansible_user_dir }}"
+openstack_clients_venv_name: "openstackclient-venv"
+openstack_clients_venv_path: "{{ openstack_clients_venv_base }}/{{ openstack_clients_venv_name }}"
diff --git a/roles/openstack-clients/tasks/main.yml b/roles/openstack-clients/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e2924c1b77b93c0d34125a1fe3c12409bf869bc5
--- /dev/null
+++ b/roles/openstack-clients/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: Setup OpenStack clients
+  ansible.builtin.pip:
+    name: "{{ item.package }}"
+    virtualenv: "{{ openstack_clients_venv_path }}"
+    virtualenv_command: "python3 -m venv"
+  become: true
+  when: item.enabled
+  loop: "{{ openstack_clients_pip_packages }}"
diff --git a/tests/run.yml b/tests/run.yml
index ffaf2816923c127cc7b55cee186052bb52c6522a..03e90e23209d3131c08e3fe342f3ca7778ed9835 100644
--- a/tests/run.yml
+++ b/tests/run.yml
@@ -334,6 +334,10 @@
       args:
         executable: /bin/bash
 
+    - name: Ensure OpenStack clients are installed
+      import_role:
+        name: openstack-clients
+
     # NOTE(mgoddard): We are using the script module here and later to ensure
     # we use the local copy of these scripts, rather than the one on the remote
     # host, which could be checked out to a previous release (in an upgrade
diff --git a/tests/setup_gate.sh b/tests/setup_gate.sh
index f8651156340137e8f3471d28c7d40ce6f534131d..c2245c072d613db15bc9f07ff1ed4cf7b64538f4 100755
--- a/tests/setup_gate.sh
+++ b/tests/setup_gate.sh
@@ -7,39 +7,6 @@ set -o pipefail
 # Enable unbuffered output for Ansible in Jenkins.
 export PYTHONUNBUFFERED=1
 
-
-function setup_openstack_clients {
-    # Prepare virtualenv for openstack deployment tests
-    local packages=(python-openstackclient python-heatclient)
-    if [[ $SCENARIO == zun ]]; then
-        packages+=(python-zunclient)
-    fi
-    if [[ $SCENARIO == ironic ]]; then
-        packages+=(python-ironicclient python-ironic-inspector-client)
-    fi
-    if [[ $SCENARIO == magnum ]]; then
-        packages+=(python-designateclient python-magnumclient python-troveclient)
-    fi
-    if [[ $SCENARIO == octavia ]]; then
-        packages+=(python-octaviaclient)
-    fi
-    if [[ $SCENARIO == masakari ]]; then
-        packages+=(python-masakariclient)
-    fi
-    if [[ $SCENARIO == scenario_nfv ]]; then
-        packages+=(python-tackerclient python-barbicanclient python-mistralclient)
-    fi
-    if [[ $SCENARIO == ovn ]]; then
-        packages+=(python-octaviaclient)
-    fi
-    if [[ "debian" == $BASE_DISTRO ]]; then
-        sudo apt -y install python3-venv
-    fi
-    python3 -m venv ~/openstackclient-venv
-    ~/openstackclient-venv/bin/pip install -U pip
-    ~/openstackclient-venv/bin/pip install -c $UPPER_CONSTRAINTS ${packages[@]}
-}
-
 function prepare_images {
     if [[ "${BUILD_IMAGE}" == "False" ]]; then
         return
@@ -146,8 +113,6 @@ EOF
 }
 
 
-setup_openstack_clients
-
 RAW_INVENTORY=/etc/kolla/inventory
 
 source $KOLLA_ANSIBLE_VENV_PATH/bin/activate