Created
September 10, 2015 08:27
-
-
Save toabctl/6885cccfae490d4c9343 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
# Author: Thomas Bechtold <tbechtold@suse.com> | |
from __future__ import print_function | |
import os | |
import sys | |
import argparse | |
import requests | |
import subprocess | |
import xml.etree.ElementTree as ET | |
from collections import defaultdict | |
from rpmUtils.miscutils import splitFilename | |
from prettytable import PrettyTable | |
import pkg_resources | |
# OpenBuildService api url | |
IBS_API_URL = 'https://build.suse.de' | |
OBS_API_URL = 'https://build.opensuse.org' | |
class ZypperVersionCompare(object): | |
""" class to compare version strings with zypper""" | |
def __init__(self, version): | |
self.version_str = version | |
def __repr__(self): | |
return self.version_str | |
def __cmp__(self, other): | |
# zypper's return val is negative if v1 is older than v2. | |
# See 'man zypper' | |
ret = subprocess.check_output("zypper --terse versioncmp %s %s" % ( | |
self.version_str, other.version_str), shell=True) | |
return int(ret) | |
def _do_get(uri, debug=False): | |
if debug: | |
print(uri) | |
r = requests.get(uri, verify=False) | |
if r.status_code != 200: | |
raise Exception('Can not get {0} (status code: {1})'.format( | |
uri, r.status_code)) | |
return r.text | |
def get_published_packages_list(obs_instance, project_name, repository_name, | |
archs=["x86_64", "noarch"], debug=False): | |
# get openbuildservice API url | |
if obs_instance == 'ibs': | |
apiurl = IBS_API_URL | |
elif obs_instance == 'obs': | |
apiurl = OBS_API_URL | |
else: | |
raise Exception("Invalid OpenBuildService instance: '%s'" % ( | |
obs_instance)) | |
packages_info = list() | |
for arch in archs: | |
content = _do_get(apiurl + '/published/{0}/{1}/{2}/'.format( | |
project_name, repository_name, arch), debug=debug) | |
tree = ET.fromstring(content) | |
for child in tree: | |
info = dict() | |
if not child.attrib['name'].startswith('_') and \ | |
child.attrib['name'].endswith('.rpm') and not \ | |
child.attrib['name'].endswith('.src.rpm'): | |
(name, version, release, epoch, arch) = splitFilename( | |
child.attrib['name']) | |
info['name'] = name | |
info['filename'] = child.attrib['name'] | |
info['version'] = version | |
info['release'] = release | |
info['epoch'] = epoch | |
packages_info.append(info) | |
return packages_info | |
class PackageCollector(object): | |
"""Collect packages and export the data as text, html, ...""" | |
def __init__(self, args): | |
self.__binary_packages = defaultdict(dict) | |
self.__project_names = list() # list with unique project names | |
self.__args = args # command line arguments | |
@property | |
def binary_packages(self): | |
return self.__binary_packages | |
@property | |
def project_names(self): | |
"""set of all available project names""" | |
return self.__project_names | |
def add_binary_package(self, project_name, bp_name, bp_info): | |
if project_name not in self.project_names: | |
self.project_names.append(project_name) | |
self.binary_packages[bp_name][project_name] = bp_info | |
def _as_pretty_table(self): | |
"""get a pretty table""" | |
columns = ['packages'] + self.project_names | |
t = PrettyTable(columns) | |
t.align["packages"] = 'l' | |
for bp, bp_info in self.binary_packages.items(): | |
params = [bp] | |
for proj in self.project_names: | |
if proj in bp_info: | |
version = bp_info[proj]['version'] | |
else: | |
version = None | |
params.append(version) | |
# check if all versions are equal (first column is name) | |
if 'version_compare' in args and args['version_compare']: | |
comp = args['version_compare'] | |
for i in range(len(params) - 2): # skip first and last element | |
v1 = params[i+1] or "0" | |
v2 = params[i+2] or "0" | |
if comp == '==': | |
if ZypperVersionCompare(v2) == ZypperVersionCompare(v1): | |
t.add_row(params) | |
elif comp == '>=': | |
if ZypperVersionCompare(v2) >= ZypperVersionCompare(v1): | |
t.add_row(params) | |
elif comp == '>': | |
if ZypperVersionCompare(v2) > ZypperVersionCompare(v1): | |
t.add_row(params) | |
elif comp == '<=': | |
if ZypperVersionCompare(v2) <= ZypperVersionCompare(v1): | |
t.add_row(params) | |
elif comp == '<': | |
if ZypperVersionCompare(v2) < ZypperVersionCompare(v1): | |
t.add_row(params) | |
else: | |
raise Exception("Invalid version comp '%s'" % comp) | |
else: | |
t.add_row(params) | |
t.sortby = "packages" | |
return t | |
def as_text(self): | |
"""output binary packages list as text - useful for console usage""" | |
print(self._as_pretty_table()) | |
def as_html(self): | |
"""output binary packages list as html""" | |
t = self._as_pretty_table() | |
print(t.get_html_string(attributes={"class": "foo"})) | |
def as_plain(self): | |
"""output binary packages list as plain""" | |
t = self._as_pretty_table() | |
print(t.get_string(border=False, header=False, | |
left_padding_width=0, right_padding_width=0, | |
padding_width=0, vertical_char=',')) | |
def compare_projects(args): | |
"""create a list of packages with version info for all given projects""" | |
collector = PackageCollector(args) | |
# if python requirements_file is given, use that as source | |
requires = dict() | |
if 'from_requirements_file' in args: | |
with open(args['from_requirements_file'], "r") as f: | |
for l in f.readlines(): | |
l = l.strip() | |
# TODO(toabctl): parse and use markers | |
l = l.split(";")[0] | |
if not l or l.startswith("#"): | |
continue | |
pkg = pkg_resources.Requirement.parse(l) | |
lowest_version = None | |
# find lowest version (can be >=1.0,>=2.0) so select 1.0 | |
for dep in pkg.specs: | |
if not lowest_version or \ | |
pkg_resources.parse_version(dep[1]) < \ | |
pkg_resources.parse_version(lowest_version): | |
lowest_version = dep[1] | |
# create a obs like info for the "package" | |
info = dict() | |
info['filename'] = pkg.unsafe_name | |
info['name'] = "python-%s" % pkg.unsafe_name | |
info['version'] = lowest_version | |
info['release'] = None | |
info['epoch'] = None | |
# add the requires from the file to the collector | |
collector.add_binary_package( | |
os.path.basename(args['from_requirements_file']), | |
info['name'], info) | |
for project in args['projects']: | |
obs_instance, pro, reponame = project.split(",") | |
for bp in get_published_packages_list(obs_instance.lower(), pro, | |
reponame, debug=args['debug']): | |
collector.add_binary_package(project, bp['name'], bp) | |
return collector | |
def process_args(): | |
"""process cli arguments""" | |
parser = argparse.ArgumentParser(prog=sys.argv[0]) | |
parser.add_argument('-d', '--debug', action='store_true', | |
help='enable debugging') | |
parser.add_argument('--projects', nargs='*', default=[], | |
help='list with projects combined with OBS instance' \ | |
'and repo name.' \ | |
'i.e. "ibs,Devel:Cloud:4,SLE_12 obs,Cloud:OpenStack:Master,SLE_12"') | |
parser.add_argument('--from-requirements-file', | |
help='read packages from requirements file' | |
'"i.e. global-requirements.txt from OpenStack"') | |
parser.add_argument('--version-compare', choices=['==', '>=', '>', '<=', '<'], | |
help='show only packages where version compare is true.' | |
'order of given projects is important here!') | |
parser.add_argument('--out-format', choices=['table', 'html', 'plain'], | |
default='table', help='output format') | |
return vars(parser.parse_args()) | |
if __name__ == "__main__": | |
args = process_args() | |
coll = compare_projects(args) | |
if args['out_format'] == 'table': | |
coll.as_text() | |
elif args['out_format'] == 'html': | |
coll.as_html() | |
elif args['out_format'] == 'plain': | |
coll.as_plain() | |
else: | |
raise Exception("Invalid output format %s" % args['out_format']) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment