| |
@@ -0,0 +1,206 @@
|
| |
+ #!/usr/bin/python3
|
| |
+
|
| |
+ import argparse
|
| |
+ import koji
|
| |
+ from requests import Session
|
| |
+ import posixpath
|
| |
+ import operator
|
| |
+ from prettytable import PrettyTable
|
| |
+
|
| |
+
|
| |
+ DESCRIPTION = """
|
| |
+ Show the RPM package differences between two container builds.
|
| |
+ """
|
| |
+
|
| |
+
|
| |
+ def get_session(profile):
|
| |
+ """
|
| |
+ Return an anonymous koji session for this profile name.
|
| |
+
|
| |
+ :param str profile: profile name, like "koji" or "cbs"
|
| |
+ :returns: anonymous koji.ClientSession
|
| |
+ """
|
| |
+ # Note: this raises koji.ConfigurationError if we could not find this
|
| |
+ # profile name.
|
| |
+ # (ie. "check /etc/koji.conf.d/*.conf")
|
| |
+ conf = koji.read_config(profile)
|
| |
+ conf['authtype'] = 'noauth'
|
| |
+ hub = conf['server']
|
| |
+ return koji.ClientSession(hub, {})
|
| |
+
|
| |
+
|
| |
+ def get_koji_pathinfo(profile):
|
| |
+ """
|
| |
+ Return a Koji PathInfo object for our profile.
|
| |
+
|
| |
+ :param str profile: profile name, like "koji" or "cbs"
|
| |
+ :returns: koji.PathInfo
|
| |
+ """
|
| |
+ conf = koji.read_config(profile)
|
| |
+ top = conf['topurl'] # or 'topdir' here for NFS access
|
| |
+ pathinfo = koji.PathInfo(topdir=top)
|
| |
+ return pathinfo
|
| |
+
|
| |
+
|
| |
+ def get_package_url(profile, package):
|
| |
+ conf = koji.read_config(profile)
|
| |
+ top = conf['weburl']
|
| |
+ url = posixpath.join(top, 'packageinfo?packageID=%(id)d' % package)
|
| |
+ return url
|
| |
+
|
| |
+
|
| |
+ def get_metadata_url(profile, build):
|
| |
+ """
|
| |
+ Return the URL to the metadata.json for this build.
|
| |
+
|
| |
+ :param str profile: profile name, like "koji" or "cbs"
|
| |
+ :param dict build: Koji build information
|
| |
+ """
|
| |
+ pathinfo = get_koji_pathinfo(profile)
|
| |
+ builddir = pathinfo.build(build)
|
| |
+ url = posixpath.join(builddir, 'metadata.json')
|
| |
+ return url
|
| |
+
|
| |
+
|
| |
+ def get_metadata(profile, session, rsession, build):
|
| |
+ """
|
| |
+ Get the content-generator metadata for this Koji build.
|
| |
+
|
| |
+ :param str profile: profile name, like "koji" or "cbs"
|
| |
+ :param session: koji.ClientSession
|
| |
+ :param rsession: requests.Session
|
| |
+ :param dict build: Koji build information
|
| |
+ :returns: dict of entire content-generator metadata.
|
| |
+ """
|
| |
+ url = get_metadata_url(profile, build)
|
| |
+ response = rsession.get(url)
|
| |
+ response.raise_for_status()
|
| |
+ return response.json()
|
| |
+
|
| |
+
|
| |
+ def list_rpms(metadata):
|
| |
+ """
|
| |
+ Parse an OSBS container's metadata for the list of RPMs therein.
|
| |
+
|
| |
+ :param dict metadata: Koji content-generator metadata.
|
| |
+ :returns: a dict of rpm version-releases, keyed by name.
|
| |
+ """
|
| |
+ output_items = metadata['output'] # logs, plus per-arch container images.
|
| |
+ nvrs = dict()
|
| |
+ for output_item in output_items:
|
| |
+ components = output_item.get('components', [])
|
| |
+ for component in components:
|
| |
+ if component['type'] != 'rpm':
|
| |
+ continue
|
| |
+ nvrs[component['name']] = {
|
| |
+ 'version': component['version'],
|
| |
+ 'release': component['release'],
|
| |
+ }
|
| |
+ return nvrs
|
| |
+
|
| |
+
|
| |
+ def diff_rpms(old_metadata, new_metadata):
|
| |
+ """
|
| |
+ Diff two OSBS container's metadata for RPM differences.
|
| |
+
|
| |
+ :param dict old_metadata: Koji content-generator metadata.
|
| |
+ :param dict new_metadata: Koji content-generator metadata.
|
| |
+ :returns: a prettytable.PrettyTable object
|
| |
+ """
|
| |
+ # TODO: move this out to a separate method, and pass in "old_nvrs" and
|
| |
+ # "new_nvrs".
|
| |
+ old_nvrs = list_rpms(old_metadata)
|
| |
+ new_nvrs = list_rpms(new_metadata)
|
| |
+ print('found %d old NVRs' % len(old_nvrs))
|
| |
+ print('found %d new NVRs' % len(new_nvrs))
|
| |
+ added = new_nvrs.keys() - old_nvrs.keys()
|
| |
+ removed = old_nvrs.keys() - new_nvrs.keys()
|
| |
+
|
| |
+ table = PrettyTable()
|
| |
+ # TODO: instead of "Old" and "New" here, use the container NVRs.
|
| |
+ table.field_names = ['RPM', 'Old', 'New']
|
| |
+ table.align = 'l'
|
| |
+
|
| |
+ for name in removed:
|
| |
+ nvr = old_nvrs[name]
|
| |
+ table.add_row([name, '%(version)s-%(release)s' % nvr, None])
|
| |
+ for name in added:
|
| |
+ nvr = new_nvrs[name]
|
| |
+ table.add_row([name, None, '%(version)s-%(release)s' % nvr])
|
| |
+
|
| |
+ # TODO: sort this old_nvrs dict:
|
| |
+ for name, version_release in old_nvrs.items():
|
| |
+ if name not in new_nvrs:
|
| |
+ continue
|
| |
+ old = '%(version)s-%(release)s' % version_release
|
| |
+ new = '%(version)s-%(release)s' % new_nvrs[name]
|
| |
+ if new != old:
|
| |
+ table.add_row([name, old, new])
|
| |
+ return table
|
| |
+
|
| |
+
|
| |
+ def parse_args():
|
| |
+ parser = argparse.ArgumentParser(description=DESCRIPTION)
|
| |
+ parser.add_argument('--profile',
|
| |
+ help='Koji profile. Your Koji client profile'
|
| |
+ ' definitions are stored in'
|
| |
+ ' /etc/koji.conf.d/*.conf.',
|
| |
+ required=True)
|
| |
+ parser.add_argument('old_nvr',
|
| |
+ help='old container to compare, eg'
|
| |
+ ' "rhceph-container-1-1"')
|
| |
+ parser.add_argument('new_nvr',
|
| |
+ help='old container to compare, eg'
|
| |
+ ' "rhceph-container-1-2"')
|
| |
+ args = parser.parse_args()
|
| |
+
|
| |
+ for nvr in (args.old_nvr, args.new_nvr):
|
| |
+ if '-container' not in nvr:
|
| |
+ raise ValueError('%s must be a "-container" build' % nvr)
|
| |
+
|
| |
+ return args
|
| |
+
|
| |
+
|
| |
+ def help_missing_nvr(profile, session, nvr):
|
| |
+ """
|
| |
+ A user has specified a build NVR that does not exist. Link to the package
|
| |
+ in kojiweb.
|
| |
+ """
|
| |
+ print('"%s" is not a Koji build' % nvr)
|
| |
+ build = koji.parse_NVR(nvr)
|
| |
+ name = build['name']
|
| |
+ package = session.getPackage(name)
|
| |
+ if not package:
|
| |
+ print('There is no "%s" package in Koji.' % name)
|
| |
+ return
|
| |
+ print('Please choose a valid %s build:' % name)
|
| |
+ url = get_package_url(profile, package)
|
| |
+ print(url)
|
| |
+
|
| |
+
|
| |
+ def main():
|
| |
+ args = parse_args()
|
| |
+ session = get_session(args.profile)
|
| |
+
|
| |
+ old_build = session.getBuild(args.old_nvr)
|
| |
+ if not old_build:
|
| |
+ help_missing_nvr(args.profile, session, args.old_nvr)
|
| |
+ raise SystemExit(1)
|
| |
+
|
| |
+ new_build = session.getBuild(args.new_nvr)
|
| |
+ if not new_build:
|
| |
+ help_missing_nvr(args.profile, session, args.new_nvr)
|
| |
+ raise SystemExit(1)
|
| |
+
|
| |
+ rsession = Session()
|
| |
+ old_metadata = get_metadata(args.profile, session, rsession, old_build)
|
| |
+ new_metadata = get_metadata(args.profile, session, rsession, new_build)
|
| |
+
|
| |
+ table = diff_rpms(old_metadata, new_metadata)
|
| |
+
|
| |
+ print('Found %d differences:' % len(table._rows))
|
| |
+ print(table.get_string(sort_key=operator.itemgetter(1, 0), sortby="RPM"))
|
| |
+
|
| |
+
|
| |
+ if __name__ == '__main__':
|
| |
+ main()
|
| |
Use this to compare RPM NVRs between two container builds.
A lot of this code is copied & pasted between
koji-search-containers
. Longer-term it would make sense to share that in a common library instead (or maybe koji-containerbuild's CLI).