#!/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" obs_status={ 'status': STATE.unknown, 'broken': [], 'building': [], 'disabled': [], 'failed': [], 'finished': [], 'other': [], 'unresolvable': [], } version = { 'srcmd5': "", 'version': "", 'rev': -1, 'time': "", } 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_status, jenkins_status, version = {} ): filename = "status.succeeded" status = STATE.succeeded component="" if obs_status['status'] != STATE.succeeded and obs_status['status'] != STATE.disabled: component = "obs" status = obs_status['status'] 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" ) 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" ) for key in sorted(version): out.write( "BUILD_" + key.upper() +"="+str(version[key])+"\n") 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_obs_results( prj, pkg ): # 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", prj, 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( prj, pkg, 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", prj, 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 get_obs_prj_results(project_name): cmd = format_command( [ osc, osc_paramter, "results", "--xml", project_name ] ) # "--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 #logger.debug("rc: " + str(rc)) if rc != 0: logger.error("failed to get osc results: " + str(rc)) exit(rc) prj = {} #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) prj[dist] = [] for status in result.getiterator('status'): #print status.attrib if status.attrib['code'] == 'succeeded': prj[dist].append(status.attrib['package']) elif status.attrib['code'] == 'disabled': pass else: logger.error("%s %s (%s) = %s" % (project_name, status.attrib['package'], result.attrib['repository'], status.attrib['code'])) exit(1) return prj def check_obs_status(): obs_results=get_obs_results( prj, pkg ) for i in obs_results: logger.debug( i ) if i['state'] == 'published' and i['dirty'] == 'False' and i['code'] == 'succeeded': obs_status['finished'].append(i) elif i['state'] == 'building': obs_status['building'].append(i) elif i['code'] == 'disabled': obs_status['disabled'].append(i) elif i['code'] == 'failed': obs_status['failed'].append(i) elif i['code'] == 'broken': obs_status['broken'].append(i) elif i['code'] == 'unresolvable': obs_status['unresolvable'].append(i) else: obs_status['other'].append(i) if obs_status['building'] or obs_status['other']: obs_status['status'] = STATE.pending #return obs_status['status'] elif obs_status['failed'] or obs_status['broken'] or obs_status['unresolvable']: obs_status['status'] = STATE.failed #return obs_status['status'] else: obs_status['status'] = STATE.succeeded # else: all builds should be finished rv=[] for i in obs_status['finished']: last_build=get_obs_last_build( prj, pkg, i['repository'], i['arch'] ) 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 > version['rev'] ) or ( rev == version['rev'] and time > version['time'] ): version['rev'] = rev version['time'] = time version['srcmd5'] = last_build['srcmd5'] version['version'] = last_build['version'] rv.append( { 'result': i, 'buildhist': last_build } ) else: logger.warn( "no buildhistory definied for " + i['repository'] + "-" + i['arch'] ) logger.debug( "result (max): " + str(version) ) for i in rv: if int(i['buildhist']['rev']) != version['rev']: logger.error( "UNKNOWN: " + pformat( i ) ) logger.debug( "finished:" ) for i in rv: if int(i['buildhist']['rev']) == version['rev']: logger.debug( pformat( i ) ) return obs_status['status'] 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 = '' for dist in project_packages: pkg += '%s:%s\n' % (dist, ','.join(project_packages[dist])) #build_params['PACKAGES_' + dist] = ','.join(project_packages[dist]) 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 if check_obs_status() == STATE.succeeded and version['version']: if args.jenkinsurl and args.jenkinsjob: project_packages = get_obs_prj_results(prj) # run and check jenkins check_jenkins_status(args.jenkinsurl, args.jenkinsjob, jenkins_status, get_repo_list( obs_status['finished'], jenkins=True ), version, project_packages) else: logger.info( "skipped jenkins tests, because prior steps" ) logger.debug( "write status" ) write_status( obs_status, jenkins_status, version )