diff --git a/update_crawler.py b/update_crawler.py new file mode 100755 index 00000000..060a1367 --- /dev/null +++ b/update_crawler.py @@ -0,0 +1,182 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2015 SUSE Linux GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import argparse +import itertools +import logging +import sys +from xml.etree import cElementTree as ET + +import osc.conf +import osc.core + +from osclib.memoize import memoize + +OPENSUSE = 'openSUSE:42' +SLE = 'SUSE:SLE-12:Update' + +makeurl = osc.core.makeurl +http_GET = osc.core.http_GET + + +class UpdateCrawler(object): + def __init__(self, from_prj, to_prj): + self.from_prj = from_prj + self.to_prj = to_prj + self.apiurl = osc.conf.config['apiurl'] + self.debug = osc.conf.config['debug'] + + def get_source_packages(self, project, expand=False): + """Return the list of packages in a project.""" + query = {'expand': 1} if expand else {} + root = ET.parse( + http_GET(makeurl(self.apiurl, + ['source', project], + query=query))).getroot() + packages = [i.get('name') for i in root.findall('entry')] + return packages + + @memoize() + def _get_source_package(self, project, package): + return http_GET(makeurl(self.apiurl, + ['source', project, package], + {'view': 'info'})).read() + + def get_source_verifymd5(self, project, package): + """ Return the verifymd5 of a source package.""" + root = ET.fromstring(self._get_source_package(project, package)) + return root.get('verifymd5') + + def get_update_candidates(self): + """Get the grouped update list from `fron_prj` project. + + Return a list of updates for every package, including only the + last update for every package. Every element is a tuple, where + the first element is the name of the package and the second + one the most update version of the package: + [ + ('DirectFB', 'DirectFB.577'), + ('MozillaFirefox', 'MozillaFirefox.544'), + ('PackageKit', 'PackageKit.103'), + ..., + ] + + """ + # From the list of packages, filter non-updates and the + # 'patchinfo' + packages = self.get_source_packages(self.from_prj) + packages = [i for i in packages + if not i.startswith('patchinfo') and i.split('.')[-1].isdigit()] + # Group by package name and revert the order of updates + updates = [list(reversed(list(i))) + for _, i in itertools.groupby(packages, + lambda x: x.split('.')[0])] + # Get the last version of every package + updates = [(i[0].split('.')[0], i[0]) for i in updates] + return updates + + def _submitrequest(self, src_project, src_package, dst_project, + dst_package, msg): + """Create a submit request.""" + states = ['new', 'review', 'declined'] + reqs = osc.core.get_exact_request_list(self.apiurl, + src_project, + dst_project, + src_package, + dst_package, + req_type='submit', + req_state=states) + res = None + if not reqs: + res = osc.core.create_submit_request(self.apiurl, + src_project, + src_package, + dst_project, + dst_package, + message=msg) + return res + + def submitrequest(self, src_package, dst_package): + """Create a submit request using the osc.commandline.Osc class.""" + src_project = self.from_prj + dst_project = self.to_prj + msg = 'Automatic request from %s by UpdateCrawler' % src_project + self._submitrequest(src_project, src_package, dst_project, + dst_package, msg) + raise Exception() + + def crawl(self): + """Main method of the class that run the crawler.""" + updates = self.get_update_candidates() + + packages = set(self.get_source_packages(self.to_prj, expand=True)) + to_update = [] + for package, update in updates: + if package not in packages: + logging.info('Package %s not found in %s' % (package, self.to_prj)) + continue + + md5_from = self.get_source_verifymd5(self.from_prj, update) + md5_to = self.get_source_verifymd5(self.to_prj, package) + if md5_from != md5_to: + to_update.append((package, update)) + logging.info('Package %s marked for update' % package) + else: + logging.info('Package %s not marked for update' % package) + + for package, update in to_update: + res = self.submitrequest(update, package) + if res: + logging.info('Created request %s for %s' % (res, package)) + else: + logging.error('Error creating the request for %s' % package) + + +def main(args): + # Configure OSC + osc.conf.get_config(override_apiurl=args.apiurl) + osc.conf.config['debug'] = args.debug + + uc = UpdateCrawler(args.from_prj, args.to_prj) + uc.crawl() + +if __name__ == '__main__': + description = 'Create SR from SLE to the new openSUSE:42 project for '\ + 'every new update.' + parser = argparse.ArgumentParser(description=description) + parser.add_argument('-A', '--apiurl', metavar='URL', help='API URL') + parser.add_argument('-d', '--debug', action='store_true', + help='print info useful for debuging') + parser.add_argument('-f', '--from', dest='from_prj', metavar='PROJECT', + help='project where to get the updates (default: %s)' % SLE, + default=SLE) + parser.add_argument('-t', '--to', dest='to_prj', metavar='PROJECT', + help='project where to submit the updates (default: %s)' % OPENSUSE, + default=OPENSUSE) + + args = parser.parse_args() + + # Set logging configuration + logging.basicConfig(level=logging.DEBUG if args.debug + else logging.INFO) + + sys.exit(main(args))