#!/usr/bin/env python import argparse from csv import DictReader from glob import glob import logging import os from pprint import pprint,pformat import jenkinsapi.custom_exceptions from jenkinsapi.jenkins import Jenkins import shlex import subprocess import sys import xml.etree.ElementTree as etree destdir="" class STATE: disabled="DISABLED" unknown="UNKNOWN" pending="PENDING" failed="FAILED" succeeded="SUCCEEDED" def write_status(obs, jenkins_status): filename = "status.succeeded" status = STATE.succeeded component="" if obs.get_state() != STATE.succeeded and obs.get_state() != STATE.disabled: component = "obs" status = obs.get_state() elif jenkins_status['status'] != STATE.succeeded and jenkins_status['status'] != STATE.disabled: component = "jenkins" status = jenkins_status['status'] if component: filename="status." + status.lower() + "." + component if destdir: filepath=destdir + "/" + filename out=open(filepath, 'w') logger.info("status will be written to " + filepath) # remove outdated files for i in glob(destdir + "/status*"): if not os.path.samefile(i, filepath): logger.debug("remove outdated status file " + i) os.remove(i) else: out=sys.stdout out.write( "#\n" ) out.write( "STATUS="+status+"\n" ) out.write( "#\n" ) out.write( "NAME="+filename+"\n" ) obs_status = obs.get_status() for key in sorted(obs_status): out.write( "#\n" ) if key == "status": out.write( "OBS_STATUS" +"="+ obs_status[key] +"\n" ) else: out.write( "OBS_STATUS_" + key.upper() +"="+",".join(get_repo_list(obs_status[key]))+"\n" ) out.write( "#\n" ) version = obs.get_pkg_info() for key in sorted(version): out.write( "BUILD_" + key.upper() +"="+str(version[key])+"\n") out.write( "#\n" ) out.write("PACKAGES_SUCCESSFUL='%s'\n" % (get_packages_string(obs.get_successful_packages()))) out.write( "#\n" ) out.write("PACKAGES_FAILED='%s'\n" % (get_packages_string(obs.get_failed_packages()))) out.write( "#\n" ) for key in sorted(jenkins_status): out.write( "JENKINS_" + key.upper() +"="+str(jenkins_status[key])+"\n") def get_repo_name(repository, arch, jenkins=False): # obs: DISTRIBUTION_VERSION # jenkins: DISTRIBUTION-VERSION-ARCH # # use "-" as separator between repository and arch, # because repository and arch can contain: # repository: "." and "_" (openSUSE_13.1) # arch: "_" (x86_64) if jenkins: repo = str(repository).replace( "_", "-", 1 ) + "-" + str(arch) else: repo = str(repository) + "-" + str(arch) return repo def get_repo_list(array, jenkins=False): result=[] for i in sorted( array, key=lambda k: ( k['repository'], k['arch'] ) ): repo = get_repo_name(i['repository'], i['arch'], jenkins) result.append(repo) return result def get_packages_string(packages): pkg = '' for dist in packages: pkg += '%s:%s\n' % (dist, ','.join(packages[dist])) return pkg class Osc: def __init__(self, osc_paramter): self.osc="osc" self.paramter=osc_paramter def get_command(self, args): return self.__format_command([ self.osc, self.paramter ] + args) def __format_command(self, cmd): logger=logging.getLogger(__name__) #logger.debug( "cmd1:" + str(cmd) ) cmd2=" ".join( cmd ) logger.debug( "cmd2:" + str(cmd2) ) cmd3=shlex.split( cmd2 ) #logger.debug( "cmd3:" + str(cmd3) ) return cmd3 class ObsStatus: def __init__(self, osc, prj, pkg): self.logger = logging.getLogger() self.osc = osc self.prj = prj self.pkg = pkg self.state = STATE.unknown self.status = { 'broken': [], 'building': [], 'disabled': [], 'failed': [], 'finished': [], 'other': [], 'unresolvable': [], } self.pkg_info = { 'srcmd5': "", 'version': "", 'rev': -1, 'time': "", } self.packages_successful = {} self.packages_failed = {} #def get_repo_name(self, repository, arch): #return get_repo_name(repository, arch, jenkins=False): def __get_obs_results(self): # get results of a project: # %(repository)s|%(arch)s|%(state)s|%(dirty)s|%(code)s|%(details)s # Debian_5.0|i586|published|False|succeeded| # Debian_5.0|x86_64|published|False|succeeded| cmd = self.osc.get_command( ["results", "--csv", "--format='%(state)s|%(repository)s|%(arch)s|%(dirty)s|%(code)s|%(details)s'", "--verbose", self.prj, self.pkg] ) # "--last-build": NO, because if --last-build, disabled in 'code' is replaced by succeeded/failed results=subprocess.Popen( cmd, stdout=subprocess.PIPE) rc=results.wait() if rc != 0: logger.error( "failed to get osc results: " + str(rc) ) exit( rc ) reader = DictReader(results.stdout, delimiter='|', fieldnames=['state', 'repository', 'arch', 'dirty', 'code', 'details']) return reader def __get_obs_last_build(self, repository, arch): # {'rev': '99', 'version': '1.2.1170-1.3', 'srcmd5': '611f626d431d06dc81a32e0e021da0d7', 'time': '2014-03-12 16:43:55'} cmd = self.osc.get_command( [ "buildhist", "--csv", self.prj, self.pkg, repository, arch ] ) buildhist=subprocess.Popen( cmd, stdout=subprocess.PIPE ) rc=buildhist.wait() reader={} if rc != 0: logger.error( "failed: " + rc ) else: buildhist2 = subprocess.Popen(['tail', '-n', '1'], stdin=buildhist.stdout, stdout=subprocess.PIPE, ) reader = DictReader(buildhist2.stdout, delimiter='|', fieldnames=['time', 'srcmd5', 'rev', 'version']) try: # there should only be one entry return reader.next() except StopIteration: return def __update_packages(self): cmd = self.osc.get_command( [ "results", "--xml", self.prj ] ) # "--last-build": NO, because if --last-build, disabled in 'code' is replaced by succeeded/failed results=subprocess.Popen(cmd, stdout=subprocess.PIPE) #rc=results.wait() (xmldata, stderr) = results.communicate() rc=results.returncode if rc != 0: logger.error("failed to get osc results: " + str(rc)) return False #root = etree.parse(xmldata).getroot() root = etree.fromstring(xmldata) #print etree.dump(root) for result in root.getiterator('result'): #print result.attrib #print result.attrib['repository'], result.attrib['arch'] dist = get_repo_name(result.attrib['repository'], result.attrib['arch'], jenkins=True) self.packages_successful[dist] = [] self.packages_failed[dist] = [] for status in result.getiterator('status'): #print status.attrib if status.attrib['code'] == 'succeeded': self.packages_successful[dist].append(status.attrib['package']) elif status.attrib['code'] == 'disabled': pass else: logger.error("%s %s (%s) = %s" % (self.prj, status.attrib['package'], result.attrib['repository'], status.attrib['code'])) self.packages_failed[dist].append(status.attrib['package']) # if no failed packages are added to the dist, remove the dist if not self.packages_failed[dist]: del(self.packages_failed[dist]) return True def __update_status(self): obs_results=self.__get_obs_results() for i in obs_results: logger.debug( i ) if i['state'] == 'published' and i['dirty'] == 'False' and i['code'] == 'succeeded': self.status['finished'].append(i) elif i['state'] == 'building': self.status['building'].append(i) elif i['code'] == 'disabled': self.status['disabled'].append(i) elif i['code'] == 'failed': self.status['failed'].append(i) elif i['code'] == 'broken': self.status['broken'].append(i) elif i['code'] == 'unresolvable': self.status['unresolvable'].append(i) else: self.status['other'].append(i) return True def __update_pkg_info(self): rv=[] # get max 'rev' and 'time' from finished builds for i in self.status['finished']: last_build=self.__get_obs_last_build(i['repository'], i['arch']) self.logger.debug( str(last_build) ) #rv.append( dict(i.items() + j.items() ) ) if last_build: rev=int(last_build['rev']) time=last_build['time'].replace(" ","_") if ( rev > self.pkg_info['rev'] ) or ( rev == self.pkg_info['rev'] and time > self.pkg_info['time'] ): self.pkg_info['rev'] = rev self.pkg_info['time'] = time self.pkg_info['srcmd5'] = last_build['srcmd5'] self.pkg_info['version'] = last_build['version'] rv.append( { 'result': i, 'buildhist': last_build } ) else: self.logger.warn( "no buildhistory definied for " + i['repository'] + "-" + i['arch'] ) self.logger.debug( "result (max): " + str(self.pkg_info) ) for i in rv: if int(i['buildhist']['rev']) != self.pkg_info['rev']: self.logger.error( "UNKNOWN: " + pformat( i ) ) self.logger.debug( "finished:" ) for i in rv: if int(i['buildhist']['rev']) == self.pkg_info['rev']: self.logger.debug( pformat( i ) ) return True def __update_state(self): if self.status['building'] or self.status['other']: self.state = STATE.pending elif self.status['failed'] or self.status['broken'] or self.status['unresolvable'] or self.packages_failed: self.state = STATE.failed else: self.state = STATE.succeeded return True def update(self): self.__update_status() self.__update_packages() self.__update_pkg_info() self.__update_state() return True def get_state(self): return self.state def get_status(self, statustype=None): result = None if statustype == None: result = self.status else: result = self.status[statustype] return result def get_pkg_info(self): return self.pkg_info def get_pkg_version(self): return self.pkg_info['version'] def get_successful_packages(self): return self.packages_successful def get_failed_packages(self): return self.packages_failed class JenkinsTrigger: def __init__(self, url, jobname, obs): self.logger = logging.getLogger() self.url = url self.jobname = jobname self.obs = obs self.status = { 'status': STATE.unknown, 'jobname': jobname, } self.jenkins = Jenkins(self.url) # J.keys() # Jenkins objects appear to be dict-like, mapping keys (job-names) to #['foo', 'test_jenkinsapi'] self.job = self.jenkins.get_job(jobname) self.distreleases = get_repo_list(self.obs.get_status('finished'), jenkins=True) self.version = self.obs.get_pkg_info() self.project_packages = get_packages_string(self.obs.get_successful_packages()) def __add_jenkins_build_parameter(self, xmlConfig, build_params_add): # check, if build_params_add are already parameter of Jenkins job for child in xmlConfig.findall("./properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.StringParameterDefinition"): name=child.find('name').text if name in build_params_add.keys(): # remove item from configuration parameter self.logger.debug( "build parameter " + name + " already in jenkins build configuration" ) build_params_add.pop( name ) #etree.dump( xmlConfig ) # add remaining build parameter parameterDefinitions = xmlConfig.find("./properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions") if len(parameterDefinitions) == 0: self.logger.error( "no jenkins build paramter defined. This should not happen. Skipping adding build parameter" ) return #etree.dump( parameterDefinitions ) for item in build_params_add: self.logger.debug( "add build parameter " + str(item) ) # example: # #BUILD_VERSION #automatically set by obs-status # # new = etree.Element('hudson.model.StringParameterDefinition') # add name name = etree.Element( 'name' ) name.text = item new.append(name) # add description description = etree.Element( 'description' ) description.text = "BUILD parameter, required by and added from repo-status" new.append(description) # add defaultValue defaultValue = etree.Element( 'defaultValue' ) # no default value new.append(defaultValue) parameterDefinitions.append(new) return def __get_jenkins_build_parameter(self, parameter): try: build=self.job.get_last_build() parameters = build.get_actions()['parameters'] for i in parameters: if i['name'] == parameter: return i['value'] except jenkinsapi.custom_exceptions.NoBuildData: pass logger.warn( "jenkins build parameter " + parameter + " not defined" ) return def __set_jenkins_matrix_distrelease(self, xmlConfig): for child in xmlConfig.findall("./axes/hudson.matrix.TextAxis"): name=child.find('name').text if name == "DISTRELEASE": #etree.dump( child ) values=child.find('values') for value in child.findall('values/string'): logger.debug( "distrelease old: " + value.text ) values.remove( value ) for i in self.distreleases: new = etree.Element('string') new.text=i values.append( new ) #etree.dump( child ) #print "new:" for value in child.findall('values/string'): self.logger.debug( "distrelease new: " + str(value.text) ) return def checkAndTrigger(self, wait = False): self.logger.debug("check_jenkins_status") try: build=self.job.get_last_build() self.status['BUILD_NUMBER']=build.get_number() self.status['BUILD_URL']=build.get_result_url() except jenkinsapi.custom_exceptions.NoBuildData: pass if self.job.is_queued_or_running(): # TODO: if job is queue, but not running, BUILD_NUMBER is wrong self.logger.debug( "jenkins job " + self.jobname + " is running" ) self.status['status']=STATE.pending return self.status['status'] # jenkins job is not running self.logger.debug( "no jenkins job is running for " + self.jobname ) jenkins_job_build_version=self.__get_jenkins_build_parameter('BUILD_VERSION') self.logger.debug( "OBS version: " + str(self.version['version']) + ", " + "jenkins version: " + str(jenkins_job_build_version) ) if jenkins_job_build_version == self.obs.get_pkg_version(): # TODO: check if number DISTRELEASES have changed logger.debug( "skipped jenkins, as it has already tested the current OBS version" ) if build.is_good(): # success self.status['status']=STATE.succeeded else: self.status['status']=STATE.failed return self.status['status'] # jenkins job is not running and last_build has not been for current version # get config data in xml format configString=self.job.get_config() # https://docs.python.org/2.7/library/xml.etree.elementtree.html xmlConfig=etree.fromstring( configString ) build_params = { 'BUILD_VERSION': self.version['version'], 'BUILD_REV': self.version['rev'], 'BUILD_SRCMD5': self.version['srcmd5'], 'PACKAGES': self.project_packages, } # build paramter that must be present as Jenkins parameter configuration self.__add_jenkins_build_parameter( xmlConfig, build_params.copy() ) self.__set_jenkins_matrix_distrelease(xmlConfig) xmlString=etree.tostring( xmlConfig ) self.job.update_config( xmlString ) self.logger.info( "starting jenkins for obs build " + self.version['version'] ) invocation=self.job.invoke( build_params=build_params ) if wait: try: invocation.block() except jenkinsapi.custom_exceptions.TimeOut as e: self.logger.exception( "timeout while waiting for jenkins job" ) # recusive return self.checkAndTrigger() def get_status(self): return self.status if __name__ == '__main__': logging.basicConfig(level=logging.INFO) logger=logging.getLogger(__name__) parser = argparse.ArgumentParser(description='check overall build status of an OBS package.' ) parser.add_argument( '--debug', action='store_true', help="enable debugging output" ) parser.add_argument( '--obsapiurl', help="OBS API url" ) parser.add_argument( '--jenkinsurl', help="Jenkins base url" ) parser.add_argument( '--jenkinsjob', help="Jenkins job name" ) parser.add_argument( '--destdir', help="directory to write status info to. If not given, status will be written to stdout" ) parser.add_argument( '--wait', action='store_true', help="wait untils jobs are finished (currently only jenkins)" ) parser.add_argument( 'project', help="OBS project" ) parser.add_argument( 'package', help="OBS package" ) args = parser.parse_args() if args.debug: logger.setLevel(logging.DEBUG) wait=args.wait prj=args.project pkg=args.package if args.obsapiurl: osc_paramter="--apiurl=" + args.obsapiurl if args.destdir: destdir=args.destdir jenkins_status = { 'status': STATE.disabled } osc = Osc(osc_paramter) # check obs obs = ObsStatus(osc, prj, pkg) obs.update() if obs.get_state() == STATE.succeeded and obs.get_pkg_version(): if args.jenkinsurl and args.jenkinsjob: # run and check jenkins jenkins = JenkinsTrigger(args.jenkinsurl, args.jenkinsjob, obs) jenkins.checkAndTrigger(wait) jenkins_status = jenkins.get_status() else: logger.info( "skipped jenkins tests, because prior steps" ) logger.debug( "write status" ) write_status(obs, jenkins_status)