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

Last change on this file since 1203 was 1203, checked in by joergs, on Nov 29, 2015 at 7:02:21 PM

current version, working

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