#!/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 hashlib import yaml import xml.etree.ElementTree as XML from xml.dom import minidom import jenkins import re import pkg_resources import logging import copy import itertools logger = logging.getLogger(__name__) def deep_format(obj, paramdict): """Apply the paramdict via str.format() to all string objects found within the supplied obj. Lists and dicts are traversed recursively.""" # YAML serialisation was originally used to achieve this, but that places # limitations on the values in paramdict - the post-format result must # still be valid YAML (so substituting-in a string containing quotes, for # example, is problematic). if isinstance(obj, str): ret = obj.format(**paramdict) elif isinstance(obj, list): ret = [] for item in obj: ret.append(deep_format(item, paramdict)) elif isinstance(obj, dict): ret = {} for item in obj: ret[item] = deep_format(obj[item], paramdict) else: ret = obj return ret class YamlParser(object): def __init__(self, config=None): self.registry = ModuleRegistry(config) self.data = {} self.jobs = [] def parse(self, fn): data = yaml.load(open(fn)) for item in data: cls, dfn = item.items()[0] group = self.data.get(cls, {}) name = dfn['name'] group[name] = dfn self.data[cls] = group def getJob(self, name): job = self.data.get('job', {}).get(name, None) if not job: return job return self.applyDefaults(job) def getJobGroup(self, name): return self.data.get('job-group', {}).get(name, None) def getJobTemplate(self, name): job = self.data.get('job-template', {}).get(name, None) if not job: return job return self.applyDefaults(job) def applyDefaults(self, data): whichdefaults = data.get('defaults', 'global') defaults = self.data.get('defaults', {}).get(whichdefaults, {}) newdata = {} newdata.update(defaults) newdata.update(data) return newdata def generateXML(self): changed = True while changed: changed = False for module in self.registry.modules: if hasattr(module, 'handle_data'): if module.handle_data(self): changed = True for job in self.data.get('job', {}).values(): logger.debug("XMLifying job '{0}'".format(job['name'])) job = self.applyDefaults(job) self.getXMLForJob(job) for project in self.data.get('project', {}).values(): logger.debug("XMLifying project '{0}'".format(project['name'])) for jobspec in project.get('jobs', []): if isinstance(jobspec, dict): # Singleton dict containing dict of job-specific params jobname, jobparams = jobspec.items()[0] else: jobname = jobspec jobparams = {} job = self.getJob(jobname) if job: # Just naming an existing defined job continue # see if it's a job group group = self.getJobGroup(jobname) if group: for group_jobname in group['jobs']: job = self.getJob(group_jobname) if job: continue template = self.getJobTemplate(group_jobname) # Allow a group to override parameters set by a project d = {} d.update(project) d.update(group) # Except name, since the group's name is not useful d['name'] = project['name'] if template: self.getXMLForTemplateJob(d, template) continue # see if it's a template template = self.getJobTemplate(jobname) if template: d = {} d.update(project) d.update(jobparams) self.getXMLForTemplateJob(d, template) def getXMLForTemplateJob(self, project, template): dimensions = [] for (k, v) in project.items(): if type(v) == list and k not in ['jobs']: dimensions.append(zip([k] * len(v), v)) # XXX somewhat hackish to ensure we actually have a single # pass through the loop if len(dimensions) == 0: dimensions = [(("", ""),)] for values in itertools.product(*dimensions): params = copy.deepcopy(project) params.update(values) logger.debug("Generating XML for template job {0}" " (params {1})".format( template['name'], params)) self.getXMLForJob(deep_format(template, params)) def getXMLForJob(self, data): kind = data.get('project-type', 'freestyle') for ep in pkg_resources.iter_entry_points( group='jenkins_jobs.projects', name=kind): Mod = ep.load() mod = Mod(self.registry) xml = mod.root_xml(data) self.gen_xml(xml, data) job = XmlJob(xml, data['name']) self.jobs.append(job) break def gen_xml(self, xml, data): XML.SubElement(xml, 'actions') description = XML.SubElement(xml, 'description') description.text = data.get('description', '') XML.SubElement(xml, 'keepDependencies').text = 'false' if data.get('disabled'): XML.SubElement(xml, 'disabled').text = 'true' else: XML.SubElement(xml, 'disabled').text = 'false' XML.SubElement(xml, 'blockBuildWhenDownstreamBuilding').text = 'false' XML.SubElement(xml, 'blockBuildWhenUpstreamBuilding').text = 'false' if data.get('concurrent'): XML.SubElement(xml, 'concurrentBuild').text = 'true' else: XML.SubElement(xml, 'concurrentBuild').text = 'false' if('quiet-period' in data): XML.SubElement(xml, 'quietPeriod').text = str(data['quiet-period']) for module in self.registry.modules: if hasattr(module, 'gen_xml'): module.gen_xml(self, xml, data) class ModuleRegistry(object): def __init__(self, config): self.modules = [] self.handlers = {} self.global_config = config for entrypoint in pkg_resources.iter_entry_points( group='jenkins_jobs.modules'): Mod = entrypoint.load() mod = Mod(self) self.modules.append(mod) self.modules.sort(lambda a, b: cmp(a.sequence, b.sequence)) def registerHandler(self, category, name, method): cat_dict = self.handlers.get(category, {}) if not cat_dict: self.handlers[category] = cat_dict cat_dict[name] = method def getHandler(self, category, name): return self.handlers[category][name] class XmlJob(object): def __init__(self, xml, name): self.xml = xml self.name = name def md5(self): return hashlib.md5(self.output()).hexdigest() # Pretty printing ideas from # http://stackoverflow.com/questions/749796/pretty-printing-xml-in-python pretty_text_re = re.compile('>\n\s+([^<>\s].*?)\n\s+\g<1>