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

Last change on this file since 1235 was 1205, checked in by joergs, on Nov 29, 2015 at 11:21:15 PM

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.