From 1ea656b6a4b0eed0c468abb084b7761c62fd04c2 Mon Sep 17 00:00:00 2001
From: "James E. Blair" <jeblair@hp.com>
Date: Thu, 14 Jun 2012 16:30:56 -0700
Subject: [PATCH] Changes to builders/publishers.

Make the following changes to builders and publishers in service
of being able to handle this job:

  http://paste.openstack.org/show/18541/

* Allow builders to have attributes
* Add a generic shell script builder
* Make the 'project' job attribute optional
  (no link to github will be made in that case)
* Combine all separate publishers into one publisher module
  so that jobs may have more than one publisher
* Name the section that lists publishers "post_build_actions"
  to be consistent with the web UI
* Allow publishers to have attributes
* Add an archive artifacts publisher
* Add a parameterized build trigger publisher
* Add a parameterized build_trigger builder
* Add wrappers
* Add a build timeout wrapper
* Add a timstamp wrapper

Also, make the following small improvements:

* The 'disabled' attribute of a job is optional, defaults to false.
* Add a script to verify XML output is the same before and after
  a change to the job filler (test.sh)

Change-Id: Icf2228494e431f5bf0a5f051f63842f12f6f92f1
---
 modules/jenkins_jobs/files/jenkins_jobs.py    |   5 +-
 .../jenkins_jobs/files/modules/builders.py    |  31 +-
 .../jenkins_jobs/files/modules/properties.py  |   8 +-
 .../files/modules/publisher_coverage.py       |  63 ----
 .../files/modules/publisher_docs.py           |  45 ---
 .../files/modules/publisher_ftp.py            |  97 ------
 .../files/modules/publisher_junit.py          |  33 --
 .../files/modules/publisher_none.py           |  26 --
 .../files/modules/publisher_pep8.py           |  74 -----
 .../files/modules/publisher_ppa.py            |  29 --
 .../files/modules/publisher_tarball.py        |  39 ---
 .../files/modules/publisher_war.py            |  41 ---
 .../jenkins_jobs/files/modules/publishers.py  | 293 ++++++++++++++++++
 .../jenkins_jobs/files/modules/wrappers.py    |  44 +++
 .../projects/openstack/devstack-gate.yml      |   3 +-
 .../files/projects/openstack/devstack.yml     |   3 +-
 .../files/projects/openstack/gerrit.yml       |  33 +-
 .../openstack/openstack-ci-puppet.yml         |   6 +-
 .../files/projects/openstack/pypi-mirror.yml  |   6 +-
 .../files/projects/openstack/tempest.yml      |   9 +-
 .../files/projects/openstack/zuul.yml         |  12 +-
 .../files/projects/stackforge/mraas.yml       |   6 +-
 .../files/templates/python_jobs.yml           |  77 +++--
 modules/jenkins_jobs/files/test.sh            |  26 ++
 24 files changed, 482 insertions(+), 527 deletions(-)
 delete mode 100644 modules/jenkins_jobs/files/modules/publisher_coverage.py
 delete mode 100644 modules/jenkins_jobs/files/modules/publisher_docs.py
 delete mode 100644 modules/jenkins_jobs/files/modules/publisher_ftp.py
 delete mode 100644 modules/jenkins_jobs/files/modules/publisher_junit.py
 delete mode 100644 modules/jenkins_jobs/files/modules/publisher_none.py
 delete mode 100644 modules/jenkins_jobs/files/modules/publisher_pep8.py
 delete mode 100644 modules/jenkins_jobs/files/modules/publisher_ppa.py
 delete mode 100644 modules/jenkins_jobs/files/modules/publisher_tarball.py
 delete mode 100644 modules/jenkins_jobs/files/modules/publisher_war.py
 create mode 100644 modules/jenkins_jobs/files/modules/publishers.py
 create mode 100644 modules/jenkins_jobs/files/modules/wrappers.py
 create mode 100755 modules/jenkins_jobs/files/test.sh

diff --git a/modules/jenkins_jobs/files/jenkins_jobs.py b/modules/jenkins_jobs/files/jenkins_jobs.py
index e094f2f33e..e0ccc0da14 100644
--- a/modules/jenkins_jobs/files/jenkins_jobs.py
+++ b/modules/jenkins_jobs/files/jenkins_jobs.py
@@ -147,7 +147,10 @@ If you would like to make changes to this job, please see:\n\n\
 https://github.com/openstack/openstack-ci-puppet\n\n\
 In modules/jenkins_jobs"
         XML.SubElement(self.xml, 'keepDependencies').text = 'false'
-        XML.SubElement(self.xml, 'disabled').text = self.data['main']['disabled']
+        if self.data['main'].get('disabled'):
+            XML.SubElement(self.xml, 'disabled').text = 'true'
+        else:
+            XML.SubElement(self.xml, 'disabled').text = 'false'
         XML.SubElement(self.xml, 'blockBuildWhenDownstreamBuilding').text = 'false'
         XML.SubElement(self.xml, 'blockBuildWhenUpstreamBuilding').text = 'false'
         if self.data['main'].get('concurrent'):
diff --git a/modules/jenkins_jobs/files/modules/builders.py b/modules/jenkins_jobs/files/modules/builders.py
index 9b45660da9..d2069fd4f9 100644
--- a/modules/jenkins_jobs/files/modules/builders.py
+++ b/modules/jenkins_jobs/files/modules/builders.py
@@ -30,7 +30,11 @@ class builders(object):
     def gen_xml(self, xml_parent):
         builders = XML.SubElement(xml_parent, self.alias)
         for builder in self.data[self.alias]:
-            getattr(self, '_' + builder)(builders)
+            if isinstance(builder, dict):
+                for key, value in builder.items():
+                    getattr(self, '_' + key)(builders, value)
+            else:
+                getattr(self, '_' + builder)(builders)
 
     def _add_script(self, xml_parent, script):
         shell = XML.SubElement(xml_parent, 'hudson.tasks.Shell')
@@ -78,6 +82,31 @@ for f in `find . -iname *.erb` ; do
 done
 """)
 
+    def _shell(self, xml_parent, data):
+        self._add_script(xml_parent, data)
+
+    def _trigger_builds(self, xml_parent, data):
+        tbuilder = XML.SubElement(xml_parent, 'hudson.plugins.parameterizedtrigger.TriggerBuilder')
+        configs = XML.SubElement(tbuilder, 'configs')
+        for project_def in data:
+            tconfig = XML.SubElement(configs, 'hudson.plugins.parameterizedtrigger.BlockableBuildTriggerConfig')
+            tconfigs = XML.SubElement(tconfig, 'configs')
+            if project_def.has_key('predefined_parameters'):
+                params = XML.SubElement(tconfigs,
+                                        'hudson.plugins.parameterizedtrigger.PredefinedBuildParameters')
+                properties = XML.SubElement(params, 'properties')
+                properties.text = project_def['predefined_parameters']
+            else:
+                tconfigs.set('class', 'java.util.Collections$EmptyList')
+            projects = XML.SubElement(tconfig, 'projects')
+            projects.text = project_def['project']
+            condition = XML.SubElement(tconfig, 'condition')
+            condition.text = 'ALWAYS'
+            trigger_with_no_params = XML.SubElement(tconfig, 'triggerWithNoParameters')
+            trigger_with_no_params.text = 'false'
+            build_all_nodes_with_label = XML.SubElement(tconfig, 'buildAllNodesWithLabel')
+            build_all_nodes_with_label.text = 'false'
+            
     def _python26(self, xml_parent):
         self._add_script(xml_parent, '/usr/local/jenkins/slave_scripts/run-tox.sh 26')
 
diff --git a/modules/jenkins_jobs/files/modules/properties.py b/modules/jenkins_jobs/files/modules/properties.py
index a382e59840..32b0c4acce 100644
--- a/modules/jenkins_jobs/files/modules/properties.py
+++ b/modules/jenkins_jobs/files/modules/properties.py
@@ -26,9 +26,11 @@ class properties(object):
     def gen_xml(self, xml_parent):
         main = self.data['main']
         properties = XML.SubElement(xml_parent, 'properties')
-        github = XML.SubElement(properties, 'com.coravy.hudson.plugins.github.GithubProjectProperty')
-        github_url = XML.SubElement(github, 'projectUrl')
-        github_url.text = "https://github.com/{org}/{project}".format(org=main['github_org'], project=main['project'])
+        if main.get('project'):
+            github = XML.SubElement(properties, 'com.coravy.hudson.plugins.github.GithubProjectProperty')
+            github_url = XML.SubElement(github, 'projectUrl')
+            github_url.text = "https://github.com/{org}/{project}".format(
+                org=main['github_org'], project=main['project'])
         throttle = XML.SubElement(properties, 'hudson.plugins.throttleconcurrents.ThrottleJobProperty')
         XML.SubElement(throttle, 'maxConcurrentPerNode').text = '0'
         XML.SubElement(throttle, 'maxConcurrentTotal').text = '0'
diff --git a/modules/jenkins_jobs/files/modules/publisher_coverage.py b/modules/jenkins_jobs/files/modules/publisher_coverage.py
deleted file mode 100644
index 23e07c6a13..0000000000
--- a/modules/jenkins_jobs/files/modules/publisher_coverage.py
+++ /dev/null
@@ -1,63 +0,0 @@
-#! /usr/bin/env python
-# Copyright (C) 2012 OpenStack, LLC.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-# Jenkins Job module for coverage publishers
-# No additional YAML needed
-
-import xml.etree.ElementTree as XML
-
-class publisher_coverage(object):
-    def __init__(self, data):
-        self.data = data
-
-    def gen_xml(self, xml_parent):
-        publishers = XML.SubElement(xml_parent, 'publishers')
-        cobertura = XML.SubElement(publishers, 'hudson.plugins.cobertura.CoberturaPublisher')
-        XML.SubElement(cobertura, 'coberturaReportFile').text = '**/coverage.xml'
-        XML.SubElement(cobertura, 'onlyStable').text = 'false'
-        healthy = XML.SubElement(cobertura, 'healthyTarget')
-        targets = XML.SubElement(healthy, 'targets', {'class':'enum-map','enum-type':'hudson.plugins.cobertura.targets.CoverageMetric'})
-        entry = XML.SubElement(targets, 'entry')
-        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'CONDITIONAL'
-        XML.SubElement(entry, 'int').text = '70'
-        entry = XML.SubElement(targets, 'entry')
-        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'LINE'
-        XML.SubElement(entry, 'int').text = '80'
-        entry = XML.SubElement(targets, 'entry')
-        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'METHOD'
-        XML.SubElement(entry, 'int').text = '80'
-        unhealthy = XML.SubElement(cobertura, 'unhealthyTarget')
-        targets = XML.SubElement(unhealthy, 'targets', {'class':'enum-map','enum-type':'hudson.plugins.cobertura.targets.CoverageMetric'})
-        entry = XML.SubElement(targets, 'entry')
-        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'CONDITIONAL'
-        XML.SubElement(entry, 'int').text = '0'
-        entry = XML.SubElement(targets, 'entry')
-        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'LINE'
-        XML.SubElement(entry, 'int').text = '0'
-        entry = XML.SubElement(targets, 'entry')
-        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'METHOD'
-        XML.SubElement(entry, 'int').text = '0'
-        failing = XML.SubElement(cobertura, 'failingTarget')
-        targets = XML.SubElement(failing, 'targets', {'class':'enum-map','enum-type':'hudson.plugins.cobertura.targets.CoverageMetric'})
-        entry = XML.SubElement(targets, 'entry')
-        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'CONDITIONAL'
-        XML.SubElement(entry, 'int').text = '0'
-        entry = XML.SubElement(targets, 'entry')
-        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'LINE'
-        XML.SubElement(entry, 'int').text = '0'
-        entry = XML.SubElement(targets, 'entry')
-        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'METHOD'
-        XML.SubElement(entry, 'int').text = '0'
-        XML.SubElement(cobertura, 'sourceEncoding').text = 'ASCII'
diff --git a/modules/jenkins_jobs/files/modules/publisher_docs.py b/modules/jenkins_jobs/files/modules/publisher_docs.py
deleted file mode 100644
index 133f9ee4af..0000000000
--- a/modules/jenkins_jobs/files/modules/publisher_docs.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#! /usr/bin/env python
-# Copyright (C) 2012 OpenStack, LLC.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-# Jenkins Job module for docs publishers
-# publish:
-#   site: 'glance.openstack.org'
-
-import xml.etree.ElementTree as XML
-
-class publisher_docs(object):
-    def __init__(self, data):
-        self.data = data
-
-    def gen_xml(self, xml_parent):
-        main = self.data['main']
-        publish = self.data['publisher']
-        publishers = XML.SubElement(xml_parent, 'publishers')
-        scp = XML.SubElement(publishers, 'be.certipost.hudson.plugin.SCPRepositoryPublisher')
-        XML.SubElement(scp, 'siteName').text = publish['site']
-        entries = XML.SubElement(scp, 'entries')
-        entry = XML.SubElement(entries, 'be.certipost.hudson.plugin.Entry')
-        XML.SubElement(entry, 'filePath').text = 'docs/{proj}'.format(proj=main['project'])
-        XML.SubElement(entry, 'sourceFile').text = 'doc/build/html/**'
-        XML.SubElement(entry, 'keepHierarchy').text = 'false'
-        entry = XML.SubElement(entries, 'be.certipost.hudson.plugin.Entry')
-        XML.SubElement(entry, 'filePath').text = 'docs/{proj}/_static'.format(proj=main['project'])
-        XML.SubElement(entry, 'sourceFile').text = 'doc/build/html/_static/**'
-        XML.SubElement(entry, 'keepHierarchy').text = 'false'
-        entry = XML.SubElement(entries, 'be.certipost.hudson.plugin.Entry')
-        XML.SubElement(entry, 'filePath').text = 'docs/{proj}/_sources'.format(proj=main['project'])
-        XML.SubElement(entry, 'sourceFile').text = 'doc/build/html/_sources/**'
-        XML.SubElement(entry, 'keepHierarchy').text = 'false'
-
diff --git a/modules/jenkins_jobs/files/modules/publisher_ftp.py b/modules/jenkins_jobs/files/modules/publisher_ftp.py
deleted file mode 100644
index 92d71bfea2..0000000000
--- a/modules/jenkins_jobs/files/modules/publisher_ftp.py
+++ /dev/null
@@ -1,97 +0,0 @@
-#! /usr/bin/env python
-# Copyright (C) 2012 OpenStack, LLC.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-# Jenkins Job module for publishing via ftp
-# publish:
-#   site: 'docs.openstack.org'
-#   remote_dir: 'dest/dir'
-#   source_files: 'base/source/dir/**'
-#   remove_prefix: 'base/source/dir'
-#   excludes: '**/*.exludedfiletype'
-#
-# This will upload everything under $workspace/base/source/dir to
-# docs.openstack.org $ftpdir/dest/dir exluding the excluded file type.
-
-import xml.etree.ElementTree as XML
-
-class publisher_ftp(object):
-    def __init__(self, data):
-        self.data = data
-
-    def gen_xml(self, xml_parent):
-        """
-        Example XML:
-        <publishers>
-          <jenkins.plugins.publish__over__ftp.BapFtpPublisherPlugin>
-            <consolePrefix>FTP: </consolePrefix>
-            <delegate>
-              <publishers>
-                <jenkins.plugins.publish__over__ftp.BapFtpPublisher>
-                  <configName>docs.openstack.org</configName>
-                  <verbose>true</verbose>
-                  <transfers>
-                    <jenkins.plugins.publish__over__ftp.BapFtpTransfer>
-                      <remoteDirectory></remoteDirectory>
-                      <sourceFiles>openstack-identity-api/target/docbkx/webhelp/api/openstack-identity-service/2.0/**</sourceFiles>
-                      <excludes>**/*.xml,**/null*</excludes>
-                      <removePrefix>openstack-identity-api/target/docbkx/webhelp</removePrefix>
-                      <remoteDirectorySDF>false</remoteDirectorySDF>
-                      <flatten>false</flatten>
-                      <cleanRemote>false</cleanRemote>
-                      <asciiMode>false</asciiMode>
-                    </jenkins.plugins.publish__over__ftp.BapFtpTransfer>
-                  </transfers>
-                  <useWorkspaceInPromotion>false</useWorkspaceInPromotion>
-                  <usePromotionTimestamp>false</usePromotionTimestamp>
-                </jenkins.plugins.publish__over__ftp.BapFtpPublisher>
-              </publishers>
-              <continueOnError>false</continueOnError>
-              <failOnError>false</failOnError>
-              <alwaysPublishFromMaster>false</alwaysPublishFromMaster>
-              <hostConfigurationAccess class="jenkins.plugins.publish_over_ftp.BapFtpPublisherPlugin" reference="../.."/>
-            </delegate>
-          </jenkins.plugins.publish__over__ftp.BapFtpPublisherPlugin>
-        </publishers>
-        """
-        publish = self.data['publisher']
-        outer_publishers = XML.SubElement(xml_parent, 'publishers')
-        outer_ftp = XML.SubElement(outer_publishers, 'jenkins.plugins.publish__over__ftp.BapFtpPublisherPlugin')
-        XML.SubElement(outer_ftp, 'consolePrefix').text = 'FTP: '
-        delegate = XML.SubElement(outer_ftp, 'delegate')
-        publishers = XML.SubElement(delegate, 'publishers')
-        ftp = XML.SubElement(publishers, 'jenkins.plugins.publish__over__ftp.BapFtpPublisher')
-        XML.SubElement(ftp, 'configName').text = publish['site']
-        XML.SubElement(ftp, 'verbose').text = 'true'
-
-        transfers = XML.SubElement(ftp, 'transfers')
-        ftp_transfers = XML.SubElement(transfers, 'jenkins.plugins.publish__over__ftp.BapFtpTransfer')
-        # TODO: the next four fields are where the magic happens. Fill them in.
-        XML.SubElement(ftp_transfers, 'remoteDirectory').text = publish['remote_dir']
-        XML.SubElement(ftp_transfers, 'sourceFiles').text = publish['source_files']
-        XML.SubElement(ftp_transfers, 'excludes').text = publish['excludes']
-        XML.SubElement(ftp_transfers, 'removePrefix').text = publish['remove_prefix']
-        XML.SubElement(ftp_transfers, 'remoteDirectorySDF').text = 'false'
-        XML.SubElement(ftp_transfers, 'flatten').text = 'false'
-        XML.SubElement(ftp_transfers, 'cleanRemote').text = 'false'
-        XML.SubElement(ftp_transfers, 'asciiMode').text = 'false'
-
-        XML.SubElement(ftp, 'useWorkspaceInPromotion').text = 'false'
-        XML.SubElement(ftp, 'usePromotionTimestamp').text = 'false'
-        XML.SubElement(delegate, 'continueOnError').text = 'false'
-        XML.SubElement(delegate, 'failOnError').text = 'false'
-        XML.SubElement(delegate, 'alwaysPublishFromMaster').text = 'false'
-        XML.SubElement(delegate, 'hostConfigurationAccess',
-                {'class': 'jenkins.plugins.publish_over_ftp.BapFtpPublisherPlugin',
-                 'reference': '../..'})
diff --git a/modules/jenkins_jobs/files/modules/publisher_junit.py b/modules/jenkins_jobs/files/modules/publisher_junit.py
deleted file mode 100644
index 32ead0f436..0000000000
--- a/modules/jenkins_jobs/files/modules/publisher_junit.py
+++ /dev/null
@@ -1,33 +0,0 @@
-#! /usr/bin/env python
-# Copyright (C) 2012 OpenStack, LLC.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-# Jenkins Job module for coverage publishers
-# To use you add the following into your YAML:
-# publisher:
-#   results: 'nosetests.xml'
-
-import xml.etree.ElementTree as XML
-
-class publisher_junit(object):
-    def __init__(self, data):
-        self.data = data
-
-    def gen_xml(self, xml_parent):
-        publishers = XML.SubElement(xml_parent, 'publishers')
-        junitresult = XML.SubElement(publishers,
-            'hudson.tasks.junit.JUnitResultArchiver')
-        XML.SubElement(junitresult, 'testResults').text = self.data['publisher']['results']
-        XML.SubElement(junitresult, 'keepLongStdio').text = "true"
-        XML.SubElement(junitresult, 'testDataPublishers')
diff --git a/modules/jenkins_jobs/files/modules/publisher_none.py b/modules/jenkins_jobs/files/modules/publisher_none.py
deleted file mode 100644
index 2065412ed3..0000000000
--- a/modules/jenkins_jobs/files/modules/publisher_none.py
+++ /dev/null
@@ -1,26 +0,0 @@
-#! /usr/bin/env python
-# Copyright (C) 2012 OpenStack, LLC.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-# Jenkins Job module for no publishers
-# No additional YAML needed
-
-import xml.etree.ElementTree as XML
-
-class publisher_none(object):
-    def __init__(self, data):
-        self.data = data
-
-    def gen_xml(self, xml_parent):
-        XML.SubElement(xml_parent, 'publishers')
diff --git a/modules/jenkins_jobs/files/modules/publisher_pep8.py b/modules/jenkins_jobs/files/modules/publisher_pep8.py
deleted file mode 100644
index a9d372d34c..0000000000
--- a/modules/jenkins_jobs/files/modules/publisher_pep8.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#! /usr/bin/env python
-# Copyright (C) 2012 OpenStack, LLC.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-# Jenkins Job module for pep8 publishers
-# No additional YAML needed
-
-import xml.etree.ElementTree as XML
-
-class publisher_pep8(object):
-    def __init__(self, data):
-        self.data = data
-
-    def _add_entry(self, xml_parent, name):
-        entry = XML.SubElement(xml_parent, 'entry')
-        XML.SubElement(entry, 'string').text = name
-        tconfig = XML.SubElement(entry, 'hudson.plugins.violations.TypeConfig')
-        XML.SubElement(tconfig, 'type').text = name
-        XML.SubElement(tconfig, 'min').text = '10'
-        XML.SubElement(tconfig, 'max').text = '999'
-        XML.SubElement(tconfig, 'unstable').text = '999'
-        XML.SubElement(tconfig, 'usePattern').text = 'false'
-        XML.SubElement(tconfig, 'pattern')
-
-    def gen_xml(self, xml_parent):
-        publishers = XML.SubElement(xml_parent, 'publishers')
-        violations = XML.SubElement(publishers, 'hudson.plugins.violations.ViolationsPublisher')
-        config = XML.SubElement(violations, 'config')
-        suppressions = XML.SubElement(config, 'suppressions', {'class':'tree-set'})
-        XML.SubElement(suppressions, 'no-comparator')
-        configs = XML.SubElement(config, 'typeConfigs')
-        XML.SubElement(configs, 'no-comparator')
-
-        self._add_entry(configs, 'checkstyle')
-        self._add_entry(configs, 'codenarc')
-        self._add_entry(configs, 'cpd')
-        self._add_entry(configs, 'cpplint')
-        self._add_entry(configs, 'csslint')
-        self._add_entry(configs, 'findbugs')
-        self._add_entry(configs, 'fxcop')
-        self._add_entry(configs, 'gendarme')
-        self._add_entry(configs, 'jcreport')
-        self._add_entry(configs, 'jslint')
-
-        entry = XML.SubElement(configs, 'entry')
-        XML.SubElement(entry, 'string').text = 'pep8'
-        tconfig = XML.SubElement(entry, 'hudson.plugins.violations.TypeConfig')
-        XML.SubElement(tconfig, 'type').text = 'pep8'
-        XML.SubElement(tconfig, 'min').text = '0'
-        XML.SubElement(tconfig, 'max').text = '1'
-        XML.SubElement(tconfig, 'unstable').text = '1'
-        XML.SubElement(tconfig, 'usePattern').text = 'false'
-        XML.SubElement(tconfig, 'pattern').text = '**/pep8.txt'
-
-        self._add_entry(configs, 'pmd')
-        self._add_entry(configs, 'pylint')
-        self._add_entry(configs, 'simian')
-        self._add_entry(configs, 'stylecop')
-
-        XML.SubElement(config, 'limit').text = '100'
-        XML.SubElement(config, 'sourcePathPattern')
-        XML.SubElement(config, 'fauxProjectPath')
-        XML.SubElement(config, 'encoding').text = 'default'
diff --git a/modules/jenkins_jobs/files/modules/publisher_ppa.py b/modules/jenkins_jobs/files/modules/publisher_ppa.py
deleted file mode 100644
index 4a40e22643..0000000000
--- a/modules/jenkins_jobs/files/modules/publisher_ppa.py
+++ /dev/null
@@ -1,29 +0,0 @@
-#! /usr/bin/env python
-# Copyright (C) 2012 OpenStack, LLC.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-# Jenkins Job module for PPA publishers
-# No additional YAML needed
-
-import xml.etree.ElementTree as XML
-
-class publisher_ppa(object):
-    def __init__(self, data):
-        self.data = data
-
-    def gen_xml(self, xml_parent):
-        publishers = XML.SubElement(xml_parent, 'publishers')
-        archiver = XML.SubElement(publishers, 'hudson.tasks.ArtifactArchiver')
-        XML.SubElement(archiver, 'artifacts').text = 'build/*.dsc,build/*.tar.gz,build/*.changes'
-        XML.SubElement(archiver, 'latestOnly').text = 'false'
diff --git a/modules/jenkins_jobs/files/modules/publisher_tarball.py b/modules/jenkins_jobs/files/modules/publisher_tarball.py
deleted file mode 100644
index 721568eed6..0000000000
--- a/modules/jenkins_jobs/files/modules/publisher_tarball.py
+++ /dev/null
@@ -1,39 +0,0 @@
-#! /usr/bin/env python
-# Copyright (C) 2012 OpenStack, LLC.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-# Jenkins Job module for tarball publishers
-# To use you add the following into your YAML:
-# publish:
-#   site: 'glance.openstack.org'
-
-import xml.etree.ElementTree as XML
-
-class publisher_tarball(object):
-    def __init__(self, data):
-        self.data = data
-
-    def gen_xml(self, xml_parent):
-        site = self.data['publisher']['site']
-        publishers = XML.SubElement(xml_parent, 'publishers')
-        archiver = XML.SubElement(publishers, 'hudson.tasks.ArtifactArchiver')
-        XML.SubElement(archiver, 'artifacts').text = 'dist/*.tar.gz'
-        XML.SubElement(archiver, 'latestOnly').text = 'false'
-        scp = XML.SubElement(publishers, 'be.certipost.hudson.plugin.SCPRepositoryPublisher')
-        XML.SubElement(scp, 'siteName').text = site
-        entries = XML.SubElement(scp, 'entries')
-        entry = XML.SubElement(entries, 'be.certipost.hudson.plugin.Entry')
-        XML.SubElement(entry, 'filePath').text = 'tarballs/{proj}/'.format(proj=self.data['main']['project'])
-        XML.SubElement(entry, 'sourceFile').text = 'dist/*.tar.gz'
-        XML.SubElement(entry, 'keepHierarchy').text = 'false'
diff --git a/modules/jenkins_jobs/files/modules/publisher_war.py b/modules/jenkins_jobs/files/modules/publisher_war.py
deleted file mode 100644
index 07bb79fd48..0000000000
--- a/modules/jenkins_jobs/files/modules/publisher_war.py
+++ /dev/null
@@ -1,41 +0,0 @@
-#! /usr/bin/env python
-# Copyright (C) 2012 OpenStack, LLC.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-# Jenkins Job module for war publishers
-# To use you add the following into your YAML:
-# publish:
-#   site: 'nova.openstack.org'
-#   warfile: 'gerrit-war/target/gerrit*.war'
-#   target_path: 'tarballs/ci/'
-
-import xml.etree.ElementTree as XML
-
-class publisher_war(object):
-    def __init__(self, data):
-        self.data = data
-
-    def gen_xml(self, xml_parent):
-        site = self.data['publisher']['site']
-        publishers = XML.SubElement(xml_parent, 'publishers')
-        archiver = XML.SubElement(publishers, 'hudson.tasks.ArtifactArchiver')
-        XML.SubElement(archiver, 'artifacts').text = self.data['publisher']['warfile']
-        XML.SubElement(archiver, 'latestOnly').text = 'false'
-        scp = XML.SubElement(publishers, 'be.certipost.hudson.plugin.SCPRepositoryPublisher')
-        XML.SubElement(scp, 'siteName').text = site
-        entries = XML.SubElement(scp, 'entries')
-        entry = XML.SubElement(entries, 'be.certipost.hudson.plugin.Entry')
-        XML.SubElement(entry, 'filePath').text = self.data['publisher']['target_path']
-        XML.SubElement(entry, 'sourceFile').text = self.data['publisher']['warfile']
-        XML.SubElement(entry, 'keepHierarchy').text = 'false'
diff --git a/modules/jenkins_jobs/files/modules/publishers.py b/modules/jenkins_jobs/files/modules/publishers.py
new file mode 100644
index 0000000000..62bd825eb1
--- /dev/null
+++ b/modules/jenkins_jobs/files/modules/publishers.py
@@ -0,0 +1,293 @@
+#! /usr/bin/env python
+# Copyright (C) 2012 OpenStack, LLC.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+# Jenkins Job module for coverage publishers
+# No additional YAML needed
+
+import xml.etree.ElementTree as XML
+
+class publishers(object):
+    def __init__(self, data):
+        self.data = data
+
+    def gen_xml(self, xml_parent):
+        publishers = XML.SubElement(xml_parent, 'publishers')
+        actions = self.data.get('post_build_actions', [])
+        for action in actions:
+            if isinstance(action, dict):
+                for key, value in action.items():
+                    getattr(self, '_' + key)(publishers, value)
+            else:
+                getattr(self, '_' + action)(publishers)
+
+    def _archive(self, xml_parent, data):
+        archiver = XML.SubElement(xml_parent, 'hudson.tasks.ArtifactArchiver')
+        artifacts = XML.SubElement(archiver, 'artifacts')
+        artifacts.text = data['artifacts']
+        if 'excludes' in data:
+            excludes = XML.SubElement(archiver, 'excludes')
+            excludes.text = data['excludes']
+        latest = XML.SubElement(archiver, 'latestOnly')
+        latest_only = data.get('latest_only', False)
+        if latest_only:
+            latest.text = 'true'
+        else:
+            latest.text = 'false'
+
+    def _trigger_parameterized_builds(self, xml_parent, data):
+        tbuilder = XML.SubElement(xml_parent, 'hudson.plugins.parameterizedtrigger.BuildTrigger')
+        configs = XML.SubElement(tbuilder, 'configs')
+        for project_def in data:
+            tconfig = XML.SubElement(configs, 'hudson.plugins.parameterizedtrigger.BuildTriggerConfig')
+            tconfigs = XML.SubElement(tconfig, 'configs')
+            if project_def.has_key('predefined_parameters'):
+                params = XML.SubElement(tconfigs,
+                                        'hudson.plugins.parameterizedtrigger.PredefinedBuildParameters')
+                properties = XML.SubElement(params, 'properties')
+                properties.text = project_def['predefined_parameters']
+            else:
+                tconfigs.set('class', 'java.util.Collections$EmptyList')
+            projects = XML.SubElement(tconfig, 'projects')
+            projects.text = project_def['project']
+            condition = XML.SubElement(tconfig, 'condition')
+            condition.text = project_def.get('condition', 'ALWAYS')
+            trigger_with_no_params = XML.SubElement(tconfig, 'triggerWithNoParameters')
+            trigger_with_no_params.text = 'false'
+
+    def _coverage(self, xml_parent):
+        cobertura = XML.SubElement(xml_parent, 'hudson.plugins.cobertura.CoberturaPublisher')
+        XML.SubElement(cobertura, 'coberturaReportFile').text = '**/coverage.xml'
+        XML.SubElement(cobertura, 'onlyStable').text = 'false'
+        healthy = XML.SubElement(cobertura, 'healthyTarget')
+        targets = XML.SubElement(healthy, 'targets', {'class':'enum-map','enum-type':'hudson.plugins.cobertura.targets.CoverageMetric'})
+        entry = XML.SubElement(targets, 'entry')
+        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'CONDITIONAL'
+        XML.SubElement(entry, 'int').text = '70'
+        entry = XML.SubElement(targets, 'entry')
+        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'LINE'
+        XML.SubElement(entry, 'int').text = '80'
+        entry = XML.SubElement(targets, 'entry')
+        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'METHOD'
+        XML.SubElement(entry, 'int').text = '80'
+        unhealthy = XML.SubElement(cobertura, 'unhealthyTarget')
+        targets = XML.SubElement(unhealthy, 'targets', {'class':'enum-map','enum-type':'hudson.plugins.cobertura.targets.CoverageMetric'})
+        entry = XML.SubElement(targets, 'entry')
+        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'CONDITIONAL'
+        XML.SubElement(entry, 'int').text = '0'
+        entry = XML.SubElement(targets, 'entry')
+        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'LINE'
+        XML.SubElement(entry, 'int').text = '0'
+        entry = XML.SubElement(targets, 'entry')
+        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'METHOD'
+        XML.SubElement(entry, 'int').text = '0'
+        failing = XML.SubElement(cobertura, 'failingTarget')
+        targets = XML.SubElement(failing, 'targets', {'class':'enum-map','enum-type':'hudson.plugins.cobertura.targets.CoverageMetric'})
+        entry = XML.SubElement(targets, 'entry')
+        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'CONDITIONAL'
+        XML.SubElement(entry, 'int').text = '0'
+        entry = XML.SubElement(targets, 'entry')
+        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'LINE'
+        XML.SubElement(entry, 'int').text = '0'
+        entry = XML.SubElement(targets, 'entry')
+        XML.SubElement(entry, 'hudson.plugins.cobertura.targets.CoverageMetric').text = 'METHOD'
+        XML.SubElement(entry, 'int').text = '0'
+        XML.SubElement(cobertura, 'sourceEncoding').text = 'ASCII'
+
+    # Jenkins Job module for publishing via ftp
+    # publish:
+    #   site: 'docs.openstack.org'
+    #   remote_dir: 'dest/dir'
+    #   source_files: 'base/source/dir/**'
+    #   remove_prefix: 'base/source/dir'
+    #   excludes: '**/*.exludedfiletype'
+    #
+    # This will upload everything under $workspace/base/source/dir to
+    # docs.openstack.org $ftpdir/dest/dir exluding the excluded file type.
+
+    def _ftp(self, xml_parent, data):
+        """
+        Example XML:
+        <publishers>
+          <jenkins.plugins.publish__over__ftp.BapFtpPublisherPlugin>
+            <consolePrefix>FTP: </consolePrefix>
+            <delegate>
+              <publishers>
+                <jenkins.plugins.publish__over__ftp.BapFtpPublisher>
+                  <configName>docs.openstack.org</configName>
+                  <verbose>true</verbose>
+                  <transfers>
+                    <jenkins.plugins.publish__over__ftp.BapFtpTransfer>
+                      <remoteDirectory></remoteDirectory>
+                      <sourceFiles>openstack-identity-api/target/docbkx/webhelp/api/openstack-identity-service/2.0/**</sourceFiles>
+                      <excludes>**/*.xml,**/null*</excludes>
+                      <removePrefix>openstack-identity-api/target/docbkx/webhelp</removePrefix>
+                      <remoteDirectorySDF>false</remoteDirectorySDF>
+                      <flatten>false</flatten>
+                      <cleanRemote>false</cleanRemote>
+                      <asciiMode>false</asciiMode>
+                    </jenkins.plugins.publish__over__ftp.BapFtpTransfer>
+                  </transfers>
+                  <useWorkspaceInPromotion>false</useWorkspaceInPromotion>
+                  <usePromotionTimestamp>false</usePromotionTimestamp>
+                </jenkins.plugins.publish__over__ftp.BapFtpPublisher>
+              </publishers>
+              <continueOnError>false</continueOnError>
+              <failOnError>false</failOnError>
+              <alwaysPublishFromMaster>false</alwaysPublishFromMaster>
+              <hostConfigurationAccess class="jenkins.plugins.publish_over_ftp.BapFtpPublisherPlugin" reference="../.."/>
+            </delegate>
+          </jenkins.plugins.publish__over__ftp.BapFtpPublisherPlugin>
+        </publishers>
+        """
+        outer_ftp = XML.SubElement(xml_parent,
+                                   'jenkins.plugins.publish__over__ftp.BapFtpPublisherPlugin')
+        XML.SubElement(outer_ftp, 'consolePrefix').text = 'FTP: '
+        delegate = XML.SubElement(outer_ftp, 'delegate')
+        publishers = XML.SubElement(delegate, 'publishers')
+        ftp = XML.SubElement(publishers, 'jenkins.plugins.publish__over__ftp.BapFtpPublisher')
+        XML.SubElement(ftp, 'configName').text = data['site']
+        XML.SubElement(ftp, 'verbose').text = 'true'
+
+        transfers = XML.SubElement(ftp, 'transfers')
+        ftp_transfers = XML.SubElement(transfers, 'jenkins.plugins.publish__over__ftp.BapFtpTransfer')
+        # TODO: the next four fields are where the magic happens. Fill them in.
+        XML.SubElement(ftp_transfers, 'remoteDirectory').text = data['remote_dir']
+        XML.SubElement(ftp_transfers, 'sourceFiles').text = data['source_files']
+        XML.SubElement(ftp_transfers, 'excludes').text = data['excludes']
+        XML.SubElement(ftp_transfers, 'removePrefix').text = data['remove_prefix']
+        XML.SubElement(ftp_transfers, 'remoteDirectorySDF').text = 'false'
+        XML.SubElement(ftp_transfers, 'flatten').text = 'false'
+        XML.SubElement(ftp_transfers, 'cleanRemote').text = 'false'
+        XML.SubElement(ftp_transfers, 'asciiMode').text = 'false'
+
+        XML.SubElement(ftp, 'useWorkspaceInPromotion').text = 'false'
+        XML.SubElement(ftp, 'usePromotionTimestamp').text = 'false'
+        XML.SubElement(delegate, 'continueOnError').text = 'false'
+        XML.SubElement(delegate, 'failOnError').text = 'false'
+        XML.SubElement(delegate, 'alwaysPublishFromMaster').text = 'false'
+        XML.SubElement(delegate, 'hostConfigurationAccess',
+                {'class': 'jenkins.plugins.publish_over_ftp.BapFtpPublisherPlugin',
+                 'reference': '../..'})
+
+    # Jenkins Job module for coverage publishers
+    # To use you add the following into your YAML:
+    # publisher:
+    #   results: 'nosetests.xml'
+
+    def _junit(self, xml_parent, data):
+        junitresult = XML.SubElement(xml_parent,
+            'hudson.tasks.junit.JUnitResultArchiver')
+        XML.SubElement(junitresult, 'testResults').text = data['results']
+        XML.SubElement(junitresult, 'keepLongStdio').text = "true"
+        XML.SubElement(junitresult, 'testDataPublishers')
+
+    # Jenkins Job module for pep8 publishers
+    # No additional YAML needed
+
+    def _pep8_add_entry(self, xml_parent, name):
+        entry = XML.SubElement(xml_parent, 'entry')
+        XML.SubElement(entry, 'string').text = name
+        tconfig = XML.SubElement(entry, 'hudson.plugins.violations.TypeConfig')
+        XML.SubElement(tconfig, 'type').text = name
+        XML.SubElement(tconfig, 'min').text = '10'
+        XML.SubElement(tconfig, 'max').text = '999'
+        XML.SubElement(tconfig, 'unstable').text = '999'
+        XML.SubElement(tconfig, 'usePattern').text = 'false'
+        XML.SubElement(tconfig, 'pattern')
+
+    def _pep8(self, xml_parent):
+        violations = XML.SubElement(xml_parent, 'hudson.plugins.violations.ViolationsPublisher')
+        config = XML.SubElement(violations, 'config')
+        suppressions = XML.SubElement(config, 'suppressions', {'class':'tree-set'})
+        XML.SubElement(suppressions, 'no-comparator')
+        configs = XML.SubElement(config, 'typeConfigs')
+        XML.SubElement(configs, 'no-comparator')
+
+        self._pep8_add_entry(configs, 'checkstyle')
+        self._pep8_add_entry(configs, 'codenarc')
+        self._pep8_add_entry(configs, 'cpd')
+        self._pep8_add_entry(configs, 'cpplint')
+        self._pep8_add_entry(configs, 'csslint')
+        self._pep8_add_entry(configs, 'findbugs')
+        self._pep8_add_entry(configs, 'fxcop')
+        self._pep8_add_entry(configs, 'gendarme')
+        self._pep8_add_entry(configs, 'jcreport')
+        self._pep8_add_entry(configs, 'jslint')
+
+        entry = XML.SubElement(configs, 'entry')
+        XML.SubElement(entry, 'string').text = 'pep8'
+        tconfig = XML.SubElement(entry, 'hudson.plugins.violations.TypeConfig')
+        XML.SubElement(tconfig, 'type').text = 'pep8'
+        XML.SubElement(tconfig, 'min').text = '0'
+        XML.SubElement(tconfig, 'max').text = '1'
+        XML.SubElement(tconfig, 'unstable').text = '1'
+        XML.SubElement(tconfig, 'usePattern').text = 'false'
+        XML.SubElement(tconfig, 'pattern').text = '**/pep8.txt'
+
+        self._pep8_add_entry(configs, 'pmd')
+        self._pep8_add_entry(configs, 'pylint')
+        self._pep8_add_entry(configs, 'simian')
+        self._pep8_add_entry(configs, 'stylecop')
+
+        XML.SubElement(config, 'limit').text = '100'
+        XML.SubElement(config, 'sourcePathPattern')
+        XML.SubElement(config, 'fauxProjectPath')
+        XML.SubElement(config, 'encoding').text = 'default'
+
+    # Jenkins Job module for PPA publishers
+    # No additional YAML needed
+
+    def _ppa(self, xml_parent):
+        archiver = XML.SubElement(xml_parent, 'hudson.tasks.ArtifactArchiver')
+        XML.SubElement(archiver, 'artifacts').text = 'build/*.dsc,build/*.tar.gz,build/*.changes'
+        XML.SubElement(archiver, 'latestOnly').text = 'false'
+
+    # Jenkins Job module for tarball publishers
+    # To use you add the following into your YAML:
+    # publish:
+    #   site: 'glance.openstack.org'
+
+    def _tarball(self, xml_parent, data):
+        site = data['site']
+        archiver = XML.SubElement(xml_parent, 'hudson.tasks.ArtifactArchiver')
+        XML.SubElement(archiver, 'artifacts').text = 'dist/*.tar.gz'
+        XML.SubElement(archiver, 'latestOnly').text = 'false'
+        scp = XML.SubElement(xml_parent, 'be.certipost.hudson.plugin.SCPRepositoryPublisher')
+        XML.SubElement(scp, 'siteName').text = site
+        entries = XML.SubElement(scp, 'entries')
+        entry = XML.SubElement(entries, 'be.certipost.hudson.plugin.Entry')
+        XML.SubElement(entry, 'filePath').text = 'tarballs/{proj}/'.format(proj=self.data['main']['project'])
+        XML.SubElement(entry, 'sourceFile').text = 'dist/*.tar.gz'
+        XML.SubElement(entry, 'keepHierarchy').text = 'false'
+
+    # Jenkins Job module for war publishers
+    # To use you add the following into your YAML:
+    # publish:
+    #   site: 'nova.openstack.org'
+    #   warfile: 'gerrit-war/target/gerrit*.war'
+    #   target_path: 'tarballs/ci/'
+
+    def _war(self, xml_parent, data):
+        site = data['site']
+        archiver = XML.SubElement(xml_parent, 'hudson.tasks.ArtifactArchiver')
+        XML.SubElement(archiver, 'artifacts').text = data['warfile']
+        XML.SubElement(archiver, 'latestOnly').text = 'false'
+        scp = XML.SubElement(xml_parent, 'be.certipost.hudson.plugin.SCPRepositoryPublisher')
+        XML.SubElement(scp, 'siteName').text = site
+        entries = XML.SubElement(scp, 'entries')
+        entry = XML.SubElement(entries, 'be.certipost.hudson.plugin.Entry')
+        XML.SubElement(entry, 'filePath').text = data['target_path']
+        XML.SubElement(entry, 'sourceFile').text = data['warfile']
+        XML.SubElement(entry, 'keepHierarchy').text = 'false'
diff --git a/modules/jenkins_jobs/files/modules/wrappers.py b/modules/jenkins_jobs/files/modules/wrappers.py
new file mode 100644
index 0000000000..785f86ac24
--- /dev/null
+++ b/modules/jenkins_jobs/files/modules/wrappers.py
@@ -0,0 +1,44 @@
+#! /usr/bin/env python
+# Copyright (C) 2012 OpenStack, LLC.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+# Jenkins Job module for wrappers
+
+import xml.etree.ElementTree as XML
+
+class wrappers(object):
+    def __init__(self, data):
+        self.data = data
+
+    def gen_xml(self, xml_parent):
+        publishers = XML.SubElement(xml_parent, 'buildWrappers')
+
+        if 'timeout' in self.data['main']:
+            self._timeout(publishers)
+        if 'timestamps' in self.data['main']:
+            self._timestamps(publishers)
+
+    def _timeout(self, xml_parent):
+        twrapper = XML.SubElement(xml_parent, 'hudson.plugins.build__timeout.BuildTimeoutWrapper')
+        tminutes = XML.SubElement(twrapper, 'timeoutMinutes')
+        tminutes.text = str(self.data['main']['timeout'])
+        failbuild = XML.SubElement(twrapper, 'failBuild')
+        fail = self.data['main'].get('timeout_fail', False)
+        if fail:
+            failbuild.text = 'true'
+        else:
+            failbuild.text = 'false'
+
+    def _timestamps(self, xml_parent):
+        XML.SubElement(xml_parent, 'hudson.plugins.timestamper.TimestamperBuildWrapper')
diff --git a/modules/jenkins_jobs/files/projects/openstack/devstack-gate.yml b/modules/jenkins_jobs/files/projects/openstack/devstack-gate.yml
index c34b54f7d3..c094bedcc4 100644
--- a/modules/jenkins_jobs/files/projects/openstack/devstack-gate.yml
+++ b/modules/jenkins_jobs/files/projects/openstack/devstack-gate.yml
@@ -5,14 +5,13 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-devstack-gate-merge'
   review_site: 'review.openstack.org'
   github_org: 'openstack-ci'
   project: 'devstack-gate'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
diff --git a/modules/jenkins_jobs/files/projects/openstack/devstack.yml b/modules/jenkins_jobs/files/projects/openstack/devstack.yml
index cf175248f0..cda387b4ab 100644
--- a/modules/jenkins_jobs/files/projects/openstack/devstack.yml
+++ b/modules/jenkins_jobs/files/projects/openstack/devstack.yml
@@ -5,14 +5,13 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-devstack-merge'
   review_site: 'review.openstack.org'
   github_org: 'openstack-dev'
   project: 'devstack'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
diff --git a/modules/jenkins_jobs/files/projects/openstack/gerrit.yml b/modules/jenkins_jobs/files/projects/openstack/gerrit.yml
index 0a9402f3cb..04f738c79a 100644
--- a/modules/jenkins_jobs/files/projects/openstack/gerrit.yml
+++ b/modules/jenkins_jobs/files/projects/openstack/gerrit.yml
@@ -5,7 +5,7 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-gerrit-merge'
@@ -13,7 +13,6 @@ main:
   review_site: 'review.openstack.org'
   project: 'gerrit'
   authenticatedBuild: 'true'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
@@ -51,7 +50,7 @@ modules:
   - scm
   - assignednode
   - builders:prebuilders
-  - publisher_war
+  - publishers
 
 main:
   name: 'check-gerrit-unittests'
@@ -59,7 +58,6 @@ main:
   github_org: 'openstack-ci'
   project: 'gerrit'
   authenticatedBuild: 'true'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
@@ -93,17 +91,18 @@ maven:
 postbuilders:
   - gerrit_postrun
 
+post_build_actions:
+  - war:
+      site: 'nova.openstack.org'
+      warfile: 'gerrit-war/target/gerrit*.war'
+      target_path: 'tarballs/ci/test/'
+
 scm:
   scm: 'false'
 
 assignednode:
   node: 'precise'
 
-publisher:
-  site: 'nova.openstack.org'
-  warfile: 'gerrit-war/target/gerrit*.war'
-  target_path: 'tarballs/ci/test/'
-
 ---
 # gate-gerrit-unittests
 modules:
@@ -113,7 +112,7 @@ modules:
   - assignednode
   - builders:prebuilders
   - builders:postbuilders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-gerrit-unittests'
@@ -121,7 +120,6 @@ main:
   github_org: 'openstack-ci'
   project: 'gerrit'
   authenticatedBuild: 'true'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
@@ -170,7 +168,7 @@ modules:
   - assignednode
   - builders:prebuilders
   - builders:postbuilders
-  - publisher_war
+  - publishers
 
 main:
   name: 'gerrit-package'
@@ -178,7 +176,6 @@ main:
   github_org: 'openstack-ci'
   project: 'gerrit'
   authenticatedBulid: 'false'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
@@ -215,13 +212,15 @@ maven:
 postbuilders:
   - gerrit_postrun
 
+post_build_actions:
+  - war:
+      site: 'nova.openstack.org'
+      warfile: 'gerrit-war/target/gerrit*.war'
+      target_path: 'tarballs/ci/'
+
 scm:
   scm: 'false'
 
 assignednode:
   node: 'precise'
 
-publisher:
-  site: 'nova.openstack.org'
-  warfile: 'gerrit-war/target/gerrit*.war'
-  target_path: 'tarballs/ci/'
diff --git a/modules/jenkins_jobs/files/projects/openstack/openstack-ci-puppet.yml b/modules/jenkins_jobs/files/projects/openstack/openstack-ci-puppet.yml
index c594c77090..53f4619f7b 100644
--- a/modules/jenkins_jobs/files/projects/openstack/openstack-ci-puppet.yml
+++ b/modules/jenkins_jobs/files/projects/openstack/openstack-ci-puppet.yml
@@ -5,14 +5,13 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-ci-puppet-merge'
   review_site: 'review.openstack.org'
   github_org: 'openstack'
   project: 'openstack-ci-puppet'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
@@ -49,14 +48,13 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-ci-puppet-syntax'
   review_site: 'review.openstack.org'
   github_org: 'openstack'
   project: 'openstack-ci-puppet'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
diff --git a/modules/jenkins_jobs/files/projects/openstack/pypi-mirror.yml b/modules/jenkins_jobs/files/projects/openstack/pypi-mirror.yml
index de9b0ce416..4f3f24d6bd 100644
--- a/modules/jenkins_jobs/files/projects/openstack/pypi-mirror.yml
+++ b/modules/jenkins_jobs/files/projects/openstack/pypi-mirror.yml
@@ -5,14 +5,13 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-pypi-mirror-pyflakes'
   review_site: 'review.openstack.org'
   github_org: 'openstack-ci'
   project: 'pypi-mirror'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
@@ -50,14 +49,13 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-pypi-mirror-merge'
   review_site: 'review.openstack.org'
   github_org: 'openstack-ci'
   project: 'pypi-mirror'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
diff --git a/modules/jenkins_jobs/files/projects/openstack/tempest.yml b/modules/jenkins_jobs/files/projects/openstack/tempest.yml
index bb2d06cea7..5b91dc5d52 100644
--- a/modules/jenkins_jobs/files/projects/openstack/tempest.yml
+++ b/modules/jenkins_jobs/files/projects/openstack/tempest.yml
@@ -5,14 +5,13 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_pep8
+  - publishers
 
 main:
   name: 'gate-tempest-pep8'
   review_site: 'review.openstack.org'
   github_org: 'openstack-ci'
   project: 'tempest'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
@@ -37,6 +36,9 @@ builders:
   - gerrit_git_prep
   - pep8
 
+post_build_actions:
+  - pep8
+
 scm:
   scm: 'false'
 
@@ -50,14 +52,13 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-tempest-merge'
   review_site: 'review.openstack.org'
   github_org: 'openstack-ci'
   project: 'tempest'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
diff --git a/modules/jenkins_jobs/files/projects/openstack/zuul.yml b/modules/jenkins_jobs/files/projects/openstack/zuul.yml
index ae26a70e85..a3459b4190 100644
--- a/modules/jenkins_jobs/files/projects/openstack/zuul.yml
+++ b/modules/jenkins_jobs/files/projects/openstack/zuul.yml
@@ -5,14 +5,13 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-zuul-pyflakes'
   review_site: 'review.openstack.org'
   github_org: 'openstack-ci'
   project: 'zuul'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
@@ -50,14 +49,13 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_pep8
+  - publishers
 
 main:
   name: 'gate-zuul-pep8'
   review_site: 'review.openstack.org'
   github_org: 'openstack-ci'
   project: 'zuul'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
@@ -82,6 +80,9 @@ builders:
   - gerrit_git_prep
   - pep8
 
+post_build_actions:
+  - pep8
+
 scm:
   scm: 'false'
 
@@ -95,14 +96,13 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-zuul-merge'
   review_site: 'review.openstack.org'
   github_org: 'openstack-ci'
   project: 'zuul'
-  disabled: 'false'
   concurrent: 'true'
 
 parameters:
diff --git a/modules/jenkins_jobs/files/projects/stackforge/mraas.yml b/modules/jenkins_jobs/files/projects/stackforge/mraas.yml
index 4dce98a95a..7922fdba58 100644
--- a/modules/jenkins_jobs/files/projects/stackforge/mraas.yml
+++ b/modules/jenkins_jobs/files/projects/stackforge/mraas.yml
@@ -5,7 +5,7 @@ modules:
   - assignednode
   - trigger_gerrit
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-MRaaS-merge'
@@ -13,7 +13,6 @@ main:
   review_site: 'review.stackforge.org'
   project: 'MRaaS'
   authenticatedBuild: 'true'
-  disabled: 'false'
 
 trigger:
   triggerOnPatchsetUploadedEvent: 'false'
@@ -45,7 +44,7 @@ modules:
   - assignednode
   - trigger_gerrit
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'check-MRaaS-merge'
@@ -53,7 +52,6 @@ main:
   review_site: 'review.stackforge.org'
   project: 'MRaaS'
   authenticatedBuild: 'true'
-  disabled: 'false'
 
 trigger:
   triggerOnPatchsetUploadedEvent: 'true'
diff --git a/modules/jenkins_jobs/files/templates/python_jobs.yml b/modules/jenkins_jobs/files/templates/python_jobs.yml
index 7c42071883..b37bbcf7e8 100644
--- a/modules/jenkins_jobs/files/templates/python_jobs.yml
+++ b/modules/jenkins_jobs/files/templates/python_jobs.yml
@@ -7,7 +7,7 @@ modules:
   - assignednode
   - trigger_pollscm
   - builders
-  - publisher_coverage
+  - publishers
 
 main:
   name: '@NAME@-coverage'
@@ -15,7 +15,7 @@ main:
   github_org: '@GITHUB_ORG@'
   project: '@NAME@'
   authenticatedBuild: 'false'
-  disabled: '@DISABLED@' 
+  disabled: @DISABLED@
 
 trigger:
   pollscm: '*/15 * * * *'
@@ -23,6 +23,9 @@ trigger:
 builders:
   - coverage
 
+post_build_actions:
+  - coverage
+
 scm:
   scm: 'true'
 
@@ -42,7 +45,7 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_pep8
+  - publishers
 
 main:
   name: 'gate-@NAME@-pep8'
@@ -50,7 +53,7 @@ main:
   github_org: '@GITHUB_ORG@'
   project: '@NAME@'
   authenticatedBuild: 'true'
-  disabled: '@DISABLED@'
+  disabled: @DISABLED@
   concurrent: 'true'
 
 parameters:
@@ -75,6 +78,9 @@ builders:
   - gerrit_git_prep
   - pep8
 
+post_build_actions:
+  - pep8
+
 scm:
   scm: 'false'
 
@@ -88,7 +94,7 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_junit
+  - publishers
 
 main:
   name: 'gate-@NAME@-python26'
@@ -96,7 +102,7 @@ main:
   github_org: '@GITHUB_ORG@'
   project: '@NAME@'
   authenticatedBuild: 'true'
-  disabled: '@DISABLED@'
+  disabled: @DISABLED@
   concurrent: 'true'
 
 parameters:
@@ -121,6 +127,10 @@ builders:
   - gerrit_git_prep
   - python26
 
+post_build_actions:
+  - junit:
+      results: '**/nosetests.xml'
+
 scm:
   scm: 'false'
 
@@ -128,9 +138,6 @@ scm:
 assignednode:
   node: 'oneiric'
 
-publisher:
-  results: '**/nosetests.xml'
-
 ---
 # python27-gate
 modules:
@@ -138,7 +145,7 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_junit
+  - publishers
 
 main:
   name: 'gate-@NAME@-python27'
@@ -146,7 +153,7 @@ main:
   github_org: '@GITHUB_ORG@'
   project: '@NAME@'
   authenticatedBuild: 'true'
-  disabled: '@DISABLED@'
+  disabled: @DISABLED@
   concurrent: 'true'
 
 parameters:
@@ -171,15 +178,16 @@ builders:
   - gerrit_git_prep
   - python27
 
+post_build_actions:
+  - junit:
+      results: '**/nosetests.xml'
+
 scm:
   scm: 'false'
 
 assignednode:
   node: '@NODE@'
 
-publisher:
-  results: '**/nosetests.xml'
-
 ---
 # docs
 modules:
@@ -188,7 +196,7 @@ modules:
   - assignednode
   - trigger_pollscm
   - builders
-  - publisher_ftp
+  - publishers
 
 main:
   name: '@NAME@-docs'
@@ -196,7 +204,7 @@ main:
   github_org: '@GITHUB_ORG@'
   project: '@NAME@'
   authenticatedBuild: 'false'
-  disabled: '@DISABLED@'
+  disabled: @DISABLED@
 
 trigger:
   pollscm: '*/15 * * * *'
@@ -204,19 +212,20 @@ trigger:
 builders:
   - docs
 
+post_build_actions:
+  - ftp:
+      site: '@DOC_PUBLISHER_SITE@'
+      remote_dir: 'developer/@NAME@'
+      source_files: 'doc/build/html/**'
+      remove_prefix: 'doc/build/html'
+      excludes: ''
+
 scm:
   scm: 'true'
 
 assignednode:
   node: '@NODE@'
 
-publisher:
-  site: '@DOC_PUBLISHER_SITE@'
-  remote_dir: 'developer/@NAME@'
-  source_files: 'doc/build/html/**'
-  remove_prefix: 'doc/build/html'
-  excludes: ''
-
 ---
 # merge-gate
 modules:
@@ -224,7 +233,7 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_none
+  - publishers
 
 main:
   name: 'gate-@NAME@-merge'
@@ -232,7 +241,7 @@ main:
   github_org: '@GITHUB_ORG@'
   project: '@NAME@'
   authenticatedBuild: 'true'
-  disabled: '@DISABLED@'
+  disabled: @DISABLED@
   concurrent: 'true'
 
 parameters:
@@ -270,7 +279,7 @@ modules:
   - assignednode
   - trigger_none
   - builders
-  - publisher_ppa
+  - publishers
 
 main:
   name: '@NAME@-ppa'
@@ -278,11 +287,14 @@ main:
   github_org: '@GITHUB_ORG@'
   project: '@NAME@'
   authenticatedBuild: 'false'
-  disabled: 'true'
+  disabled: true
 
 builders:
   - ppa
 
+post_build_actions:
+  - ppa
+
 scm:
   scm: 'false'
 
@@ -296,7 +308,7 @@ modules:
   - scm
   - assignednode
   - builders
-  - publisher_tarball
+  - publishers
 
 main:
   name: '@NAME@-tarball'
@@ -304,7 +316,7 @@ main:
   github_org: '@GITHUB_ORG@'
   project: '@NAME@'
   authenticatedBulid: 'false'
-  disabled: '@DISABLED@'
+  disabled: @DISABLED@
   concurrent: 'true'
 
 parameters:
@@ -332,11 +344,12 @@ builders:
   - gerrit_git_prep
   - tarball
 
+post_build_actions:
+  - tarball:
+      site: '@PUBLISHER_SITE@'
+
 scm:
   scm: 'false'
 
 assignednode:
   node: '@NODE@'
-
-publisher:
-  site: '@PUBLISHER_SITE@'
diff --git a/modules/jenkins_jobs/files/test.sh b/modules/jenkins_jobs/files/test.sh
new file mode 100755
index 0000000000..ce2bcb5d07
--- /dev/null
+++ b/modules/jenkins_jobs/files/test.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# A simple script to verify that the XML output is unaltered after a change.
+# Before you start work, run "./test.sh save".
+# As you test your change, run "./test.sh" to see differences in XML output.
+
+mkdir -p /tmp/jenkins_jobs_test/saved
+mkdir -p /tmp/jenkins_jobs_test/test
+
+if [ "$1" == "save" ]
+then
+    for x in `find projects/ -name *.yml`
+    do
+	echo $x
+	BASENAME=`basename $x`
+	python jenkins_jobs.py test $x > /tmp/jenkins_jobs_test/saved/$BASENAME.xml
+    done
+else
+    for x in `find projects/ -name *.yml`
+    do
+	echo $x
+	BASENAME=`basename $x`
+	python jenkins_jobs.py test $x > /tmp/jenkins_jobs_test/test/$BASENAME.xml
+    done
+    diff -r /tmp/jenkins_jobs_test/saved /tmp/jenkins_jobs_test/test
+fi