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

Last change on this file since 1204 was 1204, checked in by joergs, on Nov 29, 2015 at 9:17:37 PM

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.