#!/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 osc="osc" #osc_paramter="--apiurl=https://obs.dass-it" #prj="bareos:playground" #pkg="bareos" osc_paramter="" prj="" pkg="" destdir="" wait=False class STATE: disabled="DISABLED" unknown="UNKNOWN" pending="PENDING" failed="FAILED" succeeded="SUCCEEDED" jenkins_status = { 'status': STATE.disabled, 'jobname': "", } def format_command( 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 def show( string, array ): if array: print string + ":" for i in array: print " ", i 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 ObsStatus: def __init__(self, prj, pkg): self.logger = logging.getLogger() 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 = format_command( [ osc, osc_paramter, "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 = format_command( [ osc, osc_paramter, "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 = format_command( [ osc, osc_paramter, "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 def add_jenkins_build_parameter( xmlConfig, build_params_add ): logger=logging.getLogger(__name__) # 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 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: 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: 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( job, parameter ): logger=logging.getLogger(__name__) try: build=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( xmlConfig, distreleases ): logger=logging.getLogger(__name__) 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 distreleases: new = etree.Element('string') new.text=i values.append( new ) #etree.dump( child ) #print "new:" for value in child.findall('values/string'): logger.debug( "distrelease new: " + str(value.text) ) return def check_jenkins_status(url, jobname, status, distreleases, version, project_packages): logger=logging.getLogger(__name__) logger.debug( "check_jenkins_status" ) logger.debug( str(distreleases) ) jenkins = Jenkins( url ) # J.keys() # Jenkins objects appear to be dict-like, mapping keys (job-names) to #['foo', 'test_jenkinsapi'] job=jenkins.get_job( jobname ) try: build=job.get_last_build() status['BUILD_NUMBER']=build.get_number() status['BUILD_URL']=build.get_result_url() except jenkinsapi.custom_exceptions.NoBuildData: #except NoBuildData: pass if job.is_queued_or_running(): # TODO: if job is queue, but not running, BUILD_NUMBER is wrong logger.debug( "jenkins job " + jobname + " is running" ) status['status']=STATE.pending return status['status'] # jenkins job is not running logger.debug( "no jenkins job is running for " + jobname ) jenkins_job_build_version=get_jenkins_build_parameter( job, 'BUILD_VERSION' ) logger.debug( "OBS version: " + str(version['version']) + ", " + "jenkins version: " + str(jenkins_job_build_version) ) if jenkins_job_build_version == version['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 status['status']=STATE.succeeded else: status['status']=STATE.failed return status['status'] # jenkins job is not running and last_build has not been for current version # get config data in xml format configString=job.get_config() # https://docs.python.org/2.7/library/xml.etree.elementtree.html xmlConfig=etree.fromstring( configString ) #etree.dump( xmlConfig ) pkg = get_packages_string(project_packages) build_params={ 'BUILD_VERSION': version['version'], 'BUILD_REV': version['rev'], 'BUILD_SRCMD5': version['srcmd5'], 'PACKAGES': pkg } # build paramter that must be present as Jenkins parameter configuration add_jenkins_build_parameter( xmlConfig, build_params.copy() ) set_jenkins_matrix_distrelease( xmlConfig, distreleases ) #etree.dump(xmlConfig) xmlString=etree.tostring( xmlConfig ) job.update_config( xmlString ) #for i in b.get_matrix_runs(): logger.info( "starting jenkins for obs build " + version['version'] ) invocation=job.invoke( build_params=build_params ) if wait: try: invocation.block() except jenkinsapi.custom_exceptions.TimeOut as e: logger.exception( "timeout while waiting for jenkins job" ) # recusive return check_jenkins_status( url, jobname, status, distreleases, version, project_packages ) #b.is_good() #pprint( dir(invocation) ) #pprint( dir(invocation.job) ) #build=invocation.get_build() #if invocation.is_queued_or_running(): #status['status']=STATE.pending #else: #logger.debug( "duration: " + build.get_duration() ) ##print build.get_actions() ## recusive #return status['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 if args.jenkinsjob: jenkins_status['jobname'] = args.jenkinsjob if args.jenkinsurl and args.jenkinsjob: # enable jenkins jenkins_status['status']=STATE.unknown # check obs obs = ObsStatus(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 check_jenkins_status(args.jenkinsurl, args.jenkinsjob, jenkins_status, get_repo_list( obs.get_status('finished'), jenkins=True ), obs.get_pkg_info(), obs.get_successful_packages()) else: logger.info( "skipped jenkins tests, because prior steps" ) logger.debug( "write status" ) write_status(obs, jenkins_status)