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

Last change on this file since 1204 was 1204, checked in by joergs, 6 years ago

refactoring: move obs functionality to ObsStatus class

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