diff --git a/SOURCES/0001-groups-manager-Re-introduce-yum-groups-manager-funct.patch b/SOURCES/0001-groups-manager-Re-introduce-yum-groups-manager-funct.patch
new file mode 100644
index 0000000..ddebf40
--- /dev/null
+++ b/SOURCES/0001-groups-manager-Re-introduce-yum-groups-manager-funct.patch
@@ -0,0 +1,653 @@
+From 40f08d7a22907e6292c314462c01de94584c0854 Mon Sep 17 00:00:00 2001
+From: Marek Blaha <mblaha@redhat.com>
+Date: Tue, 27 Oct 2020 15:46:03 +0100
+Subject: [PATCH 1/2] [groups-manager] Re-introduce yum-groups-manager
+ functionality (RhBug:1826016)
+
+Implements 'dnf groups-manager' command with features:
+- read, merge, print and write groups metadata files
+- edit group attributes name (with translated variants),
+  description (with translated variants), uservisible, displayorder
+- add packgages to group
+- remove packages from group
+
+= changelog =
+msg:           Re-introduce yum-groups-manager functionality
+type:          enhancement
+resolves:      https://bugzilla.redhat.com/show_bug.cgi?id=1826016
+---
+ dnf-plugins-core.spec     |  22 ++-
+ doc/CMakeLists.txt        |   2 +
+ doc/conf.py               |   2 +
+ doc/groups-manager.rst    |  94 ++++++++++++
+ doc/index.rst             |   1 +
+ libexec/dnf-utils.in      |   1 +
+ plugins/CMakeLists.txt    |   1 +
+ plugins/groups_manager.py | 314 ++++++++++++++++++++++++++++++++++++++
+ 8 files changed, 432 insertions(+), 5 deletions(-)
+ create mode 100644 doc/groups-manager.rst
+ create mode 100644 plugins/groups_manager.py
+
+diff --git a/dnf-plugins-core.spec b/dnf-plugins-core.spec
+index d13a996..42d0884 100644
+--- a/dnf-plugins-core.spec
++++ b/dnf-plugins-core.spec
+@@ -58,6 +58,7 @@ Provides:       dnf-command(debug-dump)
+ Provides:       dnf-command(debug-restore)
+ Provides:       dnf-command(debuginfo-install)
+ Provides:       dnf-command(download)
++Provides:       dnf-command(groups-manager)
+ Provides:       dnf-command(repoclosure)
+ Provides:       dnf-command(repograph)
+ Provides:       dnf-command(repomanage)
+@@ -73,6 +74,7 @@ Provides:       dnf-plugin-debuginfo-install = %{version}-%{release}
+ Provides:       dnf-plugin-download = %{version}-%{release}
+ Provides:       dnf-plugin-generate_completion_cache = %{version}-%{release}
+ Provides:       dnf-plugin-needs_restarting = %{version}-%{release}
++Provides:       dnf-plugin-groups-manager = %{version}-%{release}
+ Provides:       dnf-plugin-repoclosure = %{version}-%{release}
+ Provides:       dnf-plugin-repodiff = %{version}-%{release}
+ Provides:       dnf-plugin-repograph = %{version}-%{release}
+@@ -87,7 +89,7 @@ Conflicts:      dnf-plugins-extras-common-data < %{dnf_plugins_extra}
+ 
+ %description
+ Core Plugins for DNF. This package enhances DNF with builddep, config-manager,
+-copr, debug, debuginfo-install, download, needs-restarting, repoclosure,
++copr, debug, debuginfo-install, download, needs-restarting, groups-manager, repoclosure,
+ repograph, repomanage, reposync, changelog and repodiff commands. Additionally
+ provides generate_completion_cache passive plugin.
+ 
+@@ -129,7 +131,8 @@ Conflicts:      python-%{name} < %{version}-%{release}
+ %description -n python2-%{name}
+ Core Plugins for DNF, Python 2 interface. This package enhances DNF with builddep,
+ config-manager, copr, degug, debuginfo-install, download, needs-restarting,
+-repoclosure, repograph, repomanage, reposync, changelog and repodiff commands.
++groups-manager, repoclosure, repograph, repomanage, reposync, changelog
++and repodiff commands.
+ Additionally provides generate_completion_cache passive plugin.
+ %endif
+ 
+@@ -163,7 +166,8 @@ Conflicts:      python-%{name} < %{version}-%{release}
+ %description -n python3-%{name}
+ Core Plugins for DNF, Python 3 interface. This package enhances DNF with builddep,
+ config-manager, copr, debug, debuginfo-install, download, needs-restarting,
+-repoclosure, repograph, repomanage, reposync, changelog and repodiff commands.
++groups-manager, repoclosure, repograph, repomanage, reposync, changelog
++and repodiff commands.
+ Additionally provides generate_completion_cache passive plugin.
+ %endif
+ 
+@@ -190,8 +194,8 @@ Summary:        Yum-utils CLI compatibility layer
+ %description -n %{yum_utils_subpackage_name}
+ As a Yum-utils CLI compatibility layer, supplies in CLI shims for
+ debuginfo-install, repograph, package-cleanup, repoclosure, repomanage,
+-repoquery, reposync, repotrack, repodiff, builddep, config-manager, debug
+-and download that use new implementations using DNF.
++repoquery, reposync, repotrack, repodiff, builddep, config-manager, debug,
++download and yum-groups-manager that use new implementations using DNF.
+ %endif
+ 
+ %if 0%{?rhel} == 0 && %{with python2}
+@@ -458,6 +462,7 @@ ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-builddep
+ ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-config-manager
+ ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-debug-dump
+ ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-debug-restore
++ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-groups-manager
+ ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yumdownloader
+ # These commands don't have a dedicated man page, so let's just point them
+ # to the utils page which contains their descriptions.
+@@ -483,6 +488,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
+ %{_mandir}/man8/dnf-debuginfo-install.*
+ %{_mandir}/man8/dnf-download.*
+ %{_mandir}/man8/dnf-generate_completion_cache.*
++%{_mandir}/man8/dnf-groups-manager.*
+ %{_mandir}/man8/dnf-needs-restarting.*
+ %{_mandir}/man8/dnf-repoclosure.*
+ %{_mandir}/man8/dnf-repodiff.*
+@@ -513,6 +519,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
+ %{python2_sitelib}/dnf-plugins/debuginfo-install.*
+ %{python2_sitelib}/dnf-plugins/download.*
+ %{python2_sitelib}/dnf-plugins/generate_completion_cache.*
++%{python2_sitelib}/dnf-plugins/groups_manager.*
+ %{python2_sitelib}/dnf-plugins/needs_restarting.*
+ %{python2_sitelib}/dnf-plugins/repoclosure.*
+ %{python2_sitelib}/dnf-plugins/repodiff.*
+@@ -538,6 +545,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
+ %{python3_sitelib}/dnf-plugins/debuginfo-install.py
+ %{python3_sitelib}/dnf-plugins/download.py
+ %{python3_sitelib}/dnf-plugins/generate_completion_cache.py
++%{python3_sitelib}/dnf-plugins/groups_manager.py
+ %{python3_sitelib}/dnf-plugins/needs_restarting.py
+ %{python3_sitelib}/dnf-plugins/repoclosure.py
+ %{python3_sitelib}/dnf-plugins/repodiff.py
+@@ -552,6 +560,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
+ %{python3_sitelib}/dnf-plugins/__pycache__/debuginfo-install.*
+ %{python3_sitelib}/dnf-plugins/__pycache__/download.*
+ %{python3_sitelib}/dnf-plugins/__pycache__/generate_completion_cache.*
++%{python3_sitelib}/dnf-plugins/__pycache__/groups_manager.*
+ %{python3_sitelib}/dnf-plugins/__pycache__/needs_restarting.*
+ %{python3_sitelib}/dnf-plugins/__pycache__/repoclosure.*
+ %{python3_sitelib}/dnf-plugins/__pycache__/repodiff.*
+@@ -579,6 +588,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
+ %{_bindir}/yum-config-manager
+ %{_bindir}/yum-debug-dump
+ %{_bindir}/yum-debug-restore
++%{_bindir}/yum-groups-manager
+ %{_bindir}/yumdownloader
+ %{_mandir}/man1/debuginfo-install.*
+ %{_mandir}/man1/needs-restarting.*
+@@ -591,6 +601,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
+ %{_mandir}/man1/yum-config-manager.*
+ %{_mandir}/man1/yum-debug-dump.*
+ %{_mandir}/man1/yum-debug-restore.*
++%{_mandir}/man1/yum-groups-manager.*
+ %{_mandir}/man1/yumdownloader.*
+ %{_mandir}/man1/package-cleanup.*
+ %{_mandir}/man1/dnf-utils.*
+@@ -612,6 +623,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
+ %exclude %{_mandir}/man1/yum-config-manager.*
+ %exclude %{_mandir}/man1/yum-debug-dump.*
+ %exclude %{_mandir}/man1/yum-debug-restore.*
++%exclude %{_mandir}/man1/yum-groups-manager.*
+ %exclude %{_mandir}/man1/yumdownloader.*
+ %exclude %{_mandir}/man1/package-cleanup.*
+ %exclude %{_mandir}/man1/dnf-utils.*
+diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt
+index dd97eb2..3fb665d 100644
+--- a/doc/CMakeLists.txt
++++ b/doc/CMakeLists.txt
+@@ -26,6 +26,7 @@ INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/dnf-builddep.8
+     ${CMAKE_CURRENT_BINARY_DIR}/dnf-debuginfo-install.8
+     ${CMAKE_CURRENT_BINARY_DIR}/dnf-download.8
+     ${CMAKE_CURRENT_BINARY_DIR}/dnf-generate_completion_cache.8
++    ${CMAKE_CURRENT_BINARY_DIR}/dnf-groups-manager.8
+     ${CMAKE_CURRENT_BINARY_DIR}/dnf-leaves.8
+     ${CMAKE_CURRENT_BINARY_DIR}/dnf-needs-restarting.8
+     ${CMAKE_CURRENT_BINARY_DIR}/dnf-repoclosure.8
+@@ -61,6 +62,7 @@ INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/debuginfo-install.1
+     ${CMAKE_CURRENT_BINARY_DIR}/yum-config-manager.1
+     ${CMAKE_CURRENT_BINARY_DIR}/yum-debug-dump.1
+     ${CMAKE_CURRENT_BINARY_DIR}/yum-debug-restore.1
++    ${CMAKE_CURRENT_BINARY_DIR}/yum-groups-manager.1
+     ${CMAKE_CURRENT_BINARY_DIR}/yumdownloader.1
+     ${CMAKE_CURRENT_BINARY_DIR}/package-cleanup.1
+     ${CMAKE_CURRENT_BINARY_DIR}/dnf-utils.1
+diff --git a/doc/conf.py b/doc/conf.py
+index d760ef3..645185a 100644
+--- a/doc/conf.py
++++ b/doc/conf.py
+@@ -251,6 +251,7 @@ man_pages = [
+     ('download', 'dnf-download', u'DNF download Plugin', AUTHORS, 8),
+     ('generate_completion_cache', 'dnf-generate_completion_cache',
+         u'DNF generate_completion_cache Plugin', AUTHORS, 8),
++    ('groups-manager', 'dnf-groups-manager', u'DNF groups-manager Plugin', AUTHORS, 8),
+     ('leaves', 'dnf-leaves', u'DNF leaves Plugin', AUTHORS, 8),
+     ('local', 'dnf-local', u'DNF local Plugin', AUTHORS, 8),
+     ('needs_restarting', 'dnf-needs-restarting', u'DNF needs_restarting Plugin', AUTHORS, 8),
+@@ -268,6 +269,7 @@ man_pages = [
+     ('copr', 'yum-copr', u'redirecting to DNF copr Plugin', AUTHORS, 8),
+     ('debuginfo-install', 'debuginfo-install', u'redirecting to DNF debuginfo-install Plugin',
+      AUTHORS, 1),
++    ('groups-manager', 'yum-groups-manager', u'redirecting to DNF groups-manager Plugin', AUTHORS, 1),
+     ('needs_restarting', 'needs-restarting', u'redirecting to DNF needs-restarting Plugin',
+      AUTHORS, 1),
+     ('repoclosure', 'repoclosure', u'redirecting to DNF repoclosure Plugin', AUTHORS, 1),
+diff --git a/doc/groups-manager.rst b/doc/groups-manager.rst
+new file mode 100644
+index 0000000..f8f76a1
+--- /dev/null
++++ b/doc/groups-manager.rst
+@@ -0,0 +1,94 @@
++..
++  Copyright (C) 2020  Red Hat, Inc.
++
++  This copyrighted material is made available to anyone wishing to use,
++  modify, copy, or redistribute it subject to the terms and conditions of
++  the GNU General Public License v.2, or (at your option) any later version.
++  This program is distributed in the hope that it will be useful, but WITHOUT
++  ANY WARRANTY expressed or implied, including the implied warranties of
++  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
++  Public License for more details.  You should have received a copy of the
++  GNU General Public License along with this program; if not, write to the
++  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
++  02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
++  source code or documentation are not subject to the GNU General Public
++  License and may only be used or replicated with the express permission of
++  Red Hat, Inc.
++
++=========================
++DNF groups-manager Plugin
++=========================
++
++Create and edit groups repository metadata files.
++
++--------
++Synopsis
++--------
++
++``dnf groups-manager [options] [package-name-spec [package-name-spec ...]]``
++
++-----------
++Description
++-----------
++groups-manager plugin is used to create or edit a group metadata file for a repository. This is often much easier than writing/editing the XML by hand. The groups-manager can load an entire file of groups metadata and either create a new group or edit an existing group and then write all of the groups metadata back out.
++
++---------
++Arguments
++---------
++
++``<package-name-spec>``
++    Package to add to a group or remove from a group.
++
++-------
++Options
++-------
++
++All general DNF options are accepted, see `Options` in :manpage:`dnf(8)` for details.
++
++``--load=<path_to_comps.xml>``
++    Load the groups metadata information from the specified file before performing any operations. Metadata from all files are merged together if the option is specified multiple times.
++
++``--save=<path_to_comps.xml>``
++    Save the result to this file. You can specify the name of a file you are loading from as the data will only be saved when all the operations have been performed. This option can also be specified multiple times.
++
++``--merge=<path_to_comps.xml>``
++    This is the same as loading and saving a file, however the "merge" file is loaded before any others and saved last.
++
++``--print``
++    Also print the result to stdout.
++
++``--id=<id>``
++    The id to lookup/use for the group. If you don't specify an ``<id>``, but do specify a name that doesn't refer to an existing group, then an id for the group is generated based on the name.
++
++``-n <name>, --name=<name>``
++    The name to lookup/use for the group. If you specify an existing group id, then the group with that id will have it's name changed to this value.
++
++``--description=<description>``
++    The description to use for the group.
++
++``--display-order=<display_order>``
++    Change the integer which controls the order groups are presented in, for example in ``dnf grouplist``.
++
++``--translated-name=<lang:text>``
++    A translation of the group name in the given language. The syntax is ``lang:text``. Eg. ``en:my-group-name-in-english``
++
++``--translated-description=<lang:text>``
++    A translation of the group description in the given language. The syntax is ``lang:text``. Eg. ``en:my-group-description-in-english``.
++
++``--user-visible``
++    Make the group visible in ``dnf grouplist`` (this is the default).
++
++``--not-user-visible``
++    Make the group not visible in ``dnf grouplist``.
++
++``--mandatory``
++    Store the package names specified within the mandatory section of the specified group, the default is to use the default section.
++
++``--optional``
++    Store the package names specified within the optional section of the specified group, the default is to use the default section.
++
++``--remove``
++    Instead of adding packages remove them. Note that the packages are removed from all sections (default, mandatory and optional).
++
++``--dependencies``
++    Also include the names of the direct dependencies for each package specified.
+diff --git a/doc/index.rst b/doc/index.rst
+index 91bb36e..7213253 100644
+--- a/doc/index.rst
++++ b/doc/index.rst
+@@ -33,6 +33,7 @@ This documents core plugins of DNF:
+    debuginfo-install
+    download
+    generate_completion_cache
++   groups-manager
+    leaves
+    local
+    migrate
+diff --git a/libexec/dnf-utils.in b/libexec/dnf-utils.in
+index 667ce13..af1e893 100644
+--- a/libexec/dnf-utils.in
++++ b/libexec/dnf-utils.in
+@@ -37,6 +37,7 @@ MAPPING = {'debuginfo-install': ['debuginfo-install'],
+            'yum-config-manager': ['config-manager'],
+            'yum-debug-dump': ['debug-dump'],
+            'yum-debug-restore': ['debug-restore'],
++           'yum-groups-manager': ['groups-manager'],
+            'yumdownloader': ['download']
+            }
+ 
+diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
+index 7465e53..f66d3df 100644
+--- a/plugins/CMakeLists.txt
++++ b/plugins/CMakeLists.txt
+@@ -6,6 +6,7 @@ INSTALL (FILES config_manager.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
+ INSTALL (FILES copr.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
+ INSTALL (FILES download.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
+ INSTALL (FILES generate_completion_cache.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
++INSTALL (FILES groups_manager.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
+ INSTALL (FILES leaves.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
+ if (${WITHOUT_LOCAL} STREQUAL "0")
+ INSTALL (FILES local.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
+diff --git a/plugins/groups_manager.py b/plugins/groups_manager.py
+new file mode 100644
+index 0000000..382df37
+--- /dev/null
++++ b/plugins/groups_manager.py
+@@ -0,0 +1,314 @@
++# groups_manager.py
++# DNF plugin for managing comps groups metadata files
++#
++# Copyright (C) 2020 Red Hat, Inc.
++#
++# This copyrighted material is made available to anyone wishing to use,
++# modify, copy, or redistribute it subject to the terms and conditions of
++# the GNU General Public License v.2, or (at your option) any later version.
++# This program is distributed in the hope that it will be useful, but WITHOUT
++# ANY WARRANTY expressed or implied, including the implied warranties of
++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
++# Public License for more details.  You should have received a copy of the
++# GNU General Public License along with this program; if not, write to the
++# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
++# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
++# source code or documentation are not subject to the GNU General Public
++# License and may only be used or replicated with the express permission of
++# Red Hat, Inc.
++#
++
++from __future__ import absolute_import
++from __future__ import unicode_literals
++
++import argparse
++import gzip
++import libcomps
++import os
++import re
++import shutil
++import tempfile
++
++from dnfpluginscore import _, logger
++import dnf
++import dnf.cli
++
++
++RE_GROUP_ID_VALID = '-a-z0-9_.:'
++RE_GROUP_ID = re.compile(r'^[{}]+$'.format(RE_GROUP_ID_VALID))
++RE_LANG = re.compile(r'^[-a-zA-Z0-9_.@]+$')
++COMPS_XML_OPTIONS = {
++    'default_explicit': True,
++    'uservisible_explicit': True,
++    'empty_groups': True}
++
++
++def group_id_type(value):
++    '''group id validator'''
++    if not RE_GROUP_ID.match(value):
++        raise argparse.ArgumentTypeError(_('Invalid group id'))
++    return value
++
++
++def translation_type(value):
++    '''translated texts validator'''
++    data = value.split(':', 2)
++    if len(data) != 2:
++        raise argparse.ArgumentTypeError(
++            _("Invalid translated data, should be in form 'lang:text'"))
++    lang, text = data
++    if not RE_LANG.match(lang):
++        raise argparse.ArgumentTypeError(_('Invalid/empty language for translated data'))
++    return lang, text
++
++
++def text_to_id(text):
++    '''generate group id based on its name'''
++    group_id = text.lower()
++    group_id = re.sub('[^{}]'.format(RE_GROUP_ID_VALID), '', group_id)
++    if not group_id:
++        raise dnf.cli.CliError(
++            _("Can't generate group id from '{}'. Please specify group id using --id.").format(
++                text))
++    return group_id
++
++
++@dnf.plugin.register_command
++class GroupsManagerCommand(dnf.cli.Command):
++    aliases = ('groups-manager',)
++    summary = _('create and edit groups metadata file')
++
++    def __init__(self, cli):
++        super(GroupsManagerCommand, self).__init__(cli)
++        self.comps = libcomps.Comps()
++
++    @staticmethod
++    def set_argparser(parser):
++        # input / output options
++        parser.add_argument('--load', action='append', default=[],
++                            metavar='COMPS.XML',
++                            help=_('load groups metadata from file'))
++        parser.add_argument('--save', action='append', default=[],
++                            metavar='COMPS.XML',
++                            help=_('save groups metadata to file'))
++        parser.add_argument('--merge', metavar='COMPS.XML',
++                            help=_('load and save groups metadata to file'))
++        parser.add_argument('--print', action='store_true', default=False,
++                            help=_('print the result metadata to stdout'))
++        # group options
++        parser.add_argument('--id', type=group_id_type,
++                            help=_('group id'))
++        parser.add_argument('-n', '--name', help=_('group name'))
++        parser.add_argument('--description',
++                            help=_('group description'))
++        parser.add_argument('--display-order', type=int,
++                            help=_('group display order'))
++        parser.add_argument('--translated-name', action='append', default=[],
++                            metavar='LANG:TEXT', type=translation_type,
++                            help=_('translated name for the group'))
++        parser.add_argument('--translated-description', action='append', default=[],
++                            metavar='LANG:TEXT', type=translation_type,
++                            help=_('translated description for the group'))
++        visible = parser.add_mutually_exclusive_group()
++        visible.add_argument('--user-visible', dest='user_visible', action='store_true',
++                             default=None,
++                             help=_('make the group user visible (default)'))
++        visible.add_argument('--not-user-visible', dest='user_visible', action='store_false',
++                             default=None,
++                             help=_('make the group user invisible'))
++
++        # package list options
++        section = parser.add_mutually_exclusive_group()
++        section.add_argument('--mandatory', action='store_true',
++                             help=_('add packages to the mandatory section'))
++        section.add_argument('--optional', action='store_true',
++                             help=_('add packages to the optional section'))
++        section.add_argument('--remove', action='store_true', default=False,
++                             help=_('remove packages from the group instead of adding them'))
++        parser.add_argument('--dependencies', action='store_true',
++                            help=_('include also direct dependencies for packages'))
++
++        parser.add_argument("packages", nargs='*', metavar='PACKAGE',
++                            help=_('package specification'))
++
++    def configure(self):
++        demands = self.cli.demands
++
++        if self.opts.packages:
++            demands.sack_activation = True
++            demands.available_repos = True
++            demands.load_system_repo = False
++
++        # handle --merge option (shortcut to --load and --save the same file)
++        if self.opts.merge:
++            self.opts.load.insert(0, self.opts.merge)
++            self.opts.save.append(self.opts.merge)
++
++        # check that group is specified when editing is attempted
++        if (self.opts.description
++                or self.opts.display_order
++                or self.opts.translated_name
++                or self.opts.translated_description
++                or self.opts.user_visible is not None
++                or self.opts.packages):
++            if not self.opts.id and not self.opts.name:
++                raise dnf.cli.CliError(
++                    _("Can't edit group without specifying it (use --id or --name)"))
++
++    def load_input_files(self):
++        """
++        Loads all input xml files.
++        Returns True if at least one file was successfuly loaded
++        """
++        for file_name in self.opts.load:
++            file_comps = libcomps.Comps()
++            try:
++                if file_name.endswith('.gz'):
++                    # libcomps does not support gzipped files - decompress to temporary
++                    # location
++                    with gzip.open(file_name) as gz_file:
++                        temp_file = tempfile.NamedTemporaryFile(delete=False)
++                        try:
++                            shutil.copyfileobj(gz_file, temp_file)
++                            # close temp_file to ensure the content is flushed to disk
++                            temp_file.close()
++                            file_comps.fromxml_f(temp_file.name)
++                        finally:
++                            os.unlink(temp_file.name)
++                else:
++                    file_comps.fromxml_f(file_name)
++            except (IOError, OSError, libcomps.ParserError) as err:
++                # gzip module raises OSError on reading from malformed gz file
++                # get_last_errors() output often contains duplicit lines, remove them
++                seen = set()
++                for error in file_comps.get_last_errors():
++                    if error in seen:
++                        continue
++                    logger.error(error.strip())
++                    seen.add(error)
++                raise dnf.exceptions.Error(
++                    _("Can't load file \"{}\": {}").format(file_name, err))
++            else:
++                self.comps += file_comps
++
++    def save_output_files(self):
++        for file_name in self.opts.save:
++            try:
++                # xml_f returns a list of errors / log entries
++                errors = self.comps.xml_f(file_name, xml_options=COMPS_XML_OPTIONS)
++            except libcomps.XMLGenError as err:
++                errors = [err]
++            if errors:
++                # xml_f() method could return more than one error. In this case
++                # raise the latest of them and log the others.
++                for err in errors[:-1]:
++                    logger.error(err.strip())
++                raise dnf.exceptions.Error(_("Can't save file \"{}\": {}").format(
++                    file_name, errors[-1].strip()))
++
++
++    def find_group(self, group_id, name):
++        '''
++        Try to find group according to command line parameters - first by id
++        then by name.
++        '''
++        group = None
++        if group_id:
++            for grp in self.comps.groups:
++                if grp.id == group_id:
++                    group = grp
++                    break
++        if group is None and name:
++            for grp in self.comps.groups:
++                if grp.name == name:
++                    group = grp
++                    break
++        return group
++
++    def edit_group(self, group):
++        '''
++        Set attributes and package lists for selected group
++        '''
++        def langlist_to_strdict(lst):
++            str_dict = libcomps.StrDict()
++            for lang, text in lst:
++                str_dict[lang] = text
++            return str_dict
++
++        # set group attributes
++        if self.opts.name:
++            group.name = self.opts.name
++        if self.opts.description:
++            group.desc = self.opts.description
++        if self.opts.display_order:
++            group.display_order = self.opts.display_order
++        if self.opts.user_visible is not None:
++            group.uservisible = self.opts.user_visible
++        if self.opts.translated_name:
++            group.name_by_lang = langlist_to_strdict(self.opts.translated_name)
++        if self.opts.translated_description:
++            group.desc_by_lang = langlist_to_strdict(self.opts.translated_description)
++
++        # edit packages list
++        if self.opts.packages:
++            # find packages according to specifications from command line
++            packages = set()
++            for pkg_spec in self.opts.packages:
++                q = self.base.sack.query().filterm(name__glob=pkg_spec).latest()
++                if not q:
++                    logger.warning(_("No match for argument: {}").format(pkg_spec))
++                    continue
++                packages.update(q)
++            if self.opts.dependencies:
++                # add packages that provide requirements
++                requirements = set()
++                for pkg in packages:
++                    requirements.update(pkg.requires)
++                packages.update(self.base.sack.query().filterm(provides=requirements))
++
++            pkg_names = {pkg.name for pkg in packages}
++
++            if self.opts.remove:
++                for pkg_name in pkg_names:
++                    for pkg in group.packages_match(name=pkg_name,
++                                                    type=libcomps.PACKAGE_TYPE_UNKNOWN):
++                        group.packages.remove(pkg)
++            else:
++                if self.opts.mandatory:
++                    pkg_type = libcomps.PACKAGE_TYPE_MANDATORY
++                elif self.opts.optional:
++                    pkg_type = libcomps.PACKAGE_TYPE_OPTIONAL
++                else:
++                    pkg_type = libcomps.PACKAGE_TYPE_DEFAULT
++                for pkg_name in sorted(pkg_names):
++                    if not group.packages_match(name=pkg_name, type=pkg_type):
++                        group.packages.append(libcomps.Package(name=pkg_name, type=pkg_type))
++
++    def run(self):
++        self.load_input_files()
++
++        if self.opts.id or self.opts.name:
++            # we are adding / editing a group
++            group = self.find_group(group_id=self.opts.id, name=self.opts.name)
++            if group is None:
++                # create a new group
++                if self.opts.remove:
++                    raise dnf.exceptions.Error(_("Can't remove packages from non-existent group"))
++                group = libcomps.Group()
++                if self.opts.id:
++                    group.id = self.opts.id
++                    group.name = self.opts.id
++                elif self.opts.name:
++                    group_id = text_to_id(self.opts.name)
++                    if self.find_group(group_id=group_id, name=None):
++                        raise dnf.cli.CliError(
++                            _("Group id '{}' generated from '{}' is duplicit. "
++                              "Please specify group id using --id.").format(
++                                  group_id, self.opts.name))
++                    group.id = group_id
++                self.comps.groups.append(group)
++            self.edit_group(group)
++
++        self.save_output_files()
++        if self.opts.print or (not self.opts.save):
++            print(self.comps.xml_str(xml_options=COMPS_XML_OPTIONS))
+-- 
+2.26.2
+
diff --git a/SOURCES/0002-needs-restarting-add-s-to-list-services-RhBug-177293.patch b/SOURCES/0002-needs-restarting-add-s-to-list-services-RhBug-177293.patch
new file mode 100644
index 0000000..b832730
--- /dev/null
+++ b/SOURCES/0002-needs-restarting-add-s-to-list-services-RhBug-177293.patch
@@ -0,0 +1,138 @@
+From b2a912724d737ca7ac4350885b54117f5e043046 Mon Sep 17 00:00:00 2001
+From: Nicola Sella <nsella@redhat.com>
+Date: Thu, 5 Mar 2020 12:45:39 +0100
+Subject: [PATCH 2/2] [needs-restarting] add -s to list services
+ (RhBug:1772939)
+
+= changelog =
+msg:           [needs-restarting] add -s to list services (RhBug:1772939)
+type:          bugfix
+resolves:      https://bugzilla.redhat.com/show_bug.cgi?id=1772939
+
+Closes: #395
+Approved by: kontura
+---
+ dnf-plugins-core.spec       |  6 ++++++
+ doc/needs_restarting.rst    |  3 +++
+ plugins/needs_restarting.py | 33 +++++++++++++++++++++++++++++++++
+ 3 files changed, 42 insertions(+)
+
+diff --git a/dnf-plugins-core.spec b/dnf-plugins-core.spec
+index 42d0884..012dde8 100644
+--- a/dnf-plugins-core.spec
++++ b/dnf-plugins-core.spec
+@@ -99,8 +99,10 @@ Summary:        Core Plugins for DNF
+ %{?python_provide:%python_provide python2-%{name}}
+ BuildRequires:  python2-dnf >= %{dnf_lowest_compatible}
+ %if 0%{?rhel} && 0%{?rhel} <= 7
++BuildRequires:  dbus-python
+ BuildRequires:  python-nose
+ %else
++BuildRequires:  python2-dbus
+ BuildRequires:  python2-nose
+ %endif
+ BuildRequires:  python2-devel
+@@ -110,8 +112,10 @@ Requires:       python2-distro
+ Requires:       python2-dnf >= %{dnf_lowest_compatible}
+ Requires:       python2-hawkey >= %{hawkey_version}
+ %if 0%{?rhel} && 0%{?rhel} <= 7
++Requires:       dbus-python
+ Requires:       python-dateutil
+ %else
++Requires:       python2-dbus
+ Requires:       python2-dateutil
+ %endif
+ Provides:       python2-dnf-plugins-extras-debug = %{version}-%{release}
+@@ -140,12 +144,14 @@ Additionally provides generate_completion_cache passive plugin.
+ %package -n python3-%{name}
+ Summary:    Core Plugins for DNF
+ %{?python_provide:%python_provide python3-%{name}}
++BuildRequires:  python3-dbus
+ BuildRequires:  python3-devel
+ BuildRequires:  python3-dnf >= %{dnf_lowest_compatible}
+ BuildRequires:  python3-nose
+ %if 0%{?fedora}
+ Requires:       python3-distro
+ %endif
++Requires:       python3-dbus
+ Requires:       python3-dnf >= %{dnf_lowest_compatible}
+ Requires:       python3-hawkey >= %{hawkey_version}
+ Requires:       python3-dateutil
+diff --git a/doc/needs_restarting.rst b/doc/needs_restarting.rst
+index e79b43f..1a3fbbe 100644
+--- a/doc/needs_restarting.rst
++++ b/doc/needs_restarting.rst
+@@ -48,3 +48,6 @@ All general DNF options are accepted, see `Options` in :manpage:`dnf(8)` for det
+ ``-r, --reboothint``
+ 
+     Only report whether a reboot is required (exit code 1) or not (exit code 0).
++
++``-s, --services``
++    Only list the affected systemd services.
+diff --git a/plugins/needs_restarting.py b/plugins/needs_restarting.py
+index 69203f4..f6bf525 100644
+--- a/plugins/needs_restarting.py
++++ b/plugins/needs_restarting.py
+@@ -29,6 +29,7 @@ from dnfpluginscore import logger, _
+ 
+ import dnf
+ import dnf.cli
++import dbus
+ import functools
+ import os
+ import re
+@@ -126,6 +127,30 @@ def print_cmd(pid):
+     print('%d : %s' % (pid, command))
+ 
+ 
++def get_service_dbus(pid):
++    bus = dbus.SystemBus()
++    systemd_manager_object = bus.get_object(
++        'org.freedesktop.systemd1',
++        '/org/freedesktop/systemd1'
++    )
++    systemd_manager_interface = dbus.Interface(
++        systemd_manager_object,
++        'org.freedesktop.systemd1.Manager'
++    )
++    service_proxy = bus.get_object(
++        'org.freedesktop.systemd1',
++        systemd_manager_interface.GetUnitByPID(pid)
++    )
++    service_properties = dbus.Interface(
++        service_proxy, dbus_interface="org.freedesktop.DBus.Properties")
++    name = service_properties.Get(
++        "org.freedesktop.systemd1.Unit",
++        'Id'
++    )
++    if name.endswith(".service"):
++        return name
++    return
++
+ def smap2opened_file(pid, line):
+     slash = line.find('/')
+     if slash < 0:
+@@ -205,6 +230,8 @@ class NeedsRestartingCommand(dnf.cli.Command):
+         parser.add_argument('-r', '--reboothint', action='store_true',
+                             help=_("only report whether a reboot is required "
+                                    "(exit code 1) or not (exit code 0)"))
++        parser.add_argument('-s', '--services', action='store_true',
++                            help=_("only report affected systemd services"))
+ 
+     def configure(self):
+         demands = self.cli.demands
+@@ -251,5 +278,11 @@ class NeedsRestartingCommand(dnf.cli.Command):
+             if pkg.installtime > process_start(ofile.pid):
+                 stale_pids.add(ofile.pid)
+ 
++        if self.opts.services:
++            names = set([get_service_dbus(pid) for pid in sorted(stale_pids)])
++            for name in names:
++                if name is not None:
++                    print(name)
++            return 0
+         for pid in sorted(stale_pids):
+             print_cmd(pid)
+-- 
+2.26.2
+
diff --git a/SPECS/dnf-plugins-core.spec b/SPECS/dnf-plugins-core.spec
index 597d188..d2ed2c9 100644
--- a/SPECS/dnf-plugins-core.spec
+++ b/SPECS/dnf-plugins-core.spec
@@ -32,11 +32,13 @@
 
 Name:           dnf-plugins-core
 Version:        4.0.18
-Release:        1%{?dist}
+Release:        2%{?dist}
 Summary:        Core Plugins for DNF
 License:        GPLv2+
 URL:            https://github.com/rpm-software-management/dnf-plugins-core
 Source0:        %{url}/archive/%{version}/%{name}-%{version}.tar.gz
+Patch1:         0001-groups-manager-Re-introduce-yum-groups-manager-funct.patch
+Patch2:         0002-needs-restarting-add-s-to-list-services-RhBug-177293.patch
 BuildArch:      noarch
 BuildRequires:  cmake
 BuildRequires:  gettext
@@ -56,6 +58,7 @@ Provides:       dnf-command(debug-dump)
 Provides:       dnf-command(debug-restore)
 Provides:       dnf-command(debuginfo-install)
 Provides:       dnf-command(download)
+Provides:       dnf-command(groups-manager)
 Provides:       dnf-command(repoclosure)
 Provides:       dnf-command(repograph)
 Provides:       dnf-command(repomanage)
@@ -71,6 +74,7 @@ Provides:       dnf-plugin-debuginfo-install = %{version}-%{release}
 Provides:       dnf-plugin-download = %{version}-%{release}
 Provides:       dnf-plugin-generate_completion_cache = %{version}-%{release}
 Provides:       dnf-plugin-needs_restarting = %{version}-%{release}
+Provides:       dnf-plugin-groups-manager = %{version}-%{release}
 Provides:       dnf-plugin-repoclosure = %{version}-%{release}
 Provides:       dnf-plugin-repodiff = %{version}-%{release}
 Provides:       dnf-plugin-repograph = %{version}-%{release}
@@ -85,7 +89,7 @@ Conflicts:      dnf-plugins-extras-common-data < %{dnf_plugins_extra}
 
 %description
 Core Plugins for DNF. This package enhances DNF with builddep, config-manager,
-copr, debug, debuginfo-install, download, needs-restarting, repoclosure,
+copr, debug, debuginfo-install, download, groups-manager, needs-restarting, repoclosure,
 repograph, repomanage, reposync, changelog and repodiff commands. Additionally
 provides generate_completion_cache passive plugin.
 
@@ -95,8 +99,10 @@ Summary:        Core Plugins for DNF
 %{?python_provide:%python_provide python2-%{name}}
 BuildRequires:  python2-dnf >= %{dnf_lowest_compatible}
 %if 0%{?rhel} && 0%{?rhel} <= 7
+BuildRequires:  dbus-python
 BuildRequires:  python-nose
 %else
+BuildRequires:  python2-dbus
 BuildRequires:  python2-nose
 %endif
 BuildRequires:  python2-devel
@@ -106,8 +112,10 @@ Requires:       python2-distro
 Requires:       python2-dnf >= %{dnf_lowest_compatible}
 Requires:       python2-hawkey >= %{hawkey_version}
 %if 0%{?rhel} && 0%{?rhel} <= 7
+Requires:       dbus-python
 Requires:       python-dateutil
 %else
+Requires:       python2-dbus
 Requires:       python2-dateutil
 %endif
 Provides:       python2-dnf-plugins-extras-debug = %{version}-%{release}
@@ -126,7 +134,7 @@ Conflicts:      python-%{name} < %{version}-%{release}
 
 %description -n python2-%{name}
 Core Plugins for DNF, Python 2 interface. This package enhances DNF with builddep,
-config-manager, copr, degug, debuginfo-install, download, needs-restarting,
+config-manager, copr, degug, debuginfo-install, download, groups-manager, needs-restarting,
 repoclosure, repograph, repomanage, reposync, changelog and repodiff commands.
 Additionally provides generate_completion_cache passive plugin.
 %endif
@@ -135,12 +143,14 @@ Additionally provides generate_completion_cache passive plugin.
 %package -n python3-%{name}
 Summary:    Core Plugins for DNF
 %{?python_provide:%python_provide python3-%{name}}
+BuildRequires:  python3-dbus
 BuildRequires:  python3-devel
 BuildRequires:  python3-dnf >= %{dnf_lowest_compatible}
 BuildRequires:  python3-nose
 %if 0%{?fedora}
 Requires:       python3-distro
 %endif
+Requires:       python3-dbus
 Requires:       python3-dnf >= %{dnf_lowest_compatible}
 Requires:       python3-hawkey >= %{hawkey_version}
 Requires:       python3-dateutil
@@ -160,7 +170,7 @@ Conflicts:      python-%{name} < %{version}-%{release}
 
 %description -n python3-%{name}
 Core Plugins for DNF, Python 3 interface. This package enhances DNF with builddep,
-config-manager, copr, debug, debuginfo-install, download, needs-restarting,
+config-manager, copr, debug, debuginfo-install, download, groups-manager, needs-restarting,
 repoclosure, repograph, repomanage, reposync, changelog and repodiff commands.
 Additionally provides generate_completion_cache passive plugin.
 %endif
@@ -188,8 +198,8 @@ Summary:        Yum-utils CLI compatibility layer
 %description -n %{yum_utils_subpackage_name}
 As a Yum-utils CLI compatibility layer, supplies in CLI shims for
 debuginfo-install, repograph, package-cleanup, repoclosure, repomanage,
-repoquery, reposync, repotrack, repodiff, builddep, config-manager, debug
-and download that use new implementations using DNF.
+repoquery, reposync, repotrack, repodiff, builddep, config-manager, debug,
+download and yum-groups-manager that use new implementations using DNF.
 %endif
 
 %if 0%{?rhel} == 0 && %{with python2}
@@ -456,6 +466,7 @@ ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-builddep
 ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-config-manager
 ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-debug-dump
 ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-debug-restore
+ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-groups-manager
 ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yumdownloader
 # These commands don't have a dedicated man page, so let's just point them
 # to the utils page which contains their descriptions.
@@ -481,6 +492,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
 %{_mandir}/man8/dnf-debuginfo-install.*
 %{_mandir}/man8/dnf-download.*
 %{_mandir}/man8/dnf-generate_completion_cache.*
+%{_mandir}/man8/dnf-groups-manager.*
 %{_mandir}/man8/dnf-needs-restarting.*
 %{_mandir}/man8/dnf-repoclosure.*
 %{_mandir}/man8/dnf-repodiff.*
@@ -511,6 +523,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
 %{python2_sitelib}/dnf-plugins/debuginfo-install.*
 %{python2_sitelib}/dnf-plugins/download.*
 %{python2_sitelib}/dnf-plugins/generate_completion_cache.*
+%{python2_sitelib}/dnf-plugins/groups_manager.*
 %{python2_sitelib}/dnf-plugins/needs_restarting.*
 %{python2_sitelib}/dnf-plugins/repoclosure.*
 %{python2_sitelib}/dnf-plugins/repodiff.*
@@ -536,6 +549,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
 %{python3_sitelib}/dnf-plugins/debuginfo-install.py
 %{python3_sitelib}/dnf-plugins/download.py
 %{python3_sitelib}/dnf-plugins/generate_completion_cache.py
+%{python3_sitelib}/dnf-plugins/groups_manager.py
 %{python3_sitelib}/dnf-plugins/needs_restarting.py
 %{python3_sitelib}/dnf-plugins/repoclosure.py
 %{python3_sitelib}/dnf-plugins/repodiff.py
@@ -550,6 +564,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
 %{python3_sitelib}/dnf-plugins/__pycache__/debuginfo-install.*
 %{python3_sitelib}/dnf-plugins/__pycache__/download.*
 %{python3_sitelib}/dnf-plugins/__pycache__/generate_completion_cache.*
+%{python3_sitelib}/dnf-plugins/__pycache__/groups_manager.*
 %{python3_sitelib}/dnf-plugins/__pycache__/needs_restarting.*
 %{python3_sitelib}/dnf-plugins/__pycache__/repoclosure.*
 %{python3_sitelib}/dnf-plugins/__pycache__/repodiff.*
@@ -577,6 +592,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
 %{_bindir}/yum-config-manager
 %{_bindir}/yum-debug-dump
 %{_bindir}/yum-debug-restore
+%{_bindir}/yum-groups-manager
 %{_bindir}/yumdownloader
 %{_mandir}/man1/debuginfo-install.*
 %{_mandir}/man1/needs-restarting.*
@@ -589,6 +605,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
 %{_mandir}/man1/yum-config-manager.*
 %{_mandir}/man1/yum-debug-dump.*
 %{_mandir}/man1/yum-debug-restore.*
+%{_mandir}/man1/yum-groups-manager.*
 %{_mandir}/man1/yumdownloader.*
 %{_mandir}/man1/package-cleanup.*
 %{_mandir}/man1/dnf-utils.*
@@ -610,6 +627,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
 %exclude %{_mandir}/man1/yum-config-manager.*
 %exclude %{_mandir}/man1/yum-debug-dump.*
 %exclude %{_mandir}/man1/yum-debug-restore.*
+%exclude %{_mandir}/man1/yum-groups-manager.*
 %exclude %{_mandir}/man1/yumdownloader.*
 %exclude %{_mandir}/man1/package-cleanup.*
 %exclude %{_mandir}/man1/dnf-utils.*
@@ -742,6 +760,10 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
 %endif
 
 %changelog
+* Tue Dec 8 2020 Marek Blaha <mblaha@redhat.com> - 4.0.18-2
+- Introduce groups-manager plugin (RhBug:1826016)
+- [needs-restarting] add -s to list services (RhBug:1772939)
+
 * Tue Nov 10 2020 Nicola Sella <nsella@redhat.com> - 4.0.18-1
 - Update to 4.0.18
 - [needs-restarting] Fix plugin fail if needs-restarting.d does not exist