# DNF Use Cases

## Introduction

Every feature present in DNF should be based on a reasonable use case. All the supported use cases are supposed to be enumerated in this document.

In case you use DNF to achieve a goal which is not documented here, either you found an error in the documentation or you misuse DNF. In either case we would appreciate if you share the case with us so we can help you to use DNF in the correct way or add the case to the list. You can only benefit from such a report because then you can be sure that the behavior that you expect will not change without prior notice in the DNF Release Notes and that the behavior will be covered by our test suite.

Important

Please consult every usage of DNF with our reference documentation to be sure what are you doing. The examples mentioned here are supposed to be as simple as possible and may ignore some minor corner cases.

Warning

The list is not complete yet - the use cases are being added incrementally these days.

## General assumptions

The user in question must have the appropriate permissions.

## Ensure that my system contains given mix of features (packages/files/providers/groups)

A system administrator has a list of features that has to be present in an operating system. The features must be provided by RPM packages in system repositories that must be accessible.

A feature may be for example a concrete version of a package (`hawkey-0.5.3-1.fc21.i686`), a pathname of a binary RPM file (`/var/lib/mock/fedora-21-i386/result/hawkey-0.5.3-2.20150116gitd002c90.fc21.i686.rpm`), an URL of a binary RPM file (`http://jenkins.cloud.fedoraproject.org/job/DNF/lastSuccessfulBuild/artifact/fedora-21-i386-build/hawkey-0.5.3-99.649.20150116gitd002c90233fc96893806836a258f14a50ee0cf47.fc21.i686.rpm`), a configuration file (`/etc/yum.repos.d/fedora-rawhide.repo`), a language interpreter (`ruby(runtime_executable)`), an extension (`python3-dnf`), a support for building modules for the current running kernel (`kernel-devel-uname-r = $(uname -r)`), an executable (`*/binaryname`) or a collection of packages specified by any available identifier (`kde-desktop`).

The most recent packages that provide the missing features and suit installation (that are not obsoleted and do not conflict with each other or with the other installed packages) are installed if the given feature is not present already. If any of the packages cannot be installed, the operation fails.

### CLI

    SPECS="hawkey-0.5.3-1.fc21.i686 @kde-desktop"  # Set the features here.

    dnf install $SPECS

### Plugins/CLI API

    """A plugin that ensures that given features are present."""

    import dnf.cli
    from dnf.i18n import _
    from dnf.cli.option_parser import OptionParser

    # The parent class allows registration to the CLI manager.
    class Command(dnf.cli.Command):

        """A command that ensures that given features are present."""

        # An alias is needed to invoke the command from command line.
        aliases = ['foo']  # <-- SET YOUR ALIAS HERE.

        def configure(self):
            """Setup the demands."""
            # Repositories are needed if we want to install anything.
            self.cli.demands.available_repos = True
            # A sack is required by marking methods and dependency resolving.
            self.cli.demands.sack_activation = True
            # Resolving performs a transaction that installs the packages.
            self.cli.demands.resolving = True
            # Based on the system, privileges are required to do an installation.
            self.cli.demands.root_user = True  # <-- SET YOUR FLAG HERE.

        @staticmethod
        def set_argparser(parser):
            """Parse command line arguments."""
            parser.add_argument('package', nargs='+', metavar=_('PACKAGE'),
                                action=OptionParser.ParseSpecGroupFileCallback,
                                help=_('Package to install'))

        def run(self):
            """Run the command."""
            # Feature marking methods set the user request.
            for ftr_spec in self.opts.pkg_specs:
                try:
                    self.base.install(ftr_spec)
                except dnf.exceptions.MarkingError:
                    raise dnf.exceptions.Error('feature(s) not found: ' + ftr_spec)
            # Package marking methods set the user request.
            for pkg in self.base.add_remote_rpms(self.opts.filenames, strict=False):
                try:
                    self.base.package_install(pkg, strict=False)
                except dnf.exceptions.MarkingError as e:
                    raise dnf.exceptions.Error(e)
            # Comps data reading initializes the base.comps attribute.
            if self.opts.grp_specs:
                self.base.read_comps(arch_filter=True)
            # Group marking methods set the user request.
            for grp_spec in self.opts.grp_specs:
                group = self.base.comps.group_by_pattern(grp_spec)
                if not group:
                    raise dnf.exceptions.Error('group not found: ' + grp_spec)
                self.base.group_install(group.id, ['mandatory', 'default'])

    # Every plugin must be a subclass of dnf.Plugin.
    class Plugin(dnf.Plugin):

        """A plugin that registers our custom command."""

        # Every plugin must provide its name.
        name = 'foo'  # <-- SET YOUR NAME HERE.

        # Every plugin must provide its own initialization function.
        def __init__(self, base, cli):
            """Initialize the plugin."""
            super(Plugin, self).__init__(base, cli)
            if cli:
                cli.register_command(Command)

If it makes any sense, the plugin can do the operation in appropriate hooks instead of registering a new command that needs to be called from the command line.

### Extension API

    """An extension that ensures that given features are present."""

    import sys

    import dnf
    import dnf.module
    import dnf.rpm

    if __name__ == '__main__':
        FTR_SPECS = {'acpi-1.7-10.fc29.x86_64'}  # <-- SET YOUR FEATURES HERE.
        RPM_SPECS = {'./acpi-1.7-10.fc29.x86_64.rpm'}  # <-- SET YOUR RPMS HERE.
        GRP_SPECS = {'kde-desktop'}  # <-- SET YOUR GROUPS HERE.
        MODULE_SPEC = {"nodejs:10/default"}  # <-- SET YOUR MODULES HERE.

        with dnf.Base() as base:
            # Substitutions are needed for correct interpretation of repo files.
            RELEASEVER, MAJOR, MINOR = dnf.rpm.detect_releasever(base.conf.installroot)
            base.conf.substitutions['releasever'] = RELEASEVER
            if MAJOR is not None:
                base.conf.substitutions['releasever_major'] = MAJOR
            if MINOR is not None:
                base.conf.substitutions['releasever_minor'] = MINOR
            # Repositories are needed if we want to install anything.
            base.read_all_repos()
            # A sack is required by marking methods and dependency resolving.
            base.fill_sack()
            # Feature marking methods set the user request.
            for ftr_spec in FTR_SPECS:
                try:
                    base.install(ftr_spec)
                except dnf.exceptions.MarkingError:
                    sys.exit('Feature(s) cannot be found: ' + ftr_spec)
            # Package marking methods set the user request.
            for pkg in base.add_remote_rpms(RPM_SPECS, strict=False):
                try:
                    base.package_install(pkg, strict=False)
                except dnf.exceptions.MarkingError:
                    sys.exit('RPM cannot be found: ' + pkg)
            # Comps data reading initializes the base.comps attribute.
            if GRP_SPECS:
                base.read_comps(arch_filter=True)
            # Group marking methods set the user request.
            if MODULE_SPEC:
                module_base = dnf.module.module_base.ModuleBase(base)
                module_base.install(MODULE_SPEC, strict=False)
            for grp_spec in GRP_SPECS:
                group = base.comps.group_by_pattern(grp_spec)
                if not group:
                    sys.exit('Group cannot be found: ' + grp_spec)
                base.group_install(group.id, ['mandatory', 'default'])
            # Resolving finds a transaction that allows the packages installation.
            try:
                base.resolve()
            except dnf.exceptions.DepsolveError:
                sys.exit('Dependencies cannot be resolved.')
            # The packages to be installed must be downloaded first.
            try:
                base.download_packages(base.transaction.install_set)
            except dnf.exceptions.DownloadError:
                sys.exit('Required package cannot be downloaded.')
            # The request can finally be fulfilled.
            base.do_transaction()

## Get a list of available packages filtered by their relation to the system

A system user wants to obtain a list of available RPM packages for their consecutive automatic processing or for informative purpose only. The list of RPM packages is filtered by requested relation to the system or user provided <package-name-specs>. The obtained list of packages is based on available data supplied by accessible system repositories.

A relation to the system might be for example one of the following:

installed - packages already installed on the system

available - packages available in any accessible repository

extras - packages installed on the system not available in any known repository

obsoletes - installed packages that are obsoleted by packages in any accessible repository

recent - packages recently added into accessible repositories

upgrades - available packages upgrading some installed packages

### CLI

    dnf list *dnf*
    dnf list installed *debuginfo
    dnf list available gtk*devel
    dnf list extras
    dnf list obsoletes
    dnf list recent
    dnf list upgrades

### Plugins/CLI API

    """A plugin that lists installed packages that are obsoleted by any available package"""

    from dnf.i18n import _
    import dnf
    import dnf.cli

    # If you only plan to create a new dnf subcommand in a plugin
    # you can use @dnf.plugin.register_command decorator instead of creating
    # a Plugin class which only registers the command
    # (for full-fledged Plugin class see examples/install_plugin.py)
    @dnf.plugin.register_command
    class Command(dnf.cli.Command):

        """A command that lists packages installed on the system that are
           obsoleted by packages in any known repository."""

        # An alias is needed to invoke the command from command line.
        aliases = ['foo']  # <-- SET YOUR ALIAS HERE.

        @staticmethod
        def set_argparser(parser):
            parser.add_argument("package", nargs='*', metavar=_('PACKAGE'))

        def configure(self):
            """Setup the demands."""
            # Repositories serve as sources of information about packages.
            self.cli.demands.available_repos = True
            # A sack is needed for querying.
            self.cli.demands.sack_activation = True

        def run(self):
            """Run the command."""

            obs_tuples = []
            # A query matches all available packages
            q = self.base.sack.query()

            if not self.opts.package:
                # Narrow down query to match only installed packages
                inst = q.installed()
                # A dictionary containing list of obsoleted packages
                for new in q.filter(obsoletes=inst):
                    obs_reldeps = new.obsoletes
                    obsoleted = inst.filter(provides=obs_reldeps).run()
                    obs_tuples.extend([(new, old) for old in obsoleted])
            else:
                for pkg_spec in self.opts.package:
                    # A subject serves for parsing package format from user input
                    subj = dnf.subject.Subject(pkg_spec)
                    # A query restricted to installed packages matching given subject
                    inst = subj.get_best_query(self.base.sack).installed()
                    for new in q.filter(obsoletes=inst):
                        obs_reldeps = new.obsoletes
                        obsoleted = inst.filter(provides=obs_reldeps).run()
                        obs_tuples.extend([(new, old) for old in obsoleted])

            if not obs_tuples:
                raise dnf.exceptions.Error('No matching Packages to list')

            for (new, old) in obs_tuples:
                print('%s.%s obsoletes %s.%s' %
                      (new.name, new.arch, old.name, old.arch))

### Extension API

    """An extension that lists installed packages not available
       in any remote repository.
    """

    import dnf

    if __name__ == '__main__':

        with dnf.Base() as base:
            # Repositories serve as sources of information about packages.
            base.read_all_repos()
            # A sack is needed for querying.
            base.fill_sack()

            # A query matches all packages in sack
            q = base.sack.query()

            # Derived query matches only available packages
            q_avail = q.available()
            # Derived query matches only installed packages
            q_inst = q.installed()

            available = q_avail.run()
            for pkg in q_inst.run():
                if pkg not in available:
                    print(str(pkg))