diff --git a/.zuul.yaml b/.zuul.yaml
index fb9eeb48f1..f0bf1934bc 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -41,27 +41,64 @@
 - job:
     name: system-config-zuul-role-integration-centos-7
     parent: system-config-zuul-role-integration
-    nodeset: centos-7
+    nodeset:
+      nodes:
+        - name: base
+          label: centos-7
+        - name: puppet4
+          label: centos-7
+      groups:
+        - name: puppet3
+          nodes:
+            - base
 
 - job:
     name: system-config-zuul-role-integration-trusty
     parent: system-config-zuul-role-integration
-    nodeset: ubuntu-trusty
+    nodeset:
+      nodes:
+        - name: base
+          label: ubuntu-trusty
+        - name: puppet4
+          label: ubuntu-trusty
+      groups:
+        - name: puppet3
+          nodes:
+            - base
 
 - job:
     name: system-config-zuul-role-integration-xenial
     parent: system-config-zuul-role-integration
-    nodeset: ubuntu-xenial
+    nodeset:
+      nodes:
+        - name: base
+          label: ubuntu-xenial
+        - name: puppet4
+          label: ubuntu-xenial
+      groups:
+        - name: puppet3
+          nodes:
+            - base
 
 - job:
     name: system-config-zuul-role-integration-bionic
     parent: system-config-zuul-role-integration
-    nodeset: ubuntu-bionic
+    nodeset:
+      nodes:
+        - name: base
+          label: ubuntu-bionic
+      groups:
+        - name: puppet5
+          nodes:
+            - base
 
 - job:
     name: system-config-zuul-role-integration-debian-stable
     parent: system-config-zuul-role-integration
-    nodeset: debian-stable
+    nodeset:
+      nodes:
+        - name: base
+          label: debian-stretch
 
 - project-template:
     name: system-config-zuul-role-integration
diff --git a/roles-test/base.yaml b/roles-test/base.yaml
index 78c93137d4..5ebf92fdc4 100644
--- a/roles-test/base.yaml
+++ b/roles-test/base.yaml
@@ -6,6 +6,12 @@
 # to run under Zuul (here) and also directly under Ansible for the
 # control-plane (see system-config-run-base)
 #
+# Note playbooks should by default use the "base" node (i.e. hosts:
+# base); some roles (like puppet) may also use another node if they're
+# testing an alternative path.
+
+# Puppet installation
+- import_playbook: puppet-install.yaml
 
 # Kerberos & afs roles
-- import_playbook: openafs-client.yaml
\ No newline at end of file
+- import_playbook: openafs-client.yaml
diff --git a/roles-test/openafs-client.yaml b/roles-test/openafs-client.yaml
index 69150419fe..d7633bb094 100644
--- a/roles-test/openafs-client.yaml
+++ b/roles-test/openafs-client.yaml
@@ -1,5 +1,5 @@
 - name: Kerberos and OpenAFS client installation
-  hosts: all
+  hosts: base
   roles:
     - role: kerberos-client
       kerberos_realm: 'OPENSTACK.ORG'
diff --git a/roles-test/puppet-install.yaml b/roles-test/puppet-install.yaml
new file mode 100644
index 0000000000..7978ace6e7
--- /dev/null
+++ b/roles-test/puppet-install.yaml
@@ -0,0 +1,17 @@
+- name: Install puppet3
+  hosts: puppet3
+  roles:
+    - role: puppet-install
+      puppet_install_version: 3
+
+- name: Install puppet4
+  hosts: puppet4
+  roles:
+    - role: puppet-install
+      puppet_install_version: 4
+
+- name: Install puppet5
+  hosts: puppet5
+  roles:
+    - role: puppet-install
+      puppet_install_version: 5
\ No newline at end of file
diff --git a/roles/puppet-install/README.rst b/roles/puppet-install/README.rst
new file mode 100644
index 0000000000..d19343cd13
--- /dev/null
+++ b/roles/puppet-install/README.rst
@@ -0,0 +1,23 @@
+Install puppet on a host
+
+.. note:: This role uses ``puppetlabs`` versions where available in
+          preference to system packages.
+
+This roles installs puppet on a host
+
+**Role Variables**
+
+.. zuul:rolevar:: puppet_install_version
+   :default: 3
+
+   The puppet version to install.  Platform support for various
+   version varies.
+
+.. zuul:rolevar:: puppet_install_system_config_modules
+   :default: yes
+
+   If we should clone and run `install_modules.sh
+   <https://git.openstack.org/cgit/openstack-infra/system-config/tree/install_modules.sh>`__
+   from OpenStack Infra ``system-config`` repository to populate
+   required puppet modules on the host.
+
diff --git a/roles/puppet-install/defaults/main.yaml b/roles/puppet-install/defaults/main.yaml
new file mode 100644
index 0000000000..898626dd95
--- /dev/null
+++ b/roles/puppet-install/defaults/main.yaml
@@ -0,0 +1,2 @@
+puppet_install_version: 3
+puppet_install_system_config_modules: yes
\ No newline at end of file
diff --git a/roles/puppet-install/tasks/main.yaml b/roles/puppet-install/tasks/main.yaml
new file mode 100644
index 0000000000..e4cd691632
--- /dev/null
+++ b/roles/puppet-install/tasks/main.yaml
@@ -0,0 +1,32 @@
+- name: Install puppet packages
+  include: "{{ lookup('first_found', params) }}"
+  vars:
+    params:
+      files:
+        - "{{ ansible_distribution_release }}.yaml"
+        - "{{ ansible_distribution }}.yaml"
+        - "{{ ansible_os_family }}.yaml"
+        - "default.yaml"
+      paths:
+        - puppet-install
+
+- name: Install system-config modules
+  when: puppet_install_system_config_modules
+  become: true
+  block:
+    - name: Make sure git is installed
+      package:
+        name: git
+        state: present
+
+    - name: Make sure system-config repo is up to date
+      git:
+        repo: https://git.openstack.org/openstack-infra/system-config
+        dest: /opt/system-config
+        force: yes
+
+    - name: Clone puppet modules to /etc/puppet/modules
+      command: ./install_modules.sh
+      args:
+        chdir: /opt/system-config
+
diff --git a/roles/puppet-install/tasks/puppet-install/CentOS.yaml b/roles/puppet-install/tasks/puppet-install/CentOS.yaml
new file mode 100644
index 0000000000..64dc70e968
--- /dev/null
+++ b/roles/puppet-install/tasks/puppet-install/CentOS.yaml
@@ -0,0 +1,49 @@
+- fail:
+    msg: "Unsupported puppet version '{{ puppet_install_version }}' on this platform"
+  when: puppet_install_version not in [3, 4]
+
+- name: Install puppet 3 packages
+  when: puppet_install_version == 3
+  become: true
+  block:
+    - name: Install puppetlabs repo
+      yum:
+        name: https://yum.puppetlabs.com/puppetlabs-release-el-7.noarch.rpm
+
+    - name: Install puppet packages
+      yum:
+        name:
+          - puppet
+          - ruby
+        state: present
+        update_cache: yes
+
+    # wipe out templatedir so we don't get warnings
+    - name: Remove templatedir
+      lineinfile:
+        path: /etc/puppet/puppet.conf
+        state: absent
+        regexp: 'templatedir'
+
+    # wipe out server, as we don't have one
+    - name: Remove server
+      lineinfile:
+        path: /etc/puppet/puppet.conf
+        state: absent
+        regexp: 'server'
+
+- name: Install puppet 4 packages
+  when: puppet_install_version == 4
+  become: true
+  block:
+    - name: Install puppetlabs repo
+      yum:
+        name: https://yum.puppetlabs.com/puppetlabs-release-pc1-el-7.noarch.rpm
+
+    - name: Install puppet packages
+      yum:
+        name:
+          - puppet-agent
+          - ruby
+        state: present
+        update_cache: yes
diff --git a/roles/puppet-install/tasks/puppet-install/bionic.yaml b/roles/puppet-install/tasks/puppet-install/bionic.yaml
new file mode 100644
index 0000000000..7f63c6fdf6
--- /dev/null
+++ b/roles/puppet-install/tasks/puppet-install/bionic.yaml
@@ -0,0 +1,26 @@
+# Prior versions not supported on Bionic
+- fail:
+    msg: "Unsupported puppet version '{{ puppet_install_version }}' on this platform"
+  when: puppet_install_version not in [5,]
+
+- name: Install puppet 5 packages
+  when: puppet_install_version == 5
+  become: true
+  block:
+    - name: Install puppetlabs repo
+      apt:
+        deb: https://apt.puppetlabs.com/puppet5-release-bionic.deb
+
+    - name: Install puppet packages
+      apt:
+        name:
+          - puppet-agent
+          - ruby
+        update_cache: yes
+
+- name: Stop and disable puppet service
+  service:
+    name: puppet
+    state: stopped
+    enabled: no
+  become: yes
diff --git a/roles/puppet-install/tasks/puppet-install/default.yaml b/roles/puppet-install/tasks/puppet-install/default.yaml
new file mode 100644
index 0000000000..e73dbe00d0
--- /dev/null
+++ b/roles/puppet-install/tasks/puppet-install/default.yaml
@@ -0,0 +1,2 @@
+- fail:
+    msg: Platform not currently supported
\ No newline at end of file
diff --git a/roles/puppet-install/tasks/puppet-install/trusty.yaml b/roles/puppet-install/tasks/puppet-install/trusty.yaml
new file mode 100644
index 0000000000..d1c73feeb5
--- /dev/null
+++ b/roles/puppet-install/tasks/puppet-install/trusty.yaml
@@ -0,0 +1,57 @@
+- fail:
+    msg: "Unsupported puppet version '{{ puppet_install_version }}' on this platform"
+  when: puppet_install_version not in [3, 4]
+
+- name: Install puppet 3 packages
+  when: puppet_install_version == 3
+  become: true
+  block:
+    # Note https doesn't work here due to certificate issues and
+    # python versions and SNI etc; not worth the effort of workarounds
+    # at this point.
+    - name: Install puppetlabs repo
+      apt:
+        deb: http://apt.puppetlabs.com/puppetlabs-release-trusty.deb
+
+    - name: Install puppet packages
+      package:
+        state: present
+        name: '{{ item }}'
+      loop:
+        - puppet
+        - ruby
+    # wipe out templatedir so we don't get warnings
+    - name: Remove templatedir
+      lineinfile:
+        path: /etc/puppet/puppet.conf
+        state: absent
+        regexp: 'templatedir'
+    # wipe out server, as we don't have one
+    - name: Remove server
+      lineinfile:
+        path: /etc/puppet/puppet.conf
+        state: absent
+        regexp: 'server'
+
+
+- name: Install puppet 4 packages
+  when: puppet_install_version == 4
+  become: true
+  block:
+    - name: Install puppetlabs repo
+      apt:
+        deb: http://apt.puppetlabs.com/puppetlabs-release-pc1-trusty.deb
+
+    - name: Install puppet packages
+      apt:
+        name:
+          - puppet-agent
+          - ruby
+        update_cache: yes
+
+- name: Stop and disable puppet service
+  service:
+    name: puppet
+    state: stopped
+    enabled: no
+  become: yes
\ No newline at end of file
diff --git a/roles/puppet-install/tasks/puppet-install/xenial.yaml b/roles/puppet-install/tasks/puppet-install/xenial.yaml
new file mode 100644
index 0000000000..c8914e32d2
--- /dev/null
+++ b/roles/puppet-install/tasks/puppet-install/xenial.yaml
@@ -0,0 +1,53 @@
+- fail:
+    msg: "Unsupported puppet version '{{ puppet_install_version }}' on this platform"
+  when: puppet_install_version not in [3,4]
+
+- name: Install puppet 3 packages
+  when: puppet_install_version == 3
+  become: true
+  block:
+    # Puppetlabs does not support Xenial for puppet 3, so we're using
+    # system packages
+    - name: Install puppet packages
+      package:
+        state: present
+        name: '{{ item }}'
+      loop:
+        - puppet
+        - ruby
+
+    # wipe out templatedir so we don't get warnings
+    - name: Remove templatedir
+      lineinfile:
+        path: /etc/puppet/puppet.conf
+        state: absent
+        regexp: 'templatedir'
+
+    # wipe out server, as we don't have one
+    - name: Remove server
+      lineinfile:
+        path: /etc/puppet/puppet.conf
+        state: absent
+        regexp: 'server'
+
+- name: Install puppet 4 packages
+  when: puppet_install_version == 4
+  become: true
+  block:
+    - name: Install puppetlabs repo
+      apt:
+        deb: https://apt.puppetlabs.com/puppetlabs-release-pc1-xenial.deb
+
+    - name: Install puppet packages
+      apt:
+        name:
+          - puppet-agent
+          - ruby
+        update_cache: yes
+
+- name: Stop and disable puppet service
+  service:
+    name: puppet
+    state: stopped
+    enabled: no
+  become: yes