diff --git a/tools/jenkins-job.xml b/tools/jenkins-job.xml
new file mode 100644
index 0000000000..e51bc57d85
--- /dev/null
+++ b/tools/jenkins-job.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+ false
+
+
+
+
+ UUID
+
+
+
+
+
+
+ %s
+ true
+ false
+ false
+ false
+
+ true
+
+
+ sleep 30
+
+
+
+
+
+
diff --git a/tools/test-jenkins-api.py b/tools/test-jenkins-api.py
new file mode 100755
index 0000000000..de3955ed00
--- /dev/null
+++ b/tools/test-jenkins-api.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python
+#
+# Test all of the Jenkins API features used by the
+# OpenStack Infrastructure project
+#
+# Copyright (C) 2013 OpenStack Foundation
+#
+# 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.
+
+import jenkins
+import urllib
+import urllib2
+import urlparse
+from pprint import pprint
+import time
+from uuid import uuid4
+import ConfigParser
+import os
+import re
+
+import sys
+sys.path.insert(0, '../../zuul/zuul')
+from launcher.jenkins import ExtendedJenkins
+
+sys.path.insert(0, '../../devstack-gate/')
+from myjenkins import Jenkins as DGJenkins
+
+JOB_NAME = 'test-job'
+NODE_NAME = 'test-node'
+
+class JenkinsTest(object):
+ def __init__(self):
+ self.config = ConfigParser.ConfigParser()
+ if len(sys.argv) < 2:
+ print "Usage: %s zuul.conf" % sys.argv[0]
+ sys.exit(1)
+ fp = sys.argv[1]
+ if os.path.exists(os.path.expanduser(fp)):
+ self.config.read(os.path.expanduser(fp))
+
+ server = self.config.get('jenkins', 'server')
+ user = self.config.get('jenkins', 'user')
+ apikey = self.config.get('jenkins', 'apikey')
+
+ self.jenkins = ExtendedJenkins(server, user, apikey)
+ self.dgjenkins = DGJenkins(server, user, apikey)
+
+ def nodeExists(self):
+ return self.dgjenkins.node_exists(NODE_NAME)
+
+ def createNode(self):
+ assert not self.nodeExists()
+ priv_key = '/var/lib/jenkins/.ssh/id_rsa'
+ self.dgjenkins.create_node(
+ NODE_NAME, numExecutors=1,
+ nodeDescription='Test node',
+ remoteFS='/home/jenkins',
+ labels='testnode',
+ exclusive=True,
+ launcher='hudson.plugins.sshslaves.SSHLauncher',
+ launcher_params={'port': 22,
+ 'username': 'jenkins',
+ 'privatekey': priv_key,
+ 'host': 'nowhere.example.com'})
+ assert self.nodeExists()
+
+ def reconfigNode(self):
+ LABEL_RE = re.compile(r'')
+ config = self.dgjenkins.get_node_config(NODE_NAME)
+ assert '' in config
+ config = LABEL_RE.sub('', config)
+ self.dgjenkins.reconfig_node(NODE_NAME, config)
+ config = self.dgjenkins.get_node_config(NODE_NAME)
+ assert '' in config
+
+ def deleteNode(self):
+ assert self.nodeExists()
+ self.dgjenkins.delete_node(NODE_NAME)
+ assert not self.nodeExists()
+
+ def findBuildInQueue(self, build):
+ for item in self.jenkins.get_queue_info():
+ if 'actions' not in item:
+ continue
+ for action in item['actions']:
+ if 'parameters' not in action:
+ continue
+ parameters = action['parameters']
+ for param in parameters:
+ # UUID is deprecated in favor of ZUUL_UUID
+ if ((param['name'] in ['ZUUL_UUID', 'UUID'])
+ and build == param['value']):
+ return item
+ return False
+
+ def addJob(self, quiet_period):
+ assert not self.jobExists()
+ xml = open('jenkins-job.xml').read()
+ xml = xml % quiet_period
+
+ self.jenkins.create_job(JOB_NAME, xml)
+ assert self.jobExists()
+
+ def reconfigJob(self, quiet_period):
+ assert self.jobExists()
+ xml = open('jenkins-job.xml').read()
+ xml = xml % quiet_period
+
+ self.jenkins.reconfig_job(JOB_NAME, xml)
+ xml2 = self.jenkins.get_job_config(JOB_NAME)
+ s = '%s' % quiet_period
+ assert s in xml2
+
+ def jobExists(self):
+ return self.jenkins.job_exists(JOB_NAME)
+
+ def deleteJob(self):
+ assert self.jobExists()
+ self.jenkins.delete_job(JOB_NAME)
+ assert not self.jobExists()
+
+ def getJobs(self):
+ pprint(self.jenkins.get_jobs())
+
+ def testCancelQueue(self):
+ uuid = str(uuid4().hex)
+ self.jenkins.build_job(JOB_NAME, parameters=dict(UUID=uuid))
+
+ item = self.findBuildInQueue(uuid)
+ assert item
+ self.jenkins.cancel_queue(item['id'])
+ assert not self.findBuildInQueue(uuid)
+
+ def testCancelBuild(self):
+ uuid = str(uuid4().hex)
+ self.jenkins.build_job(JOB_NAME, parameters=dict(UUID=uuid))
+
+ assert self.findBuildInQueue(uuid)
+ for x in range(60):
+ if not self.findBuildInQueue(uuid):
+ break
+ assert not self.findBuildInQueue(uuid)
+ time.sleep(1)
+
+ buildno = self.jenkins.get_job_info(JOB_NAME)['lastBuild']['number']
+ info = self.jenkins.get_build_info(JOB_NAME, buildno)
+ assert info['building']
+ self.jenkins.stop_build(JOB_NAME, buildno)
+ time.sleep(1)
+ info = self.jenkins.get_build_info(JOB_NAME, buildno)
+ assert not info['building']
+
+ console_url = urlparse.urljoin(info['url'], 'consoleFull')
+ self.jenkins.jenkins_open(urllib2.Request(console_url))
+
+ self.jenkins.set_build_description(JOB_NAME, buildno,
+ "test description")
+
+ info = self.jenkins.get_build_info(JOB_NAME, buildno)
+ assert info['description'] == 'test description'
+
+j = JenkinsTest()
+if j.nodeExists():
+ j.deleteNode()
+j.createNode()
+j.reconfigNode()
+j.deleteNode()
+if j.jobExists():
+ j.deleteJob()
+j.addJob(5)
+j.reconfigJob(10)
+j.testCancelQueue()
+j.testCancelBuild()
+j.deleteJob()