diff --git a/modules/openstack_project/files/zuul/layout.yaml b/modules/openstack_project/files/zuul/layout.yaml
index f2250ae380..7b4148bd5d 100644
--- a/modules/openstack_project/files/zuul/layout.yaml
+++ b/modules/openstack_project/files/zuul/layout.yaml
@@ -399,21 +399,11 @@ project-templates:
 
 jobs:
   - name: ^.*$
-    parameter-function: single_use_node
+    parameter-function: set_node_options_default_precise
   # tempest and d-g branchless tests need to only run on master
   # (needs to be early to not impact non-voting stanzas later)
   - name: ^.*(-icehouse|-havana)$
     branch: ^master.*$
-  # jobs run on the proposal worker
-  - name: ^.*(merge-release-tags|(propose|upstream)-(requirements|translation)-updates?)$
-    parameter-function: reusable_node
-  # jobs run on the pypi worker
-  - name: ^.*-(jenkinsci|mavencentral|pypi-(both|wheel))-upload$
-    parameter-function: reusable_node
-  # jobs run on the mirror26, mirror27 and mirror33 workers
-  - name: ^(periodic|post)-mirror-python(26|27|33)$
-    parameter-function: reusable_node
-  # the salt-trigger worker has no jobs yet
   - name: gate-tempest-dsvm-full
     queue-name: integrated
   - name: ^(gate|check)-tempest-dsvm-neutron-full$
diff --git a/modules/openstack_project/files/zuul/openstack_functions.py b/modules/openstack_project/files/zuul/openstack_functions.py
index ffc51cb175..7e48872a7e 100644
--- a/modules/openstack_project/files/zuul/openstack_functions.py
+++ b/modules/openstack_project/files/zuul/openstack_functions.py
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import re
+
 
 def set_log_url(item, job, params):
     if hasattr(item.change, 'refspec'):
@@ -29,10 +31,76 @@ def set_log_url(item, job, params):
                                             params['ZUUL_UUID'][:7])
 
 
-def single_use_node(item, job, params):
-    set_log_url(item, job, params)
-    params['OFFLINE_NODE_WHEN_COMPLETE'] = '1'
-
-
 def reusable_node(item, job, params):
+    if 'OFFLINE_NODE_WHEN_COMPLETE' in params:
+        del params['OFFLINE_NODE_WHEN_COMPLETE']
+
+
+def devstack_params(item, job, params):
+    params['ZUUL_NODE'] = 'devstack-precise'
+
+
+def default_params_precise(item, job, params):
+    params['ZUUL_NODE'] = 'bare-precise'
+
+
+def default_params_trusty(item, job, params):
+    change = item.change
+    # Note we can't fallback on the default labels because
+    # jenkins uses 'bare-precise || bare-trusty'.
+    # This is necessary to get the gearman plugin to register
+    # gearman jobs with both node labels.
+    if (hasattr(change, 'branch') and
+        change.branch == 'stable/havana' or
+        change.branch == 'stable/icehouse'):
+        params['ZUUL_NODE'] = 'bare-precise'
+    else:
+        params['ZUUL_NODE'] = 'bare-trusty'
+
+
+def set_node_options(item, job, params, default):
+    # Set up log url paramter for all jobs
     set_log_url(item, job, params)
+    # Default to single use node. Potentially overriden below.
+    # Select node to run job on.
+    params['OFFLINE_NODE_WHEN_COMPLETE'] = '1'
+    proposal_re = r'^.*(merge-release-tags|(propose|upstream)-(requirements|translation)-updates?)$'  # noqa
+    pypi_re = r'^.*-(jenkinsci|mavencentral|pypi-(both|wheel))-upload$'
+    mirror_re = r'^(periodic|post)-mirror-python(26|27|33)$'
+    python26_re = r'^.*-py(thon)?26.*$'
+    python33_re = r'^.*-py(py|(thon)?33).*$'
+    tripleo_re = r'^.*-tripleo.*$'
+    devstack_re = r'^.*-dsvm.*$'
+    # jobs run on the proposal worker
+    if re.match(proposal_re, job.name) or re.match(pypi_re, job.name):
+        reusable_node(item, job, params)
+    # jobs run on the mirror26, mirror27 and mirror33 workers
+    elif re.match(mirror_re, job.name):
+        reusable_node(item, job, params)
+    # Jobs needing python26
+    elif re.match(python26_re, job.name):
+        # Pass because job specified label is always correct.
+        pass
+    # Jobs needing py33/pypy slaves
+    elif re.match(python33_re, job.name):
+        # Pass because job specified label is always correct.
+        pass
+    # Jobs needing tripleo slaves
+    elif re.match(tripleo_re, job.name):
+        # Pass because job specified label is always correct.
+        pass
+    # Jobs needing devstack slaves
+    elif re.match(devstack_re, job.name):
+        devstack_params(item, job, params)
+    elif default == 'trusty':
+        default_params_trusty(item, job, params)
+    else:
+        default_params_precise(item, job, params)
+
+
+def set_node_options_default_precise(item, job, params):
+    set_node_options(item, job, params, 'precise')
+
+
+def set_node_options_default_trusty(item, job, params):
+    set_node_options(item, job, params, 'trusty')