#5 Helper tool for managing explicit package filter
Opened 6 years ago by pbabinca. Modified 6 years ago
pbabinca/koji-tools manage-package-list  into  master

file modified
+3
@@ -23,6 +23,9 @@ 

  

  %install

  rm -rf $RPM_BUILD_ROOT

+ install -d $RPM_BUILD_ROOT%{python_sitelib}/koji_utils

+ install -pm 0644 src/koji_utils/__init__.py $RPM_BUILD_ROOT%{python_sitelib}/koji_utils

+ install -pm 0644 src/koji_utils/manage_package_filter.py $RPM_BUILD_ROOT%{python_sitelib}/koji_utils

  install -d $RPM_BUILD_ROOT%{_bindir}

  install -pm 0755 src/bin/* $RPM_BUILD_ROOT%{_bindir}

  

file added
+23
@@ -0,0 +1,23 @@ 

+ import glob

+ 

+ import setuptools

+ 

+ setuptools.setup(

+     name='koji-tools',

+     description='A collection of libraries and tools for interacting with Koji.',

+     url='https://pagure.io/koji-utils',

+     scripts=glob.glob("src/bin/*"),

+     packages=[

+         'koji_tools',

+     ],

+     package_dir={

+         '': 'src',

+     },

+     install_requires=[

+         'koji',

+     ],

+     license='LGPLv2 GPLv2+',

+     dependency_links=[

+         'git+https://pagure.io/forks/tkopecek/koji.git@pypi#egg=koji-1.13.0',

+     ]

+ )

@@ -0,0 +1,100 @@ 

+ #!/usr/bin/env python

+ # -*- coding: utf-8 -*-

+ """

+ Helper tool for managing explicit package filter

+ """

+ 

+ import argparse

+ import sys

+ 

+ from koji_tools import manage_package_filter

+ 

+ 

+ def make_arg_parser():

+     parser = argparse.ArgumentParser(description=__doc__)

+     parser.add_argument('--debug',

+                         action='store_true',

+                         help="Print traceback instead of error messages"

+                         )

+     parser.add_argument('--koji-profile',

+                         help="Koji profile to use")

+ 

+     subparsers = parser.add_subparsers(dest='action')

+     parser_a = subparsers.add_parser('add')

+     parser_a.add_argument('--parent', metavar='TAG')

+     parser_a.add_argument('--dry-run', action='store_true')

+     parser_a.add_argument('tag')

+     parser_a.add_argument('packages', metavar='package', nargs='+')

+ 

+     parser_r = subparsers.add_parser('remove')

+     parser_r.add_argument('--parent', metavar='TAG')

+     parser_r.add_argument('--dry-run', action='store_true')

+     parser_r.add_argument('tag')

+     parser_r.add_argument('package')

+ 

+     parser_n = subparsers.add_parser('normalize')

+     parser_n.add_argument('--parent', metavar='TAG')

+     parser_n.add_argument('--dry-run', action='store_true')

+     parser_n.add_argument('tag')

+ 

+     parser_e = subparsers.add_parser('exists')

+     parser_e.add_argument('--parent', metavar='TAG')

+     parser_e.add_argument('tag')

+     parser_e.add_argument('package')

+ 

+     parser_l = subparsers.add_parser('list')

+     parser_l.add_argument('--parent', metavar='TAG')

+     parser_l.add_argument('tag')

+ 

+     return parser

+ 

+ 

+ def main(args):

+     """ Main entry point of the app """

+ 

+     pfm = manage_package_filter.PackageFilterManager(args.koji_profile)

+ 

+     if args.action == 'list':

+         inner_pkg_filter = pfm.cmd_list(args.tag, parent_tag=args.parent)

+         print '\n'.join(inner_pkg_filter)

+         return

+ 

+     elif args.action == 'remove':

+         pfm.cmd_remove(args.tag, args.package, parent_tag=args.parent,

+                        dry_run=args.dry_run)

+         return

+ 

+     elif args.action == 'add':

+         pfm.cmd_add(args.tag, args.packages, parent_tag=args.parent,

+                     dry_run=args.dry_run)

+         return

+ 

+     elif args.action == 'normalize':

+         pfm.cmd_normalize(args.tag, parent_tag=args.parent, dry_run=args.dry_run)

+         return

+ 

+     elif args.action == 'exists':

+         exists = pfm.cmd_exists(args.tag, args.package, parent_tag=args.parent)

+         if exists:

+             print "Package {} exists in {}.".format(args.package, args.tag)

+         else:

+             print "Package {} doesn't exist in {}.".format(args.package,

+                                                            args.tag)

+         return

+ 

+ 

+ if __name__ == "__main__":

+     """ This is executed when run from the command line """

+     parser = make_arg_parser()

+     rv = 0

+     args = parser.parse_args(sys.argv[1:])

+     try:

+         rv = main(args)

+     except (KeyboardInterrupt, SystemExit):

+         rv = 1

+     except:

+         if args and args.debug:

+             raise

+         exctype, value = sys.exc_info()[:2]

+         print("%s: %s" % (exctype.__name__, value))

+     sys.exit(rv)

empty or binary file added
@@ -0,0 +1,237 @@ 

+ import optparse

+ 

+ import koji

+ from koji_cli.lib import activate_session

+ 

+ 

+ class PackageFilterManager(object):

+     def __init__(self, koji_profile):

+         self.koji_profile = koji_profile

+         self._koji_session = None

+         self._koji_configuration = None

+         self._koji_options = None

+ 

+     @property

+     def koji_configuration(self):

+         if self._koji_configuration:

+             return self._koji_configuration

+         try:

+             self._koji_configuration = koji.read_config(self.koji_profile)

+         except koji.ConfigurationError, e:

+             raise RuntimeError("Failed to read koji configuration: {}".format(e))

+             assert False  # pragma: no cover

+ 

+         if not self._koji_configuration:

+             raise RuntimeError("Configuration is empty: '{}'".repr(self._koji_configuration))

+ 

+         return self._koji_configuration

+ 

+     def _extend_koji_options(self):

+         """These options are present only in koji CLI but are required by

+         activate_session"""

+         additional_koji_cli_options = {

+             'noauth': False,

+             'user': None,

+             'runas': None,

+         }

+         for name, value in additional_koji_cli_options.items():

+             if getattr(self._koji_options, name, None) is None:

+                 setattr(self._koji_options, name, value)

+ 

+     @property

+     def koji_options(self):

+         if self._koji_options:

+             return self._koji_options

+         self._koji_options = optparse.Values(self.koji_configuration)

+         self._extend_koji_options()

+ 

+         if not self._koji_options:

+             raise RuntimeError("Configuration is empty: '{}'".repr(self._koji_options))

+ 

+         return self._koji_options

+ 

+     @property

+     def koji_session(self):

+         if self._koji_session:

+             return self._koji_session

+ 

+         server = self.koji_options.server

+         session_opts = koji.grab_session_options(self.koji_options)

+         self._koji_session = koji.ClientSession(server, session_opts)

+         if not self._koji_session:

+             raise RuntimeError("Failed to open koji session: '{}'".repr(self._koji_session))

+         self.koji_activate_session()

+ 

+         return self._koji_session

+ 

+     def koji_activate_session(self):

+         return activate_session(self.koji_session, self.koji_options)

+ 

+     def inheritance_data(self, tag):

+         return self.koji_session.getInheritanceData(tag)

+ 

+     def tag_data(self, tag, parent_tag=None):

+         parent = None

+         if parent_tag:

+             parent = self.koji_session.getTag(parent_tag)

+             if not parent:

+                 raise ValueError("Invalid parent tag: %s".format(parent_tag))

+ 

+         all_data = data = self.inheritance_data(tag)

+         if parent and all_data:

+             data = [datum for datum in all_data if datum['parent_id'] == parent['id']]

+ 

+         if len(data) == 0:

+             raise ValueError("No inheritance with specified arguments")

+         elif len(data) > 1:

+             parent_names = [t['name'] for t in data]

+             if parent:

+                 # This shouldn't ever happen

+                 msg = (

+                     "Inheritance data returned multiple entries "

+                     "even for specified tag and parent."

+                 )

+                 raise RuntimeError(msg)

+                 assert False  # noqa

+ 

+             msg = (

+                 "Tag has multiple parents: {}. "

+                 "Please specify a parent tag on the command line."

+             )

+             raise ValueError(msg.format(', '.join(parent_names)))

+ 

+         assert len(data) == 1

+         return data[0]

+ 

+     def _apply_filter_changes(self, tag, old_data, inner_pkg_filter, dry_run,

+                               parent_tag=None):

+         normalized_filter = normalize_filter(inner_pkg_filter)

+ 

+         old_pkg_filter = old_data.get('pkg_filter')

+         if old_pkg_filter:

+             old_inner_pkg_filter = parse_pkg_filter(old_pkg_filter)

+             if old_inner_pkg_filter == list(normalized_filter):

+                 raise RuntimeError("Nothing has changed.")

+ 

+         all_data = self.inheritance_data(tag)

+         # Get position of tag data which will be modified.

+         # I'm not really sure if I ever get non-zero index here.

+         index = all_data.index(old_data)

+ 

+         new_data = old_data.copy()

+         new_all_data = list(all_data)

+ 

+         new_data['pkg_filter'] = construct_pkg_filter_regex(normalized_filter)

+         new_all_data[index] = new_data

+         self._print_and_action(tag, new_all_data, dry_run, parent_tag)

+ 

+     def _print_and_action(self, tag, new_data, dry_run, parent_tag):

+         print_action(dry_run)

+         print_cli_command(tag, new_data, self.koji_profile, parent_tag)

+         if not dry_run:

+             self.koji_session.setInheritanceData(tag, new_data)

+ 

+     def cmd_list(self, tag, parent_tag=None):

+         data = self.tag_data(tag, parent_tag)

+         pkg_filter = data.get('pkg_filter')

+         if pkg_filter:

+             return parse_pkg_filter(pkg_filter)

+         return ()

+ 

+     def cmd_remove(self, tag, package, parent_tag=None, dry_run=False):

+         data = self.tag_data(tag, parent_tag)

+         pkg_filter = data.get('pkg_filter')

+         if not pkg_filter:

+             raise ValueError("Nothing to do. Filter is already empty.")

+ 

+         inner_pkg_filter = parse_pkg_filter(pkg_filter)

+         if package not in inner_pkg_filter:

+             raise ValueError("Nothing to do, is already missing from filter")

+ 

+         inner_pkg_filter.remove(package)

+ 

+         self._apply_filter_changes(tag, data, inner_pkg_filter, dry_run,

+                                    parent_tag)

+ 

+     def cmd_add(self, tag, packages, parent_tag=None, dry_run=False):

+         data = self.tag_data(tag, parent_tag)

+         pkg_filter = data.get('pkg_filter')

+         inner_pkg_filter = []

+         if pkg_filter:

+             inner_pkg_filter = parse_pkg_filter(pkg_filter)

+         already_present = []

+         to_be_added = []

+         for package in packages:

+             if package in inner_pkg_filter:

+                 already_present.append(package)

+             else:

+                 to_be_added.append(package)

+             #raise ValueError("Nothing to do, package already exists in filter")

+         if already_present:

+             print "These packages are already present: {}".format(','.join(list(set(already_present))))

+ 

+         inner_pkg_filter = inner_pkg_filter + to_be_added

+ 

+         self._apply_filter_changes(tag, data, inner_pkg_filter, dry_run,

+                                    parent_tag)

+ 

+     def cmd_normalize(self, tag, parent_tag=None, dry_run=False):

+         data = self.tag_data(tag, parent_tag)

+         pkg_filter = data.get('pkg_filter')

+         if not pkg_filter:

+             return

+             #raise ValueError("Something is wrong with package filter. It is empty.")

+ 

+         inner_pkg_filter = parse_pkg_filter(pkg_filter)

+ 

+         normalized_filter = normalize_filter(inner_pkg_filter)

+         if inner_pkg_filter == list(normalized_filter):

+             return

+             #raise RuntimeError("Nothing to do. Package filter is already normalized")

+ 

+         self._apply_filter_changes(tag, data, inner_pkg_filter, dry_run,

+                                    parent_tag)

+ 

+     def cmd_exists(self, tag, package, parent_tag=None):

+         data = self.tag_data(tag, parent_tag)

+         pkg_filter = data.get('pkg_filter')

+         if not pkg_filter:

+             raise ValueError("Something is wrong with package filter. It is empty.")

+ 

+         inner_pkg_filter = parse_pkg_filter(pkg_filter)

+         return package in inner_pkg_filter

+ 

+ 

+ def parse_pkg_filter(data):

+     if not (data.startswith('^(') and data.endswith(')$')):

+         raise ValueError("Unsupported filter format")

+     return data[2:-2].split('|')

+ 

+ 

+ def construct_pkg_filter_regex(data):

+     inner = '|'.join(data)

+     return "^({})$".format(inner)

+ 

+ 

+ def normalize_filter(inner_pkg_filter):

+     return sorted(set(inner_pkg_filter), key=str.lower)

+ 

+ 

+ def print_cli_command(tag, data, koji_profile=None, parent_tag=None):

+     fmt = "koji {}edit-tag-inheritance --pkg-filter=\"{}\" {} {}"

+     if koji_profile:

+         profile_opt = "--profile=\"{}\" ".format(koji_profile)

+     else:

+         profile_opt = ""

+     for parent in data:

+         if parent_tag:

+             if parent['name'] != parent_tag:

+                 continue

+         print fmt.format(profile_opt, parent['pkg_filter'], tag, parent['name'])

+ 

+ 

+ def print_action(dry_run):

+     if dry_run:

+         print "Would run:"

+     else:

+         print "Running:"

This tools simplifies management of package filter for tags. It expects package filter in format ^(package1|package2)$. Commands:
- list lists packages one per line
- add adds a package to the filter
- remove removes a package from the filter
- exists tests existence of a package in the filter
- normalize sorts packages in filter alphabetically and makes sure they are unique. If somebody adds packages manually tool is able to parse that. But normalized package list helps people to read that out of the tool. Normalization is done on all operations that changes package filter.

Some commands accept parent tag - it is required only if tag has more than one parent.

All actions that modifies stuff accept --dry-run.

Finally I'm not sure where to write a documentation for this new command. Could you advise?

3 new commits added

  • Include python package koji_tools in setup.py
  • Add new tool - koji-manage-package-filter
  • Provide minimal distutils setup.py
6 years ago

I would propose to rewrite this one as a CLI plugin. Two CLI plugins are in core koji, so you can look there how to write it (https://pagure.io/koji/blob/master/f/plugins/cli). Does it make sense or do you want it more as a stand-alone script?

adding a setup.py should be handled separately. If we want to do this in koji-tools, let's add an issue for discussion first.

I generally discourage folks from using pkg_filter. It was a questionable design decision on my part way back when.

If anyone is using pkg_filter so much that they require a tool to manage it, I think there much be a problem with their tag structure.