Blame plugins/groups_manager.py

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