|
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))
|