Blame plugins/reposync.py

Packit 3a9065
# reposync.py
Packit 3a9065
# DNF plugin adding a command to download all packages from given remote repo.
Packit 3a9065
#
Packit 3a9065
# Copyright (C) 2014 Red Hat, Inc.
Packit 3a9065
#
Packit 3a9065
# This copyrighted material is made available to anyone wishing to use,
Packit 3a9065
# modify, copy, or redistribute it subject to the terms and conditions of
Packit 3a9065
# the GNU General Public License v.2, or (at your option) any later version.
Packit 3a9065
# This program is distributed in the hope that it will be useful, but WITHOUT
Packit 3a9065
# ANY WARRANTY expressed or implied, including the implied warranties of
Packit 3a9065
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
Packit 3a9065
# Public License for more details.  You should have received a copy of the
Packit 3a9065
# GNU General Public License along with this program; if not, write to the
Packit 3a9065
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
Packit 3a9065
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
Packit 3a9065
# source code or documentation are not subject to the GNU General Public
Packit 3a9065
# License and may only be used or replicated with the express permission of
Packit 3a9065
# Red Hat, Inc.
Packit 3a9065
#
Packit 3a9065
Packit 3a9065
from __future__ import absolute_import
Packit 3a9065
from __future__ import unicode_literals
Packit 3a9065
Packit 3a9065
import hawkey
Packit 3a9065
import os
Packit 3a9065
import shutil
Packit Service d89df3
import types
Packit 3a9065
Packit 3a9065
from dnfpluginscore import _, logger
Packit 3a9065
from dnf.cli.option_parser import OptionParser
Packit 3a9065
import dnf
Packit 3a9065
import dnf.cli
Packit 3a9065
Packit 3a9065
Packit 3a9065
def _pkgdir(intermediate, target):
Packit 3a9065
    cwd = dnf.i18n.ucd(os.getcwd())
Packit 3a9065
    return os.path.realpath(os.path.join(cwd, intermediate, target))
Packit 3a9065
Packit 3a9065
Packit 3a9065
class RPMPayloadLocation(dnf.repo.RPMPayload):
Packit 3a9065
    def __init__(self, pkg, progress, pkg_location):
Packit 3a9065
        super(RPMPayloadLocation, self).__init__(pkg, progress)
Packit 3a9065
        self.package_dir = os.path.dirname(pkg_location)
Packit 3a9065
Packit 3a9065
    def _target_params(self):
Packit 3a9065
        tp = super(RPMPayloadLocation, self)._target_params()
Packit 3a9065
        dnf.util.ensure_dir(self.package_dir)
Packit 3a9065
        tp['dest'] = self.package_dir
Packit 3a9065
        return tp
Packit 3a9065
Packit 3a9065
Packit 3a9065
@dnf.plugin.register_command
Packit 3a9065
class RepoSyncCommand(dnf.cli.Command):
Packit 3a9065
    aliases = ('reposync',)
Packit 3a9065
    summary = _('download all packages from remote repo')
Packit 3a9065
Packit 3a9065
    def __init__(self, cli):
Packit 3a9065
        super(RepoSyncCommand, self).__init__(cli)
Packit 3a9065
Packit 3a9065
    @staticmethod
Packit 3a9065
    def set_argparser(parser):
Packit 3a9065
        parser.add_argument('-a', '--arch', dest='arches', default=[],
Packit 3a9065
                            action=OptionParser._SplitCallback, metavar='[arch]',
Packit 3a9065
                            help=_('download only packages for this ARCH'))
Packit 3a9065
        parser.add_argument('--delete', default=False, action='store_true',
Packit 3a9065
                            help=_('delete local packages no longer present in repository'))
Packit Service 1a5bdb
        parser.add_argument('--download-metadata', default=False, action='store_true',
Packit Service 1a5bdb
                            help=_('download all the metadata.'))
Packit Service d89df3
        parser.add_argument('-g', '--gpgcheck', default=False, action='store_true',
Packit Service d89df3
                            help=_('Remove packages that fail GPG signature checking '
Packit Service d89df3
                                   'after downloading'))
Packit Service d89df3
        parser.add_argument('-m', '--downloadcomps', default=False, action='store_true',
Packit Service d89df3
                            help=_('also download and uncompress comps.xml'))
Packit Service d89df3
        parser.add_argument('--metadata-path',
Packit Service d89df3
                            help=_('where to store downloaded repository metadata. '
Packit Service d89df3
                                   'Defaults to the value of --download-path.'))
Packit 3a9065
        parser.add_argument('-n', '--newest-only', default=False, action='store_true',
Packit 3a9065
                            help=_('download only newest packages per-repo'))
Packit Service 1a5bdb
        parser.add_argument('--norepopath', default=False, action='store_true',
Packit Service 1a5bdb
                            help=_("Don't add the reponame to the download path."))
Packit Service d89df3
        parser.add_argument('-p', '--download-path', default='./',
Packit Service d89df3
                            help=_('where to store downloaded repositories'))
Packit 3a9065
        parser.add_argument('--remote-time', default=False, action='store_true',
Packit 3a9065
                            help=_('try to set local timestamps of local files by '
Packit 3a9065
                                   'the one on the server'))
Packit Service d89df3
        parser.add_argument('--source', default=False, action='store_true',
Packit Service d89df3
                            help=_('operate on source packages'))
Packit 3a9065
        parser.add_argument('-u', '--urls', default=False, action='store_true',
Packit 3a9065
                            help=_("Just list urls of what would be downloaded, "
Packit 3a9065
                                   "don't download"))
Packit 3a9065
Packit 3a9065
    def configure(self):
Packit 3a9065
        demands = self.cli.demands
Packit 3a9065
        demands.available_repos = True
Packit 3a9065
        demands.sack_activation = True
Packit 3a9065
Packit 3a9065
        repos = self.base.repos
Packit 3a9065
Packit 3a9065
        if self.opts.repo:
Packit 3a9065
            repos.all().disable()
Packit 3a9065
            for repoid in self.opts.repo:
Packit 3a9065
                try:
Packit 3a9065
                    repo = repos[repoid]
Packit 3a9065
                except KeyError:
Packit 3a9065
                    raise dnf.cli.CliError("Unknown repo: '%s'." % repoid)
Packit 3a9065
                repo.enable()
Packit 3a9065
Packit 3a9065
        if self.opts.source:
Packit 3a9065
            repos.enable_source_repos()
Packit 3a9065
Packit 3a9065
        if len(list(repos.iter_enabled())) > 1 and self.opts.norepopath:
Packit 3a9065
            raise dnf.cli.CliError(
Packit 3a9065
                _("Can't use --norepopath with multiple repositories"))
Packit 3a9065
Packit 3a9065
        for repo in repos.iter_enabled():
Packit 3a9065
            repo._repo.expire()
Packit 3a9065
            repo.deltarpm = False
Packit 3a9065
Packit 3a9065
    def run(self):
Packit 3a9065
        self.base.conf.keepcache = True
Packit Service d89df3
        gpgcheck_ok = True
Packit 3a9065
        for repo in self.base.repos.iter_enabled():
Packit 3a9065
            if self.opts.remote_time:
Packit 3a9065
                repo._repo.setPreserveRemoteTime(True)
Packit 3a9065
            if self.opts.download_metadata:
Packit 3a9065
                if self.opts.urls:
Packit 3a9065
                    for md_type, md_location in repo._repo.getMetadataLocations():
Packit 3a9065
                        url = repo.remote_location(md_location)
Packit 3a9065
                        if url:
Packit 3a9065
                            print(url)
Packit 3a9065
                        else:
Packit 3a9065
                            msg = _("Failed to get mirror for metadata: %s") % md_type
Packit 3a9065
                            logger.warning(msg)
Packit 3a9065
                else:
Packit 3a9065
                    self.download_metadata(repo)
Packit 3a9065
            if self.opts.downloadcomps:
Packit 3a9065
                if self.opts.urls:
Packit 3a9065
                    mdl = dict(repo._repo.getMetadataLocations())
Packit 3a9065
                    group_locations = [mdl[md_type]
Packit 3a9065
                                       for md_type in ('group', 'group_gz', 'group_gz_zck')
Packit 3a9065
                                       if md_type in mdl]
Packit 3a9065
                    if group_locations:
Packit 3a9065
                        for group_location in group_locations:
Packit 3a9065
                            url = repo.remote_location(group_location)
Packit 3a9065
                            if url:
Packit 3a9065
                                print(url)
Packit 3a9065
                                break
Packit 3a9065
                        else:
Packit 3a9065
                            msg = _("Failed to get mirror for the group file.")
Packit 3a9065
                            logger.warning(msg)
Packit 3a9065
                else:
Packit 3a9065
                    self.getcomps(repo)
Packit 3a9065
            pkglist = self.get_pkglist(repo)
Packit 3a9065
            if self.opts.urls:
Packit 3a9065
                self.print_urls(pkglist)
Packit 3a9065
            else:
Packit 3a9065
                self.download_packages(pkglist)
Packit Service d89df3
                if self.opts.gpgcheck:
Packit Service d89df3
                    for pkg in pkglist:
Packit Service d89df3
                        local_path = self.pkg_download_path(pkg)
Packit Service d89df3
                        # base.package_signature_check uses pkg.localPkg() to determine
Packit Service d89df3
                        # the location of the package rpm file on the disk.
Packit Service d89df3
                        # Set it to the correct download path.
Packit Service d89df3
                        pkg.localPkg  = types.MethodType(
Packit Service d89df3
                            lambda s, local_path=local_path: local_path, pkg)
Packit Service d89df3
                        result, error = self.base.package_signature_check(pkg)
Packit Service d89df3
                        if result != 0:
Packit Service d89df3
                            logger.warning(_("Removing {}: {}").format(
Packit Service d89df3
                                os.path.basename(local_path), error))
Packit Service d89df3
                            os.unlink(local_path)
Packit Service d89df3
                            gpgcheck_ok = False
Packit 3a9065
            if self.opts.delete:
Packit 3a9065
                self.delete_old_local_packages(repo, pkglist)
Packit Service d89df3
        if not gpgcheck_ok:
Packit Service d89df3
            raise dnf.exceptions.Error(_("GPG signature check failed."))
Packit 3a9065
Packit 3a9065
    def repo_target(self, repo):
Packit 3a9065
        return _pkgdir(self.opts.destdir or self.opts.download_path,
Packit 3a9065
                       repo.id if not self.opts.norepopath else '')
Packit 3a9065
Packit 3a9065
    def metadata_target(self, repo):
Packit 3a9065
        if self.opts.metadata_path:
Packit 3a9065
            return _pkgdir(self.opts.metadata_path, repo.id)
Packit 3a9065
        else:
Packit 3a9065
            return self.repo_target(repo)
Packit 3a9065
Packit 3a9065
    def pkg_download_path(self, pkg):
Packit 3a9065
        repo_target = self.repo_target(pkg.repo)
Packit 3a9065
        pkg_download_path = os.path.realpath(
Packit 3a9065
            os.path.join(repo_target, pkg.location))
Packit 3a9065
        # join() ensures repo_target ends with a path separator (otherwise the
Packit 3a9065
        # check would pass if pkg_download_path was a "sibling" path component
Packit 3a9065
        # of repo_target that has the same prefix).
Packit 3a9065
        if not pkg_download_path.startswith(os.path.join(repo_target, '')):
Packit 3a9065
            raise dnf.exceptions.Error(
Packit 3a9065
                _("Download target '{}' is outside of download path '{}'.").format(
Packit 3a9065
                    pkg_download_path, repo_target))
Packit 3a9065
        return pkg_download_path
Packit 3a9065
Packit 3a9065
    def delete_old_local_packages(self, repo, pkglist):
Packit 3a9065
        # delete any *.rpm file under target path, that was not downloaded from repository
Packit 3a9065
        downloaded_files = set(self.pkg_download_path(pkg) for pkg in pkglist)
Packit 3a9065
        for dirpath, dirnames, filenames in os.walk(self.repo_target(repo)):
Packit 3a9065
            for filename in filenames:
Packit 3a9065
                path = os.path.join(dirpath, filename)
Packit 3a9065
                if filename.endswith('.rpm') and os.path.isfile(path):
Packit 3a9065
                    if path not in downloaded_files:
Packit 3a9065
                        # Delete disappeared or relocated file
Packit 3a9065
                        try:
Packit 3a9065
                            os.unlink(path)
Packit 3a9065
                            logger.info(_("[DELETED] %s"), path)
Packit 3a9065
                        except OSError:
Packit 3a9065
                            logger.error(_("failed to delete file %s"), path)
Packit 3a9065
Packit 3a9065
    def getcomps(self, repo):
Packit 3a9065
        comps_fn = repo._repo.getCompsFn()
Packit 3a9065
        if comps_fn:
Packit 3a9065
            dest_path = self.metadata_target(repo)
Packit 3a9065
            dnf.util.ensure_dir(dest_path)
Packit 3a9065
            dest = os.path.join(dest_path, 'comps.xml')
Packit 3a9065
            dnf.yum.misc.decompress(comps_fn, dest=dest)
Packit 3a9065
            logger.info(_("comps.xml for repository %s saved"), repo.id)
Packit 3a9065
Packit 3a9065
    def download_metadata(self, repo):
Packit 3a9065
        repo_target = self.metadata_target(repo)
Packit 3a9065
        repo._repo.downloadMetadata(repo_target)
Packit 3a9065
        return True
Packit 3a9065
Packit 3a9065
    def _get_latest(self, query):
Packit 3a9065
        """
Packit Service 6746a6
        return union of these queries:
Packit Service 6746a6
        - the latest NEVRAs from non-modular packages
Packit Service 6746a6
        - all packages from stream version with the latest package NEVRA
Packit Service 6746a6
          (this should not be needed but the latest package NEVRAs might be
Packit Service 6746a6
          part of an older module version)
Packit Service 6746a6
        - all packages from the latest stream version
Packit 3a9065
        """
Packit 3a9065
        if not dnf.base.WITH_MODULES:
Packit 3a9065
            return query.latest()
Packit Service 6746a6
Packit 3a9065
        query.apply()
Packit 3a9065
        module_packages = self.base._moduleContainer.getModulePackages()
Packit 3a9065
        all_artifacts = set()
Packit 3a9065
        module_dict = {}  # {NameStream: {Version: [modules]}}
Packit Service 6746a6
        artifact_version = {} # {artifact: {NameStream: [Version]}}
Packit 3a9065
        for module_package in module_packages:
Packit Service 6746a6
            artifacts = module_package.getArtifacts()
Packit Service 6746a6
            all_artifacts.update(artifacts)
Packit 3a9065
            module_dict.setdefault(module_package.getNameStream(), {}).setdefault(
Packit 3a9065
                module_package.getVersionNum(), []).append(module_package)
Packit Service 6746a6
            for artifact in artifacts:
Packit Service 6746a6
                artifact_version.setdefault(artifact, {}).setdefault(
Packit Service 6746a6
                    module_package.getNameStream(), []).append(module_package.getVersionNum())
Packit Service 6746a6
Packit Service 6746a6
        # the latest NEVRAs from non-modular packages
Packit Service 6746a6
        latest_query = query.filter(
Packit 3a9065
            pkg__neq=query.filter(nevra_strict=all_artifacts)).latest()
Packit Service 6746a6
Packit Service 6746a6
        # artifacts from the newest version and those versions that contain an artifact
Packit Service 6746a6
        # with the highest NEVRA
Packit Service 6746a6
        latest_stream_artifacts = set()
Packit Service 6746a6
        for namestream, version_dict in module_dict.items():
Packit Service 6746a6
            # versions that will be synchronized
Packit Service 6746a6
            versions = set()
Packit Service 6746a6
            # add the newest stream version
Packit Service 6746a6
            versions.add(sorted(version_dict.keys(), reverse=True)[0])
Packit Service 6746a6
            # collect all artifacts in all stream versions
Packit Service 6746a6
            stream_artifacts = set()
Packit Service 6746a6
            for modules in version_dict.values():
Packit Service 6746a6
                for module in modules:
Packit Service 6746a6
                    stream_artifacts.update(module.getArtifacts())
Packit Service 6746a6
            # find versions to which the packages with the highest NEVRAs belong
Packit Service 6746a6
            for latest_pkg in query.filter(nevra_strict=stream_artifacts).latest():
Packit Service 6746a6
                # here we depend on modules.yaml allways containing full NEVRA (including epoch)
Packit Service 6746a6
                nevra = "{0.name}-{0.epoch}:{0.version}-{0.release}.{0.arch}".format(latest_pkg)
Packit Service 6746a6
                # download only highest version containing the latest artifact
Packit Service 6746a6
                versions.add(max(artifact_version[nevra][namestream]))
Packit Service 6746a6
            # add all artifacts from selected versions for synchronization
Packit Service 6746a6
            for version in versions:
Packit Service 6746a6
                for module in version_dict[version]:
Packit Service 6746a6
                    latest_stream_artifacts.update(module.getArtifacts())
Packit Service 6746a6
        latest_query = latest_query.union(query.filter(nevra_strict=latest_stream_artifacts))
Packit Service 6746a6
Packit Service 6746a6
        return latest_query
Packit 3a9065
Packit 3a9065
    def get_pkglist(self, repo):
Packit 3a9065
        query = self.base.sack.query(flags=hawkey.IGNORE_MODULAR_EXCLUDES).available().filterm(
Packit 3a9065
            reponame=repo.id)
Packit 3a9065
        if self.opts.newest_only:
Packit 3a9065
            query = self._get_latest(query)
Packit 3a9065
        if self.opts.source:
Packit 3a9065
            query.filterm(arch='src')
Packit 3a9065
        elif self.opts.arches:
Packit 3a9065
            query.filterm(arch=self.opts.arches)
Packit 3a9065
        return query
Packit 3a9065
Packit 3a9065
    def download_packages(self, pkglist):
Packit 3a9065
        base = self.base
Packit 3a9065
        progress = base.output.progress
Packit 3a9065
        if progress is None:
Packit 3a9065
            progress = dnf.callback.NullDownloadProgress()
Packit 3a9065
        drpm = dnf.drpm.DeltaInfo(base.sack.query(flags=hawkey.IGNORE_MODULAR_EXCLUDES).installed(),
Packit 3a9065
                                  progress, 0)
Packit 3a9065
        payloads = [RPMPayloadLocation(pkg, progress, self.pkg_download_path(pkg))
Packit 3a9065
                    for pkg in pkglist]
Packit 3a9065
        base._download_remote_payloads(payloads, drpm, progress, None)
Packit 3a9065
Packit 3a9065
    def print_urls(self, pkglist):
Packit 3a9065
        for pkg in pkglist:
Packit 3a9065
            url = pkg.remote_location()
Packit 3a9065
            if url:
Packit 3a9065
                print(url)
Packit 3a9065
            else:
Packit 3a9065
                msg = _("Failed to get mirror for package: %s") % pkg.name
Packit 3a9065
                logger.warning(msg)