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()
class Payload(metaclass=ABCMeta): """Payload is an abstract class for OS install delivery methods.""" def __init__(self, data, storage): """Initialize Payload class :param data: This param is a kickstart.AnacondaKSHandler class. :param storage: an instance of Blivet's storage model """ self.data = data self.storage = storage self.tx_id = None self._install_tree_metadata = None self._first_payload_reset = True # A list of verbose error strings from the subclass self.verbose_errors = [] self._session = util.requests_session() # Additional packages required by installer based on used features self.requirements = PayloadRequirements() @property def first_payload_reset(self): return self._first_payload_reset @property def is_hmc_enabled(self): return self.data.method.method == "hmc" def setup(self): """Do any payload-specific setup.""" self.verbose_errors = [] def unsetup(self): """Invalidate a previously setup payload.""" self._install_tree_metadata = None def post_setup(self): """Run specific payload post-configuration tasks on the end of the restart_thread call. This method could be overriden. """ self._first_payload_reset = False def release(self): """Release any resources in use by this object, but do not do final cleanup. This is useful for dealing with payload backends that do not get along well with multithreaded programs. """ pass def reset(self): """Reset the instance, not including ksdata.""" pass ### # METHODS FOR WORKING WITH REPOSITORIES ### @property def addons(self): """A list of addon repo names.""" return [r.name for r in self.data.repo.dataList()] @property def base_repo(self): """Get the identifier of the current base repo or None.""" return None @property def mirrors_available(self): """Is the closest/fastest mirror option enabled? This does not make sense for those payloads that do not support this concept. """ return conf.payload.enable_closest_mirror @property def disabled_repos(self): """A list of names of the disabled repos.""" disabled = [] for repo in self.addons: if not self.is_repo_enabled(repo): disabled.append(repo) return disabled @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 is_repo_enabled(self, repo_id): """Return True if repo is enabled.""" repo = self.get_addon_repo(repo_id) if repo: return repo.enabled else: return False 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 _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 """ network_protocols = ["http:", "ftp:", "nfs:", "nfsiso:"] for s in sources: if s and any(s.startswith(p) for p in network_protocols): log.debug("Source %s needs network for installation", s) return True log.debug("Source doesn't require network for installation") return False @property def needs_network(self): """Test base and additional repositories if they require network.""" url = "" if self.data.method.method is None: # closest mirror set return True elif self.data.method.method == "nfs": # NFS is always on network return True elif self.data.method.method == "url": if self.data.url.url: url = self.data.url.url elif self.data.url.mirrorlist: url = self.data.url.mirrorlist elif self.data.url.metalink: url = self.data.url.metalink return (self._source_needs_network([url]) or any( self._repo_needs_network(repo) for repo in self.data.repo.dataList())) def update_base_repo(self, fallback=True, checkmount=True): """Update the base repository from ksdata.method.""" pass def gather_repo_metadata(self): pass def add_repo(self, ksrepo): """Add the repo given by the pykickstart Repo object ksrepo to the system. The repo will be automatically enabled and its metadata fetched. Duplicate repos will not raise an error. They should just silently take the place of the previous value. """ # Add the repo to the ksdata so it'll appear in the output ks file. ksrepo.enabled = True self.data.repo.dataList().append(ksrepo) def add_disabled_repo(self, ksrepo): """Add the repo given by the pykickstart Repo object ksrepo to the list of known repos. The repo will be automatically disabled. Duplicate repos will not raise an error. They should just silently take the place of the previous value. """ ksrepo.enabled = False self.data.repo.dataList().append(ksrepo) 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 enable_repo(self, repo_id): repo = self.get_addon_repo(repo_id) if repo: repo.enabled = True def disable_repo(self, repo_id): repo = self.get_addon_repo(repo_id) if repo: repo.enabled = 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. This method should be overriden. """ log.debug("Install method %s is not able to verify availability", self.__class__.__name__) return False ### # METHODS FOR WORKING WITH GROUPS ### def is_language_supported(self, language): """Is the given language supported by the payload? :param language: a name of the language """ return True def is_locale_supported(self, language, locale): """Is the given locale supported by the payload? :param language: a name of the language :param locale: a name of the locale """ return True def language_groups(self): return [] def langpacks(self): return [] def selected_groups(self): """Return list of selected group names from kickstart. NOTE: This group names can be mix of group IDs and other valid identifiers. If you want group IDs use `selected_groups_IDs` instead. :return: list of group names in a format specified by a kickstart file. """ return [grp.name for grp in self.data.packages.groupList] def selected_groups_IDs(self): """Return list of IDs for selected groups. Implementation depends on a specific payload class. """ return self.selected_groups() def group_selected(self, groupid): return Group(groupid) in self.data.packages.groupList def select_group(self, groupid, default=True, optional=False): if optional: include = GROUP_ALL elif default: include = GROUP_DEFAULT else: include = GROUP_REQUIRED grp = Group(groupid, include=include) if grp in self.data.packages.groupList: # I'm not sure this would ever happen, but ensure that re-selecting # a group with a different types set works as expected. if grp.include != include: grp.include = include return if grp in self.data.packages.excludedGroupList: self.data.packages.excludedGroupList.remove(grp) self.data.packages.groupList.append(grp) def deselect_group(self, groupid): grp = Group(groupid) if grp in self.data.packages.excludedGroupList: return if grp in self.data.packages.groupList: self.data.packages.groupList.remove(grp) self.data.packages.excludedGroupList.append(grp) ### # METHODS FOR QUERYING STATE ### @property def space_required(self): """The total disk space (Size) required for the current selection.""" raise NotImplementedError() @property def kernel_version_list(self): """An iterable of the kernel versions installed by the payload.""" raise NotImplementedError() ### # METHODS FOR TREE VERIFICATION ### def _refresh_install_tree(self, url): """Refresh installation tree metadata. :param url: url of the repo :type url: string """ if not url: return if hasattr(self.data.method, "proxy"): proxy_url = self.data.method.proxy else: proxy_url = None # ssl_verify can be: # - the path to a cert file # - True, to use the system's certificates # - False, to not verify ssl_verify = getattr(self.data.method, "sslcacert", None) or conf.payload.verify_ssl ssl_client_cert = getattr(self.data.method, "ssl_client_cert", None) ssl_client_key = getattr(self.data.method, "ssl_client_key", 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 IOError 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.""" try: version = re.match(VERSION_DIGITS, productVersion).group(1) except AttributeError: version = "rawhide" log.debug("getting release version from tree at %s (%s)", url, version) if self._install_tree_metadata: version = self._install_tree_metadata.get_release_version() log.debug("using treeinfo release version of %s", version) else: log.debug("using default release version of %s", version) return version ### # METHODS FOR MEDIA MANAGEMENT (XXX should these go in another module?) ### @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.name, 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 == device.path: return else: payload_utils.unmount(real_mountpoint) try: payload_utils.setup_device(device) payload_utils.mount_device(device, mountpoint) except StorageError as e: log.error("mount failed: %s", e) payload_utils.teardown_device(device) raise PayloadSetupError(str(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) ### # METHODS FOR INSTALLING THE PAYLOAD ### def pre_install(self): """Perform pre-installation tasks.""" from pyanaconda.modules.payloads.base.initialization import PrepareSystemForInstallationTask PrepareSystemForInstallationTask(conf.target.system_root).run() def install(self): """Install the payload.""" raise NotImplementedError() @property def needs_storage_configuration(self): """Should we write the storage before doing the installation? Some payloads require that the storage configuration will be written out before doing installation. Right now, this is basically just the dnfpayload. """ return False @property def handles_bootloader_configuration(self): """Whether this payload backend writes the bootloader configuration itself; if False (the default), the generic bootloader configuration code will be used. """ return False def recreate_initrds(self): """Recreate the initrds by calling new-kernel-pkg or dracut This needs to be done after all configuration files have been written, since dracut depends on some of them. :returns: None """ if os.path.exists(conf.target.system_root + "/usr/sbin/new-kernel-pkg"): use_dracut = False else: log.warning( "new-kernel-pkg does not exist - grubby wasn't installed? " " using dracut instead.") use_dracut = True for kernel in self.kernel_version_list: log.info("recreating initrd for %s", kernel) if not conf.target.is_image: if use_dracut: util.execInSysroot("depmod", ["-a", kernel]) util.execInSysroot( "dracut", ["-f", "/boot/initramfs-%s.img" % kernel, kernel]) else: util.execInSysroot("new-kernel-pkg", [ "--mkinitrd", "--dracut", "--depmod", "--update", kernel ]) # if the installation is running in fips mode then make sure # fips is also correctly enabled in the installed system if kernel_arguments.get("fips") == "1": # We use the --no-bootcfg option as we don't want fips-mode-setup to # modify the bootloader configuration. # Anaconda already does everything needed & it would require grubby to # be available on the system. util.execInSysroot("fips-mode-setup", ["--enable", "--no-bootcfg"]) else: # hostonly is not sensible for disk image installations # using /dev/disk/by-uuid/ is necessary due to disk image naming util.execInSysroot("dracut", [ "-N", "--persistent-policy", "by-uuid", "-f", "/boot/initramfs-%s.img" % kernel, kernel ]) def post_install(self): """Perform post-installation tasks.""" # write out static config (storage, modprobe, keyboard, ??) # kickstart should handle this before we get here from pyanaconda.modules.payloads.base.initialization import CopyDriverDisksFilesTask CopyDriverDisksFilesTask(conf.target.system_root).run() log.info("Installation requirements: %s", self.requirements) if not self.requirements.applied: log.info("Some of the requirements were not applied.")
class Payload(metaclass=ABCMeta): """Payload is an abstract class for OS install delivery methods.""" def __init__(self, data): """Initialize Payload class :param data: This param is a kickstart.AnacondaKSHandler class. """ self.data = data self.storage = None self.tx_id = None self._install_tree_metadata = None self._first_payload_reset = True # A list of verbose error strings from the subclass self.verbose_errors = [] self._session = util.requests_session() # Additional packages required by installer based on used features self.requirements = PayloadRequirements() @property def first_payload_reset(self): return self._first_payload_reset def setup(self, storage): """Do any payload-specific setup.""" self.storage = storage self.verbose_errors = [] def unsetup(self): """Invalidate a previously setup payload.""" self.storage = None self._install_tree_metadata = None def post_setup(self): """Run specific payload post-configuration tasks on the end of the restart_thread call. This method could be overriden. """ self._first_payload_reset = False def release(self): """Release any resources in use by this object, but do not do final cleanup. This is useful for dealing with payload backends that do not get along well with multithreaded programs. """ pass def reset(self): """Reset the instance, not including ksdata.""" pass def prepare_mount_targets(self, storage): """Run when physical storage is mounted, but other mount points may not exist. Used by the RPMOSTreePayload subclass. """ pass ### # METHODS FOR WORKING WITH REPOSITORIES ### @property def addons(self): """A list of addon repo identifiers.""" return [r.name for r in self.data.repo.dataList()] @property def base_repo(self): """Get the identifier of the current base repo or None.""" return None @property def mirrors_available(self): """Is the closest/fastest mirror option enabled? This does not make sense for those payloads that do not support this concept. """ return conf.payload.enable_closest_mirror @property def disabled_repos(self): """A list of disabled repos.""" disabled = [] for repo in self.addons: if not self.is_repo_enabled(repo): disabled.append(repo) return disabled @property def enabled_repos(self): """A list of enabled repos.""" enabled = [] for repo in self.addons: if self.is_repo_enabled(repo): enabled.append(repo) return enabled def is_repo_enabled(self, repo_id): """Return True if repo is enabled.""" repo = self.get_addon_repo(repo_id) if repo: return repo.enabled else: return False 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 _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 """ network_protocols = ["http:", "ftp:", "nfs:", "nfsiso:"] for s in sources: if s and any(s.startswith(p) for p in network_protocols): log.debug("Source %s needs network for installation", s) return True log.debug("Source doesn't require network for installation") return False @property def needs_network(self): """Test base and additional repositories if they require network.""" url = "" if self.data.method.method is None: # closest mirror set return True elif self.data.method.method == "nfs": # NFS is always on network return True elif self.data.method.method == "url": if self.data.url.url: url = self.data.url.url elif self.data.url.mirrorlist: url = self.data.url.mirrorlist elif self.data.url.metalink: url = self.data.url.metalink return (self._source_needs_network([url]) or any(self._repo_needs_network(repo) for repo in self.data.repo.dataList())) def update_base_repo(self, fallback=True, checkmount=True): """Update the base repository from ksdata.method.""" pass def gather_repo_metadata(self): pass def add_repo(self, ksrepo): """Add the repo given by the pykickstart Repo object ksrepo to the system. The repo will be automatically enabled and its metadata fetched. Duplicate repos will not raise an error. They should just silently take the place of the previous value. """ # Add the repo to the ksdata so it'll appear in the output ks file. ksrepo.enabled = True self.data.repo.dataList().append(ksrepo) def add_disabled_repo(self, ksrepo): """Add the repo given by the pykickstart Repo object ksrepo to the list of known repos. The repo will be automatically disabled. Duplicate repos will not raise an error. They should just silently take the place of the previous value. """ ksrepo.enabled = False self.data.repo.dataList().append(ksrepo) 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 enable_repo(self, repo_id): repo = self.get_addon_repo(repo_id) if repo: repo.enabled = True def disable_repo(self, repo_id): repo = self.get_addon_repo(repo_id) if repo: repo.enabled = 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. This method should be overriden. """ log.debug("Install method %s is not able to verify availability", self.__class__.__name__) return False ### # METHODS FOR WORKING WITH GROUPS ### def is_language_supported(self, language): """Is the given language supported by the payload? :param language: a name of the language """ return True def is_locale_supported(self, language, locale): """Is the given locale supported by the payload? :param language: a name of the language :param locale: a name of the locale """ return True def language_groups(self): return [] def langpacks(self): return [] def selected_groups(self): """Return list of selected group names from kickstart. NOTE: This group names can be mix of group IDs and other valid identifiers. If you want group IDs use `selected_groups_IDs` instead. :return: list of group names in a format specified by a kickstart file. """ return [grp.name for grp in self.data.packages.groupList] def selected_groups_IDs(self): """Return list of IDs for selected groups. Implementation depends on a specific payload class. """ return self.selected_groups() def group_selected(self, groupid): return Group(groupid) in self.data.packages.groupList def select_group(self, groupid, default=True, optional=False): if optional: include = GROUP_ALL elif default: include = GROUP_DEFAULT else: include = GROUP_REQUIRED grp = Group(groupid, include=include) if grp in self.data.packages.groupList: # I'm not sure this would ever happen, but ensure that re-selecting # a group with a different types set works as expected. if grp.include != include: grp.include = include return if grp in self.data.packages.excludedGroupList: self.data.packages.excludedGroupList.remove(grp) self.data.packages.groupList.append(grp) def deselect_group(self, groupid): grp = Group(groupid) if grp in self.data.packages.excludedGroupList: return if grp in self.data.packages.groupList: self.data.packages.groupList.remove(grp) self.data.packages.excludedGroupList.append(grp) ### # METHODS FOR QUERYING STATE ### @property def space_required(self): """The total disk space (Size) required for the current selection.""" raise NotImplementedError() @property def kernel_version_list(self): """An iterable of the kernel versions installed by the payload.""" raise NotImplementedError() ### # METHODS FOR TREE VERIFICATION ### def _refresh_install_tree(self, url): """Refresh installation tree metadata. :param url: url of the repo :type url: string """ if not url: return if hasattr(self.data.method, "proxy"): proxy_url = self.data.method.proxy else: proxy_url = None # ssl_verify can be: # - the path to a cert file # - True, to use the system's certificates # - False, to not verify ssl_verify = getattr(self.data.method, "sslcacert", not flags.noverifyssl) ssl_client_cert = getattr(self.data.method, "ssl_client_cert", None) ssl_client_key = getattr(self.data.method, "ssl_client_key", 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 IOError 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.""" try: version = re.match(VERSION_DIGITS, productVersion).group(1) except AttributeError: version = "rawhide" log.debug("getting release version from tree at %s (%s)", url, version) if self._install_tree_metadata: version = self._install_tree_metadata.get_release_version() log.debug("using treeinfo release version of %s", version) else: log.debug("using default release version of %s", version) return version ### # METHODS FOR MEDIA MANAGEMENT (XXX should these go in another module?) ### @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.name, 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) mdev = payload_utils.get_mount_device(real_mountpoint) if mdev: if mdev: log.warning("%s is already mounted on %s", mdev, mountpoint) if mdev == device.path: return else: payload_utils.unmount(real_mountpoint) try: device.setup() device.format.setup(mountpoint=mountpoint) except StorageError as e: log.error("mount failed: %s", e) device.teardown(recursive=True) raise PayloadSetupError(str(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) dev = payload_utils.get_mount_device(mountpoint) # test if the mountpoint is occupied already if dev: _server, colon, _path = dev.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) ### # METHODS FOR INSTALLING THE PAYLOAD ### def pre_install(self): """Perform pre-installation tasks.""" util.mkdirChain(util.getSysroot() + "/root") self._write_module_blacklist() def install(self): """Install the payload.""" raise NotImplementedError() def _write_module_blacklist(self): """Copy modules from modprobe.blacklist=<module> on cmdline to /etc/modprobe.d/anaconda-blacklist.conf so that modules will continue to be blacklisted when the system boots. """ if "modprobe.blacklist" not in flags.cmdline: return util.mkdirChain(util.getSysroot() + "/etc/modprobe.d") with open(util.getSysroot() + "/etc/modprobe.d/anaconda-blacklist.conf", "w") as f: f.write("# Module blacklists written by anaconda\n") for module in flags.cmdline["modprobe.blacklist"].split(): f.write("blacklist %s\n" % module) def _copy_driver_disk_files(self): # Multiple driver disks may be loaded, so we need to glob for all # the firmware files in the common DD firmware directory for f in glob(DD_FIRMWARE + "/*"): try: shutil.copyfile(f, "%s/lib/firmware/" % util.getSysroot()) except IOError as e: log.error("Could not copy firmware file %s: %s", f, e.strerror) # copy RPMS for d in glob(DD_RPMS): shutil.copytree(d, util.getSysroot() + "/root/" + os.path.basename(d)) # copy modules and firmware into root's home directory if os.path.exists(DD_ALL): try: shutil.copytree(DD_ALL, util.getSysroot() + "/root/DD") except IOError as e: log.error("failed to copy driver disk files: %s", e.strerror) # XXX TODO: real error handling, as this is probably going to # prevent boot on some systems @property def needs_storage_configuration(self): """Should we write the storage before doing the installation? Some payloads require that the storage configuration will be written out before doing installation. Right now, this is basically just the dnfpayload. """ return False @property def handles_bootloader_configuration(self): """Whether this payload backend writes the bootloader configuration itself; if False (the default), the generic bootloader configuration code will be used. """ return False def recreate_initrds(self): """Recreate the initrds by calling new-kernel-pkg or dracut This needs to be done after all configuration files have been written, since dracut depends on some of them. :returns: None """ if os.path.exists(util.getSysroot() + "/usr/sbin/new-kernel-pkg"): use_dracut = False else: log.warning("new-kernel-pkg does not exist - grubby wasn't installed? " " using dracut instead.") use_dracut = True for kernel in self.kernel_version_list: log.info("recreating initrd for %s", kernel) if not conf.target.is_image: if use_dracut: util.execInSysroot("depmod", ["-a", kernel]) util.execInSysroot("dracut", ["-f", "/boot/initramfs-%s.img" % kernel, kernel]) else: util.execInSysroot("new-kernel-pkg", ["--mkinitrd", "--dracut", "--depmod", "--update", kernel]) # if the installation is running in fips mode then make sure # fips is also correctly enabled in the installed system if flags.cmdline.get("fips") == "1": # We use the --no-bootcfg option as we don't want fips-mode-setup to # modify the bootloader configuration. # Anaconda already does everything needed & it would require grubby to # be available on the system. util.execInSysroot("fips-mode-setup", ["--enable", "--no-bootcfg"]) else: # hostonly is not sensible for disk image installations # using /dev/disk/by-uuid/ is necessary due to disk image naming util.execInSysroot("dracut", ["-N", "--persistent-policy", "by-uuid", "-f", "/boot/initramfs-%s.img" % kernel, kernel]) def _set_default_boot_target(self): """Set the default systemd target for the system.""" if not os.path.exists(util.getSysroot() + "/etc/systemd/system"): log.error("systemd is not installed -- can't set default target") return # If the target was already set, we don't have to continue. services_proxy = SERVICES.get_proxy() if services_proxy.DefaultTarget: log.debug("The default target is already set.") return try: import rpm except ImportError: log.info("failed to import rpm -- not adjusting default runlevel") else: ts = rpm.TransactionSet(util.getSysroot()) # XXX one day this might need to account for anaconda's display mode if ts.dbMatch("provides", 'service(graphical-login)').count() and \ not flags.usevnc: # We only manipulate the ksdata. The symlink is made later # during the config write out. services_proxy.SetDefaultTarget(GRAPHICAL_TARGET) else: services_proxy.SetDefaultTarget(TEXT_ONLY_TARGET) def post_install(self): """Perform post-installation tasks.""" # set default systemd target self._set_default_boot_target() # write out static config (storage, modprobe, keyboard, ??) # kickstart should handle this before we get here self._copy_driver_disk_files() log.info("Installation requirements: %s", self.requirements) if not self.requirements.applied: log.info("Some of the requirements were not applied.")