#!/usr/bin/python # -*- coding: utf-8 -*- # vim: ts=4 et sw=4 sts=4 ai sta: # # Copyright (c) 2013, 2014, 2015 Intel, Inc. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. """ This script creates linked project in OBS and waits until all builds are finished. Author: Ed Bartosh """ import sys import os import tempfile import argparse import hashlib import time import urllib2 from collections import defaultdict from datetime import date from functools import wraps from urllib import quote_plus, pathname2url import M2Crypto from M2Crypto.SSL.Checker import SSLVerificationError from ssl import SSLError from xml.dom import minidom import osc.conf import osc.core PRJ_TEMPLATE = """ <description/> <link project="%(src)s"/> <person role="maintainer" userid="%(user)s"/> """ REPO_TEMPLATE = """ <repository name="%(name)s" linkedbuild="localdep"> <path repository="%(name)s" project="%(src)s"/> """ FILE_TEMPLATE = """ <entry md5="%(md5)s" name="%(fname)s"/> """ class BuildError(Exception): """Custom exception for this script.""" pass class CancelRetryError(Exception): """Exception for handling cancelling of the re_try loop. Needed for transparently re-raising the previous exception.""" def __init__(self): self.typ, self.val, self.backtrace = sys.exc_info() def re_try(exceptions, tries=3, sleep=0): """Decorator for re-trying function calls""" def decorator(func): """The "real" decorator function""" @wraps(func) def wrap(*args, **kwargs): """Wrapper for re-trying func""" for attempt in range(1, tries + 1): try: return func(*args, **kwargs) except CancelRetryError as err: raise err.typ, err.val, err.backtrace except exceptions as err: if attempt >= tries: raise elif sleep: time.sleep(sleep) return wrap return decorator def parse_cmdline(argv): """Parse commandline with argparse.""" parser = argparse.ArgumentParser(description="Submit package to OBS") parser.add_argument("--sproject") parser.add_argument("--tproject", required=True) parser.add_argument("--package") parser.add_argument("paths", nargs='*') parser.add_argument("--timeout", type=int, default=15) parser.add_argument("--wait", action='store_true') options = parser.parse_args(argv) if not options.wait and (not options.paths or not options.package): parser.error("either --wait or --package and paths " "have to be specified") return options def link_project(apiurl, src, target): """Creates linked(target) project based on existing original(src) project. """ # generate configuration for target project based on # configuration of source project targetmeta = PRJ_TEMPLATE % {'target': target, 'src': src, 'user': osc.conf.config['user']} repos = defaultdict(list) for repo in osc.core.get_repos_of_project(apiurl, src): repos[repo.name].append(repo.arch) for name in repos: targetmeta += REPO_TEMPLATE % {'name': name, 'src': src} for arch in repos[name]: targetmeta += "<arch>%s</arch>\n" % arch targetmeta += "</repository>\n" targetmeta += "</project>\n" put_meta(apiurl, "prj", quote_plus(target), targetmeta) # Set release tag in prjconf so that the rpm versions in target project # will be greater than those in the base project prjconf = "Release: %s.<CI_CNT>\n" % date.today().strftime('%Y%m%d') put_meta(apiurl, "prjconf", quote_plus(target), prjconf) def branch_package(apiurl, src_project, src_package, target_project, target_package=None): """Branch package from source to target project.""" return osc.core.branch_pkg(apiurl, src_project, src_package, target_project=target_project, target_package=target_package) def create_package(apiurl, prj, pkg): """Create package in the project.""" meta = '<package project="%s" name="%s">'\ '<title/><description/></package>' % (prj, pkg) put_meta(apiurl, "pkg", (quote_plus(prj), quote_plus(pkg)), meta) def copy_package_meta(apiurl, src_prj, src_pkg, tgt_prj, tgt_pkg, sections=None): """Copy package meta""" src_path = (quote_plus(src_prj), quote_plus(src_pkg)) tgt_path = (quote_plus(tgt_prj), quote_plus(tgt_pkg)) copy_meta(apiurl, 'pkg', src_path, tgt_path, sections) @re_try(urllib2.HTTPError) def get_meta(apiurl, metatype, path_args): """Get meta as a Document object""" url = osc.core.make_meta_url(metatype, path_args, apiurl) fobj = osc.core.http_GET(url) return minidom.parse(fobj) def put_meta(apiurl, metatype, path_args, data): """Wrapper to put metadata to OBS.""" url = osc.core.make_meta_url(metatype, path_args, apiurl, False) fileh, filename = tempfile.mkstemp(prefix="osc_metafile.", suffix=".xml", text=True) os.write(fileh, data) os.close(fileh) # Let's make 3 attempts to send the query, because sometimes # it failes with urllib2.HTTPError: HTTP Error 500: Internal Server Error @re_try(urllib2.HTTPError) def _call_put(*args, **kwargs): try: osc.core.http_PUT(*args, **kwargs) except urllib2.HTTPError as err: if err.code != 500: raise CancelRetryError else: raise _call_put(url, file=filename) os.unlink(filename) def copy_meta(apiurl, metatype, src_path_args, tgt_path_args, sections=None): """Copy selected meta sections from source to target on the remote""" src_meta = get_meta(apiurl, metatype, src_path_args) tgt_meta = get_meta(apiurl, metatype, tgt_path_args) # Remove selected "sections" from the tgt meta and replace with one # from the src meta. We only consider "top-level" elements here (i.e. no # recursion in the xml tree). Copy all sections if none are specified. for child in tgt_meta.firstChild.childNodes[:]: if not sections or child.localName in sections: tgt_meta.firstChild.removeChild(child) for child in src_meta.firstChild.childNodes: if not sections or child.localName in sections: tgt_meta.firstChild.appendChild(child.cloneNode(True)) # Write modified tgt meta back to server put_meta(apiurl, metatype, tgt_path_args, tgt_meta.toxml()) def hexdigest(fhandle, block_size=4096): """Calculates hexdigest of file content.""" md5obj = hashlib.new('md5') while True: data = fhandle.read(block_size) if not data: break md5obj.update(data) return md5obj.hexdigest() def add_files(apiurl, project, package, files): """Commits files to OBS.""" query = {'cmd' : 'commitfilelist', 'user' : osc.conf.get_apiurl_usr(apiurl), 'keeplink': 1} url = osc.core.makeurl(apiurl, ['source', project, package], query=query) xml = "<directory>" for fpath in files: with open(fpath) as fhandle: xml += FILE_TEMPLATE % {"fname": os.path.basename(fpath), "md5": hexdigest(fhandle)} xml += "</directory>" osc.core.http_POST(url, data=xml) for fpath in files: put_url = osc.core.makeurl( apiurl, ['source', project, package, pathname2url(os.path.basename(fpath))], query="rev=repository") osc.core.http_PUT(put_url, file=fpath) osc.core.http_POST(url, data=xml) def exists(apiurl, prj, pkg=''): """Check if project or/and package exists.""" if not prj: return False path_args = [quote_plus(prj)] exceptions = (urllib2.URLError, M2Crypto.m2urllib2.URLError, M2Crypto.SSL.SSLError, urllib2.HTTPError) @re_try(exceptions, sleep=3) def _call_meta_exists(*args, **kwargs): try: osc.core.meta_exists(*args, **kwargs) if pkg: return pkg in osc.core.meta_get_packagelist(apiurl, prj) except urllib2.HTTPError, err: if err.code == 404: return False raise except SSLVerificationError: raise BuildError("SSL verification error.") return True try: return _call_meta_exists(metatype='prj', path_args=tuple(path_args), create_new=False, apiurl=apiurl) except exceptions as err: raise BuildError("can't check if %s/%s exists: %s" % (prj, pkg, err)) def delete_project(apiurl, prj, force=False, msg=None): """Delete OBS project.""" query = {} if force: query['force'] = "1" if msg: query['comment'] = msg url = osc.core.makeurl(apiurl, ['source', prj], query) @re_try(urllib2.HTTPError) def _call_delete(*args, **kwargs): osc.core.http_DELETE(*args, **kwargs) try: _call_delete(url) except urllib2.HTTPError as err: raise BuildError("can't delete project %s: %s" % (prj, err)) def build_published(status_line): """Parse status line and check if project buid is published.""" for item in status_line.split(';'): if '/' in item and item.split('/')[2] != 'published': return False return True def build(apiurl, project, package, timeout): """Wait until build is successfully finished or failed.""" # waiting for project and package to appear in OBS while not exists(apiurl, project, package): time.sleep(timeout) while True: # waiting to build results to appear while True: # Let's make 3 attempts to get results, because sometimes it failes # with cElementTree.ParseError: no element found: line 1, column 0 exceptions = (osc.core.ET.ParseError, SSLError) @re_try(exceptions) def _call_get_prj_results(*args, **kwargs): return osc.core.get_prj_results(*args, **kwargs) try: results = _call_get_prj_results(apiurl, project, hide_legend=True, csv=True) except exceptions: raise BuildError("error getting build results for %s" % project) if results and len(results) > 1 and build_published(results[0]): break print 'waiting for published build' time.sleep(timeout) statuses = [] for pkginfo in results[1:]: splitted = pkginfo.split(';') pkg_status = [status for status in splitted[1:] if status not in ('excluded', 'succeeded', 'disabled', 'failed', 'unresolvable')] statuses.extend(pkg_status) package = splitted[0] if [st for st in pkg_status if st not in ('building', 'scheduled', 'dispatching', 'finished', 'blocked', 'broken')]: raise BuildError('package %s is in unknown state: %s' % \ (package, splitted[1:])) url = osc.core.makeurl(apiurl, ('source', project, package)) if 'broken' in pkg_status: @re_try(urllib2.HTTPError) def _call_http_get(*args, **kwargs): return osc.core.http_GET(*args, **kwargs) try: state = minidom.parse(_call_http_get(url)) except urllib2.HTTPError: raise BuildError('unable to get source listing for %s' % package) infos = state.firstChild.getElementsByTagName('serviceinfo') if infos: info = infos[0] if info.getAttribute('code') == 'failed': error = info.getElementsByTagName('error')[0] print "OBS source service failed:" print error.childNodes[0].nodeValue return 1 print 'waiting for %s: %s' % (package, splitted[1:]) if 'failed' in splitted[1:]: print "OBS build failed in project=%s package=%s" %(project, package) return 1 if not statuses: return 0 time.sleep(timeout) def main(argv): """Script entry point.""" # get parameters from command line and environment params = parse_cmdline(argv[1:]) sproject, tproject, package, timeout, paths = params.sproject, \ params.tproject, params.package, params.timeout, params.paths # parse osc config osc.conf.get_config() apiurl = osc.conf.config['apiurl'] if params.wait: # for restarted build project and package already exist # wait for sources reupload time.sleep(2*timeout) else: print "package %s : source project %s, target project %s" % \ (package, sproject, tproject) if sproject and not exists(apiurl, sproject): raise BuildError("Source project %s doesn't exist" % sproject) if sproject: # !!! this is dangerous when used incorrectly as it can # remove target repo if exists(apiurl, tproject): print "linked project %s already exists, deleting" % tproject delete_project(apiurl, tproject) print "linking project %s to %s" % (sproject, tproject) link_project(apiurl, sproject, tproject) if not exists(apiurl, tproject, package): print "create package %s/%s" % (tproject, package) create_package(apiurl, tproject, package) if sproject and exists(apiurl, sproject, package): print "copy pkg meta from %s/%s to %s/%s" % (sproject, package, tproject, package) copy_package_meta(apiurl, sproject, package, tproject, package) print "uploading files to %s/%s" % (tproject, package) add_files(apiurl, tproject, package, paths) print "project %s: waiting for the build results" % tproject return build(apiurl, tproject, package, timeout) if __name__ == "__main__": try: sys.exit(main(sys.argv)) except BuildError, error: print "build failed: %s" % str(error) except Exception: import traceback print "Uncatched Exception: " print traceback.format_exc() sys.exit(1)