Blame plugins/groups_manager.py

Packit Service 7cde16
# groups_manager.py
Packit Service 7cde16
# DNF plugin for managing comps groups metadata files
Packit Service 7cde16
#
Packit Service 7cde16
# Copyright (C) 2020 Red Hat, Inc.
Packit Service 7cde16
#
Packit Service 7cde16
# This copyrighted material is made available to anyone wishing to use,
Packit Service 7cde16
# modify, copy, or redistribute it subject to the terms and conditions of
Packit Service 7cde16
# the GNU General Public License v.2, or (at your option) any later version.
Packit Service 7cde16
# This program is distributed in the hope that it will be useful, but WITHOUT
Packit Service 7cde16
# ANY WARRANTY expressed or implied, including the implied warranties of
Packit Service 7cde16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
Packit Service 7cde16
# Public License for more details.  You should have received a copy of the
Packit Service 7cde16
# GNU General Public License along with this program; if not, write to the
Packit Service 7cde16
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
Packit Service 7cde16
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
Packit Service 7cde16
# source code or documentation are not subject to the GNU General Public
Packit Service 7cde16
# License and may only be used or replicated with the express permission of
Packit Service 7cde16
# Red Hat, Inc.
Packit Service 7cde16
#
Packit Service 7cde16
Packit Service 7cde16
from __future__ import absolute_import
Packit Service 7cde16
from __future__ import unicode_literals
Packit Service 7cde16
Packit Service 7cde16
import argparse
Packit Service 7cde16
import gzip
Packit Service 7cde16
import libcomps
Packit Service 7cde16
import os
Packit Service 7cde16
import re
Packit Service 7cde16
import shutil
Packit Service 7cde16
import tempfile
Packit Service 7cde16
Packit Service 7cde16
from dnfpluginscore import _, logger
Packit Service 7cde16
import dnf
Packit Service 7cde16
import dnf.cli
Packit Service 7cde16
Packit Service 7cde16
Packit Service 7cde16
RE_GROUP_ID_VALID = '-a-z0-9_.:'
Packit Service 7cde16
RE_GROUP_ID = re.compile(r'^[{}]+$'.format(RE_GROUP_ID_VALID))
Packit Service 7cde16
RE_LANG = re.compile(r'^[-a-zA-Z0-9_.@]+$')
Packit Service 7cde16
COMPS_XML_OPTIONS = {
Packit Service 7cde16
    'default_explicit': True,
Packit Service 7cde16
    'uservisible_explicit': True,
Packit Service 7cde16
    'empty_groups': True}
Packit Service 7cde16
Packit Service 7cde16
Packit Service 7cde16
def group_id_type(value):
Packit Service 7cde16
    '''group id validator'''
Packit Service 7cde16
    if not RE_GROUP_ID.match(value):
Packit Service 7cde16
        raise argparse.ArgumentTypeError(_('Invalid group id'))
Packit Service 7cde16
    return value
Packit Service 7cde16
Packit Service 7cde16
Packit Service 7cde16
def translation_type(value):
Packit Service 7cde16
    '''translated texts validator'''
Packit Service 7cde16
    data = value.split(':', 2)
Packit Service 7cde16
    if len(data) != 2:
Packit Service 7cde16
        raise argparse.ArgumentTypeError(
Packit Service 7cde16
            _("Invalid translated data, should be in form 'lang:text'"))
Packit Service 7cde16
    lang, text = data
Packit Service 7cde16
    if not RE_LANG.match(lang):
Packit Service 7cde16
        raise argparse.ArgumentTypeError(_('Invalid/empty language for translated data'))
Packit Service 7cde16
    return lang, text
Packit Service 7cde16
Packit Service 7cde16
Packit Service 7cde16
def text_to_id(text):
Packit Service 7cde16
    '''generate group id based on its name'''
Packit Service 7cde16
    group_id = text.lower()
Packit Service 7cde16
    group_id = re.sub('[^{}]'.format(RE_GROUP_ID_VALID), '', group_id)
Packit Service 7cde16
    if not group_id:
Packit Service 7cde16
        raise dnf.cli.CliError(
Packit Service 7cde16
            _("Can't generate group id from '{}'. Please specify group id using --id.").format(
Packit Service 7cde16
                text))
Packit Service 7cde16
    return group_id
Packit Service 7cde16
Packit Service 7cde16
Packit Service 7cde16
@dnf.plugin.register_command
Packit Service 7cde16
class GroupsManagerCommand(dnf.cli.Command):
Packit Service 7cde16
    aliases = ('groups-manager',)
Packit Service 7cde16
    summary = _('create and edit groups metadata file')
Packit Service 7cde16
Packit Service 7cde16
    def __init__(self, cli):
Packit Service 7cde16
        super(GroupsManagerCommand, self).__init__(cli)
Packit Service 7cde16
        self.comps = libcomps.Comps()
Packit Service 7cde16
Packit Service 7cde16
    @staticmethod
Packit Service 7cde16
    def set_argparser(parser):
Packit Service 7cde16
        # input / output options
Packit Service 7cde16
        parser.add_argument('--load', action='append', default=[],
Packit Service 7cde16
                            metavar='COMPS.XML',
Packit Service 7cde16
                            help=_('load groups metadata from file'))
Packit Service 7cde16
        parser.add_argument('--save', action='append', default=[],
Packit Service 7cde16
                            metavar='COMPS.XML',
Packit Service 7cde16
                            help=_('save groups metadata to file'))
Packit Service 7cde16
        parser.add_argument('--merge', metavar='COMPS.XML',
Packit Service 7cde16
                            help=_('load and save groups metadata to file'))
Packit Service 7cde16
        parser.add_argument('--print', action='store_true', default=False,
Packit Service 7cde16
                            help=_('print the result metadata to stdout'))
Packit Service 7cde16
        # group options
Packit Service 7cde16
        parser.add_argument('--id', type=group_id_type,
Packit Service 7cde16
                            help=_('group id'))
Packit Service 7cde16
        parser.add_argument('-n', '--name', help=_('group name'))
Packit Service 7cde16
        parser.add_argument('--description',
Packit Service 7cde16
                            help=_('group description'))
Packit Service 7cde16
        parser.add_argument('--display-order', type=int,
Packit Service 7cde16
                            help=_('group display order'))
Packit Service 7cde16
        parser.add_argument('--translated-name', action='append', default=[],
Packit Service 7cde16
                            metavar='LANG:TEXT', type=translation_type,
Packit Service 7cde16
                            help=_('translated name for the group'))
Packit Service 7cde16
        parser.add_argument('--translated-description', action='append', default=[],
Packit Service 7cde16
                            metavar='LANG:TEXT', type=translation_type,
Packit Service 7cde16
                            help=_('translated description for the group'))
Packit Service 7cde16
        visible = parser.add_mutually_exclusive_group()
Packit Service 7cde16
        visible.add_argument('--user-visible', dest='user_visible', action='store_true',
Packit Service 7cde16
                             default=None,
Packit Service 7cde16
                             help=_('make the group user visible (default)'))
Packit Service 7cde16
        visible.add_argument('--not-user-visible', dest='user_visible', action='store_false',
Packit Service 7cde16
                             default=None,
Packit Service 7cde16
                             help=_('make the group user invisible'))
Packit Service 7cde16
Packit Service 7cde16
        # package list options
Packit Service 7cde16
        section = parser.add_mutually_exclusive_group()
Packit Service 7cde16
        section.add_argument('--mandatory', action='store_true',
Packit Service 7cde16
                             help=_('add packages to the mandatory section'))
Packit Service 7cde16
        section.add_argument('--optional', action='store_true',
Packit Service 7cde16
                             help=_('add packages to the optional section'))
Packit Service 7cde16
        section.add_argument('--remove', action='store_true', default=False,
Packit Service 7cde16
                             help=_('remove packages from the group instead of adding them'))
Packit Service 7cde16
        parser.add_argument('--dependencies', action='store_true',
Packit Service 7cde16
                            help=_('include also direct dependencies for packages'))
Packit Service 7cde16
Packit Service 7cde16
        parser.add_argument("packages", nargs='*', metavar='PACKAGE',
Packit Service 7cde16
                            help=_('package specification'))
Packit Service 7cde16
Packit Service 7cde16
    def configure(self):
Packit Service 7cde16
        demands = self.cli.demands
Packit Service 7cde16
Packit Service 7cde16
        if self.opts.packages:
Packit Service 7cde16
            demands.sack_activation = True
Packit Service 7cde16
            demands.available_repos = True
Packit Service 7cde16
            demands.load_system_repo = False
Packit Service 7cde16
Packit Service 7cde16
        # handle --merge option (shortcut to --load and --save the same file)
Packit Service 7cde16
        if self.opts.merge:
Packit Service 7cde16
            self.opts.load.insert(0, self.opts.merge)
Packit Service 7cde16
            self.opts.save.append(self.opts.merge)
Packit Service 7cde16
Packit Service 7cde16
        # check that group is specified when editing is attempted
Packit Service 7cde16
        if (self.opts.description
Packit Service 7cde16
                or self.opts.display_order
Packit Service 7cde16
                or self.opts.translated_name
Packit Service 7cde16
                or self.opts.translated_description
Packit Service 7cde16
                or self.opts.user_visible is not None
Packit Service 7cde16
                or self.opts.packages):
Packit Service 7cde16
            if not self.opts.id and not self.opts.name:
Packit Service 7cde16
                raise dnf.cli.CliError(
Packit Service 7cde16
                    _("Can't edit group without specifying it (use --id or --name)"))
Packit Service 7cde16
Packit Service 7cde16
    def load_input_files(self):
Packit Service 7cde16
        """
Packit Service 7cde16
        Loads all input xml files.
Packit Service 7cde16
        Returns True if at least one file was successfuly loaded
Packit Service 7cde16
        """
Packit Service 7cde16
        for file_name in self.opts.load:
Packit Service 7cde16
            file_comps = libcomps.Comps()
Packit Service 7cde16
            try:
Packit Service 7cde16
                if file_name.endswith('.gz'):
Packit Service 7cde16
                    # libcomps does not support gzipped files - decompress to temporary
Packit Service 7cde16
                    # location
Packit Service 7cde16
                    with gzip.open(file_name) as gz_file:
Packit Service 7cde16
                        temp_file = tempfile.NamedTemporaryFile(delete=False)
Packit Service 7cde16
                        try:
Packit Service 7cde16
                            shutil.copyfileobj(gz_file, temp_file)
Packit Service 7cde16
                            # close temp_file to ensure the content is flushed to disk
Packit Service 7cde16
                            temp_file.close()
Packit Service 7cde16
                            file_comps.fromxml_f(temp_file.name)
Packit Service 7cde16
                        finally:
Packit Service 7cde16
                            os.unlink(temp_file.name)
Packit Service 7cde16
                else:
Packit Service 7cde16
                    file_comps.fromxml_f(file_name)
Packit Service 7cde16
            except (IOError, OSError, libcomps.ParserError) as err:
Packit Service 7cde16
                # gzip module raises OSError on reading from malformed gz file
Packit Service 7cde16
                # get_last_errors() output often contains duplicit lines, remove them
Packit Service 7cde16
                seen = set()
Packit Service 7cde16
                for error in file_comps.get_last_errors():
Packit Service 7cde16
                    if error in seen:
Packit Service 7cde16
                        continue
Packit Service 7cde16
                    logger.error(error.strip())
Packit Service 7cde16
                    seen.add(error)
Packit Service 7cde16
                raise dnf.exceptions.Error(
Packit Service 7cde16
                    _("Can't load file \"{}\": {}").format(file_name, err))
Packit Service 7cde16
            else:
Packit Service 7cde16
                self.comps += file_comps
Packit Service 7cde16
Packit Service 7cde16
    def save_output_files(self):
Packit Service 7cde16
        for file_name in self.opts.save:
Packit Service 7cde16
            try:
Packit Service 7cde16
                # xml_f returns a list of errors / log entries
Packit Service 7cde16
                errors = self.comps.xml_f(file_name, xml_options=COMPS_XML_OPTIONS)
Packit Service 7cde16
            except libcomps.XMLGenError as err:
Packit Service 7cde16
                errors = [err]
Packit Service 7cde16
            if errors:
Packit Service 7cde16
                # xml_f() method could return more than one error. In this case
Packit Service 7cde16
                # raise the latest of them and log the others.
Packit Service 7cde16
                for err in errors[:-1]:
Packit Service 7cde16
                    logger.error(err.strip())
Packit Service 7cde16
                raise dnf.exceptions.Error(_("Can't save file \"{}\": {}").format(
Packit Service 7cde16
                    file_name, errors[-1].strip()))
Packit Service 7cde16
Packit Service 7cde16
Packit Service 7cde16
    def find_group(self, group_id, name):
Packit Service 7cde16
        '''
Packit Service 7cde16
        Try to find group according to command line parameters - first by id
Packit Service 7cde16
        then by name.
Packit Service 7cde16
        '''
Packit Service 7cde16
        group = None
Packit Service 7cde16
        if group_id:
Packit Service 7cde16
            for grp in self.comps.groups:
Packit Service 7cde16
                if grp.id == group_id:
Packit Service 7cde16
                    group = grp
Packit Service 7cde16
                    break
Packit Service 7cde16
        if group is None and name:
Packit Service 7cde16
            for grp in self.comps.groups:
Packit Service 7cde16
                if grp.name == name:
Packit Service 7cde16
                    group = grp
Packit Service 7cde16
                    break
Packit Service 7cde16
        return group
Packit Service 7cde16
Packit Service 7cde16
    def edit_group(self, group):
Packit Service 7cde16
        '''
Packit Service 7cde16
        Set attributes and package lists for selected group
Packit Service 7cde16
        '''
Packit Service 7cde16
        def langlist_to_strdict(lst):
Packit Service 7cde16
            str_dict = libcomps.StrDict()
Packit Service 7cde16
            for lang, text in lst:
Packit Service 7cde16
                str_dict[lang] = text
Packit Service 7cde16
            return str_dict
Packit Service 7cde16
Packit Service 7cde16
        # set group attributes
Packit Service 7cde16
        if self.opts.name:
Packit Service 7cde16
            group.name = self.opts.name
Packit Service 7cde16
        if self.opts.description:
Packit Service 7cde16
            group.desc = self.opts.description
Packit Service 7cde16
        if self.opts.display_order:
Packit Service 7cde16
            group.display_order = self.opts.display_order
Packit Service 7cde16
        if self.opts.user_visible is not None:
Packit Service 7cde16
            group.uservisible = self.opts.user_visible
Packit Service 7cde16
        if self.opts.translated_name:
Packit Service 7cde16
            group.name_by_lang = langlist_to_strdict(self.opts.translated_name)
Packit Service 7cde16
        if self.opts.translated_description:
Packit Service 7cde16
            group.desc_by_lang = langlist_to_strdict(self.opts.translated_description)
Packit Service 7cde16
Packit Service 7cde16
        # edit packages list
Packit Service 7cde16
        if self.opts.packages:
Packit Service 7cde16
            # find packages according to specifications from command line
Packit Service 7cde16
            packages = set()
Packit Service 7cde16
            for pkg_spec in self.opts.packages:
Packit Service 7cde16
                q = self.base.sack.query().filterm(name__glob=pkg_spec).latest()
Packit Service 7cde16
                if not q:
Packit Service 7cde16
                    logger.warning(_("No match for argument: {}").format(pkg_spec))
Packit Service 7cde16
                    continue
Packit Service 7cde16
                packages.update(q)
Packit Service 7cde16
            if self.opts.dependencies:
Packit Service 7cde16
                # add packages that provide requirements
Packit Service 7cde16
                requirements = set()
Packit Service 7cde16
                for pkg in packages:
Packit Service 7cde16
                    requirements.update(pkg.requires)
Packit Service 7cde16
                packages.update(self.base.sack.query().filterm(provides=requirements))
Packit Service 7cde16
Packit Service 7cde16
            pkg_names = {pkg.name for pkg in packages}
Packit Service 7cde16
Packit Service 7cde16
            if self.opts.remove:
Packit Service 7cde16
                for pkg_name in pkg_names:
Packit Service 7cde16
                    for pkg in group.packages_match(name=pkg_name,
Packit Service 7cde16
                                                    type=libcomps.PACKAGE_TYPE_UNKNOWN):
Packit Service 7cde16
                        group.packages.remove(pkg)
Packit Service 7cde16
            else:
Packit Service 7cde16
                if self.opts.mandatory:
Packit Service 7cde16
                    pkg_type = libcomps.PACKAGE_TYPE_MANDATORY
Packit Service 7cde16
                elif self.opts.optional:
Packit Service 7cde16
                    pkg_type = libcomps.PACKAGE_TYPE_OPTIONAL
Packit Service 7cde16
                else:
Packit Service 7cde16
                    pkg_type = libcomps.PACKAGE_TYPE_DEFAULT
Packit Service 7cde16
                for pkg_name in sorted(pkg_names):
Packit Service 7cde16
                    if not group.packages_match(name=pkg_name, type=pkg_type):
Packit Service 7cde16
                        group.packages.append(libcomps.Package(name=pkg_name, type=pkg_type))
Packit Service 7cde16
Packit Service 7cde16
    def run(self):
Packit Service 7cde16
        self.load_input_files()
Packit Service 7cde16
Packit Service 7cde16
        if self.opts.id or self.opts.name:
Packit Service 7cde16
            # we are adding / editing a group
Packit Service 7cde16
            group = self.find_group(group_id=self.opts.id, name=self.opts.name)
Packit Service 7cde16
            if group is None:
Packit Service 7cde16
                # create a new group
Packit Service 7cde16
                if self.opts.remove:
Packit Service 7cde16
                    raise dnf.exceptions.Error(_("Can't remove packages from non-existent group"))
Packit Service 7cde16
                group = libcomps.Group()
Packit Service 7cde16
                if self.opts.id:
Packit Service 7cde16
                    group.id = self.opts.id
Packit Service 7cde16
                    group.name = self.opts.id
Packit Service 7cde16
                elif self.opts.name:
Packit Service 7cde16
                    group_id = text_to_id(self.opts.name)
Packit Service 7cde16
                    if self.find_group(group_id=group_id, name=None):
Packit Service 7cde16
                        raise dnf.cli.CliError(
Packit Service 7cde16
                            _("Group id '{}' generated from '{}' is duplicit. "
Packit Service 7cde16
                              "Please specify group id using --id.").format(
Packit Service 7cde16
                                  group_id, self.opts.name))
Packit Service 7cde16
                    group.id = group_id
Packit Service 7cde16
                self.comps.groups.append(group)
Packit Service 7cde16
            self.edit_group(group)
Packit Service 7cde16
Packit Service 7cde16
        self.save_output_files()
Packit Service 7cde16
        if self.opts.print or (not self.opts.save):
Packit Service 7cde16
            print(self.comps.xml_str(xml_options=COMPS_XML_OPTIONS))