source: obs/repo-status/repo-status.py @ 1205

Last change on this file since 1205 was 1205, checked in by joergs, 8 years ago

refactoring Jenkins functions to JenkinsTrigger class

  • Property svn:executable set to *
File size: 19.8 KB
Line 
1#!/usr/bin/env python
2
3import  argparse
4from    csv import DictReader
5from    glob import glob
6import  logging
7import  os
8from    pprint import pprint,pformat
9import  jenkinsapi.custom_exceptions
10from    jenkinsapi.jenkins import Jenkins
11import  shlex
12import  subprocess
13import  sys
14import  xml.etree.ElementTree as etree
15
16
17destdir=""
18
19class STATE:
20    disabled="DISABLED"
21    unknown="UNKNOWN"
22    pending="PENDING"
23    failed="FAILED"
24    succeeded="SUCCEEDED"
25
26def write_status(obs, jenkins_status):
27    filename = "status.succeeded"
28    status = STATE.succeeded
29    component=""
30    if obs.get_state() != STATE.succeeded and obs.get_state() != STATE.disabled:
31        component = "obs"
32        status = obs.get_state()
33    elif jenkins_status['status'] != STATE.succeeded and jenkins_status['status'] != STATE.disabled:
34        component = "jenkins"
35        status = jenkins_status['status']
36
37    if component:
38        filename="status." + status.lower() + "." + component
39
40    if destdir:
41        filepath=destdir + "/" + filename
42        out=open(filepath, 'w')
43        logger.info("status will be written to " + filepath)
44        # remove outdated files
45        for i in glob(destdir + "/status*"):
46            if not os.path.samefile(i, filepath):
47                logger.debug("remove outdated status file " + i)
48                os.remove(i)
49    else:
50        out=sys.stdout
51
52    out.write( "#\n" )
53    out.write( "STATUS="+status+"\n" )
54    out.write( "#\n" )
55    out.write( "NAME="+filename+"\n" )
56    obs_status = obs.get_status()
57    for key in sorted(obs_status):
58        out.write( "#\n" )
59        if key == "status":
60            out.write( "OBS_STATUS" +"="+ obs_status[key] +"\n" )
61        else:
62            out.write( "OBS_STATUS_" + key.upper() +"="+",".join(get_repo_list(obs_status[key]))+"\n" )
63
64    out.write( "#\n" )
65    version = obs.get_pkg_info()
66    for key in sorted(version):
67        out.write( "BUILD_" + key.upper() +"="+str(version[key])+"\n")
68
69    out.write( "#\n" )
70    out.write("PACKAGES_SUCCESSFUL='%s'\n" % (get_packages_string(obs.get_successful_packages())))
71
72    out.write( "#\n" )
73    out.write("PACKAGES_FAILED='%s'\n" % (get_packages_string(obs.get_failed_packages())))
74
75    out.write( "#\n" )
76    for key in sorted(jenkins_status):
77        out.write( "JENKINS_" + key.upper() +"="+str(jenkins_status[key])+"\n")
78
79
80
81def get_repo_name(repository, arch, jenkins=False):
82        # obs:     DISTRIBUTION_VERSION
83        # jenkins: DISTRIBUTION-VERSION-ARCH
84        #
85        # use "-" as separator between repository and arch,
86        # because repository and arch can contain:
87        # repository: "." and "_" (openSUSE_13.1)
88        # arch: "_" (x86_64)
89        if jenkins:
90            repo = str(repository).replace( "_", "-", 1 ) + "-" + str(arch)
91        else:
92            repo = str(repository) + "-" + str(arch)
93        return repo
94
95def get_repo_list(array, jenkins=False):
96    result=[]
97    for i in sorted( array, key=lambda k: ( k['repository'], k['arch'] ) ):
98        repo = get_repo_name(i['repository'], i['arch'], jenkins)
99        result.append(repo)
100    return result
101
102def get_packages_string(packages):
103    pkg = ''
104    for dist in packages:
105        pkg += '%s:%s\n' % (dist, ','.join(packages[dist]))
106    return pkg
107
108class Osc:
109    def __init__(self, osc_paramter):
110        self.osc="osc"
111        self.paramter=osc_paramter
112
113    def get_command(self, args):
114        return self.__format_command([ self.osc, self.paramter ] + args)
115
116    def __format_command(self, cmd):
117        logger=logging.getLogger(__name__)
118        #logger.debug( "cmd1:" + str(cmd) )
119        cmd2=" ".join( cmd )
120        logger.debug( "cmd2:" + str(cmd2) )
121        cmd3=shlex.split( cmd2 )
122        #logger.debug( "cmd3:" + str(cmd3) )
123        return cmd3
124
125
126class ObsStatus:
127    def __init__(self, osc, prj, pkg):
128        self.logger = logging.getLogger()
129        self.osc = osc
130        self.prj = prj
131        self.pkg = pkg
132        self.state = STATE.unknown
133        self.status = {
134            'broken': [],
135            'building': [],
136            'disabled': [],
137            'failed': [],
138            'finished': [],
139            'other': [],
140            'unresolvable': [],
141        }
142        self.pkg_info = {
143            'srcmd5': "",
144            'version': "",
145            'rev':     -1,
146            'time':   "",
147        }
148        self.packages_successful = {}
149        self.packages_failed = {}
150
151    #def get_repo_name(self, repository, arch):
152        #return get_repo_name(repository, arch, jenkins=False):
153
154    def __get_obs_results(self):
155        # get results of a project:
156        # %(repository)s|%(arch)s|%(state)s|%(dirty)s|%(code)s|%(details)s
157        # Debian_5.0|i586|published|False|succeeded|
158        # Debian_5.0|x86_64|published|False|succeeded|
159        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] )
160        # "--last-build": NO, because if --last-build, disabled in 'code' is replaced by succeeded/failed
161        results=subprocess.Popen( cmd, stdout=subprocess.PIPE)
162        rc=results.wait()
163        if rc != 0:
164            logger.error( "failed to get osc results: " + str(rc) )
165            exit( rc )
166
167        reader = DictReader(results.stdout,
168                            delimiter='|',
169                            fieldnames=['state', 'repository',
170                                        'arch', 'dirty',
171                                        'code', 'details'])
172        return reader
173
174
175    def __get_obs_last_build(self, repository, arch):
176        # {'rev': '99', 'version': '1.2.1170-1.3', 'srcmd5': '611f626d431d06dc81a32e0e021da0d7', 'time': '2014-03-12 16:43:55'}
177        cmd = self.osc.get_command( [ "buildhist", "--csv", self.prj, self.pkg, repository, arch ] )
178        buildhist=subprocess.Popen( cmd, stdout=subprocess.PIPE )
179        rc=buildhist.wait()
180        reader={}
181        if rc != 0:
182            logger.error( "failed: " + rc )
183        else:
184            buildhist2 = subprocess.Popen(['tail', '-n', '1'],
185                            stdin=buildhist.stdout,
186                            stdout=subprocess.PIPE,
187                            )
188            reader = DictReader(buildhist2.stdout,
189                            delimiter='|',
190                            fieldnames=['time', 'srcmd5', 'rev', 'version'])
191        try:
192            # there should only be one entry
193            return reader.next()
194        except StopIteration:
195            return
196
197
198    def __update_packages(self):
199        cmd = self.osc.get_command( [ "results", "--xml", self.prj ] )
200        # "--last-build": NO, because if --last-build, disabled in 'code' is replaced by succeeded/failed
201        results=subprocess.Popen(cmd, stdout=subprocess.PIPE)
202        #rc=results.wait()
203        (xmldata, stderr) = results.communicate()
204        rc=results.returncode
205        if rc != 0:
206            logger.error("failed to get osc results: " + str(rc))
207            return False
208        #root = etree.parse(xmldata).getroot()
209        root = etree.fromstring(xmldata)
210        #print etree.dump(root)
211        for result in root.getiterator('result'):
212            #print result.attrib
213            #print result.attrib['repository'], result.attrib['arch']
214            dist = get_repo_name(result.attrib['repository'], result.attrib['arch'], jenkins=True)
215            self.packages_successful[dist] = []
216            self.packages_failed[dist] = []
217            for status in result.getiterator('status'):
218                #print status.attrib
219                if status.attrib['code'] == 'succeeded':
220                    self.packages_successful[dist].append(status.attrib['package'])
221                elif status.attrib['code'] == 'disabled':
222                    pass
223                else:
224                    logger.error("%s %s (%s) = %s" % (self.prj, status.attrib['package'], result.attrib['repository'], status.attrib['code']))
225                    self.packages_failed[dist].append(status.attrib['package'])
226            # if no failed packages are added to the dist, remove the dist
227            if not self.packages_failed[dist]:
228                del(self.packages_failed[dist])
229        return True
230
231
232    def __update_status(self):
233        obs_results=self.__get_obs_results()
234
235        for i in obs_results:
236            logger.debug( i )
237            if i['state'] == 'published' and i['dirty'] == 'False' and i['code'] == 'succeeded':
238                self.status['finished'].append(i)
239            elif i['state'] == 'building':
240                self.status['building'].append(i)
241            elif i['code'] == 'disabled':
242                self.status['disabled'].append(i)
243            elif i['code'] == 'failed':
244                self.status['failed'].append(i)
245            elif i['code'] == 'broken':
246                self.status['broken'].append(i)
247            elif i['code'] == 'unresolvable':
248                self.status['unresolvable'].append(i)
249            else:
250                self.status['other'].append(i)
251        return True
252
253
254    def __update_pkg_info(self):
255        rv=[]
256        # get max 'rev' and 'time' from finished builds
257        for i in self.status['finished']:
258            last_build=self.__get_obs_last_build(i['repository'], i['arch'])
259            self.logger.debug( str(last_build) )
260            #rv.append( dict(i.items() + j.items() ) )
261            if last_build:
262                rev=int(last_build['rev'])
263                time=last_build['time'].replace(" ","_")
264                if ( rev > self.pkg_info['rev'] ) or ( rev == self.pkg_info['rev'] and time > self.pkg_info['time'] ):
265                        self.pkg_info['rev'] = rev
266                        self.pkg_info['time'] = time
267                        self.pkg_info['srcmd5'] = last_build['srcmd5']
268                        self.pkg_info['version'] = last_build['version']
269                rv.append( { 'result': i, 'buildhist': last_build  } )
270            else:
271                self.logger.warn( "no buildhistory definied for " + i['repository'] + "-" + i['arch'] )
272        self.logger.debug( "result (max): " + str(self.pkg_info) )
273
274        for i in rv:
275            if int(i['buildhist']['rev']) != self.pkg_info['rev']:
276                self.logger.error( "UNKNOWN: " + pformat( i ) )
277        self.logger.debug( "finished:" )
278        for i in rv:
279            if int(i['buildhist']['rev']) == self.pkg_info['rev']:
280                self.logger.debug( pformat( i ) )
281        return True
282
283
284    def __update_state(self):
285        if self.status['building'] or self.status['other']:
286            self.state = STATE.pending
287        elif self.status['failed'] or self.status['broken'] or self.status['unresolvable'] or self.packages_failed:
288            self.state = STATE.failed
289        else:
290            self.state = STATE.succeeded
291        return True
292
293
294    def update(self):
295        self.__update_status()
296        self.__update_packages()
297        self.__update_pkg_info()
298        self.__update_state()
299        return True
300
301    def get_state(self):
302        return self.state
303   
304    def get_status(self, statustype=None):
305        result = None
306        if statustype == None:
307            result = self.status
308        else:
309            result = self.status[statustype]
310        return result
311   
312    def get_pkg_info(self):
313        return self.pkg_info
314   
315    def get_pkg_version(self):
316        return self.pkg_info['version']
317   
318    def get_successful_packages(self):
319        return self.packages_successful
320
321    def get_failed_packages(self):
322        return self.packages_failed
323
324
325class JenkinsTrigger:
326    def __init__(self, url, jobname, obs):
327        self.logger = logging.getLogger()
328        self.url = url
329        self.jobname = jobname
330        self.obs = obs
331        self.status = {
332            'status': STATE.unknown,
333            'jobname': jobname,
334        }
335        self.jenkins = Jenkins(self.url)
336        # J.keys() # Jenkins objects appear to be dict-like, mapping keys (job-names) to
337        #['foo', 'test_jenkinsapi']
338        self.job = self.jenkins.get_job(jobname)
339       
340        self.distreleases = get_repo_list(self.obs.get_status('finished'), jenkins=True)
341        self.version = self.obs.get_pkg_info()
342       
343        self.project_packages = get_packages_string(self.obs.get_successful_packages())
344
345
346    def __add_jenkins_build_parameter(self, xmlConfig, build_params_add):
347        # check, if build_params_add are already parameter of Jenkins job
348        for child in xmlConfig.findall("./properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.StringParameterDefinition"):
349            name=child.find('name').text
350            if name in build_params_add.keys():
351                # remove item from configuration parameter
352                self.logger.debug( "build parameter " + name + " already in jenkins build configuration" )
353                build_params_add.pop( name )
354
355        #etree.dump( xmlConfig )
356
357        # add remaining build parameter
358        parameterDefinitions = xmlConfig.find("./properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions")
359
360        if len(parameterDefinitions) == 0:
361            self.logger.error( "no jenkins build paramter defined. This should not happen. Skipping adding build parameter" )
362            return
363
364        #etree.dump( parameterDefinitions )
365
366        for item in build_params_add:
367            self.logger.debug( "add build parameter " + str(item) )
368            # example:
369            #<hudson.model.StringParameterDefinition>
370                #<name>BUILD_VERSION</name>
371                #<description>automatically set by obs-status</description>
372                #<defaultValue />
373            #</hudson.model.StringParameterDefinition>
374            new = etree.Element('hudson.model.StringParameterDefinition')
375
376            # add name
377            name = etree.Element( 'name' )
378            name.text = item
379            new.append(name)
380
381            # add description
382            description = etree.Element( 'description' )
383            description.text = "BUILD parameter, required by and added from repo-status"
384            new.append(description)
385
386            # add defaultValue
387            defaultValue = etree.Element( 'defaultValue' )
388            # no default value
389            new.append(defaultValue)
390
391            parameterDefinitions.append(new)
392        return
393
394
395    def __get_jenkins_build_parameter(self, parameter):
396        try:
397            build=self.job.get_last_build()
398            parameters = build.get_actions()['parameters']
399            for i in parameters:
400                if i['name'] == parameter:
401                    return i['value']
402        except jenkinsapi.custom_exceptions.NoBuildData:
403            pass
404        logger.warn( "jenkins build parameter " + parameter + " not defined" )
405        return
406
407
408    def __set_jenkins_matrix_distrelease(self, xmlConfig):
409        for child in xmlConfig.findall("./axes/hudson.matrix.TextAxis"):
410            name=child.find('name').text
411            if name == "DISTRELEASE":
412                #etree.dump( child )
413                values=child.find('values')
414                for value in child.findall('values/string'):
415                    logger.debug( "distrelease old: " + value.text )
416                    values.remove( value )
417                for i in self.distreleases:
418                    new = etree.Element('string')
419                    new.text=i
420                    values.append( new )
421                #etree.dump( child )
422                #print "new:"
423                for value in child.findall('values/string'):
424                    self.logger.debug( "distrelease new: " + str(value.text) )
425        return
426
427
428    def checkAndTrigger(self, wait = False):
429        self.logger.debug("check_jenkins_status")
430
431        try:
432            build=self.job.get_last_build()
433            self.status['BUILD_NUMBER']=build.get_number()
434            self.status['BUILD_URL']=build.get_result_url()
435        except jenkinsapi.custom_exceptions.NoBuildData:
436            pass
437
438        if self.job.is_queued_or_running():
439            # TODO: if job is queue, but not running, BUILD_NUMBER is wrong
440            self.logger.debug( "jenkins job " + self.jobname + " is running" )
441            self.status['status']=STATE.pending
442            return self.status['status']
443
444        # jenkins job is not running
445        self.logger.debug( "no jenkins job is running for " + self.jobname )
446        jenkins_job_build_version=self.__get_jenkins_build_parameter('BUILD_VERSION')
447
448        self.logger.debug( "OBS version: " + str(self.version['version']) + ", " + "jenkins version: " + str(jenkins_job_build_version) )
449
450        if jenkins_job_build_version == self.obs.get_pkg_version():
451            # TODO: check if number DISTRELEASES have changed
452            logger.debug( "skipped jenkins, as it has already tested the current OBS version" )
453            if build.is_good():
454                # success
455                self.status['status']=STATE.succeeded
456            else:
457                self.status['status']=STATE.failed
458            return self.status['status']
459
460        # jenkins job is not running and last_build has not been for current version
461
462        # get config data in xml format
463        configString=self.job.get_config()
464
465        # https://docs.python.org/2.7/library/xml.etree.elementtree.html
466        xmlConfig=etree.fromstring( configString )
467
468        build_params = {
469            'BUILD_VERSION': self.version['version'],
470            'BUILD_REV': self.version['rev'],
471            'BUILD_SRCMD5': self.version['srcmd5'],
472            'PACKAGES': self.project_packages,
473        }
474        # build paramter that must be present as Jenkins parameter configuration
475        self.__add_jenkins_build_parameter( xmlConfig, build_params.copy() )
476        self.__set_jenkins_matrix_distrelease(xmlConfig)
477
478        xmlString=etree.tostring( xmlConfig )
479        self.job.update_config( xmlString )
480
481        self.logger.info( "starting jenkins for obs build " + self.version['version'] )
482        invocation=self.job.invoke( build_params=build_params )
483
484        if wait:
485            try:
486                invocation.block()
487            except jenkinsapi.custom_exceptions.TimeOut as e:
488                self.logger.exception( "timeout while waiting for jenkins job" )
489
490        # recusive
491        return self.checkAndTrigger()
492
493
494    def get_status(self):
495        return self.status
496
497
498if __name__ == '__main__':
499    logging.basicConfig(level=logging.INFO)
500    logger=logging.getLogger(__name__)
501
502    parser = argparse.ArgumentParser(description='check overall build status of an OBS package.' )
503
504    parser.add_argument( '--debug', action='store_true', help="enable debugging output" )
505    parser.add_argument( '--obsapiurl', help="OBS API url" )
506    parser.add_argument( '--jenkinsurl', help="Jenkins base url" )
507    parser.add_argument( '--jenkinsjob', help="Jenkins job name" )
508    parser.add_argument( '--destdir', help="directory to write status info to. If not given, status will be written to stdout" )
509    parser.add_argument( '--wait', action='store_true', help="wait untils jobs are finished (currently only jenkins)" )
510    parser.add_argument( 'project', help="OBS project" )
511    parser.add_argument( 'package', help="OBS package" )
512
513    args = parser.parse_args()
514
515    if args.debug:
516        logger.setLevel(logging.DEBUG)
517
518    wait=args.wait
519
520    prj=args.project
521    pkg=args.package
522
523    if args.obsapiurl:
524        osc_paramter="--apiurl=" + args.obsapiurl
525
526    if args.destdir:
527        destdir=args.destdir
528
529    jenkins_status = {
530        'status': STATE.disabled
531    }
532
533    osc = Osc(osc_paramter)
534    # check obs
535    obs = ObsStatus(osc, prj, pkg)
536    obs.update()
537    if obs.get_state() == STATE.succeeded and obs.get_pkg_version():
538        if args.jenkinsurl and args.jenkinsjob:
539            # run and check jenkins
540            jenkins = JenkinsTrigger(args.jenkinsurl, args.jenkinsjob, obs)
541            jenkins.checkAndTrigger(wait)
542            jenkins_status = jenkins.get_status()
543    else:
544        logger.info( "skipped jenkins tests, because prior steps" )
545
546    logger.debug( "write status" )
547    write_status(obs, jenkins_status)
Note: See TracBrowser for help on using the repository browser.