Skip to content

Instantly share code, notes, and snippets.

@toabctl
Created September 10, 2015 08:27
Show Gist options
  • Save toabctl/6885cccfae490d4c9343 to your computer and use it in GitHub Desktop.
Save toabctl/6885cccfae490d4c9343 to your computer and use it in GitHub Desktop.
#!/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