Andrew Hutchings c3db5b210a Jenkins Job builder 2.0
This uses a python script with modules for parts of the XML.  The parameters for the projects are provided using YAML scripts.

It also includes a Jenkins API module to directly inject jobs into Jenkins without requiring a restart/reload as well as a memory of which jobs have been pushed to Jenkins.

It is currently configured to replace the original Jenkins Jobs in StackForge.

What it won't yet do:
1. Delete jobs (although it isn't far off being able to)
2. check-* jobs (need to modify the trigger_gerrit module to support that)

Documentation to follow

Fixes bug #995599

Change-Id: I2a67ee2d9e8f43cbced56425ef7f80dc6a30a814
2012-05-11 14:43:52 +01:00

174 lines
5.5 KiB
Python

#! /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.
# Manage jobs in Jenkins server
import os
import argparse
import hashlib
import yaml
import sys
import xml.etree.ElementTree as XML
import pycurl
import jenkins_talker
import ConfigParser
from xml.dom.ext import PrettyPrint
from StringIO import StringIO
from xml.dom.ext.reader import Sax2
parser = argparse.ArgumentParser()
subparser = parser.add_subparsers(help='update or delete job', dest='command')
parser_update = subparser.add_parser('update')
parser_update.add_argument('file', help='YAML file for update', type=file)
parser_delete = subparser.add_parser('delete')
parser_delete.add_argument('name', help='name of job')
parser.add_argument('--conf', dest='conf', help='Configuration file')
options = parser.parse_args()
if options.conf:
conf = options.conf
else:
conf = 'jenkins_jobs.ini'
conffp = open(conf, 'r')
config = ConfigParser.ConfigParser()
config.readfp(conffp)
class YamlParser(object):
def __init__(self, yfile):
self.data = yaml.load_all(yfile)
self.it = self.data.__iter__()
self.current = ''
def get_next_xml(self):
self.current = self.it.next()
return XmlParser(self.current)
def get_name(self):
return self.current['main']['name']
class XmlParser(object):
def __init__(self, data):
self.data = data
self.xml = XML.Element('project')
self.modules = []
self._load_modules()
self._build()
def _load_modules(self):
for modulename in self.data['modules']:
modulename = 'modules.{name}'.format(name=modulename)
self._register_module(modulename)
def _register_module(self, modulename):
classname = modulename.rsplit('.', 1)[1]
module = __import__(modulename, fromlist=[classname])
cla = getattr(module, classname)
self.modules.append(cla(self.data))
def _build(self):
XML.SubElement(self.xml, 'actions')
description = XML.SubElement(self.xml, 'description')
description.text = "THIS JOB IS MANAGED BY PUPPET AND WILL BE OVERWRITTEN.\n\n\
DON'T EDIT THIS JOB THROUGH THE WEB\n\n\
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']
XML.SubElement(self.xml, 'blockBuildWhenDownstreamBuilding').text = 'false'
XML.SubElement(self.xml, 'blockBuildWhenUpstreamBuilding').text = 'false'
XML.SubElement(self.xml, 'concurrentBuild').text = 'false'
XML.SubElement(self.xml, 'buildWrappers')
self._insert_modules()
def _insert_modules(self):
for module in self.modules:
module.gen_xml(self.xml)
def md5(self):
return hashlib.md5(self.output()).hexdigest()
def output(self):
reader = Sax2.Reader()
docNode = reader.fromString(XML.tostring(self.xml))
tmpStream = StringIO()
PrettyPrint(docNode, stream=tmpStream)
return tmpStream.getvalue()
class CacheStorage(object):
def __init__(self):
self.cachefilename = os.path.expanduser('~/.jenkins_jobs_cache.yml')
try:
yfile = file(self.cachefilename, 'r')
except IOError:
self.data = {}
return
self.data = yaml.load(yfile)
yfile.close()
def set(self, job, md5):
self.data[job] = md5
yfile = file(self.cachefilename, 'w')
yaml.dump(self.data, yfile)
yfile.close()
def is_cached(self, job):
if self.data.has_key(job):
return True
return False
def has_changed(self, job, md5):
if self.data.has_key(job) and self.data[job] == md5:
return False
return True
class Jenkins(object):
def __init__(self, url, user, password):
self.jenkins = jenkins_talker.JenkinsTalker(url, user, password)
def update_job(self, job_name, xml):
if self.jenkins.is_job(job_name):
self.jenkins.update_job(job_name, xml)
else:
self.jenkins.create_job(job_name, xml)
def is_job(self, job_name):
return self.jenkins.is_job(job_name)
def get_job_md5(self, job_name):
xml = self.jenkins.get_job_config(job_name)
return hashlib.md5(xml).hexdigest()
yparse = YamlParser(options.file)
cache = CacheStorage()
remote_jenkins = Jenkins(config.get('jenkins','url'), config.get('jenkins','user'), config.get('jenkins','password'))
while True:
try:
xml = yparse.get_next_xml()
job = yparse.get_name()
md5 = xml.md5()
if remote_jenkins.is_job(job) and not cache.is_cached(job):
old_md5 = remote_jenkins.get_job_md5(job)
cache.set(job, old_md5)
if cache.has_changed(job, md5):
remote_jenkins.update_job(job, xml.output())
cache.set(job, md5)
except StopIteration:
break