Exemplo n.º 1
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A list of verbose error strings
        self.verbose_errors = []

        # Get a DBus payload to use.
        self._payload_proxy = get_payload(self.type)

        self.tx_id = None
        self._install_tree_metadata = None

        self._dnf_manager = DNFManager()
        self._updates_enabled = True

        # Configure the DNF logging.
        configure_dnf_logging()

        # FIXME: Don't call this method before set_from_opts.
        # This will create a default source if there is none.
        self._configure()

        # Protect access to _base.repos to ensure that the dictionary is not
        # modified while another thread is attempting to iterate over it. The
        # lock only needs to be held during operations that change the number
        # of repos or that iterate over the repos.
        self._repos_lock = threading.RLock()

        # save repomd metadata
        self._repoMD_list = []
Exemplo n.º 2
0
    def test_run_nonexistent(self, shutil_mock):
        """Run the CleanUpDownloadLocationTask class for nonexistent location."""
        dnf_manager = DNFManager()
        dnf_manager.set_download_location("/my/nonexistent/path")

        task = CleanUpDownloadLocationTask(dnf_manager)
        task.run()

        shutil_mock.rmtree.assert_not_called()
Exemplo n.º 3
0
    def get_default_environment_test(self, mock_environments):
        """Test the get_default_environment function"""
        mock_environments.return_value = []
        self.assertEqual(get_default_environment(DNFManager()), None)

        mock_environments.return_value = [
            "environment-1",
            "environment-2",
            "environment-3",
        ]
        self.assertEqual(get_default_environment(DNFManager()), "environment-1")
Exemplo n.º 4
0
 def _run_task(self, sysroot, repositories):
     """Run the task."""
     dnf_manager = DNFManager()
     task = WriteRepositoriesTask(
         sysroot=sysroot,
         dnf_manager=dnf_manager,
         repositories=repositories
     )
     task.run()
Exemplo n.º 5
0
    def __init__(self, data):
        super().__init__()
        self.data = data

        # A list of verbose error strings
        self.verbose_errors = []

        # Get a DBus payload to use.
        self._payload_proxy = get_payload(self.type)

        self.tx_id = None

        self._dnf_manager = DNFManager()

        # List of internal mount points.
        self._mount_points = []

        # Configure the DNF logging.
        configure_dnf_logging()
Exemplo n.º 6
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A list of verbose error strings
        self.verbose_errors = []

        # Get a DBus payload to use.
        self._payload_proxy = get_payload(self.type)

        self.tx_id = None

        self._dnf_manager = DNFManager()
        self._updates_enabled = True

        # Configure the DNF logging.
        configure_dnf_logging()

        # FIXME: Don't call this method before set_from_opts.
        # This will create a default source if there is none.
        self._configure()
Exemplo n.º 7
0
    def test_run(self):
        """Run the CleanUpDownloadLocationTask class."""
        dnf_manager = DNFManager()

        with tempfile.TemporaryDirectory() as path:
            # Mock the download location.
            dnf_manager.set_download_location(path)

            # Create files in the download location.
            os.mknod(os.path.join(path, "f1"))
            os.mknod(os.path.join(path, "f2"))
            os.mknod(os.path.join(path, "f3"))

            task = CleanUpDownloadLocationTask(dnf_manager)
            task.run()

            # The files should be deleted.
            assert not os.path.exists(os.path.join(path, "f1"))
            assert not os.path.exists(os.path.join(path, "f2"))
            assert not os.path.exists(os.path.join(path, "f3"))
Exemplo n.º 8
0
    def test_reset_dnf_manager_task(self):
        """Test the ResetDNFManagerTask task."""
        dnf_manager = DNFManager()
        dnf_base = dnf_manager._base

        task = ResetDNFManagerTask(
            dnf_manager=dnf_manager
        )
        task.run()

        assert dnf_base._closed
Exemplo n.º 9
0
class DNFPayload(Payload):
    def __init__(self, data):
        super().__init__()
        self.data = data

        # A list of verbose error strings
        self.verbose_errors = []

        # Get a DBus payload to use.
        self._payload_proxy = get_payload(self.type)

        self.tx_id = None

        self._dnf_manager = DNFManager()

        # List of internal mount points.
        self._mount_points = []

        # Configure the DNF logging.
        configure_dnf_logging()

    @property
    def dnf_manager(self):
        """The DNF manager."""
        return self._dnf_manager

    @property
    def _base(self):
        """Return a DNF base.

        FIXME: This is a temporary property.
        """
        return self._dnf_manager._base

    @property
    def _repos_lock(self):
        """A lock for a dictionary of DNF repositories.

        FIXME: This is a temporary property.
        """
        return self._dnf_manager._lock

    def set_from_opts(self, opts):
        """Set the payload from the Anaconda cmdline options.

        :param opts: a namespace of options
        """
        self._set_source_from_opts(opts)
        self._set_source_configuration_from_opts(opts)
        self._set_additional_repos_from_opts(opts)
        self._generate_driver_disk_repositories()
        self._set_packages_from_opts(opts)

    def _set_source_from_opts(self, opts):
        """Change the source based on the Anaconda options.

        Set the source based on opts.method if it isn't already set
        - opts.method is currently set by command line/boot options.
        """
        if opts.method and (not self.proxy.Sources
                            or self._is_source_default()):
            try:
                source = SourceFactory.parse_repo_cmdline_string(opts.method)
            except PayloadSourceTypeUnrecognized:
                log.error("Unknown method: %s", opts.method)
            else:
                source_proxy = source.create_proxy()
                set_source(self.proxy, source_proxy)

    def _set_source_configuration_from_opts(self, opts):
        """Configure the source based on the Anaconda options."""
        source_proxy = self.get_source_proxy()

        if source_proxy.Type == SOURCE_TYPE_URL:
            # Get the repo configuration.
            repo_configuration = RepoConfigurationData.from_structure(
                source_proxy.RepoConfiguration)

            if opts.proxy:
                repo_configuration.proxy = opts.proxy

            if not conf.payload.verify_ssl:
                repo_configuration.ssl_verification_enabled = conf.payload.verify_ssl

            # Update the repo configuration.
            source_proxy.RepoConfiguration = \
                RepoConfigurationData.to_structure(repo_configuration)

    def _set_additional_repos_from_opts(self, opts):
        """Set additional repositories based on the Anaconda options."""
        for repo_name, repo_url in opts.addRepo:
            try:
                source = SourceFactory.parse_repo_cmdline_string(repo_url)
            except PayloadSourceTypeUnrecognized:
                log.error(
                    "Type for additional repository %s is not recognized!",
                    repo_url)
                return

            if self.get_addon_repo(repo_name):
                log.warning(
                    "Repository name %s is not unique. Only the first "
                    "repo will be used!", repo_name)

            is_supported = source.is_nfs \
                or source.is_http \
                or source.is_https \
                or source.is_ftp \
                or source.is_file \
                or source.is_harddrive

            if not is_supported:
                log.error(
                    "Source type %s for additional repository %s is not supported!",
                    source.source_type.value, repo_url)
                continue

            repo = RepoConfigurationData()
            repo.name = repo_name
            repo.enabled = True
            repo.type = URL_TYPE_BASEURL
            repo.url = repo_url
            repo.installation_enabled = False

            ks_repo = convert_repo_data_to_ks_repo(repo)
            self.data.repo.dataList().append(ks_repo)

    def _generate_driver_disk_repositories(self):
        """Append generated driver disk repositories."""
        for data in generate_driver_disk_repositories():
            ks_repo = convert_repo_data_to_ks_repo(data)
            self.data.repo.dataList().append(ks_repo)

    def _set_packages_from_opts(self, opts):
        """Configure packages based on the Anaconda options."""
        if opts.multiLib:
            configuration = self.get_packages_configuration()
            configuration.multilib_policy = MULTILIB_POLICY_ALL
            self.set_packages_configuration(configuration)

    @property
    def type(self):
        """The DBus type of the payload."""
        return PAYLOAD_TYPE_DNF

    def get_source_proxy(self):
        """Get the DBus proxy of the RPM source.

        The default source for the DNF payload is set via
        the default_source option in the payload section
        of the Anaconda config file.

        :return: a DBus proxy
        """
        return get_source(self.proxy)

    @property
    def source_type(self):
        """The DBus type of the source."""
        source_proxy = self.get_source_proxy()
        return source_proxy.Type

    def get_packages_configuration(self) -> PackagesConfigurationData:
        """Get the DBus data with the packages configuration."""
        return PackagesConfigurationData.from_structure(
            self.proxy.PackagesConfiguration)

    def set_packages_configuration(self, data: PackagesConfigurationData):
        """Set the DBus data with the packages configuration."""
        self.proxy.PackagesConfiguration = \
            PackagesConfigurationData.to_structure(data)

    def get_packages_selection(self) -> PackagesSelectionData:
        """Get the DBus data with the packages selection."""
        return PackagesSelectionData.from_structure(
            self.proxy.PackagesSelection)

    def set_packages_selection(self, data: PackagesSelectionData):
        """Set the DBus data with the packages selection."""
        self.proxy.PackagesSelection = \
            PackagesSelectionData.to_structure(data)

    def is_ready(self):
        """Is the payload ready?"""
        enabled_repos = self._dnf_manager.enabled_repositories

        # If CDN is used as the installation source and we have
        # a subscription attached then any of the enabled repos
        # should be fine as the base repo.
        # If CDN is used but subscription has not been attached
        # there will be no redhat.repo file to parse and we
        # don't need to do anything.
        if self.source_type == SOURCE_TYPE_CDN:
            return self._is_cdn_set_up() and enabled_repos

        # Otherwise, a base repository has to be enabled.
        return any(map(self._is_base_repo, enabled_repos))

    def _is_cdn_set_up(self):
        """Is the CDN source set up?"""
        if not self.source_type == SOURCE_TYPE_CDN:
            return False

        if not is_module_available(SUBSCRIPTION):
            return False

        subscription_proxy = SUBSCRIPTION.get_proxy()
        return subscription_proxy.IsSubscriptionAttached

    def _is_base_repo(self, repo_id):
        """Is it a base repository?"""
        return repo_id == constants.BASE_REPO_NAME \
            or repo_id in constants.DEFAULT_REPOS

    def is_complete(self):
        """Is the payload complete?"""
        return self.source_type not in SOURCE_REPO_FILE_TYPES or self.is_ready(
        )

    def setup(self):
        self.verbose_errors = []

    def unsetup(self):
        self._dnf_manager.reset_base()
        tear_down_sources(self.proxy)

    @property
    def needs_network(self):
        """Test base and additional repositories if they require network."""
        return (self.service_proxy.IsNetworkRequired() or any(
            self._repo_needs_network(repo)
            for repo in self.data.repo.dataList()))

    def _repo_needs_network(self, repo):
        """Returns True if the ksdata repo requires networking."""
        urls = [repo.baseurl]
        if repo.mirrorlist:
            urls.extend(repo.mirrorlist)
        elif repo.metalink:
            urls.extend(repo.metalink)
        return self._source_needs_network(urls)

    def _source_needs_network(self, sources):
        """Return True if the source requires network.

        :param sources: Source paths for testing
        :type sources: list
        :returns: True if any source requires network
        """
        for s in sources:
            if has_network_protocol(s):
                log.debug("Source %s needs network for installation", s)
                return True

        log.debug("Source doesn't require network for installation")
        return False

    def bump_tx_id(self):
        if self.tx_id is None:
            self.tx_id = 1
        else:
            self.tx_id += 1
        return self.tx_id

    def _get_proxy_url(self):
        """Get a proxy of the current source.

        :return: a proxy or None
        """
        source_proxy = self.get_source_proxy()
        source_type = source_proxy.Type

        if source_type != SOURCE_TYPE_URL:
            return None

        data = RepoConfigurationData.from_structure(
            source_proxy.RepoConfiguration)

        return data.proxy

    ###
    # METHODS FOR WORKING WITH REPOSITORIES
    ###

    def get_addon_repo(self, repo_id):
        """Return a ksdata Repo instance matching the specified repo id."""
        repo = None
        for r in self.data.repo.dataList():
            if r.name == repo_id:
                repo = r
                break

        return repo

    def _handle_system_repository(self, data):
        """Handle a system repository.

        The user is trying to do "repo --name=updates" in a kickstart file.
        We can only enable or disable the already existing on-disk repo config.

        :raise: SourceSetupError if the system repository is not available
        """
        try:
            self._dnf_manager.set_repository_enabled(data.name, data.enabled)
        except UnknownRepositoryError:
            msg = "The '{}' repository is not one of the pre-defined repositories."
            raise SourceSetupError(msg.format(data.name)) from None

    def _set_up_additional_repository(self, data):
        """Set up sources for the additional repository."""
        # Check the validity of the repository.
        if not data.url:
            msg = "The '{}' repository has no mirror, baseurl or metalink set."
            raise SourceSetupError(msg.format(data.name)) from None

        # Set up the NFS source with a substituted URL.
        elif data.url.startswith("nfs://"):
            device_mount = self._create_mount_point(constants.MOUNT_DIR,
                                                    data.name + "-nfs-device")
            iso_mount = self._create_mount_point(constants.MOUNT_DIR,
                                                 data.name + "-nfs-iso")
            task = SetUpNFSSourceTask(device_mount=device_mount,
                                      iso_mount=iso_mount,
                                      url=self._dnf_manager.substitute(
                                          data.url))
            mount_point = task.run()
            data.url = "file://" + mount_point

        # Set up the HDD source.
        elif data.url.startswith("hd:"):
            device_mount = self._create_mount_point(ISO_DIR + "-" + data.name +
                                                    "-hdd-device")
            iso_mount = self._create_mount_point(INSTALL_TREE + "-" +
                                                 data.name + "-hdd-iso")

            partition, directory = parse_hdd_url(data.url)

            task = SetUpHardDriveSourceTask(
                device_mount=device_mount,
                iso_mount=iso_mount,
                partition=partition,
                directory=directory,
            )
            result = task.run()
            data.url = "file://" + result.install_tree_path

    def _create_mount_point(self, *paths):
        """Create a mount point from specified paths.

        FIXME: This is a temporary workaround.
        """
        mount_point = join_paths(*paths)
        self._mount_points.append(mount_point)
        return mount_point

    def _tear_down_additional_sources(self):
        """Tear down sources of additional repositories.

        FIXME: This is a temporary workaround.
        """
        while self._mount_points:
            mount_point = self._mount_points.pop()
            task = TearDownMountTask(mount_point)
            task.run()

    @property
    def space_required(self):
        return calculate_required_space(self._dnf_manager)

    def install(self):
        self._progress_cb(0, _('Starting package installation process'))

        # Get the packages configuration and selection data.
        configuration = self.get_packages_configuration()
        selection = self.get_packages_selection()

        # Add the rpm macros to the global transaction environment
        task = SetRPMMacrosTask(configuration)
        task.run()

        try:
            # Resolve packages.
            task = ResolvePackagesTask(self._dnf_manager, selection)
            task.run()
        except NonCriticalInstallationError as e:
            # FIXME: This is a temporary workaround.
            # Allow users to handle the error. If they don't want
            # to continue with the installation, raise a different
            # exception to make sure that we will not run the error
            # handler again.
            if error_handler.cb(e) == ERROR_RAISE:
                raise InstallationError(str(e)) from e

        # Set up the download location.
        task = PrepareDownloadLocationTask(self._dnf_manager)
        task.run()

        # Download the packages.
        task = DownloadPackagesTask(self._dnf_manager)
        task.progress_changed_signal.connect(self._progress_cb)
        task.run()

        # Install the packages.
        task = InstallPackagesTask(self._dnf_manager)
        task.progress_changed_signal.connect(self._progress_cb)
        task.run()

        # Clean up the download location.
        task = CleanUpDownloadLocationTask(self._dnf_manager)
        task.run()

    def _is_source_default(self):
        """Report if the current source type is the default source type.

        NOTE: If no source was set previously a new default one
              will be created.
        """
        return self.source_type == conf.payload.default_source

    def update_base_repo(self, fallback=True, try_media=True):
        """Update the base repository from the DBus source."""
        log.debug("Tearing down sources")
        tear_down_sources(self.proxy)
        self._tear_down_additional_sources()

        log.debug("Preparing the DNF base")
        self.tx_id = None

        self._dnf_manager.clear_cache()
        self._dnf_manager.reset_substitution()
        self._dnf_manager.configure_base(self.get_packages_configuration())
        self._dnf_manager.configure_proxy(self._get_proxy_url())
        self._dnf_manager.dump_configuration()
        self._dnf_manager.read_system_repositories()

        log.info("Configuring the base repo")
        # Find the source and its type.
        source_proxy = self.get_source_proxy()
        source_type = source_proxy.Type

        # Change the default source to CDROM if there is a valid install media.
        # FIXME: Set up the default source earlier.
        if try_media and self._is_source_default(
        ) and find_optical_install_media():
            source_type = SOURCE_TYPE_CDROM
            source_proxy = create_source(source_type)
            set_source(self.proxy, source_proxy)

        # Set up the source.
        set_up_sources(self.proxy)

        # Add a new repo.
        if source_type not in SOURCE_REPO_FILE_TYPES:
            try:
                self._add_base_repository()
            except DNFManagerError as e:
                # Fail if the fallback is not enabled.
                if not fallback:
                    raise e

                # Fallback to the default source
                #
                # This is at the moment CDN on RHEL
                # and closest mirror everywhere else.
                tear_down_sources(self.proxy)

                source_type = conf.payload.default_source
                source_proxy = create_source(source_type)
                set_source(self.proxy, source_proxy)

                set_up_sources(self.proxy)

        # We need to check this again separately in case REPO_FILES were set above.
        if source_type in SOURCE_REPO_FILE_TYPES:
            # Remove all treeinfo repositories.
            self._remove_treeinfo_repositories()

            # If this is a kickstart install, just return now as we normally do not
            # want to read the on media repo files in such a case. On the other hand,
            # the local repo files are a valid use case if the system is subscribed
            # and the CDN is selected as the installation source.
            if flags.automatedInstall and not self._is_cdn_set_up():
                return

            # Otherwise, fall back to the default repos that we disabled above
            self._enable_system_repositories()

        self._include_additional_repositories()
        self._validate_enabled_repositories()

    def _add_base_repository(self):
        """Add the base repository.

        Try to add a valid base repository to the DNF base.

        :raise: DNFManagerError if the repo is invalid
        """
        # Get the repo configuration of the first source.
        data = RepoConfigurationData.from_structure(
            self.proxy.GetRepoConfigurations()[0])

        log.debug("Using the repo configuration: %s", data)
        data.name = constants.BASE_REPO_NAME

        # Process the treeinfo metadata.
        treeinfo_base_repo_url = self._reload_treeinfo_metadata(data)

        if treeinfo_base_repo_url:
            data.type = URL_TYPE_BASEURL
            data.url = treeinfo_base_repo_url

        log.debug("Add the base repository at %s.", data.url)

        try:
            self._dnf_manager.add_repository(data)
            self._dnf_manager.load_repository(data.name)
        except DNFManagerError as e:
            log.error("The base repository is invalid: %s", str(e))
            self._dnf_manager.remove_repository(data.name)
            raise e

    def _enable_system_repositories(self):
        """Enable system repositories.

        * Restore previously disabled system repositories.
        * Enable or disable system repositories based on the current configuration.
        """
        self._dnf_manager.restore_system_repositories()

        log.debug("Enable or disable updates repositories.")
        updates_enabled = self._get_updates_enabled()
        self._set_repositories_enabled(conf.payload.updates_repositories,
                                       updates_enabled)

        log.debug(
            "Disable repositories based on the Anaconda configuration file.")
        self._set_repositories_enabled(conf.payload.disabled_repositories,
                                       False)

        if constants.isFinal:
            log.debug("Disable rawhide repositories.")
            self._set_repositories_enabled(["*rawhide*"], False)

    def _get_updates_enabled(self):
        """Are latest updates enabled?"""
        source_proxy = self.get_source_proxy()
        source_type = source_proxy.Type

        if source_type == SOURCE_TYPE_CLOSEST_MIRROR:
            return source_proxy.UpdatesEnabled
        else:
            return False

    def _set_repositories_enabled(self, patterns, enabled):
        """Enable or disable matching repositories.

        :param patterns: a list of patterns to match the repo ids
        :param enabled: True to enable, False to disable
        """
        repo_ids = set()

        for pattern in patterns:
            repo_ids.update(
                self._dnf_manager.get_matching_repositories(pattern))

        for repo_id in sorted(repo_ids):
            self.dnf_manager.set_repository_enabled(repo_id, enabled)

    def _include_additional_repositories(self):
        """Add additional repositories to DNF."""
        for ks_repo in self.data.repo.dataList():
            # Convert the repository.
            data = convert_ks_repo_to_repo_data(ks_repo)
            log.debug("Add the '%s' repository (%s).", data.name, data)

            # A system repository can be only enabled or disabled.
            if data.origin == REPO_ORIGIN_SYSTEM:
                self._handle_system_repository(data)
                return

            # Set up additional sources.
            self._set_up_additional_repository(data)

            # Add a new repository.
            self._dnf_manager.add_repository(data)

            # Load an enabled repository to check its validity.
            self._dnf_manager.load_repository(data.name)

    def _validate_enabled_repositories(self):
        """Validate all enabled repositories.

        Collect error messages about invalid repositories.
        All invalid repositories are disabled.

        The user repositories are validated when we add them
        to DNF, so this covers invalid system repositories.
        """
        for repo_id in self.dnf_manager.enabled_repositories:
            try:
                self.dnf_manager.load_repository(repo_id)
            except MetadataError as e:
                self.verbose_errors.append(str(e))

    def _reload_treeinfo_metadata(self, repo_data):
        """Reload treeinfo metadata.

        :param RepoConfigurationData repo_data: configuration data of the base repo
        :return: a URL of the base repository
        """
        log.debug("Reload treeinfo metadata.")
        base_repo_url = None

        # Remove previous treeinfo repositories.
        removed_repos = self._remove_treeinfo_repositories()
        disabled_names = [r.name for r in removed_repos if not r.enabled]

        # Collect URLs of existing repositories.
        existing_urls = [
            r.baseurl for r in self.data.repo.dataList() if r.baseurl
        ]

        try:
            # Load the treeinfo metadata.
            tree_info_metadata = TreeInfoMetadata()
            tree_info_metadata.load_data(repo_data)

            # Set up the substitutions.
            self._dnf_manager.configure_substitution(
                tree_info_metadata.release_version)

            # Get the new base repo URL.
            base_repo_url = tree_info_metadata.get_base_repo_url()
            existing_urls.append(base_repo_url)

            # Add the treeinfo repositories.
            repositories = generate_treeinfo_repositories(
                repo_data, tree_info_metadata)

            self._add_treeinfo_repositories(
                repositories=repositories,
                disabled_names=disabled_names,
                existing_urls=existing_urls,
            )
        except NoTreeInfoError as e:
            log.debug("No treeinfo metadata to use: %s", str(e))
        except TreeInfoMetadataError as e:
            log.warning("Couldn't use treeinfo metadata: %s", str(e))

        return base_repo_url

    def _add_treeinfo_repositories(self, repositories, disabled_names,
                                   existing_urls):
        """Add the treeinfo repositories.

        :param [RepoConfigurationData] repositories: configuration data of treeinfo repositories
        :param [str] disabled_names: a list of repository names that should be disabled
        :param [str] existing_urls: a list of repository URLs that already exist
        """
        log.debug("Add treeinfo repositories.")

        for repo in repositories:
            # Skip existing repositories.
            if repo.url in existing_urls:
                continue

            # Disable if previously disabled.
            if repo.name in disabled_names:
                repo.enabled = False

            log.debug("Add the '%s' treeinfo repository: %s", repo.name, repo)

            # Add the repository to user repositories,
            # so it'll appear in the output ks file.
            ks_repo = convert_repo_data_to_ks_repo(repo)
            self.data.repo.dataList().append(ks_repo)

    def _remove_treeinfo_repositories(self):
        """Remove all old treeinfo repositories before loading new ones.

        Find all repositories added from treeinfo file and remove them.
        After this step new repositories will be loaded from the new link.

        :return: list of repositories that were removed
        """
        log.debug("Remove treeinfo repositories.")
        removed_repos = []

        for ks_repo in list(self.data.repo.dataList()):
            if not ks_repo.treeinfo_origin:
                continue

            self.data.repo.dataList().remove(ks_repo)
            removed_repos.append(ks_repo)

            log.debug("Removed the '%s' treeinfo repository.", ks_repo.name)

        return removed_repos

    def post_install(self):
        """Perform post-installation tasks."""
        super().post_install()

        # Write selected kickstart repos to target system
        repositories = list(
            map(convert_ks_repo_to_repo_data, self.data.repo.dataList()))

        task = WriteRepositoriesTask(
            sysroot=conf.target.system_root,
            dnf_manager=self.dnf_manager,
            repositories=repositories,
        )
        task.run()

        # rpm needs importing installed certificates manually, see rhbz#748320 and rhbz#185800
        task = ImportRPMKeysTask(sysroot=conf.target.system_root,
                                 gpg_keys=conf.payload.default_rpm_gpg_keys)
        task.run()

        # Update the DNF configuration.
        task = UpdateDNFConfigurationTask(
            sysroot=conf.target.system_root,
            data=self.get_packages_configuration())
        task.run()

        # Close the DNF base.
        task = ResetDNFManagerTask(dnf_manager=self.dnf_manager)
        task.run()

    @property
    def kernel_version_list(self):
        return get_kernel_version_list()
Exemplo n.º 10
0
class DNFMangerTestCase(unittest.TestCase):
    """Test the abstraction of the DNF base."""
    def setUp(self):
        self.maxDiff = None
        self.dnf_manager = DNFManager()

    def _check_configuration(self, *attributes):
        """Check the DNF configuration."""
        configuration = self.dnf_manager._base.conf.dump()
        configuration = configuration.splitlines(keepends=False)

        for attribute in attributes:
            self.assertIn(attribute, configuration)

    def _check_substitutions(self, substitutions):
        """Check the DNF substitutions."""
        self.assertEqual(dict(self.dnf_manager._base.conf.substitutions),
                         substitutions)

    def create_base_test(self):
        """Test the creation of the DNF base."""
        self.assertIsNotNone(self.dnf_manager._base)

    def reset_base_test(self):
        """Test the reset of the DNF base."""
        base_1 = self.dnf_manager._base
        self.assertEqual(self.dnf_manager._base, base_1)
        self.dnf_manager.reset_base()

        base_2 = self.dnf_manager._base
        self.assertEqual(self.dnf_manager._base, base_2)
        self.assertNotEqual(self.dnf_manager._base, base_1)

    def clear_cache_test(self):
        """Test the clear_cache method."""
        self.dnf_manager.clear_cache()

    def set_default_configuration_test(self):
        """Test the default configuration of the DNF base."""
        self._check_configuration(
            "cachedir = /tmp/dnf.cache",
            "pluginconfpath = /tmp/dnf.pluginconf",
            "logdir = /tmp/",
        )
        self._check_configuration("installroot = /mnt/sysroot",
                                  "persistdir = /mnt/sysroot/var/lib/dnf")
        self._check_configuration("reposdir = "
                                  "/etc/yum.repos.d, "
                                  "/etc/anaconda.repos.d")
        self._check_substitutions({
            "arch": "x86_64",
            "basearch": "x86_64",
            "releasever": "rawhide"
        })

    @patch(
        "pyanaconda.modules.payloads.payload.dnf.dnf_manager.get_os_release_value"
    )
    def set_module_platform_id_test(self, get_platform_id):
        """Test the configuration of module_platform_id."""
        get_platform_id.return_value = "platform:f32"
        self.dnf_manager.reset_base()
        self._check_configuration("module_platform_id = platform:f32")

    def configure_proxy_test(self):
        """Test the proxy configuration."""
        self.dnf_manager.configure_proxy("http://*****:*****@example.com/proxy")
        self._check_configuration(
            "proxy = http://example.com:3128",
            "proxy_username = user",
            "proxy_password = pass",
        )

        self.dnf_manager.configure_proxy("@:/invalid")
        self._check_configuration(
            "proxy = ",
            "proxy_username = "******"proxy_password = "******"http://example.com/proxy")
        self._check_configuration(
            "proxy = http://example.com:3128",
            "proxy_username = "******"proxy_password = "******"proxy = ",
            "proxy_username = "******"proxy_password = "******"""Test the configuration of the DNF base."""
        data = PackagesConfigurationData()

        self.dnf_manager.configure_base(data)
        self._check_configuration(
            "multilib_policy = best",
            "timeout = 30",
            "retries = 10",
            "install_weak_deps = 1",
        )

        self.assertEqual(self.dnf_manager._ignore_broken_packages, False)
        self.assertEqual(self.dnf_manager._ignore_missing_packages, False)

        data.multilib_policy = MULTILIB_POLICY_ALL
        data.timeout = 100
        data.retries = 5
        data.broken_ignored = True
        data.missing_ignored = True
        data.weakdeps_excluded = True

        self.dnf_manager.configure_base(data)
        self._check_configuration(
            "multilib_policy = all",
            "timeout = 100",
            "retries = 5",
            "install_weak_deps = 0",
        )

        self.assertEqual(self.dnf_manager._ignore_broken_packages, True)
        self.assertEqual(self.dnf_manager._ignore_missing_packages, True)

    def dump_configuration_test(self):
        """Test the dump of the DNF configuration."""
        with self.assertLogs(level="DEBUG") as cm:
            self.dnf_manager.dump_configuration()

        msg = "DNF configuration:"
        self.assertTrue(any(map(lambda x: msg in x, cm.output)))

        msg = "installroot = /mnt/sysroot"
        self.assertTrue(any(map(lambda x: msg in x, cm.output)))

    def get_installation_size_test(self):
        """Test the get_installation_size method."""
        # No transaction.
        size = self.dnf_manager.get_installation_size()
        self.assertEqual(size, Size("3000 MiB"))

        # Fake transaction.
        tsi_1 = Mock()
        tsi_1.pkg.installsize = 1024 * 100
        tsi_1.pkg.files = ["/file"] * 10

        tsi_2 = Mock()
        tsi_2.pkg.installsize = 1024 * 200
        tsi_2.pkg.files = ["/file"] * 20

        self.dnf_manager._base.transaction = [tsi_1, tsi_2]
        size = self.dnf_manager.get_installation_size()
        size = size.round_to_nearest("KiB", ROUND_UP)

        self.assertEqual(size, Size("528 KiB"))

    def get_download_size_test(self):
        """Test the get_download_size method."""
        # No transaction.
        size = self.dnf_manager.get_download_size()
        self.assertEqual(size, Size(0))

        # Fake transaction.
        tsi_1 = Mock()
        tsi_1.pkg.downloadsize = 1024 * 1024 * 100

        tsi_2 = Mock()
        tsi_2.pkg.downloadsize = 1024 * 1024 * 200

        self.dnf_manager._base.transaction = [tsi_1, tsi_2]
        size = self.dnf_manager.get_download_size()

        self.assertEqual(size, Size("450 MiB"))

    def environments_test(self):
        """Test the environments property."""
        self.assertEqual(self.dnf_manager.environments, [])

        # Fake environments.
        env_1 = Mock(id="environment-1")
        env_2 = Mock(id="environment-2")
        env_3 = Mock(id="environment-3")

        # Fake comps.
        comps = Mock(environments=[env_1, env_2, env_3])

        self.dnf_manager._base._comps = comps
        self.assertEqual(self.dnf_manager.environments, [
            "environment-1",
            "environment-2",
            "environment-3",
        ])

    @patch("dnf.module.module_base.ModuleBase.enable")
    def enable_modules_test(self, module_base_enable):
        """Test the enable_modules method."""
        self.dnf_manager.enable_modules(module_specs=["m1", "m2:latest"])
        module_base_enable.assert_called_once_with(["m1", "m2:latest"])

    @patch("dnf.module.module_base.ModuleBase.enable")
    def enable_modules_error_test(self, module_base_enable):
        """Test the failed enable_modules method."""
        module_base_enable.side_effect = MarkingErrors(
            module_depsolv_errors=["e1", "e2"])

        with self.assertRaises(MarkingErrors):
            self.dnf_manager.enable_modules(module_specs=["m1", "m2:latest"])

    @patch("dnf.module.module_base.ModuleBase.disable")
    def disable_modules_test(self, module_base_disable):
        """Test the enable_modules method."""
        self.dnf_manager.disable_modules(module_specs=["m1", "m2:latest"])
        module_base_disable.assert_called_once_with(["m1", "m2:latest"])

    @patch("dnf.module.module_base.ModuleBase.disable")
    def disable_modules_error_test(self, module_base_disable):
        """Test the failed enable_modules method."""
        module_base_disable.side_effect = MarkingErrors(
            module_depsolv_errors=["e1", "e2"])

        with self.assertRaises(MarkingErrors):
            self.dnf_manager.disable_modules(module_specs=["m1", "m2:latest"])

    @patch("dnf.base.Base.install_specs")
    def apply_specs_test(self, install_specs):
        """Test the apply_specs method."""
        self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                     exclude_list=["@g2", "p2"])

        install_specs.assert_called_once_with(install=["@g1", "p1"],
                                              exclude=["@g2", "p2"],
                                              strict=True)

    @patch("dnf.base.Base.install_specs")
    def apply_specs_error_test(self, install_specs):
        """Test the apply_specs method with an error."""
        install_specs.side_effect = MarkingErrors(error_group_specs=["@g1"])

        with self.assertRaises(MarkingErrors):
            self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                         exclude_list=["@g2", "p2"])

    @patch("dnf.base.Base.install_specs")
    def apply_specs_ignore_broken_test(self, install_specs):
        """Test the apply_specs method with ignored broken packages."""
        self.dnf_manager._ignore_broken_packages = True
        self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                     exclude_list=["@g2", "p2"])

        install_specs.assert_called_once_with(install=["@g1", "p1"],
                                              exclude=["@g2", "p2"],
                                              strict=False)

    @patch("dnf.base.Base.install_specs")
    def apply_specs_ignore_missing_test(self, install_specs):
        """Test the apply_specs method with ignored missing packages."""
        self.dnf_manager._ignore_missing_packages = True

        # Ignore a missing package.
        install_specs.side_effect = MarkingErrors(no_match_pkg_specs=["p1"])

        self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                     exclude_list=["@g2", "p2"])

        install_specs.assert_called_once_with(install=["@g1", "p1"],
                                              exclude=["@g2", "p2"],
                                              strict=True)

        # Don't ignore a broken transaction.
        install_specs.side_effect = MarkingErrors(error_pkg_specs=["p1"])

        with self.assertRaises(MarkingErrors):
            self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                         exclude_list=["@g2", "p2"])

    @patch("dnf.base.Base.download_packages")
    @patch("dnf.base.Base.transaction")
    def download_packages_test(self, transaction, download_packages):
        """Test the download_packages method."""
        callback = Mock()
        transaction.install_set = ["p1", "p2", "p3"]
        download_packages.side_effect = self._download_packages

        self.dnf_manager.download_packages(callback)

        callback.assert_has_calls([
            call('Downloading 3 RPMs, 25 B / 300 B (8%) done.'),
            call('Downloading 3 RPMs, 75 B / 300 B (25%) done.'),
            call('Downloading 3 RPMs, 100 B / 300 B (33%) done.'),
            call('Downloading 3 RPMs, 125 B / 300 B (41%) done.'),
            call('Downloading 3 RPMs, 175 B / 300 B (58%) done.'),
            call('Downloading 3 RPMs, 200 B / 300 B (66%) done.'),
            call('Downloading 3 RPMs, 225 B / 300 B (75%) done.'),
            call('Downloading 3 RPMs, 275 B / 300 B (91%) done.'),
            call('Downloading 3 RPMs, 300 B / 300 B (100%) done.')
        ])

    def _download_packages(self, packages, progress):
        """Simulate the download of packages."""
        progress.start(total_files=3, total_size=300)

        for name in packages:
            payload = Mock()
            payload.__str__ = Mock(return_value=name)
            payload.download_size = 100

            progress.last_time = 0
            progress.progress(payload, 25)

            progress.last_time += 3600
            progress.progress(payload, 50)

            progress.last_time = 0
            progress.progress(payload, 75)

            progress.last_time = 0
            progress.end(payload, STATUS_OK, "Message!")

        self.assertEqual(progress.downloads, {"p1": 100, "p2": 100, "p3": 100})

    @patch("dnf.base.Base.download_packages")
    @patch("dnf.base.Base.transaction")
    def download_packages_failed_test(self, transaction, download_packages):
        """Test the download_packages method with failed packages."""
        callback = Mock()
        transaction.install_set = ["p1", "p2", "p3"]
        download_packages.side_effect = self._download_packages_failed

        self.dnf_manager.download_packages(callback)

        callback.assert_has_calls([
            call('Downloading 3 RPMs, 25 B / 300 B (8%) done.'),
            call('Downloading 3 RPMs, 50 B / 300 B (16%) done.'),
            call('Downloading 3 RPMs, 75 B / 300 B (25%) done.')
        ])

    def _download_packages_failed(self, packages, progress):
        """Simulate the failed download of packages."""
        progress.start(total_files=3, total_size=300)

        for name in packages:
            payload = Mock()
            payload.__str__ = Mock(return_value=name)
            payload.download_size = 100

            progress.last_time = 0
            progress.progress(payload, 25)

            progress.last_time = 0
            progress.end(payload, STATUS_FAILED, "Message!")

        self.assertEqual(progress.downloads, {"p1": 25, "p2": 25, "p3": 25})

    @patch("dnf.base.Base.do_transaction")
    def install_packages_test(self, do_transaction):
        """Test the install_packages method."""
        calls = []
        do_transaction.side_effect = self._install_packages

        self.dnf_manager.install_packages(calls.append)

        self.assertEqual(calls, [
            'Installing p1.x86_64 (0/3)',
            'Installing p2.x86_64 (1/3)',
            'Installing p3.x86_64 (2/3)',
            'Performing post-installation setup tasks',
            'Configuring p1.x86_64',
            'Configuring p2.x86_64',
            'Configuring p3.x86_64',
            'Verifying p1.x86_64 (1/3)',
            'Verifying p2.x86_64 (2/3)',
            'Verifying p3.x86_64 (3/3)',
        ])

    def _get_package(self, name):
        """Get a mocked package of the specified name."""
        package = Mock(spec=Package)
        package.name = name
        package.arch = "x86_64"
        package.evr = "1.2-3"
        package.buildtime = 100
        package.returnIdSum.return_value = ("", "1a2b3c")
        return package

    def _install_packages(self, progress):
        """Simulate the installation of packages."""
        packages = list(map(self._get_package, ["p1", "p2", "p3"]))
        ts_total = len(packages)

        for ts_done, package in enumerate(packages):
            progress.progress(package, PKG_INSTALL, 0, 100, ts_done, ts_total)
            progress.progress(package, PKG_INSTALL, 50, 100, ts_done, ts_total)
            progress.progress(package, PKG_SCRIPTLET, 75, 100, ts_done,
                              ts_total)
            progress.progress(package, PKG_INSTALL, 100, 100, ts_done + 1,
                              ts_total)

        progress.progress(None, TRANS_POST, None, None, None, None)

        for ts_done, package in enumerate(packages):
            progress.progress(package, PKG_SCRIPTLET, 100, 100, ts_done + 1,
                              ts_total)

        for ts_done, package in enumerate(packages):
            progress.progress(package, PKG_VERIFY, 100, 100, ts_done + 1,
                              ts_total)

    @patch("dnf.base.Base.do_transaction")
    def install_packages_failed_test(self, do_transaction):
        """Test the failed install_packages method."""
        calls = []
        do_transaction.side_effect = self._install_packages_failed

        with self.assertRaises(PayloadInstallationError) as cm:
            self.dnf_manager.install_packages(calls.append)

        msg = "An error occurred during the transaction: " \
              "The p1 package couldn't be installed!"

        self.assertEqual(str(cm.exception), msg)
        self.assertEqual(calls, [])

    def _install_packages_failed(self, progress):
        """Simulate the failed installation of packages."""
        progress.error("The p1 package couldn't be installed!")

    @patch("dnf.base.Base.do_transaction")
    def install_packages_quit_test(self, do_transaction):
        """Test the terminated install_packages method."""
        calls = []
        do_transaction.side_effect = self._install_packages_quit

        with self.assertRaises(RuntimeError) as cm:
            self.dnf_manager.install_packages(calls.append)

        msg = "The transaction process has ended abruptly: " \
              "Something went wrong with the p1 package!"

        self.assertIn(msg, str(cm.exception))
        self.assertEqual(calls, [])

    def _install_packages_quit(self, progress):
        """Simulate the terminated installation of packages."""
        raise IOError("Something went wrong with the p1 package!")

    def _add_repo(self, name, enabled=True):
        """Add the DNF repo object."""
        repo = Repo(name, self.dnf_manager._base.conf)
        self.dnf_manager._base.repos.add(repo)

        if enabled:
            repo.enable()

        return repo

    def set_download_location_test(self):
        """Test the set_download_location method."""
        r1 = self._add_repo("r1")
        r2 = self._add_repo("r2")
        r3 = self._add_repo("r3")

        self.dnf_manager.set_download_location("/my/download/location")

        self.assertEqual(r1.pkgdir, "/my/download/location")
        self.assertEqual(r2.pkgdir, "/my/download/location")
        self.assertEqual(r3.pkgdir, "/my/download/location")

    def download_location_test(self):
        """Test the download_location property."""
        self.assertEqual(self.dnf_manager.download_location, None)

        self.dnf_manager.set_download_location("/my/location")
        self.assertEqual(self.dnf_manager.download_location, "/my/location")

        self.dnf_manager.reset_base()
        self.assertEqual(self.dnf_manager.download_location, None)
 def setUp(self):
     self.maxDiff = None
     self.dnf_manager = DNFManager()
class DNFManagerReposTestCase(unittest.TestCase):
    """Test the repo abstraction of the DNF base."""
    def setUp(self):
        self.maxDiff = None
        self.dnf_manager = DNFManager()

    def _add_repo(self, repo_id):
        """Add a mocked repo with the specified id."""
        repo = Repo(repo_id, self.dnf_manager._base.conf)
        self.dnf_manager._base.repos.add(repo)
        return repo

    def _check_repo(self, repo_id, attributes):
        """Check the DNF repo configuration."""
        repo = self.dnf_manager._base.repos[repo_id]
        repo_conf = repo.dump()
        repo_conf = repo_conf.splitlines(keepends=False)

        print(repo.dump())

        for attribute in attributes:
            assert attribute in repo_conf

    def _check_content(self, repo_data, expected_content):
        """Check the generated content of the .repo file."""
        expected_content = dedent(expected_content).strip()
        content = self.dnf_manager.generate_repo_file(repo_data)
        assert content == expected_content

        expected_attrs = expected_content.splitlines(keepends=False)
        self.dnf_manager.add_repository(repo_data)
        self._check_repo(repo_data.name, expected_attrs)

    def test_repositories(self):
        """Test the repositories property."""
        assert self.dnf_manager.repositories == []

        self._add_repo("r1")
        self._add_repo("r2")
        self._add_repo("r3")

        assert self.dnf_manager.repositories == ["r1", "r2", "r3"]

    def test_enabled_repositories(self):
        """Test the enabled_repositories property."""
        assert self.dnf_manager.enabled_repositories == []

        self._add_repo("r1").disable()
        self._add_repo("r2").enable()
        self._add_repo("r3").disable()
        self._add_repo("r4").enable()

        assert self.dnf_manager.enabled_repositories == ["r2", "r4"]

    def test_set_repository_enabled(self):
        """Test the set_repository_enabled function."""
        self._add_repo("r1")

        self.dnf_manager.set_repository_enabled("r1", True)
        assert "r1" in self.dnf_manager.enabled_repositories

        self.dnf_manager.set_repository_enabled("r1", False)
        assert "r1" not in self.dnf_manager.enabled_repositories

        with pytest.raises(UnknownRepositoryError):
            self.dnf_manager.set_repository_enabled("r2", True)

    def test_add_repository_default(self):
        """Test the add_repository method with defaults."""
        data = RepoConfigurationData()
        data.name = "r1"

        self.dnf_manager.add_repository(data)

        self._check_repo("r1", [
            "baseurl = ",
            "proxy = ",
            "sslverify = 1",
            "sslcacert = ",
            "sslclientcert = ",
            "sslclientkey = ",
            "cost = 1000",
            "includepkgs = ",
            "excludepkgs = ",
        ])

    def test_add_repository_enabled(self):
        """Test the add_repository method with enabled repo."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.enabled = True

        self.dnf_manager.add_repository(data)

        self._check_repo("r1", [
            "enabled = 1",
        ])

    def test_add_repository_disabled(self):
        """Test the add_repository method with disabled repo."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.enabled = False

        self.dnf_manager.add_repository(data)

        self._check_repo("r1", [
            "enabled = 0",
        ])

    def test_add_repository_baseurl(self):
        """Test the add_repository method with baseurl."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.type = URL_TYPE_BASEURL
        data.url = "http://repo"

        self.dnf_manager.add_repository(data)

        self._check_repo("r1", [
            "baseurl = http://repo",
        ])

    def test_add_repository_mirrorlist(self):
        """Test the add_repository method with mirrorlist."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.type = URL_TYPE_MIRRORLIST
        data.url = "http://mirror"

        self.dnf_manager.add_repository(data)
        self._check_repo("r1", [
            "mirrorlist = http://mirror",
        ])

    def test_add_repository_metalink(self):
        """Test the add_repository method with metalink."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.type = URL_TYPE_METALINK
        data.url = "http://metalink"

        self.dnf_manager.add_repository(data)
        self._check_repo("r1", [
            "metalink = http://metalink",
        ])

    def test_add_repository_no_ssl_configuration(self):
        """Test the add_repository method without the ssl configuration."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.ssl_verification_enabled = False

        self.dnf_manager.add_repository(data)
        self._check_repo("r1", [
            "sslverify = 0",
        ])

    def test_add_repository_ssl_configuration(self):
        """Test the add_repository method with the ssl configuration."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.ssl_verification_enabled = True
        data.ssl_configuration.ca_cert_path = "file:///ca-cert"
        data.ssl_configuration.client_cert_path = "file:///client-cert"
        data.ssl_configuration.client_key_path = "file:///client-key"

        self.dnf_manager.add_repository(data)
        self._check_repo("r1", [
            "sslverify = 1",
            "sslcacert = file:///ca-cert",
            "sslclientcert = file:///client-cert",
            "sslclientkey = file:///client-key",
        ])

    def test_add_repository_invalid_proxy(self):
        """Test the add_repository method the invalid proxy configuration."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.proxy = "@:/invalid"

        self.dnf_manager.add_repository(data)
        self._check_repo("r1", [
            "proxy = ",
        ])

    def test_add_repository_no_auth_proxy(self):
        """Test the add_repository method the no auth proxy configuration."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.proxy = "http://example.com:1234"

        self.dnf_manager.add_repository(data)
        self._check_repo("r1", [
            "proxy = http://example.com:1234",
        ])

    def test_add_repository_proxy(self):
        """Test the add_repository method with the proxy configuration."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.proxy = "http://*****:*****@example.com:1234"

        self.dnf_manager.add_repository(data)
        self._check_repo("r1", [
            "proxy = http://example.com:1234",
            "proxy_username = user",
            "proxy_password = pass",
        ])

    def test_add_repository_cost(self):
        """Test the add_repository method with a cost."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.cost = 256

        self.dnf_manager.add_repository(data)
        self._check_repo("r1", ["cost = 256"])

    def test_add_repository_packages(self):
        """Test the add_repository method with packages."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.included_packages = ["p1", "p2"]
        data.excluded_packages = ["p3", "p4"]

        self.dnf_manager.add_repository(data)
        self._check_repo("r1", [
            "includepkgs = p1, p2",
            "excludepkgs = p3, p4",
        ])

    def test_add_repository_replace(self):
        """Test the add_repository method with a replacement."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.url = "http://u1"

        self.dnf_manager.add_repository(data)
        self._check_repo("r1", [
            "baseurl = http://u1",
        ])

        data.url = "http://u2"

        self.dnf_manager.add_repository(data)
        self._check_repo("r1", [
            "baseurl = http://u2",
        ])

    def test_generate_repo_file_baseurl(self):
        """Test the generate_repo_file method with baseurl."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.type = URL_TYPE_BASEURL
        data.url = "http://repo"
        data.proxy = "http://example.com:1234"
        data.cost = 256

        self._check_content(
            data, """
            [r1]
            name = r1
            enabled = 1
            baseurl = http://repo
            proxy = http://example.com:1234
            cost = 256
            """)

    def test_generate_repo_file_mirrorlist(self):
        """Test the generate_repo_file method with mirrorlist."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.type = URL_TYPE_MIRRORLIST
        data.url = "http://mirror"
        data.ssl_verification_enabled = False
        data.proxy = "http://*****:*****@example.com:1234"

        self._check_content(
            data, """
            [r1]
            name = r1
            enabled = 1
            mirrorlist = http://mirror
            sslverify = 0
            proxy = http://example.com:1234
            proxy_username = user
            proxy_password = pass
            """)

    def test_generate_repo_file_metalink(self):
        """Test the generate_repo_file method with metalink."""
        data = RepoConfigurationData()
        data.name = "r1"
        data.enabled = False
        data.type = URL_TYPE_METALINK
        data.url = "http://metalink"
        data.included_packages = ["p1", "p2"]
        data.excluded_packages = ["p3", "p4"]

        self._check_content(
            data, """
            [r1]
            name = r1
            enabled = 0
            metalink = http://metalink
            includepkgs = p1, p2
            excludepkgs = p3, p4
            """)

    def test_load_repository_unknown(self):
        """Test the load_repository method with an unknown repo."""
        with pytest.raises(UnknownRepositoryError):
            self.dnf_manager.load_repository("r1")

    def test_load_repository_failed(self):
        """Test the load_repository method with a failure."""
        repo = self._add_repo("r1")
        repo.load = Mock(side_effect=RepoError("Fake error!"))

        with pytest.raises(MetadataError) as cm:
            self.dnf_manager.load_repository("r1")

        repo.load.assert_called_once()
        assert repo.enabled is False
        assert str(cm.value) == "Fake error!"

    def test_load_repository(self):
        """Test the load_repository method."""
        repo = self._add_repo("r1")
        repo.load = Mock()

        self.dnf_manager.load_repository("r1")

        repo.load.assert_called_once()
        assert repo.enabled is True

    def _create_repo(self, repo, repo_dir):
        """Generate fake metadata for the repo."""
        # Create the repodata directory.
        os.makedirs(os.path.join(repo_dir, "repodata"))

        # Create the repomd.xml file.
        md_path = os.path.join(repo_dir, "repodata", "repomd.xml")
        md_content = "Metadata for {}.".format(repo.id)

        with open(md_path, 'w') as f:
            f.write(md_content)

        # Set up the baseurl.
        repo.baseurl.append("file://" + repo_dir)

    def test_load_no_repomd_hashes(self):
        """Test the load_repomd_hashes method with no repositories."""
        self.dnf_manager.load_repomd_hashes()
        assert self.dnf_manager._md_hashes == {}

    def test_load_one_repomd_hash(self):
        """Test the load_repomd_hashes method with one repository."""
        with TemporaryDirectory() as d:
            r1 = self._add_repo("r1")
            self._create_repo(r1, d)

            self.dnf_manager.load_repomd_hashes()
            assert self.dnf_manager._md_hashes == {
                'r1':
                b"\x90\xa0\xb7\xce\xc2H\x85#\xa3\xfci"
                b"\x9e+\xf4\xe2\x19D\xbc\x9b'\xeb\xb7"
                b"\x90\x1d\xcey\xb3\xd4p\xc3\x1d\xfb",
            }

    def test_load_repomd_hashes(self):
        """Test the load_repomd_hashes method."""
        with TemporaryDirectory() as d:
            r1 = self._add_repo("r1")
            r1.baseurl = [
                "file://nonexistent/1",
                "file://nonexistent/2",
                "file://nonexistent/3",
            ]
            self._create_repo(r1, d + "/r1")

            r2 = self._add_repo("r2")
            r2.baseurl = [
                "file://nonexistent/1",
                "file://nonexistent/2",
                "file://nonexistent/3",
            ]

            r3 = self._add_repo("r3")
            r3.metalink = "file://metalink"

            r4 = self._add_repo("r4")
            r4.mirrorlist = "file://mirrorlist"

            self.dnf_manager.load_repomd_hashes()

            assert self.dnf_manager._md_hashes == {
                'r1':
                b"\x90\xa0\xb7\xce\xc2H\x85#\xa3\xfci"
                b"\x9e+\xf4\xe2\x19D\xbc\x9b'\xeb\xb7"
                b"\x90\x1d\xcey\xb3\xd4p\xc3\x1d\xfb",
                'r2':
                None,
                'r3':
                None,
                'r4':
                None,
            }

    def test_verify_repomd_hashes(self):
        """Test the verify_repomd_hashes method."""
        with TemporaryDirectory() as d:
            # Test no repository.
            assert self.dnf_manager.verify_repomd_hashes() is False

            # Create a repository.
            r = self._add_repo("r1")
            self._create_repo(r, d)

            # Test no loaded repository.
            assert self.dnf_manager.verify_repomd_hashes() is False

            # Test a loaded repository.
            self.dnf_manager.load_repomd_hashes()
            assert self.dnf_manager.verify_repomd_hashes() is True

            # Test a different content of metadata.
            with open(os.path.join(d, "repodata", "repomd.xml"), 'w') as f:
                f.write("Different metadata for r1.")

            assert self.dnf_manager.verify_repomd_hashes() is False

            # Test a reloaded repository.
            self.dnf_manager.load_repomd_hashes()
            assert self.dnf_manager.verify_repomd_hashes() is True

            # Test the base reset.
            self.dnf_manager.reset_base()
            assert self.dnf_manager.verify_repomd_hashes() is False
 def setUp(self):
     self.maxDiff = None
     self.dnf_manager = DNFManager()
     self.dnf_manager._base._comps = self._create_comps()
class DNFManagerCompsTestCase(unittest.TestCase):
    """Test the comps abstraction of the DNF base."""
    def setUp(self):
        self.maxDiff = None
        self.dnf_manager = DNFManager()
        self.dnf_manager._base._comps = self._create_comps()

    @property
    def comps(self):
        """The mocked comps object."""
        return self.dnf_manager._base._comps

    def _create_comps(self):
        """Create a mocked comps object."""
        comps = Mock(spec=Comps)
        comps.environments = []
        comps.groups = []

        def environment_by_pattern(name):
            for e in comps.environments:
                # pylint: disable=no-member
                if name in (e.id, e.ui_name):
                    return e

            return None

        comps.environment_by_pattern = environment_by_pattern

        def group_by_pattern(name):
            for e in comps.groups:
                # pylint: disable=no-member
                if name in (e.id, e.ui_name):
                    return e

            return None

        comps.group_by_pattern = group_by_pattern
        return comps

    def _add_group(self, grp_id, visible=True):
        """Add a mocked group with the specified id."""
        group = Mock(spec=Group)
        group.id = grp_id
        group.ui_name = "The '{}' group".format(grp_id)
        group.ui_description = "This is the '{}' group.".format(grp_id)
        group.visible = visible

        self.comps.groups.append(group)

    def _add_environment(self, env_id, optional=(), default=()):
        """Add a mocked environment with the specified id."""
        environment = Mock(spec=Environment)
        environment.id = env_id
        environment.ui_name = "The '{}' environment".format(env_id)
        environment.ui_description = "This is the '{}' environment.".format(
            env_id)
        environment.option_ids = []

        for opt_id in optional:
            option = Mock()
            option.name = opt_id
            option.default = opt_id in default
            environment.option_ids.append(option)

        self.comps.environments.append(environment)

    def test_groups(self):
        """Test the groups property."""
        assert self.dnf_manager.groups == []

        self._add_group("g1")
        self._add_group("g2")
        self._add_group("g3")

        assert self.dnf_manager.groups == [
            "g1",
            "g2",
            "g3",
        ]

    def test_resolve_group(self):
        """Test the resolve_group method."""
        assert self.dnf_manager.resolve_group("") is None
        assert self.dnf_manager.resolve_group("g1") is None

        self._add_group("g1")

        assert self.dnf_manager.resolve_group("g1") == "g1"
        assert self.dnf_manager.resolve_group("g2") is None

    def test_get_group_data_error(self):
        """Test the failed get_group_data method."""
        with pytest.raises(UnknownCompsGroupError):
            self.dnf_manager.get_group_data("g1")

    def test_get_group_data(self):
        """Test the get_group_data method."""
        self._add_group("g1")

        expected = CompsGroupData()
        expected.id = "g1"
        expected.name = "The 'g1' group"
        expected.description = "This is the 'g1' group."

        data = self.dnf_manager.get_group_data("g1")
        assert isinstance(data, CompsGroupData)
        assert compare_data(data, expected)

    def test_no_default_environment(self):
        """Test the default_environment property with no environments."""
        assert self.dnf_manager.default_environment is None

    def test_default_environment(self):
        """Test the default_environment property with some environments."""
        self._add_environment("e1")
        self._add_environment("e2")
        self._add_environment("e3")

        with patch("pyanaconda.modules.payloads.payload.dnf.dnf_manager.conf"
                   ) as conf:
            # Choose the first environment.
            conf.payload.default_environment = ""
            assert self.dnf_manager.default_environment == "e1"

            # Choose the configured environment.
            conf.payload.default_environment = "e2"
            assert self.dnf_manager.default_environment == "e2"

    def test_environments(self):
        """Test the environments property."""
        assert self.dnf_manager.environments == []

        self._add_environment("e1")
        self._add_environment("e2")
        self._add_environment("e3")

        assert self.dnf_manager.environments == [
            "e1",
            "e2",
            "e3",
        ]

    def test_resolve_environment(self):
        """Test the resolve_environment method."""
        assert self.dnf_manager.resolve_environment("") is None
        assert self.dnf_manager.resolve_environment("e1") is None

        self._add_environment("e1")

        assert self.dnf_manager.resolve_environment("e1") == "e1"
        assert self.dnf_manager.resolve_environment("e2") is None

    def test_is_environment_valid(self):
        """Test the is_environment_valid method."""
        assert self.dnf_manager.is_environment_valid("") is False
        assert self.dnf_manager.is_environment_valid("e1") is False

        self._add_environment("e1")

        assert self.dnf_manager.is_environment_valid("e1") is True
        assert self.dnf_manager.is_environment_valid("e2") is False

    def test_get_environment_data_error(self):
        """Test the failed get_environment_data method."""
        with pytest.raises(UnknownCompsEnvironmentError):
            self.dnf_manager.get_environment_data("e1")

    def test_get_environment_data(self):
        """Test the get_environment_data method."""
        self._add_environment("e1")

        expected = CompsEnvironmentData()
        expected.id = "e1"
        expected.name = "The 'e1' environment"
        expected.description = "This is the 'e1' environment."

        data = self.dnf_manager.get_environment_data("e1")
        assert isinstance(data, CompsEnvironmentData)
        assert compare_data(data, expected)

    def test_get_environment_data_visible_groups(self):
        """Test the get_environment_data method with visible groups."""
        self._add_group("g1")
        self._add_group("g2", visible=False)
        self._add_group("g3")
        self._add_group("g4", visible=False)

        self._add_environment("e1")

        data = self.dnf_manager.get_environment_data("e1")
        assert data.visible_groups == ["g1", "g3"]

    def test_get_environment_data_optional_groups(self):
        """Test the get_environment_data method with optional groups."""
        self._add_group("g1")
        self._add_group("g2")
        self._add_group("g3")
        self._add_group("g4")

        self._add_environment("e1", optional=["g1", "g3"])

        data = self.dnf_manager.get_environment_data("e1")
        assert data.optional_groups == ["g1", "g3"]

    def test_get_environment_data_default_groups(self):
        """Test the get_environment_data method with default groups."""
        self._add_group("g1")
        self._add_group("g2")
        self._add_group("g3")
        self._add_group("g4")

        self._add_environment("e1",
                              optional=["g1", "g2", "g3"],
                              default=["g1", "g3"])

        data = self.dnf_manager.get_environment_data("e1")
        assert data.default_groups == ["g1", "g3"]

    def test_environment_data_available_groups(self):
        """Test the get_available_groups method."""
        data = CompsEnvironmentData()
        assert data.get_available_groups() == []

        data.optional_groups = ["g1", "g2", "g3"]
        data.visible_groups = ["g3", "g4", "g5"]
        data.default_groups = ["g1", "g3"]

        assert data.get_available_groups() == ["g1", "g2", "g3", "g4", "g5"]
class DNFManagerTestCase(unittest.TestCase):
    """Test the abstraction of the DNF base."""
    def setUp(self):
        self.maxDiff = None
        self.dnf_manager = DNFManager()

    def _check_configuration(self, *attributes):
        """Check the DNF configuration."""
        configuration = self.dnf_manager._base.conf.dump()
        configuration = configuration.splitlines(keepends=False)

        for attribute in attributes:
            assert attribute in configuration

    def _check_substitutions(self, substitutions):
        """Check the DNF substitutions."""
        assert dict(self.dnf_manager._base.conf.substitutions) == substitutions

    def test_create_base(self):
        """Test the creation of the DNF base."""
        assert self.dnf_manager._base is not None

    def test_reset_base(self):
        """Test the reset of the DNF base."""
        base_1 = self.dnf_manager._base
        assert self.dnf_manager._base == base_1
        self.dnf_manager.reset_base()

        base_2 = self.dnf_manager._base
        assert self.dnf_manager._base == base_2
        assert self.dnf_manager._base != base_1

    def test_clear_cache(self):
        """Test the clear_cache method."""
        self.dnf_manager.clear_cache()

    def test_set_default_configuration(self):
        """Test the default configuration of the DNF base."""
        self._check_configuration(
            "cachedir = /tmp/dnf.cache",
            "pluginconfpath = /tmp/dnf.pluginconf",
            "logdir = /tmp/",
        )
        self._check_configuration("installroot = /mnt/sysroot",
                                  "persistdir = /mnt/sysroot/var/lib/dnf")
        self._check_configuration("reposdir = "
                                  "/etc/yum.repos.d, "
                                  "/etc/anaconda.repos.d")
        self._check_substitutions({
            "arch": "x86_64",
            "basearch": "x86_64",
            "releasever": "rawhide"
        })

    @patch(
        "pyanaconda.modules.payloads.payload.dnf.dnf_manager.get_os_release_value"
    )
    def test_set_module_platform_id(self, get_platform_id):
        """Test the configuration of module_platform_id."""
        get_platform_id.return_value = "platform:f32"
        self.dnf_manager.reset_base()
        self._check_configuration("module_platform_id = platform:f32")

    def test_configure_proxy(self):
        """Test the proxy configuration."""
        self.dnf_manager.configure_proxy("http://*****:*****@example.com/proxy")
        self._check_configuration(
            "proxy = http://example.com:3128",
            "proxy_username = user",
            "proxy_password = pass",
        )

        self.dnf_manager.configure_proxy("@:/invalid")
        self._check_configuration(
            "proxy = ",
            "proxy_username = "******"proxy_password = "******"http://example.com/proxy")
        self._check_configuration(
            "proxy = http://example.com:3128",
            "proxy_username = "******"proxy_password = "******"proxy = ",
            "proxy_username = "******"proxy_password = "******"""Test the configuration of the DNF base."""
        data = PackagesConfigurationData()

        self.dnf_manager.configure_base(data)
        self._check_configuration(
            "multilib_policy = best",
            "timeout = 30",
            "retries = 10",
            "install_weak_deps = 1",
        )

        assert self.dnf_manager._ignore_broken_packages is False
        assert self.dnf_manager._ignore_missing_packages is False

        data.multilib_policy = MULTILIB_POLICY_ALL
        data.timeout = 100
        data.retries = 5
        data.broken_ignored = True
        data.missing_ignored = True
        data.weakdeps_excluded = True

        self.dnf_manager.configure_base(data)
        self._check_configuration(
            "multilib_policy = all",
            "timeout = 100",
            "retries = 5",
            "install_weak_deps = 0",
        )

        assert self.dnf_manager._ignore_broken_packages is True
        assert self.dnf_manager._ignore_missing_packages is True

    def test_dump_configuration(self):
        """Test the dump of the DNF configuration."""
        with self.assertLogs(level="DEBUG") as cm:
            self.dnf_manager.dump_configuration()

        msg = "DNF configuration:"
        assert any(map(lambda x: msg in x, cm.output))

        msg = "installroot = /mnt/sysroot"
        assert any(map(lambda x: msg in x, cm.output))

    def test_get_installation_size(self):
        """Test the get_installation_size method."""
        # No transaction.
        size = self.dnf_manager.get_installation_size()
        assert size == Size("3000 MiB")

        # Fake transaction.
        tsi_1 = Mock()
        tsi_1.pkg.installsize = 1024 * 100
        tsi_1.pkg.files = ["/file"] * 10

        tsi_2 = Mock()
        tsi_2.pkg.installsize = 1024 * 200
        tsi_2.pkg.files = ["/file"] * 20

        self.dnf_manager._base.transaction = [tsi_1, tsi_2]
        size = self.dnf_manager.get_installation_size()
        size = size.round_to_nearest("KiB", ROUND_UP)

        assert size == Size("528 KiB")

    def test_get_download_size(self):
        """Test the get_download_size method."""
        # No transaction.
        size = self.dnf_manager.get_download_size()
        assert size == Size(0)

        # Fake transaction.
        tsi_1 = Mock()
        tsi_1.pkg.downloadsize = 1024 * 1024 * 100

        tsi_2 = Mock()
        tsi_2.pkg.downloadsize = 1024 * 1024 * 200

        self.dnf_manager._base.transaction = [tsi_1, tsi_2]
        size = self.dnf_manager.get_download_size()

        assert size == Size("450 MiB")

    @patch("dnf.module.module_base.ModuleBase.enable")
    def test_enable_modules(self, module_base_enable):
        """Test the enable_modules method."""
        self.dnf_manager.enable_modules(module_specs=["m1", "m2:latest"])
        module_base_enable.assert_called_once_with(["m1", "m2:latest"])

    @patch("dnf.module.module_base.ModuleBase.enable")
    def test_enable_modules_error(self, module_base_enable):
        """Test the failed enable_modules method."""
        module_base_enable.side_effect = MarkingErrors(
            module_depsolv_errors=["e1", "e2"])

        with pytest.raises(BrokenSpecsError):
            self.dnf_manager.enable_modules(module_specs=["m1", "m2:latest"])

    @patch("dnf.module.module_base.ModuleBase.disable")
    def test_disable_modules(self, module_base_disable):
        """Test the enable_modules method."""
        self.dnf_manager.disable_modules(module_specs=["m1", "m2:latest"])
        module_base_disable.assert_called_once_with(["m1", "m2:latest"])

    @patch("dnf.module.module_base.ModuleBase.disable")
    def test_disable_modules_error(self, module_base_disable):
        """Test the failed enable_modules method."""
        module_base_disable.side_effect = MarkingErrors(
            module_depsolv_errors=["e1", "e2"])

        with pytest.raises(BrokenSpecsError):
            self.dnf_manager.disable_modules(module_specs=["m1", "m2:latest"])

    @patch("dnf.base.Base.install_specs")
    def test_apply_specs(self, install_specs):
        """Test the apply_specs method."""
        self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                     exclude_list=["@g2", "p2"])

        install_specs.assert_called_once_with(install=["@g1", "p1"],
                                              exclude=["@g2", "p2"],
                                              strict=True)

    @patch("dnf.base.Base.install_specs")
    def test_apply_specs_error(self, install_specs):
        """Test the apply_specs method with an error."""
        install_specs.side_effect = MarkingErrors(error_group_specs=["@g1"])

        with pytest.raises(BrokenSpecsError):
            self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                         exclude_list=["@g2", "p2"])

        install_specs.side_effect = MarkingErrors(no_match_group_specs=["@g1"])

        with pytest.raises(MissingSpecsError):
            self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                         exclude_list=["@g2", "p2"])

    @patch("dnf.base.Base.install_specs")
    def test_apply_specs_ignore_broken(self, install_specs):
        """Test the apply_specs method with ignored broken packages."""
        self.dnf_manager._ignore_broken_packages = True
        self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                     exclude_list=["@g2", "p2"])

        install_specs.assert_called_once_with(install=["@g1", "p1"],
                                              exclude=["@g2", "p2"],
                                              strict=False)

    @patch("dnf.base.Base.install_specs")
    def test_apply_specs_ignore_missing(self, install_specs):
        """Test the apply_specs method with ignored missing packages."""
        self.dnf_manager._ignore_missing_packages = True

        # Ignore a missing package.
        install_specs.side_effect = MarkingErrors(no_match_pkg_specs=["p1"])

        self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                     exclude_list=["@g2", "p2"])

        install_specs.assert_called_once_with(install=["@g1", "p1"],
                                              exclude=["@g2", "p2"],
                                              strict=True)

        # Don't ignore a broken transaction.
        install_specs.side_effect = MarkingErrors(error_pkg_specs=["p1"])

        with pytest.raises(BrokenSpecsError):
            self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                         exclude_list=["@g2", "p2"])

    @patch("dnf.base.Base.download_packages")
    @patch("dnf.base.Base.transaction")
    def test_download_packages(self, transaction, download_packages):
        """Test the download_packages method."""
        callback = Mock()
        transaction.install_set = ["p1", "p2", "p3"]
        download_packages.side_effect = self._download_packages

        self.dnf_manager.download_packages(callback)

        callback.assert_has_calls([
            call('Downloading 3 RPMs, 25 B / 300 B (8%) done.'),
            call('Downloading 3 RPMs, 75 B / 300 B (25%) done.'),
            call('Downloading 3 RPMs, 100 B / 300 B (33%) done.'),
            call('Downloading 3 RPMs, 125 B / 300 B (41%) done.'),
            call('Downloading 3 RPMs, 175 B / 300 B (58%) done.'),
            call('Downloading 3 RPMs, 200 B / 300 B (66%) done.'),
            call('Downloading 3 RPMs, 225 B / 300 B (75%) done.'),
            call('Downloading 3 RPMs, 275 B / 300 B (91%) done.'),
            call('Downloading 3 RPMs, 300 B / 300 B (100%) done.')
        ])

    def _download_packages(self, packages, progress):
        """Simulate the download of packages."""
        progress.start(total_files=3, total_size=300)

        for name in packages:
            payload = Mock()
            payload.__str__ = Mock(return_value=name)
            payload.download_size = 100

            progress.last_time = 0
            progress.progress(payload, 25)

            progress.last_time += 3600
            progress.progress(payload, 50)

            progress.last_time = 0
            progress.progress(payload, 75)

            progress.last_time = 0
            progress.end(payload, STATUS_OK, "Message!")

        assert progress.downloads == {"p1": 100, "p2": 100, "p3": 100}

    @patch("dnf.base.Base.download_packages")
    @patch("dnf.base.Base.transaction")
    def test_download_packages_failed(self, transaction, download_packages):
        """Test the download_packages method with failed packages."""
        callback = Mock()
        transaction.install_set = ["p1", "p2", "p3"]
        download_packages.side_effect = self._download_packages_failed

        self.dnf_manager.download_packages(callback)

        callback.assert_has_calls([
            call('Downloading 3 RPMs, 25 B / 300 B (8%) done.'),
            call('Downloading 3 RPMs, 50 B / 300 B (16%) done.'),
            call('Downloading 3 RPMs, 75 B / 300 B (25%) done.')
        ])

    def _download_packages_failed(self, packages, progress):
        """Simulate the failed download of packages."""
        progress.start(total_files=3, total_size=300)

        for name in packages:
            payload = Mock()
            payload.__str__ = Mock(return_value=name)
            payload.download_size = 100

            progress.last_time = 0
            progress.progress(payload, 25)

            progress.last_time = 0
            progress.end(payload, STATUS_FAILED, "Message!")

        assert progress.downloads == {"p1": 25, "p2": 25, "p3": 25}

    @patch("dnf.base.Base.do_transaction")
    def test_install_packages(self, do_transaction):
        """Test the install_packages method."""
        calls = []
        do_transaction.side_effect = self._install_packages

        self.dnf_manager.install_packages(calls.append)

        assert calls == [
            'Installing p1.x86_64 (0/3)',
            'Installing p2.x86_64 (1/3)',
            'Installing p3.x86_64 (2/3)',
            'Performing post-installation setup tasks',
            'Configuring p1.x86_64',
            'Configuring p2.x86_64',
            'Configuring p3.x86_64',
            'Verifying p1.x86_64 (1/3)',
            'Verifying p2.x86_64 (2/3)',
            'Verifying p3.x86_64 (3/3)',
        ]

    def _get_package(self, name):
        """Get a mocked package of the specified name."""
        package = Mock(spec=Package)
        package.name = name
        package.arch = "x86_64"
        package.evr = "1.2-3"
        package.buildtime = 100
        package.returnIdSum.return_value = ("", "1a2b3c")
        return package

    def _install_packages(self, progress):
        """Simulate the installation of packages."""
        packages = list(map(self._get_package, ["p1", "p2", "p3"]))
        ts_total = len(packages)

        for ts_done, package in enumerate(packages):
            progress.progress(package, PKG_INSTALL, 0, 100, ts_done, ts_total)
            progress.progress(package, PKG_INSTALL, 50, 100, ts_done, ts_total)
            progress.progress(package, PKG_SCRIPTLET, 75, 100, ts_done,
                              ts_total)
            progress.progress(package, PKG_INSTALL, 100, 100, ts_done + 1,
                              ts_total)

        progress.progress(None, TRANS_POST, None, None, None, None)

        for ts_done, package in enumerate(packages):
            progress.progress(package, PKG_SCRIPTLET, 100, 100, ts_done + 1,
                              ts_total)

        for ts_done, package in enumerate(packages):
            progress.progress(package, PKG_VERIFY, 100, 100, ts_done + 1,
                              ts_total)

    @patch("dnf.base.Base.do_transaction")
    def test_install_packages_failed(self, do_transaction):
        """Test the failed install_packages method."""
        calls = []
        do_transaction.side_effect = self._install_packages_failed

        with pytest.raises(PayloadInstallationError) as cm:
            self.dnf_manager.install_packages(calls.append)

        msg = "An error occurred during the transaction: " \
              "The p1 package couldn't be installed!"

        assert str(cm.value) == msg
        assert calls == []

    def _install_packages_failed(self, progress):
        """Simulate the failed installation of packages."""
        progress.error("The p1 package couldn't be installed!")

    @patch("dnf.base.Base.do_transaction")
    def test_install_packages_quit(self, do_transaction):
        """Test the terminated install_packages method."""
        calls = []
        do_transaction.side_effect = self._install_packages_quit

        with pytest.raises(RuntimeError) as cm:
            self.dnf_manager.install_packages(calls.append)

        msg = "The transaction process has ended abruptly: " \
              "Something went wrong with the p1 package!"

        assert msg in str(cm.value)
        assert calls == []

    def _install_packages_quit(self, progress):
        """Simulate the terminated installation of packages."""
        raise IOError("Something went wrong with the p1 package!")

    def _add_repo(self, name, enabled=True):
        """Add the DNF repo object."""
        repo = Repo(name, self.dnf_manager._base.conf)
        self.dnf_manager._base.repos.add(repo)

        if enabled:
            repo.enable()

        return repo

    def test_set_download_location(self):
        """Test the set_download_location method."""
        r1 = self._add_repo("r1")
        r2 = self._add_repo("r2")
        r3 = self._add_repo("r3")

        self.dnf_manager.set_download_location("/my/download/location")

        assert r1.pkgdir == "/my/download/location"
        assert r2.pkgdir == "/my/download/location"
        assert r3.pkgdir == "/my/download/location"

    def test_download_location(self):
        """Test the download_location property."""
        assert self.dnf_manager.download_location is None

        self.dnf_manager.set_download_location("/my/location")
        assert self.dnf_manager.download_location == "/my/location"

        self.dnf_manager.reset_base()
        assert self.dnf_manager.download_location is None

    def test_substitute(self):
        """Test the substitute method."""
        # No variables.
        assert self.dnf_manager.substitute(None) == ""
        assert self.dnf_manager.substitute("") == ""
        assert self.dnf_manager.substitute("/") == "/"
        assert self.dnf_manager.substitute("/text") == "/text"

        # Unknown variables.
        assert self.dnf_manager.substitute("/$unknown") == "/$unknown"

        # Supported variables.
        assert self.dnf_manager.substitute("/$basearch") != "/$basearch"
        assert self.dnf_manager.substitute("/$releasever") != "/$releasever"

    def test_configure_substitution(self):
        """Test the configure_substitution function."""
        self.dnf_manager.configure_substitution(release_version="123")
        self._check_substitutions({
            "arch": "x86_64",
            "basearch": "x86_64",
            "releasever": "123"
        })

    def test_reset_substitution(self):
        """Test the reset_substitution method."""
        self.dnf_manager.configure_substitution(release_version="123")
        self._check_substitutions({
            "arch": "x86_64",
            "basearch": "x86_64",
            "releasever": "123"
        })

        self.dnf_manager.reset_substitution()
        self._check_substitutions({
            "arch": "x86_64",
            "basearch": "x86_64",
            "releasever": "rawhide"
        })

    @patch("dnf.subject.Subject.get_best_query")
    def test_is_package_available(self, get_best_query):
        """Test the is_package_available method."""
        self.dnf_manager._base._sack = Mock()
        assert self.dnf_manager.is_package_available("kernel") is True

        # No package.
        get_best_query.return_value = None
        assert self.dnf_manager.is_package_available("kernel") is False

        # No metadata.
        self.dnf_manager._base._sack = None

        with self.assertLogs(level="WARNING") as cm:
            assert self.dnf_manager.is_package_available("kernel") is False

        msg = "There is no metadata about packages!"
        assert any(map(lambda x: msg in x, cm.output))

    def test_match_available_packages(self):
        """Test the match_available_packages method"""
        p1 = self._get_package("langpacks-cs")
        p2 = self._get_package("langpacks-core-cs")
        p3 = self._get_package("langpacks-core-font-cs")

        sack = Mock()
        sack.query.return_value.available.return_value.filter.return_value = [
            p1, p2, p3
        ]

        # With metadata.
        self.dnf_manager._base._sack = sack
        assert self.dnf_manager.match_available_packages("langpacks-*") == [
            "langpacks-cs", "langpacks-core-cs", "langpacks-core-font-cs"
        ]

        # No metadata.
        self.dnf_manager._base._sack = None

        with self.assertLogs(level="WARNING") as cm:
            assert self.dnf_manager.match_available_packages(
                "langpacks-*") == []

        msg = "There is no metadata about packages!"
        assert any(map(lambda x: msg in x, cm.output))

    @patch("dnf.base.Base.resolve")
    def test_resolve_selection(self, resolve):
        """Test the resolve_selection method."""
        self.dnf_manager._base.transaction = [Mock(), Mock()]

        with self.assertLogs(level="INFO") as cm:
            self.dnf_manager.resolve_selection()

        expected = "The software selection has been resolved (2 packages selected)."
        assert expected in "\n".join(cm.output)

        resolve.assert_called_once()

    @patch("dnf.base.Base.resolve")
    def test_resolve_selection_failed(self, resolve):
        """Test the failed resolve_selection method."""
        resolve.side_effect = DepsolveError("e1")

        with pytest.raises(InvalidSelectionError) as cm:
            self.dnf_manager.resolve_selection()

        expected = \
            "The following software marked for installation has errors.\n" \
            "This is likely caused by an error with your installation source.\n\n" \
            "e1"

        assert expected == str(cm.value)

    def test_clear_selection(self):
        """Test the clear_selection method."""
        self.dnf_manager.clear_selection()
Exemplo n.º 16
0
class DNFManagerReposTestCase(unittest.TestCase):
    """Test the repo abstraction of the DNF base."""

    def setUp(self):
        self.maxDiff = None
        self.dnf_manager = DNFManager()

    def _add_repo(self, repo_id):
        """Add a mocked repo with the specified id."""
        repo = Repo(repo_id, self.dnf_manager._base.conf)
        self.dnf_manager._base.repos.add(repo)
        return repo

    def test_repositories(self):
        """Test the repositories property."""
        assert self.dnf_manager.repositories == []

        self._add_repo("r1")
        self._add_repo("r2")
        self._add_repo("r3")

        assert self.dnf_manager.repositories == ["r1", "r2", "r3"]

    def test_load_repository_unknown(self):
        """Test the load_repository method with an unknown repo."""
        with pytest.raises(UnknownRepositoryError):
            self.dnf_manager.load_repository("r1")

    def test_load_repository_failed(self):
        """Test the load_repository method with a failure."""
        repo = self._add_repo("r1")
        repo.load = Mock(side_effect=RepoError("Fake error!"))

        with pytest.raises(MetadataError) as cm:
            self.dnf_manager.load_repository("r1")

        repo.load.assert_called_once()
        assert repo.enabled is False
        assert str(cm.value) == "Fake error!"

    def test_load_repository(self):
        """Test the load_repository method."""
        repo = self._add_repo("r1")
        repo.load = Mock()

        self.dnf_manager.load_repository("r1")

        repo.load.assert_called_once()
        assert repo.enabled is True

    def _create_repo(self, repo, repo_dir):
        """Generate fake metadata for the repo."""
        # Create the repodata directory.
        os.makedirs(os.path.join(repo_dir, "repodata"))

        # Create the repomd.xml file.
        md_path = os.path.join(repo_dir, "repodata", "repomd.xml")
        md_content = "Metadata for {}.".format(repo.id)

        with open(md_path, 'w') as f:
            f.write(md_content)

        # Set up the baseurl.
        repo.baseurl.append("file://" + repo_dir)

    def test_load_no_repomd_hashes(self):
        """Test the load_repomd_hashes method with no repositories."""
        self.dnf_manager.load_repomd_hashes()
        assert self.dnf_manager._md_hashes == {}

    def test_load_one_repomd_hash(self):
        """Test the load_repomd_hashes method with one repository."""
        with TemporaryDirectory() as d:
            r1 = self._add_repo("r1")
            self._create_repo(r1, d)

            self.dnf_manager.load_repomd_hashes()
            assert self.dnf_manager._md_hashes == {
                'r1': b"\x90\xa0\xb7\xce\xc2H\x85#\xa3\xfci"
                      b"\x9e+\xf4\xe2\x19D\xbc\x9b'\xeb\xb7"
                      b"\x90\x1d\xcey\xb3\xd4p\xc3\x1d\xfb",
            }

    def test_load_repomd_hashes(self):
        """Test the load_repomd_hashes method."""
        with TemporaryDirectory() as d:
            r1 = self._add_repo("r1")
            r1.baseurl = [
                "file://nonexistent/1",
                "file://nonexistent/2",
                "file://nonexistent/3",
            ]
            self._create_repo(r1, d + "/r1")

            r2 = self._add_repo("r2")
            r2.baseurl = [
                "file://nonexistent/1",
                "file://nonexistent/2",
                "file://nonexistent/3",
            ]

            r3 = self._add_repo("r3")
            r3.metalink = "file://metalink"

            r4 = self._add_repo("r4")
            r4.mirrorlist = "file://mirrorlist"

            self.dnf_manager.load_repomd_hashes()

            assert self.dnf_manager._md_hashes == {
                'r1': b"\x90\xa0\xb7\xce\xc2H\x85#\xa3\xfci"
                      b"\x9e+\xf4\xe2\x19D\xbc\x9b'\xeb\xb7"
                      b"\x90\x1d\xcey\xb3\xd4p\xc3\x1d\xfb",
                'r2': None,
                'r3': None,
                'r4': None,
            }

    def test_verify_repomd_hashes(self):
        """Test the verify_repomd_hashes method."""
        with TemporaryDirectory() as d:
            # Test no repository.
            assert self.dnf_manager.verify_repomd_hashes() is False

            # Create a repository.
            r = self._add_repo("r1")
            self._create_repo(r, d)

            # Test no loaded repository.
            assert self.dnf_manager.verify_repomd_hashes() is False

            # Test a loaded repository.
            self.dnf_manager.load_repomd_hashes()
            assert self.dnf_manager.verify_repomd_hashes() is True

            # Test a different content of metadata.
            with open(os.path.join(d, "repodata", "repomd.xml"), 'w') as f:
                f.write("Different metadata for r1.")

            assert self.dnf_manager.verify_repomd_hashes() is False

            # Test a reloaded repository.
            self.dnf_manager.load_repomd_hashes()
            assert self.dnf_manager.verify_repomd_hashes() is True

            # Test the base reset.
            self.dnf_manager.reset_base()
            assert self.dnf_manager.verify_repomd_hashes() is False
Exemplo n.º 17
0
class DNFMangerTestCase(unittest.TestCase):
    """Test the abstraction of the DNF base."""
    def setUp(self):
        self.maxDiff = None
        self.dnf_manager = DNFManager()

    def _check_configuration(self, *attributes):
        """Check the DNF configuration."""
        configuration = self.dnf_manager._base.conf.dump()
        configuration = configuration.splitlines(keepends=False)

        for attribute in attributes:
            self.assertIn(attribute, configuration)

    def _check_substitutions(self, substitutions):
        """Check the DNF substitutions."""
        self.assertEqual(dict(self.dnf_manager._base.conf.substitutions),
                         substitutions)

    def create_base_test(self):
        """Test the creation of the DNF base."""
        self.assertIsNotNone(self.dnf_manager._base)

    def reset_base_test(self):
        """Test the reset of the DNF base."""
        base_1 = self.dnf_manager._base
        self.assertEqual(self.dnf_manager._base, base_1)
        self.dnf_manager.reset_base()

        base_2 = self.dnf_manager._base
        self.assertEqual(self.dnf_manager._base, base_2)
        self.assertNotEqual(self.dnf_manager._base, base_1)

    def clear_cache_test(self):
        """Test the clear_cache method."""
        self.dnf_manager.clear_cache()

    def set_default_configuration_test(self):
        """Test the default configuration of the DNF base."""
        self._check_configuration(
            "cachedir = /tmp/dnf.cache",
            "pluginconfpath = /tmp/dnf.pluginconf",
            "logdir = /tmp/",
        )
        self._check_configuration("installroot = /mnt/sysroot",
                                  "persistdir = /mnt/sysroot/var/lib/dnf")
        self._check_configuration(
            "reposdir = "
            "/etc/yum.repos.d, "
            "/etc/anaconda.repos.d, "
            "/tmp/updates/anaconda.repos.d, "
            "/tmp/product/anaconda.repos.d", )
        self._check_substitutions({
            "arch": "x86_64",
            "basearch": "x86_64",
            "releasever": "rawhide"
        })

    @patch(
        "pyanaconda.modules.payloads.payload.dnf.dnf_manager.get_os_release_value"
    )
    def set_module_platform_id_test(self, get_platform_id):
        """Test the configuration of module_platform_id."""
        get_platform_id.return_value = "platform:f32"
        self.dnf_manager.reset_base()
        self._check_configuration("module_platform_id = platform:f32")

    def configure_proxy_test(self):
        """Test the proxy configuration."""
        self.dnf_manager.configure_proxy("http://*****:*****@example.com/proxy")
        self._check_configuration(
            "proxy = http://example.com:3128",
            "proxy_username = user",
            "proxy_password = pass",
        )

        self.dnf_manager.configure_proxy("@:/invalid")
        self._check_configuration(
            "proxy = ",
            "proxy_username = "******"proxy_password = "******"http://example.com/proxy")
        self._check_configuration(
            "proxy = http://example.com:3128",
            "proxy_username = "******"proxy_password = "******"proxy = ",
            "proxy_username = "******"proxy_password = "******"""Test the configuration of the DNF base."""
        data = KickstartSpecificationHandler(PayloadKickstartSpecification)

        self.dnf_manager.configure_base(data)
        self._check_configuration(
            "multilib_policy = best",
            "timeout = 30",
            "retries = 10",
            "install_weak_deps = 1",
        )

        self.assertEqual(self.dnf_manager._ignore_broken_packages, False)
        self.assertEqual(self.dnf_manager._ignore_missing_packages, False)

        data.packages.multiLib = True
        data.packages.timeout = 100
        data.packages.retries = 5
        data.packages.handleBroken = KS_BROKEN_IGNORE
        data.packages.handleMissing = KS_MISSING_IGNORE
        data.packages.excludeWeakdeps = True

        self.dnf_manager.configure_base(data)
        self._check_configuration(
            "multilib_policy = all",
            "timeout = 100",
            "retries = 5",
            "install_weak_deps = 0",
        )

        self.assertEqual(self.dnf_manager._ignore_broken_packages, True)
        self.assertEqual(self.dnf_manager._ignore_missing_packages, True)

    def dump_configuration_test(self):
        """Test the dump of the DNF configuration."""
        with self.assertLogs(level="DEBUG") as cm:
            self.dnf_manager.dump_configuration()

        msg = "DNF configuration:"
        self.assertTrue(any(map(lambda x: msg in x, cm.output)))

        msg = "installroot = /mnt/sysroot"
        self.assertTrue(any(map(lambda x: msg in x, cm.output)))

    def get_installation_size_test(self):
        """Test the get_installation_size method."""
        # No transaction.
        size = self.dnf_manager.get_installation_size()
        self.assertEqual(size, Size("3000 MiB"))

        # Fake transaction.
        tsi_1 = Mock()
        tsi_1.pkg.installsize = 1024 * 100
        tsi_1.pkg.files = ["/file"] * 10

        tsi_2 = Mock()
        tsi_2.pkg.installsize = 1024 * 200
        tsi_2.pkg.files = ["/file"] * 20

        self.dnf_manager._base.transaction = [tsi_1, tsi_2]
        size = self.dnf_manager.get_installation_size()
        size = size.round_to_nearest("KiB", ROUND_UP)

        self.assertEqual(size, Size("528 KiB"))

    def get_download_size_test(self):
        """Test the get_download_size method."""
        # No transaction.
        size = self.dnf_manager.get_download_size()
        self.assertEqual(size, Size(0))

        # Fake transaction.
        tsi_1 = Mock()
        tsi_1.pkg.downloadsize = 1024 * 1024 * 100

        tsi_2 = Mock()
        tsi_2.pkg.downloadsize = 1024 * 1024 * 200

        self.dnf_manager._base.transaction = [tsi_1, tsi_2]
        size = self.dnf_manager.get_download_size()

        self.assertEqual(size, Size("450 MiB"))

    def environments_test(self):
        """Test the environments property."""
        self.assertEqual(self.dnf_manager.environments, [])

        # Fake environments.
        env_1 = Mock(id="environment-1")
        env_2 = Mock(id="environment-2")
        env_3 = Mock(id="environment-3")

        # Fake comps.
        comps = Mock(environments=[env_1, env_2, env_3])

        self.dnf_manager._base._comps = comps
        self.assertEqual(self.dnf_manager.environments, [
            "environment-1",
            "environment-2",
            "environment-3",
        ])

    @patch("dnf.base.Base.install_specs")
    def apply_specs_test(self, install_specs):
        """Test the apply_specs method."""
        self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                     exclude_list=["@g2", "p2"])

        install_specs.assert_called_once_with(install=["@g1", "p1"],
                                              exclude=["@g2", "p2"],
                                              strict=True)

    @patch("dnf.base.Base.install_specs")
    def apply_specs_error_test(self, install_specs):
        """Test the apply_specs method with an error."""
        install_specs.side_effect = MarkingErrors(error_group_specs=["@g1"])

        with self.assertRaises(MarkingErrors):
            self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                         exclude_list=["@g2", "p2"])

    @patch("dnf.base.Base.install_specs")
    def apply_specs_ignore_broken_test(self, install_specs):
        """Test the apply_specs method with ignored broken packages."""
        self.dnf_manager._ignore_broken_packages = True
        self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                     exclude_list=["@g2", "p2"])

        install_specs.assert_called_once_with(install=["@g1", "p1"],
                                              exclude=["@g2", "p2"],
                                              strict=False)

    @patch("dnf.base.Base.install_specs")
    def apply_specs_ignore_missing_test(self, install_specs):
        """Test the apply_specs method with ignored missing packages."""
        self.dnf_manager._ignore_missing_packages = True

        # Ignore a missing package.
        install_specs.side_effect = MarkingErrors(no_match_pkg_specs=["p1"])

        self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                     exclude_list=["@g2", "p2"])

        install_specs.assert_called_once_with(install=["@g1", "p1"],
                                              exclude=["@g2", "p2"],
                                              strict=True)

        # Don't ignore a broken transaction.
        install_specs.side_effect = MarkingErrors(error_pkg_specs=["p1"])

        with self.assertRaises(MarkingErrors):
            self.dnf_manager.apply_specs(include_list=["@g1", "p1"],
                                         exclude_list=["@g2", "p2"])
Exemplo n.º 18
0
class DNFPayload(Payload):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A list of verbose error strings
        self.verbose_errors = []

        # Get a DBus payload to use.
        self._payload_proxy = get_payload(self.type)

        self.tx_id = None

        self._dnf_manager = DNFManager()
        self._updates_enabled = True

        # Configure the DNF logging.
        configure_dnf_logging()

        # FIXME: Don't call this method before set_from_opts.
        # This will create a default source if there is none.
        self._configure()

    @property
    def dnf_manager(self):
        """The DNF manager."""
        return self._dnf_manager

    @property
    def _base(self):
        """Return a DNF base.

        FIXME: This is a temporary property.
        """
        return self._dnf_manager._base

    @property
    def _repos_lock(self):
        """A lock for a dictionary of DNF repositories.

        FIXME: This is a temporary property.
        """
        return self._dnf_manager._lock

    def set_from_opts(self, opts):
        """Set the payload from the Anaconda cmdline options.

        :param opts: a namespace of options
        """
        # Set the source based on opts.method if it isn't already set
        # - opts.method is currently set by command line/boot options
        if opts.method and (not self.proxy.Sources
                            or self._is_source_default()):
            try:
                source = SourceFactory.parse_repo_cmdline_string(opts.method)
            except PayloadSourceTypeUnrecognized:
                log.error("Unknown method: %s", opts.method)
            else:
                source_proxy = source.create_proxy()
                set_source(self.proxy, source_proxy)

        # Set up the current source.
        source_proxy = self.get_source_proxy()

        if source_proxy.Type == SOURCE_TYPE_URL:
            # Get the repo configuration.
            repo_configuration = RepoConfigurationData.from_structure(
                source_proxy.RepoConfiguration)

            if opts.proxy:
                repo_configuration.proxy = opts.proxy

            if not conf.payload.verify_ssl:
                repo_configuration.ssl_verification_enabled = conf.payload.verify_ssl

            # Update the repo configuration.
            source_proxy.SetRepoConfiguration(
                RepoConfigurationData.to_structure(repo_configuration))

        # Set up packages.
        if opts.multiLib:
            configuration = self.get_packages_configuration()
            configuration.multilib_policy = MULTILIB_POLICY_ALL
            self.set_packages_configuration(configuration)

        # Reset all the other things now that we have new configuration.
        self._configure()

    @property
    def type(self):
        """The DBus type of the payload."""
        return PAYLOAD_TYPE_DNF

    def get_source_proxy(self):
        """Get the DBus proxy of the RPM source.

        The default source for the DNF payload is set via
        the default_source option in the payload section
        of the Anaconda config file.

        :return: a DBus proxy
        """
        return get_source(self.proxy, conf.payload.default_source)

    @property
    def source_type(self):
        """The DBus type of the source."""
        source_proxy = self.get_source_proxy()
        return source_proxy.Type

    def get_packages_configuration(self) -> PackagesConfigurationData:
        """Get the DBus data with the packages configuration."""
        return PackagesConfigurationData.from_structure(
            self.proxy.PackagesConfiguration)

    def set_packages_configuration(self, data: PackagesConfigurationData):
        """Set the DBus data with the packages configuration."""
        return self.proxy.SetPackagesConfiguration(
            PackagesConfigurationData.to_structure(data))

    def get_packages_selection(self) -> PackagesSelectionData:
        """Get the DBus data with the packages selection."""
        return PackagesSelectionData.from_structure(
            self.proxy.PackagesSelection)

    def set_packages_selection(self, data: PackagesSelectionData):
        """Set the DBus data with the packages selection."""
        return self.proxy.SetPackagesSelection(
            PackagesSelectionData.to_structure(data))

    def is_ready(self):
        """Is the payload ready?"""
        enabled_repos = self._dnf_manager.enabled_repositories

        # If CDN is used as the installation source and we have
        # a subscription attached then any of the enabled repos
        # should be fine as the base repo.
        # If CDN is used but subscription has not been attached
        # there will be no redhat.repo file to parse and we
        # don't need to do anything.
        if self.source_type == SOURCE_TYPE_CDN:
            return self._is_cdn_set_up() and enabled_repos

        # Otherwise, a base repository has to be enabled.
        return any(map(self._is_base_repo, enabled_repos))

    def _is_cdn_set_up(self):
        """Is the CDN source set up?"""
        if not self.source_type == SOURCE_TYPE_CDN:
            return False

        if not is_module_available(SUBSCRIPTION):
            return False

        subscription_proxy = SUBSCRIPTION.get_proxy()
        return subscription_proxy.IsSubscriptionAttached

    def _is_base_repo(self, repo_id):
        """Is it a base repository?"""
        return repo_id == constants.BASE_REPO_NAME \
            or repo_id in constants.DEFAULT_REPOS

    def is_complete(self):
        """Is the payload complete?"""
        return self.source_type not in SOURCE_REPO_FILE_TYPES or self.is_ready(
        )

    def setup(self):
        self.verbose_errors = []

    def unsetup(self):
        self._configure()
        tear_down_sources(self.proxy)

    @property
    def needs_network(self):
        """Test base and additional repositories if they require network."""
        return (self.service_proxy.IsNetworkRequired() or any(
            self._repo_needs_network(repo)
            for repo in self.data.repo.dataList()))

    def _repo_needs_network(self, repo):
        """Returns True if the ksdata repo requires networking."""
        urls = [repo.baseurl]
        if repo.mirrorlist:
            urls.extend(repo.mirrorlist)
        elif repo.metalink:
            urls.extend(repo.metalink)
        return self._source_needs_network(urls)

    def _source_needs_network(self, sources):
        """Return True if the source requires network.

        :param sources: Source paths for testing
        :type sources: list
        :returns: True if any source requires network
        """
        for s in sources:
            if has_network_protocol(s):
                log.debug("Source %s needs network for installation", s)
                return True

        log.debug("Source doesn't require network for installation")
        return False

    def bump_tx_id(self):
        if self.tx_id is None:
            self.tx_id = 1
        else:
            self.tx_id += 1
        return self.tx_id

    def _get_proxy_url(self):
        """Get a proxy of the current source.

        :return: a proxy or None
        """
        source_proxy = self.get_source_proxy()
        source_type = source_proxy.Type

        if source_type != SOURCE_TYPE_URL:
            return None

        data = RepoConfigurationData.from_structure(
            source_proxy.RepoConfiguration)

        return data.proxy

    def _configure(self):
        self._dnf_manager.reset_base()
        self._dnf_manager.configure_base(self.get_packages_configuration())
        self._dnf_manager.configure_proxy(self._get_proxy_url())
        self._dnf_manager.dump_configuration()

    def _sync_metadata(self, dnf_repo):
        try:
            dnf_repo.load()
        except dnf.exceptions.RepoError as e:
            id_ = dnf_repo.id
            log.info('_sync_metadata: addon repo error: %s', e)
            self._set_repo_enabled(id_, False)
            self.verbose_errors.append(str(e))
        log.debug('repo %s: _sync_metadata success from %s', dnf_repo.id,
                  dnf_repo.baseurl or dnf_repo.mirrorlist or dnf_repo.metalink)

    ###
    # METHODS FOR WORKING WITH REPOSITORIES
    ###

    def get_addon_repo(self, repo_id):
        """Return a ksdata Repo instance matching the specified repo id."""
        repo = None
        for r in self.data.repo.dataList():
            if r.name == repo_id:
                repo = r
                break

        return repo

    def _add_repo_to_dnf_and_ks(self, ksrepo):
        """Add an enabled repo to dnf and kickstart repo lists.

        Add the repo given by the pykickstart Repo object ksrepo to the
        system.

        Duplicate repos will not raise an error.  They should just silently
        take the place of the previous value.

        :param ksrepo: Kickstart Repository to add
        :type ksrepo: Kickstart RepoData object.
        :returns: None
        """
        if ksrepo.enabled:
            self._add_repo_to_dnf(ksrepo)
            self._dnf_manager.load_repository(ksrepo.name)

        # Add the repo to the ksdata so it'll appear in the output ks file.
        self.data.repo.dataList().append(ksrepo)

    def _add_repo_to_dnf(self, ksrepo):
        """Add a repo to the dnf repo object.

        :param ksrepo: Kickstart Repository to add
        :type ksrepo: Kickstart RepoData object.
        :returns: None
        """
        data = convert_ks_repo_to_repo_data(ksrepo)
        enabled = ksrepo.enabled

        # An existing repository can be only enabled or disabled.
        if self._is_existing_repo_configuration(data):
            self._dnf_manager.set_repository_enabled(data.name, enabled)
            return

        # Set up the NFS source with a substituted URL.
        if data.url.startswith("nfs://"):
            url = self._dnf_manager.substitute(data.url)
            (server, path) = url[6:].split(":", 1)
            mountpoint = "%s/%s.nfs" % (constants.MOUNT_DIR, data.name)
            self._setup_NFS(mountpoint, server, path, None)
            data.url = "file://" + mountpoint

        # Add a new repository.
        self._dnf_manager.add_repository(data)

    def _is_existing_repo_configuration(self, data):
        """Is it a configuration of an existing repository?

        The user is trying to do "repo --name=updates" in a kickstart file.
        We can only enable or disable the already existing on-disk repo config.
        """
        return not data.url and data.name in self._dnf_manager.repositories

    def _remove_repo(self, repo_id):
        repos = self.data.repo.dataList()
        try:
            idx = [repo.name for repo in repos].index(repo_id)
        except ValueError:
            log.error("failed to remove repo %s: not found", repo_id)
        else:
            repos.pop(idx)

    def add_driver_repos(self):
        """Add driver repositories and packages.

        FIXME: Don't run this code on every payload restart.
        """
        # Drivers are loaded by anaconda-dracut, their repos are copied
        # into /run/install/DD-X where X is a number starting at 1. The list of
        # packages that were selected is in /run/install/dd_packages

        # Add repositories
        dir_num = 0
        while True:
            dir_num += 1
            repo = "/run/install/DD-%d/" % dir_num
            if not os.path.isdir(repo):
                break

            # Run createrepo if there are rpms and no repodata
            if not os.path.isdir(repo + "/repodata"):
                rpms = glob(repo + "/*rpm")
                if not rpms:
                    continue
                log.info("Running createrepo on %s", repo)
                util.execWithRedirect("createrepo_c", [repo])

            # Generate the repo name.
            repo_name = "DD-%d" % dir_num

            # The repo has been already created (#1268357).
            for ks_repo in self.data.repo.dataList():
                if repo_name == ks_repo.name:
                    continue

            # Or create a new one.
            ks_repo = self.data.RepoData(name=repo_name,
                                         baseurl="file://" + repo,
                                         enabled=True)

            self._add_repo_to_dnf_and_ks(ks_repo)

    @property
    def space_required(self):
        return calculate_required_space(self._dnf_manager)

    def set_updates_enabled(self, state):
        """Enable or Disable the repos used to update closest mirror.

        :param bool state: True to enable updates, False to disable.
        """
        self._updates_enabled = state

        # Enable or disable updates.
        for repo_id in conf.payload.updates_repositories:
            self._set_repo_enabled(repo_id, state)

        # Disable updates-testing.
        self._set_repo_enabled("updates-testing", False)
        self._set_repo_enabled("updates-testing-modular", False)

    def _set_repo_enabled(self, repo_id, enabled):
        """Enable or disable the repo in DNF and its data representation."""
        try:
            self._dnf_manager.set_repository_enabled(repo_id, enabled)
        except UnknownRepositoryError:
            pass

        repo = self.get_addon_repo(repo_id)
        if repo:
            repo.enabled = enabled

    def gather_repo_metadata(self):
        with self._repos_lock:
            for repo in self._base.repos.iter_enabled():
                self._sync_metadata(repo)
        self._base.fill_sack(load_system_repo=False)
        self._base.read_comps(arch_filter=True)

    def install(self):
        self._progress_cb(0, _('Starting package installation process'))

        # Get the packages configuration and selection data.
        configuration = self.get_packages_configuration()
        selection = self.get_packages_selection()

        # Add the rpm macros to the global transaction environment
        task = SetRPMMacrosTask(configuration)
        task.run()

        try:
            # Resolve packages.
            task = ResolvePackagesTask(self._dnf_manager, selection)
            task.run()
        except NonCriticalInstallationError as e:
            # FIXME: This is a temporary workaround.
            # Allow users to handle the error. If they don't want
            # to continue with the installation, raise a different
            # exception to make sure that we will not run the error
            # handler again.
            if error_handler.cb(e) == ERROR_RAISE:
                raise InstallationError(str(e)) from e

        # Set up the download location.
        task = PrepareDownloadLocationTask(self._dnf_manager)
        task.run()

        # Download the packages.
        task = DownloadPackagesTask(self._dnf_manager)
        task.progress_changed_signal.connect(self._progress_cb)
        task.run()

        # Install the packages.
        task = InstallPackagesTask(self._dnf_manager)
        task.progress_changed_signal.connect(self._progress_cb)
        task.run()

        # Clean up the download location.
        task = CleanUpDownloadLocationTask(self._dnf_manager)
        task.run()

        # Don't close the mother base here, because we still need it.

    def is_repo_enabled(self, repo_id):
        """Return True if repo is enabled."""
        try:
            return self._base.repos[repo_id].enabled
        except (dnf.exceptions.RepoError, KeyError):
            repo = self.get_addon_repo(repo_id)
            if repo:
                return repo.enabled
            else:
                return False

    def _reset_configuration(self):
        tear_down_sources(self.proxy)
        self._reset_additional_repos()
        self.tx_id = None
        self._dnf_manager.clear_cache()
        self._dnf_manager.reset_substitution()
        self._dnf_manager.configure_proxy(self._get_proxy_url())

    def _reset_additional_repos(self):
        for name in self._find_mounted_additional_repos():
            installation_dir = INSTALL_TREE + "-" + name
            self._unmount_source_directory(installation_dir)

            iso_dir = ISO_DIR + "-" + name
            self._unmount_source_directory(iso_dir)

    def _find_mounted_additional_repos(self):
        prefix = ISO_DIR + "-"
        prefix_len = len(prefix)
        result = []

        for dir_path in glob(prefix + "*"):
            result.append(dir_path[prefix_len:])

        return result

    def _unmount_source_directory(self, mount_point):
        if os.path.ismount(mount_point):
            device_path = payload_utils.get_mount_device_path(mount_point)
            device = payload_utils.resolve_device(device_path)
            if device:
                payload_utils.teardown_device(device)
            else:
                payload_utils.unmount(mount_point, raise_exc=True)

    def _is_source_default(self):
        """Report if the current source type is the default source type.

        NOTE: If no source was set previously a new default one
              will be created.
        """
        return self.source_type == conf.payload.default_source

    def update_base_repo(self, fallback=True, checkmount=True):
        """Update the base repository from the DBus source."""
        log.info("Configuring the base repo")
        self._reset_configuration()

        disabled_treeinfo_repo_names = self._cleanup_old_treeinfo_repositories(
        )

        # Find the source and its type.
        source_proxy = self.get_source_proxy()
        source_type = source_proxy.Type

        # Change the default source to CDROM if there is a valid install media.
        # FIXME: Set up the default source earlier.
        if checkmount and self._is_source_default(
        ) and find_optical_install_media():
            source_type = SOURCE_TYPE_CDROM
            source_proxy = create_source(source_type)
            set_source(self.proxy, source_proxy)

        # Set up the source.
        set_up_sources(self.proxy)

        # Read in all the repos from the installation environment, make a note of which
        # are enabled, and then disable them all.  If the user gave us a method, we want
        # to use that instead of the default repos.
        self._base.read_all_repos()

        # Enable or disable updates.
        self.set_updates_enabled(self._updates_enabled)

        # Repo files are always loaded from the system.
        # When reloaded their state needs to be synchronized with the user configuration.
        # So we disable them now and enable them later if required.
        enabled = []
        with self._repos_lock:
            for repo in self._base.repos.iter_enabled():
                enabled.append(repo.id)
                self._dnf_manager.set_repository_enabled(repo.id, False)

        # Add a new repo.
        if source_type not in SOURCE_REPO_FILE_TYPES:
            # Get the repo configuration of the first source.
            data = RepoConfigurationData.from_structure(
                self.proxy.GetRepoConfigurations()[0])

            log.debug("Using the repo configuration: %s", data)

            # Get the URL.
            install_tree_url = data.url if data.type == URL_TYPE_BASEURL else ""
            mirrorlist = data.url if data.type == URL_TYPE_MIRRORLIST else ""
            metalink = data.url if data.type == URL_TYPE_METALINK else ""

            # Fallback to the installation root.
            base_repo_url = install_tree_url

            try:
                tree_info_metadata = TreeInfoMetadata()
                tree_info_metadata.load_data(data)

                self._dnf_manager.configure_substitution(
                    tree_info_metadata.release_version)

                base_repo_url = tree_info_metadata.get_base_repo_url()

                self._load_treeinfo_repositories(tree_info_metadata,
                                                 base_repo_url,
                                                 disabled_treeinfo_repo_names,
                                                 data)
            except NoTreeInfoError as e:
                log.debug("No treeinfo metadata to use: %s", str(e))
            except TreeInfoMetadataError as e:
                log.warning("Couldn't use treeinfo metadata: %s", str(e))

            try:
                base_ksrepo = self.data.RepoData(
                    name=constants.BASE_REPO_NAME,
                    baseurl=base_repo_url,
                    mirrorlist=mirrorlist,
                    metalink=metalink,
                    noverifyssl=not data.ssl_verification_enabled,
                    proxy=data.proxy,
                    sslcacert=data.ssl_configuration.ca_cert_path,
                    sslclientcert=data.ssl_configuration.client_cert_path,
                    sslclientkey=data.ssl_configuration.client_key_path)
                self._add_repo_to_dnf(base_ksrepo)
                self._dnf_manager.load_repository(base_ksrepo.name)
            except (DNFManagerError, PayloadError) as e:
                log.error("base repo (%s/%s) not valid -- removing it",
                          source_type, base_repo_url)
                log.error("reason for repo removal: %s", e)
                with self._repos_lock:
                    self._base.repos.pop(constants.BASE_REPO_NAME, None)
                if not fallback:
                    with self._repos_lock:
                        for repo in self._base.repos.iter_enabled():
                            self._set_repo_enabled(repo.id, False)
                    return

                # Fallback to the default source
                #
                # This is at the moment CDN on RHEL
                # and closest mirror everywhere else.
                tear_down_sources(self.proxy)

                source_type = conf.payload.default_source
                source_proxy = create_source(source_type)
                set_source(self.proxy, source_proxy)

                set_up_sources(self.proxy)

        # We need to check this again separately in case REPO_FILES were set above.
        if source_type in SOURCE_REPO_FILE_TYPES:
            # If this is a kickstart install, just return now as we normally do not
            # want to read the on media repo files in such a case. On the other hand,
            # the local repo files are a valid use case if the system is subscribed
            # and the CDN is selected as the installation source.
            if flags.automatedInstall and not self._is_cdn_set_up():
                return

            # Otherwise, fall back to the default repos that we disabled above
            for repo_id in enabled:
                log.debug("repo %s: fall back enabled from default repos",
                          repo_id)
                try:
                    self._dnf_manager.set_repository_enabled(repo_id, True)
                except UnknownRepositoryError:
                    pass

        for ksrepo in self.data.repo.dataList():
            if ksrepo.is_harddrive_based():
                ksrepo.baseurl = self._setup_harddrive_addon_repo(ksrepo)

            log.debug("repo %s: mirrorlist %s, baseurl %s, metalink %s",
                      ksrepo.name, ksrepo.mirrorlist, ksrepo.baseurl,
                      ksrepo.metalink)
            # one of these must be set to create new repo
            if not (ksrepo.mirrorlist or ksrepo.baseurl or ksrepo.metalink
                    or ksrepo.name in self._base.repos):
                raise PayloadSetupError(
                    "Repository %s has no mirror, baseurl or "
                    "metalink set and is not one of "
                    "the pre-defined repositories" % ksrepo.name)

            self._add_repo_to_dnf(ksrepo)

        with self._repos_lock:

            # disable unnecessary repos
            for repo in self._base.repos.iter_enabled():
                id_ = repo.id
                if 'source' in id_ or 'debuginfo' in id_:
                    self._dnf_manager.set_repository_enabled(id_, False)
                elif constants.isFinal and 'rawhide' in id_:
                    self._dnf_manager.set_repository_enabled(id_, False)

            # fetch md for enabled repos
            for ks_repo in self.data.repo.dataList():
                if self.is_repo_enabled(ks_repo.name):
                    self._dnf_manager.load_repository(ks_repo.name)

    def _find_and_mount_iso(self, device, device_mount_dir, iso_path,
                            iso_mount_dir):
        """Find and mount installation source from ISO on device.

        Return changed path to the iso to save looking for iso in the future call.
        """
        self._setup_device(device, mountpoint=device_mount_dir)

        # check for ISO images in the newly mounted dir
        path = device_mount_dir
        if iso_path:
            path = os.path.normpath("%s/%s" % (path, iso_path))

        # XXX it would be nice to streamline this when we're just setting
        #     things back up after storage activation instead of having to
        #     pretend we don't already know which ISO image we're going to
        #     use
        image = find_first_iso_image(path)
        if not image:
            payload_utils.teardown_device(device)
            raise PayloadSetupError("failed to find valid iso image")

        if path.endswith(".iso"):
            path = os.path.dirname(path)

        # this could already be set up the first time through
        if not os.path.ismount(iso_mount_dir):
            # mount the ISO on a loop
            image = os.path.normpath("%s/%s" % (path, image))
            payload_utils.mount(image,
                                iso_mount_dir,
                                fstype='iso9660',
                                options="ro")

        if not iso_path.endswith(".iso"):
            result_path = os.path.normpath("%s/%s" %
                                           (iso_path, os.path.basename(image)))
            while result_path.startswith("/"):
                # ridiculous
                result_path = result_path[1:]

            return result_path

        return iso_path

    @staticmethod
    def _setup_device(device, mountpoint):
        """Prepare an install CD/DVD for use as a package source."""
        log.info("setting up device %s and mounting on %s", device, mountpoint)
        # Is there a symlink involved?  If so, let's get the actual path.
        # This is to catch /run/install/isodir vs. /mnt/install/isodir, for
        # instance.
        real_mountpoint = os.path.realpath(mountpoint)
        mount_device_path = payload_utils.get_mount_device_path(
            real_mountpoint)

        if mount_device_path:
            log.warning("%s is already mounted on %s", mount_device_path,
                        mountpoint)

            if mount_device_path == payload_utils.get_device_path(device):
                return
            else:
                payload_utils.unmount(real_mountpoint)

        try:
            payload_utils.setup_device(device)
            payload_utils.mount_device(device, mountpoint)
        except (DeviceSetupError, MountFilesystemError) as e:
            log.error("mount failed: %s", e)
            payload_utils.teardown_device(device)
            raise PayloadSetupError(str(e)) from e

    @staticmethod
    def _setup_NFS(mountpoint, server, path, options):
        """Prepare an NFS directory for use as an install source."""
        log.info("mounting %s:%s:%s on %s", server, path, options, mountpoint)
        device_path = payload_utils.get_mount_device_path(mountpoint)

        # test if the mountpoint is occupied already
        if device_path:
            _server, colon, _path = device_path.partition(":")
            if colon == ":" and server == _server and path == _path:
                log.debug("%s:%s already mounted on %s", server, path,
                          mountpoint)
                return
            else:
                log.debug("%s already has something mounted on it", mountpoint)
                payload_utils.unmount(mountpoint)

        # mount the specified directory
        url = "%s:%s" % (server, path)

        if not options:
            options = "nolock"
        elif "nolock" not in options:
            options += ",nolock"

        payload_utils.mount(url, mountpoint, fstype="nfs", options=options)

    def _setup_harddrive_addon_repo(self, ksrepo):
        iso_device = payload_utils.resolve_device(ksrepo.partition)
        if not iso_device:
            raise PayloadSetupError(
                "device for HDISO addon repo install %s does not exist" %
                ksrepo.partition)

        ksrepo.generate_mount_dir()

        device_mount_dir = ISO_DIR + "-" + ksrepo.mount_dir_suffix
        install_root_dir = INSTALL_TREE + "-" + ksrepo.mount_dir_suffix

        self._find_and_mount_iso(iso_device, device_mount_dir, ksrepo.iso_path,
                                 install_root_dir)
        url = "file://" + install_root_dir

        return url

    def _load_treeinfo_repositories(self, tree_info_metadata, base_repo_url,
                                    repo_names_to_disable, data):
        """Load new repositories from treeinfo file.

        :param base_repo_url: base repository url. This is not saved anywhere when the function
                              is called. It will be add to the existing urls if not None.
        :param repo_names_to_disable: list of repository names which should be disabled after load
        :type repo_names_to_disable: [str]
        :param data: repo configuration data
        """
        existing_urls = []

        if base_repo_url is not None:
            existing_urls.append(base_repo_url)

        for ks_repo in self.data.repo.dataList():
            baseurl = ks_repo.baseurl
            existing_urls.append(baseurl)

        for repo_md in tree_info_metadata.repositories:
            if repo_md.path in existing_urls:
                continue

            # disable repositories disabled by user manually before
            repo_enabled = repo_md.enabled \
                and repo_md.name not in repo_names_to_disable

            repo = RepoData(
                name=repo_md.name,
                baseurl=repo_md.path,
                noverifyssl=not data.ssl_verification_enabled,
                proxy=data.proxy,
                sslcacert=data.ssl_configuration.ca_cert_path,
                sslclientcert=data.ssl_configuration.client_cert_path,
                sslclientkey=data.ssl_configuration.client_key_path,
                install=False,
                enabled=repo_enabled)

            repo.treeinfo_origin = True
            log.debug("Adding new treeinfo repository: %s enabled: %s",
                      repo_md.name, repo_enabled)

            self._add_repo_to_dnf_and_ks(repo)

    def _cleanup_old_treeinfo_repositories(self):
        """Remove all old treeinfo repositories before loading new ones.

        Find all repositories added from treeinfo file and remove them. After this step new
        repositories will be loaded from the new link.

        :return: list of repository names which were disabled before removal
        :rtype: [str]
        """
        disabled_repo_names = []

        for ks_repo in list(self.data.repo.dataList()):
            if ks_repo.treeinfo_origin:
                log.debug("Removing old treeinfo repository %s", ks_repo.name)

                if not ks_repo.enabled:
                    disabled_repo_names.append(ks_repo.name)

                self._remove_repo(ks_repo.name)

        return disabled_repo_names

    def post_install(self):
        """Perform post-installation tasks."""
        # Write selected kickstart repos to target system
        repositories = list(
            map(convert_ks_repo_to_repo_data, self.data.repo.dataList()))

        task = WriteRepositoriesTask(
            sysroot=conf.target.system_root,
            dnf_manager=self.dnf_manager,
            repositories=repositories,
        )
        task.run()

        # We don't need the mother base anymore. Close it.
        self._base.close()
        super().post_install()

        # rpm needs importing installed certificates manually, see rhbz#748320 and rhbz#185800
        task = ImportRPMKeysTask(sysroot=conf.target.system_root,
                                 gpg_keys=conf.payload.default_rpm_gpg_keys)
        task.run()

        # Update the DNF configuration.
        task = UpdateDNFConfigurationTask(
            sysroot=conf.target.system_root,
            data=self.get_packages_configuration())
        task.run()

    @property
    def kernel_version_list(self):
        return get_kernel_version_list()
Exemplo n.º 19
0
class DNFPayload(Payload):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A list of verbose error strings
        self.verbose_errors = []

        # Get a DBus payload to use.
        self._payload_proxy = get_payload(self.type)

        self.tx_id = None
        self._install_tree_metadata = None

        self._dnf_manager = DNFManager()
        self._updates_enabled = True

        # Configure the DNF logging.
        configure_dnf_logging()

        # FIXME: Don't call this method before set_from_opts.
        # This will create a default source if there is none.
        self._configure()

        # Protect access to _base.repos to ensure that the dictionary is not
        # modified while another thread is attempting to iterate over it. The
        # lock only needs to be held during operations that change the number
        # of repos or that iterate over the repos.
        self._repos_lock = threading.RLock()

        # save repomd metadata
        self._repoMD_list = []

    @property
    def dnf_manager(self):
        """The DNF manager."""
        return self._dnf_manager

    @property
    def _base(self):
        """Return a DNF base.

        FIXME: This is a temporary property.
        """
        return self._dnf_manager._base

    def set_from_opts(self, opts):
        """Set the payload from the Anaconda cmdline options.

        :param opts: a namespace of options
        """
        # Set the source based on opts.method if it isn't already set
        # - opts.method is currently set by command line/boot options
        if opts.method and (not self.proxy.Sources or self._is_source_default()):
            try:
                source = SourceFactory.parse_repo_cmdline_string(opts.method)
            except PayloadSourceTypeUnrecognized:
                log.error("Unknown method: %s", opts.method)
            else:
                source_proxy = source.create_proxy()
                set_source(self.proxy, source_proxy)

        # Set up the current source.
        source_proxy = self.get_source_proxy()

        if source_proxy.Type == SOURCE_TYPE_URL:
            # Get the repo configuration.
            repo_configuration = RepoConfigurationData.from_structure(
                source_proxy.RepoConfiguration
            )

            if opts.proxy:
                repo_configuration.proxy = opts.proxy

            if not conf.payload.verify_ssl:
                repo_configuration.ssl_verification_enabled = conf.payload.verify_ssl

            # Update the repo configuration.
            source_proxy.SetRepoConfiguration(
                RepoConfigurationData.to_structure(repo_configuration)
            )

        # Set up packages.
        if opts.multiLib:
            configuration = self.get_packages_configuration()
            configuration.multilib_policy = MULTILIB_POLICY_ALL
            self.set_packages_configuration(configuration)

        # Reset all the other things now that we have new configuration.
        self._configure()

    @property
    def type(self):
        """The DBus type of the payload."""
        return PAYLOAD_TYPE_DNF

    def get_source_proxy(self):
        """Get the DBus proxy of the RPM source.

        The default source for the DNF payload is set via
        the default_source option in the payload section
        of the Anaconda config file.

        :return: a DBus proxy
        """
        return get_source(self.proxy, conf.payload.default_source)

    @property
    def source_type(self):
        """The DBus type of the source."""
        source_proxy = self.get_source_proxy()
        return source_proxy.Type

    def get_packages_configuration(self) -> PackagesConfigurationData:
        """Get the DBus data with the packages configuration."""
        return PackagesConfigurationData.from_structure(
            self.proxy.PackagesConfiguration
        )

    def set_packages_configuration(self, data: PackagesConfigurationData):
        """Set the DBus data with the packages configuration."""
        return self.proxy.SetPackagesConfiguration(
            PackagesConfigurationData.to_structure(data)
        )

    def get_packages_selection(self) -> PackagesSelectionData:
        """Get the DBus data with the packages selection."""
        return PackagesSelectionData.from_structure(
            self.proxy.PackagesSelection
        )

    def set_packages_selection(self, data: PackagesSelectionData):
        """Set the DBus data with the packages selection."""
        return self.proxy.SetPackagesSelection(
            PackagesSelectionData.to_structure(data)
        )

    def is_ready(self):
        """Is the payload ready?"""
        return self.base_repo is not None

    def is_complete(self):
        """Is the payload complete?"""
        return self.source_type not in SOURCE_REPO_FILE_TYPES or self.base_repo

    def setup(self):
        self.verbose_errors = []

    def unsetup(self):
        self._configure()
        self._repoMD_list = []
        self._install_tree_metadata = None
        tear_down_sources(self.proxy)

    @property
    def needs_network(self):
        """Test base and additional repositories if they require network."""
        return (self.service_proxy.IsNetworkRequired() or
                any(self._repo_needs_network(repo) for repo in self.data.repo.dataList()))

    def _repo_needs_network(self, repo):
        """Returns True if the ksdata repo requires networking."""
        urls = [repo.baseurl]
        if repo.mirrorlist:
            urls.extend(repo.mirrorlist)
        elif repo.metalink:
            urls.extend(repo.metalink)
        return self._source_needs_network(urls)

    def _source_needs_network(self, sources):
        """Return True if the source requires network.

        :param sources: Source paths for testing
        :type sources: list
        :returns: True if any source requires network
        """
        for s in sources:
            if has_network_protocol(s):
                log.debug("Source %s needs network for installation", s)
                return True

        log.debug("Source doesn't require network for installation")
        return False

    def bump_tx_id(self):
        if self.tx_id is None:
            self.tx_id = 1
        else:
            self.tx_id += 1
        return self.tx_id

    def _get_proxy_url(self):
        """Get a proxy of the current source.

        :return: a proxy or None
        """
        source_proxy = self.get_source_proxy()
        source_type = source_proxy.Type

        if source_type != SOURCE_TYPE_URL:
            return None

        data = RepoConfigurationData.from_structure(
            source_proxy.RepoConfiguration
        )

        return data.proxy

    def _configure(self):
        self._dnf_manager.reset_base()
        self._dnf_manager.configure_base(self.get_packages_configuration())
        self._dnf_manager.configure_proxy(self._get_proxy_url())
        self._dnf_manager.dump_configuration()

    def _sync_metadata(self, dnf_repo):
        try:
            dnf_repo.load()
        except dnf.exceptions.RepoError as e:
            id_ = dnf_repo.id
            log.info('_sync_metadata: addon repo error: %s', e)
            self._disable_repo(id_)
            self.verbose_errors.append(str(e))
        log.debug('repo %s: _sync_metadata success from %s', dnf_repo.id,
                  dnf_repo.baseurl or dnf_repo.mirrorlist or dnf_repo.metalink)

    @property
    def base_repo(self):
        """Get the identifier of the current base repo or None."""
        # is any locking needed here?
        repo_names = [constants.BASE_REPO_NAME] + constants.DEFAULT_REPOS
        with self._repos_lock:
            if self.source_type == SOURCE_TYPE_CDN:
                if is_module_available(SUBSCRIPTION):
                    subscription_proxy = SUBSCRIPTION.get_proxy()
                    if subscription_proxy.IsSubscriptionAttached:
                        # If CDN is used as the installation source and we have
                        # a subscription attached then any of the enabled repos
                        # should be fine as the base repo.
                        # If CDN is used but subscription has not been attached
                        # there will be no redhat.repo file to parse and we
                        # don't need to do anything.
                        for repo in self._base.repos.iter_enabled():
                            return repo.id
                else:
                    log.error("CDN install source set but Subscription module is not available")
            else:
                for repo in self._base.repos.iter_enabled():
                    if repo.id in repo_names:
                        return repo.id

        return None

    ###
    # METHODS FOR WORKING WITH REPOSITORIES
    ###

    @property
    def repos(self):
        """A list of repo identifiers, not objects themselves."""
        with self._repos_lock:
            return [r.id for r in self._base.repos.values()]

    @property
    def addons(self):
        """A list of addon repo names."""
        return [r.name for r in self.data.repo.dataList()]

    @property
    def _enabled_repos(self):
        """A list of names of the enabled repos."""
        enabled = []
        for repo in self.addons:
            if self.is_repo_enabled(repo):
                enabled.append(repo)

        return enabled

    def get_addon_repo(self, repo_id):
        """Return a ksdata Repo instance matching the specified repo id."""
        repo = None
        for r in self.data.repo.dataList():
            if r.name == repo_id:
                repo = r
                break

        return repo

    def _add_repo_to_dnf_and_ks(self, ksrepo):
        """Add an enabled repo to dnf and kickstart repo lists.

        Add the repo given by the pykickstart Repo object ksrepo to the
        system.

        Duplicate repos will not raise an error.  They should just silently
        take the place of the previous value.

        :param ksrepo: Kickstart Repository to add
        :type ksrepo: Kickstart RepoData object.
        :returns: None
        """
        if ksrepo.enabled:
            self._add_repo_to_dnf(ksrepo)
            self._fetch_md(ksrepo.name)

        # Add the repo to the ksdata so it'll appear in the output ks file.
        self.data.repo.dataList().append(ksrepo)

    def _add_repo_to_dnf(self, ksrepo):
        """Add a repo to the dnf repo object.

        :param ksrepo: Kickstart Repository to add
        :type ksrepo: Kickstart RepoData object.
        :returns: None
        """
        repo = dnf.repo.Repo(ksrepo.name, self._base.conf)
        url = self._dnf_manager.substitute(ksrepo.baseurl)
        mirrorlist = self._dnf_manager.substitute(ksrepo.mirrorlist)
        metalink = self._dnf_manager.substitute(ksrepo.metalink)

        if url and url.startswith("nfs://"):
            (server, path) = url[6:].split(":", 1)
            # DNF is dynamically creating properties which seems confusing for Pylint here
            # pylint: disable=no-member
            mountpoint = "%s/%s.nfs" % (constants.MOUNT_DIR, repo.name)
            self._setup_NFS(mountpoint, server, path, None)

            url = "file://" + mountpoint

        if url:
            repo.baseurl = [url]
        if mirrorlist:
            repo.mirrorlist = mirrorlist
        if metalink:
            repo.metalink = metalink
        repo.sslverify = not ksrepo.noverifyssl and conf.payload.verify_ssl
        if ksrepo.proxy:
            try:
                repo.proxy = ProxyString(ksrepo.proxy).url
            except ProxyStringError as e:
                log.error("Failed to parse proxy for _add_repo %s: %s",
                          ksrepo.proxy, e)

        if ksrepo.cost:
            repo.cost = ksrepo.cost

        if ksrepo.includepkgs:
            repo.include = ksrepo.includepkgs

        if ksrepo.excludepkgs:
            repo.exclude = ksrepo.excludepkgs

        if ksrepo.sslcacert:
            repo.sslcacert = ksrepo.sslcacert

        if ksrepo.sslclientcert:
            repo.sslclientcert = ksrepo.sslclientcert

        if ksrepo.sslclientkey:
            repo.sslclientkey = ksrepo.sslclientkey

        # If this repo is already known, it's one of two things:
        # (1) The user is trying to do "repo --name=updates" in a kickstart file
        #     and we should just know to enable the already existing on-disk
        #     repo config.
        # (2) It's a duplicate, and we need to delete the existing definition
        #     and use this new one.  The highest profile user of this is livecd
        #     kickstarts.
        if repo.id in self._base.repos:
            if not url and not mirrorlist and not metalink:
                self._base.repos[repo.id].enable()
            else:
                with self._repos_lock:
                    self._base.repos.pop(repo.id)
                    self._base.repos.add(repo)
        # If the repo's not already known, we've got to add it.
        else:
            with self._repos_lock:
                self._base.repos.add(repo)

        if not ksrepo.enabled:
            self._disable_repo(repo.id)

        log.info("added repo: '%s' - %s", ksrepo.name, url or mirrorlist or metalink)

    def _fetch_md(self, repo_name):
        """Download repo metadata

        :param repo_name: name/id of repo to fetch
        :type repo_name: str
        :returns: None
        """
        repo = self._base.repos[repo_name]
        repo.enable()
        try:
            # Load the metadata to verify that the repo is valid
            repo.load()
        except dnf.exceptions.RepoError as e:
            repo.disable()
            log.debug("repo: '%s' - %s failed to load repomd", repo.id,
                      repo.baseurl or repo.mirrorlist or repo.metalink)
            raise MetadataError(e) from e

        log.info("enabled repo: '%s' - %s and got repomd", repo.id,
                 repo.baseurl or repo.mirrorlist or repo.metalink)

    def _remove_repo(self, repo_id):
        repos = self.data.repo.dataList()
        try:
            idx = [repo.name for repo in repos].index(repo_id)
        except ValueError:
            log.error("failed to remove repo %s: not found", repo_id)
        else:
            repos.pop(idx)

    def add_driver_repos(self):
        """Add driver repositories and packages."""
        # Drivers are loaded by anaconda-dracut, their repos are copied
        # into /run/install/DD-X where X is a number starting at 1. The list of
        # packages that were selected is in /run/install/dd_packages

        # Add repositories
        dir_num = 0
        while True:
            dir_num += 1
            repo = "/run/install/DD-%d/" % dir_num
            if not os.path.isdir(repo):
                break

            # Run createrepo if there are rpms and no repodata
            if not os.path.isdir(repo + "/repodata"):
                rpms = glob(repo + "/*rpm")
                if not rpms:
                    continue
                log.info("Running createrepo on %s", repo)
                util.execWithRedirect("createrepo_c", [repo])

            repo_name = "DD-%d" % dir_num
            if repo_name not in self.addons:
                ks_repo = self.data.RepoData(name=repo_name,
                                             baseurl="file://" + repo,
                                             enabled=True)
                self._add_repo_to_dnf_and_ks(ks_repo)

    @property
    def space_required(self):
        return calculate_required_space(self._dnf_manager)

    def set_updates_enabled(self, state):
        """Enable or Disable the repos used to update closest mirror.

        :param bool state: True to enable updates, False to disable.
        """
        self._updates_enabled = state

        # Enable or disable updates.
        if self._updates_enabled:
            for repo in conf.payload.updates_repositories:
                self._enable_repo(repo)
        else:
            for repo in conf.payload.updates_repositories:
                self._disable_repo(repo)

        # Disable updates-testing.
        self._disable_repo("updates-testing")
        self._disable_repo("updates-testing-modular")

    def _disable_repo(self, repo_id):
        try:
            self._base.repos[repo_id].disable()
            log.info("Disabled '%s'", repo_id)
        except KeyError:
            pass

        repo = self.get_addon_repo(repo_id)
        if repo:
            repo.enabled = False

    def _enable_repo(self, repo_id):
        try:
            self._base.repos[repo_id].enable()
            log.info("Enabled '%s'", repo_id)
        except KeyError:
            pass

        repo = self.get_addon_repo(repo_id)
        if repo:
            repo.enabled = True

    def gather_repo_metadata(self):
        with self._repos_lock:
            for repo in self._base.repos.iter_enabled():
                self._sync_metadata(repo)
        self._base.fill_sack(load_system_repo=False)
        self._base.read_comps(arch_filter=True)

    def _progress_cb(self, step, message):
        """Callback for task progress reporting."""
        progressQ.send_message(message)

    def install(self):
        progress_message(N_('Starting package installation process'))

        # Get the packages configuration and selection data.
        configuration = self.get_packages_configuration()
        selection = self.get_packages_selection()

        # Add the rpm macros to the global transaction environment
        task = SetRPMMacrosTask(configuration)
        task.run()

        try:
            # Resolve packages.
            task = ResolvePackagesTask(self._dnf_manager, selection)
            task.run()
        except NonCriticalInstallationError as e:
            # FIXME: This is a temporary workaround.
            # Allow users to handle the error. If they don't want
            # to continue with the installation, raise a different
            # exception to make sure that we will not run the error
            # handler again.
            if error_handler.cb(e) == ERROR_RAISE:
                raise InstallationError(str(e)) from e

        # Set up the download location.
        task = PrepareDownloadLocationTask(self._dnf_manager)
        task.run()

        # Download the packages.
        task = DownloadPackagesTask(self._dnf_manager)
        task.progress_changed_signal.connect(self._progress_cb)
        task.run()

        # Install the packages.
        task = InstallPackagesTask(self._dnf_manager)
        task.progress_changed_signal.connect(self._progress_cb)
        task.run()

        # Clean up the download location.
        task = CleanUpDownloadLocationTask(self._dnf_manager)
        task.run()

        # Don't close the mother base here, because we still need it.

    def _get_repo(self, repo_id):
        """Return the yum repo object."""
        return self._base.repos[repo_id]

    def is_repo_enabled(self, repo_id):
        """Return True if repo is enabled."""
        try:
            return self._base.repos[repo_id].enabled
        except (dnf.exceptions.RepoError, KeyError):
            repo = self.get_addon_repo(repo_id)
            if repo:
                return repo.enabled
            else:
                return False

    def verify_available_repositories(self):
        """Verify availability of existing repositories.

        This method tests if URL links from active repositories can be reached.
        It is useful when network settings is changed so that we can verify if repositories
        are still reachable.
        """
        if not self._repoMD_list:
            return False

        for repo in self._repoMD_list:
            if not repo.verify_repoMD():
                log.debug("Can't reach repo %s", repo.id)
                return False
        return True

    def _reset_configuration(self):
        tear_down_sources(self.proxy)
        self._reset_additional_repos()
        self._install_tree_metadata = None
        self.tx_id = None
        self._dnf_manager.clear_cache()
        self._dnf_manager.configure_proxy(self._get_proxy_url())
        self._repoMD_list = []

    def _reset_additional_repos(self):
        for name in self._find_mounted_additional_repos():
            installation_dir = INSTALL_TREE + "-" + name
            self._unmount_source_directory(installation_dir)

            iso_dir = ISO_DIR + "-" + name
            self._unmount_source_directory(iso_dir)

    def _find_mounted_additional_repos(self):
        prefix = ISO_DIR + "-"
        prefix_len = len(prefix)
        result = []

        for dir_path in glob(prefix + "*"):
            result.append(dir_path[prefix_len:])

        return result

    def _unmount_source_directory(self, mount_point):
        if os.path.ismount(mount_point):
            device_path = payload_utils.get_mount_device_path(mount_point)
            device = payload_utils.resolve_device(device_path)
            if device:
                payload_utils.teardown_device(device)
            else:
                payload_utils.unmount(mount_point, raise_exc=True)

    def _is_source_default(self):
        """Report if the current source type is the default source type.

        NOTE: If no source was set previously a new default one
              will be created.
        """
        return self.source_type == conf.payload.default_source

    def update_base_repo(self, fallback=True, checkmount=True):
        """Update the base repository from the DBus source."""
        log.info("Configuring the base repo")
        self._reset_configuration()

        disabled_treeinfo_repo_names = self._cleanup_old_treeinfo_repositories()

        # Find the source and its type.
        source_proxy = self.get_source_proxy()
        source_type = source_proxy.Type

        # Change the default source to CDROM if there is a valid install media.
        # FIXME: Set up the default source earlier.
        if checkmount and self._is_source_default() and find_optical_install_media():
            source_type = SOURCE_TYPE_CDROM
            source_proxy = create_source(source_type)
            set_source(self.proxy, source_proxy)

        # Set up the source.
        set_up_sources(self.proxy)

        # Read in all the repos from the installation environment, make a note of which
        # are enabled, and then disable them all.  If the user gave us a method, we want
        # to use that instead of the default repos.
        self._base.read_all_repos()

        # Enable or disable updates.
        self.set_updates_enabled(self._updates_enabled)

        # Repo files are always loaded from the system.
        # When reloaded their state needs to be synchronized with the user configuration.
        # So we disable them now and enable them later if required.
        enabled = []
        with self._repos_lock:
            for repo in self._base.repos.iter_enabled():
                enabled.append(repo.id)
                repo.disable()

        # Add a new repo.
        if source_type not in SOURCE_REPO_FILE_TYPES:
            # Get the repo configuration of the first source.
            data = RepoConfigurationData.from_structure(
                self.proxy.GetRepoConfigurations()[0]
            )

            log.debug("Using the repo configuration: %s", data)

            # Get the URL.
            install_tree_url = data.url if data.type == URL_TYPE_BASEURL else ""
            mirrorlist = data.url if data.type == URL_TYPE_MIRRORLIST else ""
            metalink = data.url if data.type == URL_TYPE_METALINK else ""

            # Fallback to the installation root.
            base_repo_url = install_tree_url

            try:
                self._refresh_install_tree(data)
                self._base.conf.releasever = self._get_release_version(install_tree_url)
                base_repo_url = self._get_base_repo_location(install_tree_url)
                log.debug("releasever from %s is %s", base_repo_url, self._base.conf.releasever)

                self._load_treeinfo_repositories(base_repo_url, disabled_treeinfo_repo_names, data)
            except configparser.MissingSectionHeaderError as e:
                log.error("couldn't set releasever from base repo (%s): %s", source_type, e)

            try:
                base_ksrepo = self.data.RepoData(
                    name=constants.BASE_REPO_NAME,
                    baseurl=base_repo_url,
                    mirrorlist=mirrorlist,
                    metalink=metalink,
                    noverifyssl=not data.ssl_verification_enabled,
                    proxy=data.proxy,
                    sslcacert=data.ssl_configuration.ca_cert_path,
                    sslclientcert=data.ssl_configuration.client_cert_path,
                    sslclientkey=data.ssl_configuration.client_key_path
                )
                self._add_repo_to_dnf(base_ksrepo)
                self._fetch_md(base_ksrepo.name)
            except (MetadataError, PayloadError) as e:
                log.error("base repo (%s/%s) not valid -- removing it",
                          source_type, base_repo_url)
                log.error("reason for repo removal: %s", e)
                with self._repos_lock:
                    self._base.repos.pop(constants.BASE_REPO_NAME, None)
                if not fallback:
                    with self._repos_lock:
                        for repo in self._base.repos.iter_enabled():
                            self._disable_repo(repo.id)
                    return

                # Fallback to the default source
                #
                # This is at the moment CDN on RHEL
                # and closest mirror everywhere else.
                tear_down_sources(self.proxy)

                source_type = conf.payload.default_source
                source_proxy = create_source(source_type)
                set_source(self.proxy, source_proxy)

                set_up_sources(self.proxy)

        # We need to check this again separately in case REPO_FILES were set above.
        if source_type in SOURCE_REPO_FILE_TYPES:

            # If this is a kickstart install, just return now as we normally do not
            # want to read the on media repo files in such a case. On the other hand,
            # the local repo files are a valid use case if the system is subscribed
            # and the CDN is selected as the installation source.
            if self.source_type == SOURCE_TYPE_CDN and is_module_available(SUBSCRIPTION):
                # only check if the Subscription module is available & CDN is the
                # installation source
                subscription_proxy = SUBSCRIPTION.get_proxy()
                load_cdn_repos = subscription_proxy.IsSubscriptionAttached
            else:
                # if the Subscription module is not available, we simply can't use
                # the CDN repos, making our decision here simple
                load_cdn_repos = False
            if flags.automatedInstall and not load_cdn_repos:
                return

            # Otherwise, fall back to the default repos that we disabled above
            with self._repos_lock:
                for (id_, repo) in self._base.repos.items():
                    if id_ in enabled:
                        log.debug("repo %s: fall back enabled from default repos", id_)
                        repo.enable()

        for repo in self.addons:
            ksrepo = self.get_addon_repo(repo)

            if ksrepo.is_harddrive_based():
                ksrepo.baseurl = self._setup_harddrive_addon_repo(ksrepo)

            log.debug("repo %s: mirrorlist %s, baseurl %s, metalink %s",
                      ksrepo.name, ksrepo.mirrorlist, ksrepo.baseurl, ksrepo.metalink)
            # one of these must be set to create new repo
            if not (ksrepo.mirrorlist or ksrepo.baseurl or ksrepo.metalink or
                    ksrepo.name in self._base.repos):
                raise PayloadSetupError("Repository %s has no mirror, baseurl or "
                                        "metalink set and is not one of "
                                        "the pre-defined repositories" %
                                        ksrepo.name)

            self._add_repo_to_dnf(ksrepo)

        with self._repos_lock:

            # disable unnecessary repos
            for repo in self._base.repos.iter_enabled():
                id_ = repo.id
                if 'source' in id_ or 'debuginfo' in id_:
                    self._disable_repo(id_)
                elif constants.isFinal and 'rawhide' in id_:
                    self._disable_repo(id_)

            # fetch md for enabled repos
            enabled_repos = self._enabled_repos
            for repo_name in self.addons:
                if repo_name in enabled_repos:
                    self._fetch_md(repo_name)

    def _find_and_mount_iso(self, device, device_mount_dir, iso_path, iso_mount_dir):
        """Find and mount installation source from ISO on device.

        Return changed path to the iso to save looking for iso in the future call.
        """
        self._setup_device(device, mountpoint=device_mount_dir)

        # check for ISO images in the newly mounted dir
        path = device_mount_dir
        if iso_path:
            path = os.path.normpath("%s/%s" % (path, iso_path))

        # XXX it would be nice to streamline this when we're just setting
        #     things back up after storage activation instead of having to
        #     pretend we don't already know which ISO image we're going to
        #     use
        image = find_first_iso_image(path)
        if not image:
            payload_utils.teardown_device(device)
            raise PayloadSetupError("failed to find valid iso image")

        if path.endswith(".iso"):
            path = os.path.dirname(path)

        # this could already be set up the first time through
        if not os.path.ismount(iso_mount_dir):
            # mount the ISO on a loop
            image = os.path.normpath("%s/%s" % (path, image))
            payload_utils.mount(image, iso_mount_dir, fstype='iso9660', options="ro")

        if not iso_path.endswith(".iso"):
            result_path = os.path.normpath("%s/%s" % (iso_path,
                                                      os.path.basename(image)))
            while result_path.startswith("/"):
                # ridiculous
                result_path = result_path[1:]

            return result_path

        return iso_path

    @staticmethod
    def _setup_device(device, mountpoint):
        """Prepare an install CD/DVD for use as a package source."""
        log.info("setting up device %s and mounting on %s", device, mountpoint)
        # Is there a symlink involved?  If so, let's get the actual path.
        # This is to catch /run/install/isodir vs. /mnt/install/isodir, for
        # instance.
        real_mountpoint = os.path.realpath(mountpoint)
        mount_device_path = payload_utils.get_mount_device_path(real_mountpoint)

        if mount_device_path:
            log.warning("%s is already mounted on %s", mount_device_path, mountpoint)

            if mount_device_path == payload_utils.get_device_path(device):
                return
            else:
                payload_utils.unmount(real_mountpoint)

        try:
            payload_utils.setup_device(device)
            payload_utils.mount_device(device, mountpoint)
        except (DeviceSetupError, MountFilesystemError) as e:
            log.error("mount failed: %s", e)
            payload_utils.teardown_device(device)
            raise PayloadSetupError(str(e)) from e

    @staticmethod
    def _setup_NFS(mountpoint, server, path, options):
        """Prepare an NFS directory for use as an install source."""
        log.info("mounting %s:%s:%s on %s", server, path, options, mountpoint)
        device_path = payload_utils.get_mount_device_path(mountpoint)

        # test if the mountpoint is occupied already
        if device_path:
            _server, colon, _path = device_path.partition(":")
            if colon == ":" and server == _server and path == _path:
                log.debug("%s:%s already mounted on %s", server, path, mountpoint)
                return
            else:
                log.debug("%s already has something mounted on it", mountpoint)
                payload_utils.unmount(mountpoint)

        # mount the specified directory
        url = "%s:%s" % (server, path)

        if not options:
            options = "nolock"
        elif "nolock" not in options:
            options += ",nolock"

        payload_utils.mount(url, mountpoint, fstype="nfs", options=options)

    def _setup_harddrive_addon_repo(self, ksrepo):
        iso_device = payload_utils.resolve_device(ksrepo.partition)
        if not iso_device:
            raise PayloadSetupError("device for HDISO addon repo install %s does not exist" %
                                    ksrepo.partition)

        ksrepo.generate_mount_dir()

        device_mount_dir = ISO_DIR + "-" + ksrepo.mount_dir_suffix
        install_root_dir = INSTALL_TREE + "-" + ksrepo.mount_dir_suffix

        self._find_and_mount_iso(iso_device, device_mount_dir, ksrepo.iso_path, install_root_dir)
        url = "file://" + install_root_dir

        return url

    def _refresh_install_tree(self, data: RepoConfigurationData):
        """Refresh installation tree metadata."""
        if data.type != URL_TYPE_BASEURL:
            return

        if not data.url:
            return

        url = data.url
        proxy_url = data.proxy or None

        # ssl_verify can be:
        #   - the path to a cert file
        #   - True, to use the system's certificates
        #   - False, to not verify
        ssl_verify = (data.ssl_configuration.ca_cert_path
                      or (conf.payload.verify_ssl and data.ssl_verification_enabled))
        ssl_client_cert = data.ssl_configuration.client_cert_path or None
        ssl_client_key = data.ssl_configuration.client_key_path or None
        ssl_cert = (ssl_client_cert, ssl_client_key) if ssl_client_cert else None

        log.debug("retrieving treeinfo from %s (proxy: %s ; ssl_verify: %s)",
                  url, proxy_url, ssl_verify)

        proxies = {}
        if proxy_url:
            try:
                proxy = ProxyString(proxy_url)
                proxies = {"http": proxy.url,
                           "https": proxy.url}
            except ProxyStringError as e:
                log.info("Failed to parse proxy for _getTreeInfo %s: %s",
                         proxy_url, e)

        headers = {"user-agent": USER_AGENT}
        self._install_tree_metadata = InstallTreeMetadata()
        try:
            ret = self._install_tree_metadata.load_url(url, proxies, ssl_verify, ssl_cert, headers)
        except FileNotDownloadedError as e:
            self._install_tree_metadata = None
            self.verbose_errors.append(str(e))
            log.warning("Install tree metadata fetching failed: %s", str(e))
            return

        if not ret:
            log.warning("Install tree metadata can't be loaded!")
            self._install_tree_metadata = None

    def _get_release_version(self, url):
        """Return the release version of the tree at the specified URL."""
        log.debug("getting release version from tree at %s", url)

        if self._install_tree_metadata:
            version = self._install_tree_metadata.get_release_version()
            log.debug("using treeinfo release version of %s", version)
        else:
            version = get_product_release_version()
            log.debug("using default release version of %s", version)

        return version

    def _get_base_repo_location(self, install_tree_url):
        """Try to find base repository from the treeinfo file.

        The URL can be installation tree root or a subfolder in the installation root.
        The structure of the installation root can be similar to this.

        / -
          | - .treeinfo
          | - BaseRepo -
          |            | - repodata
          |            | - Packages
          | - AddonRepo -
                        | - repodata
                        | - Packages

        The .treeinfo file contains information where repositories are placed from the
        installation root.

        User can provide us URL to the installation root or directly to the repository folder.
        Both options are valid.
        * If the URL points to an installation root we need to find position of
        repositories in the .treeinfo file.
        * If URL points to repo directly then no .treeinfo file is present. We will just use this
        repo.
        """
        if self._install_tree_metadata:
            repo_md = self._install_tree_metadata.get_base_repo_metadata()
            if repo_md:
                log.debug("Treeinfo points base repository to %s.", repo_md.path)
                return repo_md.path

        log.debug("No base repository found in treeinfo file. Using installation tree root.")
        return install_tree_url

    def _load_treeinfo_repositories(self, base_repo_url, repo_names_to_disable, data):
        """Load new repositories from treeinfo file.

        :param base_repo_url: base repository url. This is not saved anywhere when the function
                              is called. It will be add to the existing urls if not None.
        :param repo_names_to_disable: list of repository names which should be disabled after load
        :type repo_names_to_disable: [str]
        :param data: repo configuration data
        """
        if self._install_tree_metadata:
            existing_urls = []

            if base_repo_url is not None:
                existing_urls.append(base_repo_url)

            for ksrepo in self.addons:
                baseurl = self.get_addon_repo(ksrepo).baseurl
                existing_urls.append(baseurl)

            enabled_repositories_from_treeinfo = conf.payload.enabled_repositories_from_treeinfo

            for repo_md in self._install_tree_metadata.get_metadata_repos():
                if repo_md.path not in existing_urls:
                    repo_treeinfo = self._install_tree_metadata.get_treeinfo_for(repo_md.name)

                    # disable repositories disabled by user manually before
                    if repo_md.name in repo_names_to_disable:
                        repo_enabled = False
                    else:
                        repo_enabled = repo_treeinfo.type in enabled_repositories_from_treeinfo

                    repo = RepoData(
                        name=repo_md.name,
                        baseurl=repo_md.path,
                        noverifyssl=not data.ssl_verification_enabled,
                        proxy=data.proxy,
                        sslcacert=data.ssl_configuration.ca_cert_path,
                        sslclientcert=data.ssl_configuration.client_cert_path,
                        sslclientkey=data.ssl_configuration.client_key_path,
                        install=False,
                        enabled=repo_enabled
                    )
                    repo.treeinfo_origin = True
                    log.debug("Adding new treeinfo repository: %s enabled: %s",
                              repo_md.name, repo_enabled)

                    self._add_repo_to_dnf_and_ks(repo)

    def _cleanup_old_treeinfo_repositories(self):
        """Remove all old treeinfo repositories before loading new ones.

        Find all repositories added from treeinfo file and remove them. After this step new
        repositories will be loaded from the new link.

        :return: list of repository names which were disabled before removal
        :rtype: [str]
        """
        disabled_repo_names = []

        for ks_repo_name in self.addons:
            repo = self.get_addon_repo(ks_repo_name)
            if repo.treeinfo_origin:
                log.debug("Removing old treeinfo repository %s", ks_repo_name)

                if not repo.enabled:
                    disabled_repo_names.append(ks_repo_name)

                self._remove_repo(ks_repo_name)

        return disabled_repo_names

    def _write_dnf_repo(self, repo, repo_path):
        """Write a repo object to a DNF repo.conf file.

        :param repo: DNF repository object
        :param string repo_path: Path to write the repo to
        :raises: PayloadSetupError if the repo doesn't have a url
        """
        with open(repo_path, "w") as f:
            f.write("[%s]\n" % repo.id)
            f.write("name=%s\n" % repo.id)
            if self.is_repo_enabled(repo.id):
                f.write("enabled=1\n")
            else:
                f.write("enabled=0\n")

            if repo.mirrorlist:
                f.write("mirrorlist=%s\n" % repo.mirrorlist)
            elif repo.metalink:
                f.write("metalink=%s\n" % repo.metalink)
            elif repo.baseurl:
                f.write("baseurl=%s\n" % repo.baseurl[0])
            else:
                f.close()
                os.unlink(repo_path)
                raise PayloadSetupError("The repo {} has no baseurl, mirrorlist or "
                                        "metalink".format(repo.id))

            # kickstart repo modifiers
            ks_repo = self.get_addon_repo(repo.id)
            if not ks_repo:
                return

            if ks_repo.noverifyssl:
                f.write("sslverify=0\n")

            if ks_repo.proxy:
                try:
                    proxy = ProxyString(ks_repo.proxy)
                    f.write("proxy=%s\n" % proxy.url)
                except ProxyStringError as e:
                    log.error("Failed to parse proxy for _writeInstallConfig %s: %s",
                              ks_repo.proxy, e)

            if ks_repo.cost:
                f.write("cost=%d\n" % ks_repo.cost)

            if ks_repo.includepkgs:
                f.write("include=%s\n" % ",".join(ks_repo.includepkgs))

            if ks_repo.excludepkgs:
                f.write("exclude=%s\n" % ",".join(ks_repo.excludepkgs))

    def post_setup(self):
        """Perform post-setup tasks.

        Save repomd hash to test if the repositories can be reached.
        """
        self._repoMD_list = []
        proxy_url = self._get_proxy_url()

        for repo in self._base.repos.iter_enabled():
            repoMD = RepoMDMetaHash(repo, proxy_url)
            repoMD.store_repoMD_hash()
            self._repoMD_list.append(repoMD)

    def post_install(self):
        """Perform post-installation tasks."""
        # Write selected kickstart repos to target system
        for ks_repo in (ks for ks in (self.get_addon_repo(r) for r in self.addons) if ks.install):
            if ks_repo.baseurl.startswith("nfs://"):
                log.info("Skip writing nfs repo %s to target system.", ks_repo.name)
                continue

            try:
                repo = self._get_repo(ks_repo.name)
                if not repo:
                    continue
            except (dnf.exceptions.RepoError, KeyError):
                continue
            repo_path = conf.target.system_root + YUM_REPOS_DIR + "%s.repo" % repo.id
            try:
                log.info("Writing %s.repo to target system.", repo.id)
                self._write_dnf_repo(repo, repo_path)
            except PayloadSetupError as e:
                log.error(e)

        # We don't need the mother base anymore. Close it.
        self._base.close()
        super().post_install()

        # rpm needs importing installed certificates manually, see rhbz#748320 and rhbz#185800
        task = ImportRPMKeysTask(
            sysroot=conf.target.system_root,
            gpg_keys=conf.payload.default_rpm_gpg_keys
        )
        task.run()

    @property
    def kernel_version_list(self):
        return get_kernel_version_list()