You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
qubes-installer-qubes-os/anaconda/pyanaconda/packaging/dnfpayload.py

852 lines
30 KiB

# dnfpayload.py
# DNF/rpm software payload management.
#
# Copyright (C) 2013 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details. You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Red Hat Author(s): Ales Kozumplik <akozumpl@redhat.com>
#
import os
from blivet.size import Size
import blivet.arch
from pyanaconda.flags import flags
from pyanaconda.i18n import _
from pyanaconda.progress import progressQ
import ConfigParser
import collections
import itertools
import logging
import multiprocessing
import operator
from pyanaconda import constants
from pykickstart.constants import GROUP_ALL, GROUP_DEFAULT, KS_MISSING_IGNORE
import pyanaconda.errors as errors
import pyanaconda.iutil
import pyanaconda.localization
import pyanaconda.packaging as packaging
import shutil
import sys
import time
from pyanaconda.iutil import ProxyString, ProxyStringError
log = logging.getLogger("packaging")
import dnf
import dnf.exceptions
import dnf.repo
import dnf.callback
import rpm
DNF_CACHE_DIR = '/tmp/dnf.cache'
DNF_PACKAGE_CACHE_DIR_SUFFIX = 'dnf.package.cache'
DOWNLOAD_MPOINTS = {'/tmp',
'/',
'/mnt/sysimage',
'/mnt/sysimage/home',
'/mnt/sysimage/tmp',
'/mnt/sysimage/var',
}
REPO_DIRS = ['/etc/yum.repos.d',
'/etc/anaconda.repos.d',
'/tmp/updates/anaconda.repos.d',
'/tmp/product/anaconda.repos.d']
YUM_REPOS_DIR = "/etc/yum.repos.d/"
def _failure_limbo():
progressQ.send_quit(1)
while True:
time.sleep(10000)
def _df_map():
"""Return (mountpoint -> size available) mapping."""
output = pyanaconda.iutil.execWithCapture('df', ['--output=target,avail'])
output = output.rstrip()
lines = output.splitlines()
structured = {}
for line in lines:
items = line.split()
key = items[0]
val = items[1]
if not key.startswith('/'):
continue
structured[key] = Size(int(val)*1024)
return structured
def _paced(fn):
"""Execute `fn` no more often then every 2 seconds."""
def paced_fn(self, *args):
now = time.time()
if now - self.last_time < 2:
return
self.last_time = now
return fn(self, *args)
return paced_fn
def _pick_mpoint(df, requested):
def reasonable_mpoint(mpoint):
return mpoint in DOWNLOAD_MPOINTS
# reserve extra
requested = requested + Size("150 MB")
sufficients = {key : val for (key,val) in df.items() if val > requested
and reasonable_mpoint(key)}
log.info('Sufficient mountpoints found: %s', sufficients)
if not len(sufficients):
return None
# default to the biggest one:
return sorted(sufficients.items(), key=operator.itemgetter(1),
reverse=True)[0][0]
class PayloadRPMDisplay(dnf.callback.LoggingTransactionDisplay):
def __init__(self, queue):
super(PayloadRPMDisplay, self).__init__()
self._queue = queue
self._last_ts = None
self.cnt = 0
def event(self, package, action, te_current, te_total, ts_current, ts_total):
if action == self.PKG_INSTALL and te_current == 0:
# do not report same package twice
if self._last_ts == ts_current:
return
self._last_ts = ts_current
msg = '%s.%s (%d/%d)' % \
(package.name, package.arch, ts_current, ts_total)
self.cnt += 1
self._queue.put(('install', msg))
elif action == self.TRANS_POST:
self._queue.put(('post', None))
class DownloadProgress(dnf.callback.DownloadProgress):
def __init__(self):
self.downloads = collections.defaultdict(int)
self.last_time = time.time()
self.total_files = 0
self.total_size = Size(0)
@_paced
def _update(self):
msg = _('Downloading %(total_files)s RPMs, '
'%(downloaded)s / %(total_size)s (%(percent)d%%) done.')
downloaded = Size(sum(self.downloads.values()))
vals = {
'downloaded' : downloaded,
'percent' : int(100 * downloaded/self.total_size),
'total_files' : self.total_files,
'total_size' : self.total_size
}
progressQ.send_message(msg % vals)
def end(self, payload, status, err_msg):
nevra = str(payload)
if status is dnf.callback.STATUS_OK:
self.downloads[nevra] = payload.download_size
self._update()
return
log.critical("Failed to download '%s': %d - %s", nevra, status, err_msg)
def progress(self, payload, done):
nevra = str(payload)
self.downloads[nevra] = done
self._update()
def start(self, total_files, total_size):
self.total_files = total_files
self.total_size = Size(total_size)
def do_transaction(base, queue):
try:
display = PayloadRPMDisplay(queue)
base.do_transaction(display=display)
except BaseException as e:
log.error('The transaction process has ended abruptly')
log.info(e)
queue.put(('quit', str(e)))
class DNFPayload(packaging.PackagePayload):
def __init__(self, data):
packaging.PackagePayload.__init__(self, data)
self._base = None
self._download_location = None
self._configure()
def unsetup(self):
super(DNFPayload, self).unsetup()
self._base = None
self._configure()
def _replace_vars(self, url):
""" Replace url variables with their values
:param url: url string to do replacement on
:type url: string
:returns: string with variables substituted
:rtype: string or None
Currently supports $releasever and $basearch
"""
if not url:
return url
url = url.replace("$releasever", self._base.conf.releasever)
url = url.replace("$basearch", blivet.arch.getArch())
return url
def _add_repo(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, DNF_CACHE_DIR)
url = self._replace_vars(ksrepo.baseurl)
mirrorlist = self._replace_vars(ksrepo.mirrorlist)
if url:
repo.baseurl = [url]
if mirrorlist:
repo.mirrorlist = mirrorlist
repo.sslverify = not (ksrepo.noverifyssl or flags.noverifyssl)
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 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:
self._base.repos[repo.id].enable()
else:
self._base.repos.pop(repo.id)
self._base.repos.add(repo)
repo.enable()
# If the repo's not already known, we've got to add it.
else:
self._base.repos.add(repo)
repo.enable()
# Load the metadata to verify that the repo is valid
try:
self._base.repos[repo.id].load()
except dnf.exceptions.RepoError as e:
raise packaging.MetadataError(e)
log.info("added repo: '%s' - %s", ksrepo.name, url or mirrorlist)
def addRepo(self, ksrepo):
"""Add a repo to dnf and kickstart repo lists
:param ksrepo: Kickstart Repository to add
:type ksrepo: Kickstart RepoData object.
:returns: None
"""
self._add_repo(ksrepo)
super(DNFPayload, self).addRepo(ksrepo)
def _apply_selections(self):
if self.data.packages.nocore:
log.info("skipping core group due to %%packages --nocore; system may not be complete")
else:
try:
self._select_group('core', required=True)
log.info("selected group: core")
except packaging.NoSuchGroup as e:
self._miss(e)
env = None
if self.data.packages.default and self.environments:
env = self.environments[0]
elif self.data.packages.environment:
env = self.data.packages.environment
if env:
try:
self.selectEnvironment(env)
log.info("selected env: %s", env)
except packaging.NoSuchGroup as e:
self._miss(e)
for group in self.data.packages.groupList:
if group.name == 'core':
continue
default = group.include in (GROUP_ALL,
GROUP_DEFAULT)
optional = group.include == GROUP_ALL
try:
self._select_group(group.name, default=default, optional=optional)
log.info("selected group: %s", group.name)
except packaging.NoSuchGroup as e:
self._miss(e)
for group in self.data.packages.excludedGroupList:
try:
self._deselect_group(group.name)
log.info("deselected group: %s", group.name)
except packaging.NoSuchGroup:
log.info("skipped removing nonexistant group: %s", group.name)
for pkg_name in self.data.packages.packageList:
try:
self._install_package(pkg_name)
log.info("selected package: '%s'", pkg_name)
except packaging.NoSuchPackage as e:
self._miss(e)
for pkg_name in self.data.packages.excludedList:
try:
self._remove_package(pkg_name)
log.info("removed package: %s", pkg_name)
except packaging.NoSuchPackage:
log.info("skipped removing nonexistant package: %s", pkg_name)
self._select_kernel_package()
for pkg_name in self.requiredPackages:
try:
self._install_package(pkg_name, required=True)
log.debug("selected required package: %s", pkg_name)
except packaging.NoSuchPackage as e:
self._miss(e)
for group in self.requiredGroups:
try:
self._select_group(group, required=True)
log.debug("selected required group: %s", group)
except packaging.NoSuchGroup as e:
self._miss(e)
def _bump_tx_id(self):
if self.txID is None:
self.txID = 1
else:
self.txID += 1
return self.txID
def _configure(self):
self._base = dnf.Base()
conf = self._base.conf
conf.cachedir = DNF_CACHE_DIR
conf.logdir = '/tmp/'
# disable console output completely:
conf.debuglevel = 0
conf.errorlevel = 0
self._base.logging.setup_from_dnf_conf(conf)
conf.releasever = self._getReleaseVersion(None)
conf.installroot = pyanaconda.iutil.getSysroot()
conf.prepend_installroot('persistdir')
# NSS won't survive the forking we do to shield out chroot during
# transaction, disable it in RPM:
conf.tsflags.append('nocrypto')
if hasattr(self.data.method, "proxy") and self.data.method.proxy:
try:
proxy = ProxyString(self.data.method.proxy)
conf.proxy = proxy.noauth_url
if proxy.username:
conf.proxy_username = proxy.username
if proxy.password:
conf.proxy_password = proxy.password
log.info("Using %s as proxy", self.data.method.proxy)
except ProxyStringError as e:
log.error("Failed to parse proxy for dnf configure %s: %s",
self.data.method.proxy, e)
# Start with an empty comps so we can go ahead and use the environment
# and group properties. Unset reposdir to ensure dnf has nothing it can
# check automatically
conf.reposdir = []
self._base.read_comps()
conf.reposdir = REPO_DIRS
@property
def _download_space(self):
transaction = self._base.transaction
if transaction is None:
return Size(0)
size = sum(tsi.installed.downloadsize for tsi in transaction)
return Size(size)
def _install_package(self, pkg_name, required=False):
try:
return self._base.install(pkg_name)
except dnf.exceptions.MarkingError:
raise packaging.NoSuchPackage(pkg_name, required=required)
def _remove_package(self, pkg_name):
try:
return self._base.remove(pkg_name)
except dnf.exceptions.PackagesNotInstalledError:
raise packaging.NoSuchPackage(pkg_name)
def _miss(self, exn):
if self.data.packages.handleMissing == KS_MISSING_IGNORE:
return
log.error('Missed: %r', exn)
if errors.errorHandler.cb(exn) == errors.ERROR_RAISE:
# The progress bar polls kind of slowly, thus installation could
# still continue for a bit before the quit message is processed.
# Doing a sys.exit also ensures the running thread quits before
# it can do anything else.
progressQ.send_quit(1)
pyanaconda.iutil.ipmi_report(constants.IPMI_ABORTED)
sys.exit(1)
def _pick_download_location(self):
required = self._download_space
df_map = _df_map()
mpoint = _pick_mpoint(df_map, required)
log.info("Download space required: %s, use filesystem at: %s", required,
mpoint)
if mpoint is None:
msg = "Not enough disk space to download the packages."
raise packaging.PayloadError(msg)
pkgdir = '%s/%s' % (mpoint, DNF_PACKAGE_CACHE_DIR_SUFFIX)
for repo in self._base.repos.iter_enabled():
repo.pkgdir = pkgdir
return pkgdir
def _select_group(self, group_id, default=True, optional=False, required=False):
grp = self._base.comps.group_by_pattern(group_id)
if grp is None:
raise packaging.NoSuchGroup(group_id, required=required)
types = {'mandatory'}
if default:
types.add('default')
if optional:
types.add('optional')
exclude = self.data.packages.excludedList
try:
self._base.group_install(grp, types, exclude=exclude)
except dnf.exceptions.CompsError as e:
# DNF raises this when it is already selected
log.debug(e)
def _deselect_group(self, group_id):
grp = self._base.comps.group_by_pattern(group_id)
if grp is None:
raise packaging.NoSuchGroup(group_id)
try:
self._base.group_remove(grp)
except dnf.exceptions.CompsError as e:
# DNF raises this when it is already not selected
log.debug(e)
def _select_kernel_package(self):
kernels = self.kernelPackages
for kernel in kernels:
try:
self._install_package(kernel)
except packaging.NoSuchPackage:
log.info('kernel: no such package %s', kernel)
else:
log.info('kernel: selected %s', kernel)
break
else:
log.error('kernel: failed to select a kernel from %s', kernels)
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.disableRepo(id_)
@property
def baseRepo(self):
# is any locking needed here as in the yumpayload?
repo_names = [constants.BASE_REPO_NAME] + self.DEFAULT_REPOS
for repo in self._base.repos.iter_enabled():
if repo.id in repo_names:
return repo.id
return None
@property
def environments(self):
environments = self._base.comps.environments_iter()
return [env.id for env in environments]
@property
def groups(self):
groups = self._base.comps.groups_iter()
return [g.id for g in groups]
@property
def mirrorEnabled(self):
return True
@property
def repos(self):
# known repo ids
return [r.id for r in self._base.repos.values()]
@property
def spaceRequired(self):
transaction = self._base.transaction
if transaction is None:
return Size("3000 MB")
size = sum(tsi.installed.installsize for tsi in transaction)
# add 35% to account for the fact that the above method is laughably
# inaccurate:
size *= 1.35
return Size(size)
def _isGroupVisible(self, grpid):
grp = self._base.comps.group_by_pattern(grpid)
if grp is None:
raise packaging.NoSuchGroup(grpid)
return grp.visible
def _groupHasInstallableMembers(self, grpid):
return True
def checkSoftwareSelection(self):
log.info("checking software selection")
self._bump_tx_id()
self._base.reset(goal=True)
self._apply_selections()
try:
if self._base.resolve():
log.debug("checking dependencies: success.")
else:
log.debug("empty transaction")
except dnf.exceptions.DepsolveError as e:
msg = str(e)
log.warning(msg)
raise packaging.DependencyError([msg])
log.info("%d packages selected totalling %s",
len(self._base.transaction), self.spaceRequired)
def disableRepo(self, repo_id):
try:
self._base.repos[repo_id].disable()
log.info("Disabled '%s'", repo_id)
except KeyError:
pass
super(DNFPayload, self).disableRepo(repo_id)
def enableRepo(self, repo_id):
try:
self._base.repos[repo_id].enable()
log.info("Enabled '%s'", repo_id)
except KeyError:
pass
super(DNFPayload, self).enableRepo(repo_id)
def environmentDescription(self, environmentid):
env = self._base.comps.environment_by_pattern(environmentid)
if env is None:
raise packaging.NoSuchGroup(environmentid)
return (env.ui_name, env.ui_description)
def environmentGroups(self, environmentid, optional=True):
env = self._base.comps.environment_by_pattern(environmentid)
if env is None:
raise packaging.NoSuchGroup(environmentid)
group_ids = (id_.name for id_ in env.group_ids)
option_ids = (id_.name for id_ in env.option_ids)
if optional:
return list(itertools.chain(group_ids, option_ids))
else:
return list(group_ids)
def environmentHasOption(self, environmentid, grpid):
env = self._base.comps.environment_by_pattern(environmentid)
if env is None:
raise packaging.NoSuchGroup(environmentid)
return grpid in (id_.name for id_ in env.option_ids)
def environmentOptionIsDefault(self, environmentid, grpid):
env = self._base.comps.environment_by_pattern(environmentid)
if env is None:
raise packaging.NoSuchGroup(environmentid)
# Look for a group in the optionlist that matches the group_id and has
# default set
return any(grp for grp in env.option_ids if grp.name == grpid and grp.default)
def groupDescription(self, grpid):
""" Return name/description tuple for the group specified by id. """
grp = self._base.comps.group_by_pattern(grpid)
if grp is None:
raise packaging.NoSuchGroup(grpid)
return (grp.ui_name, grp.ui_description)
def gatherRepoMetadata(self):
map(self._sync_metadata, self._base.repos.iter_enabled())
self._base.fill_sack(load_system_repo=False)
self._base.read_comps()
self._refreshEnvironmentAddons()
def install(self):
progressQ.send_message(_('Starting package installation process'))
# Add the rpm macros to the global transaction environment
for macro in self.rpmMacros:
rpm.addMacro(macro[0], macro[1])
if self.install_device:
self._setupMedia(self.install_device)
try:
self.checkSoftwareSelection()
self._download_location = self._pick_download_location()
except packaging.PayloadError as e:
if errors.errorHandler.cb(e) == errors.ERROR_RAISE:
_failure_limbo()
pkgs_to_download = self._base.transaction.install_set
log.info('Downloading packages.')
progressQ.send_message(_('Downloading packages'))
progress = DownloadProgress()
try:
self._base.download_packages(pkgs_to_download, progress)
except dnf.exceptions.DownloadError as e:
msg = 'Failed to download the following packages: %s' % str(e)
exc = packaging.PayloadInstallError(msg)
if errors.errorHandler.cb(exc) == errors.ERROR_RAISE:
_failure_limbo()
log.info('Downloading packages finished.')
pre_msg = _("Preparing transaction from installation source")
progressQ.send_message(pre_msg)
queue = multiprocessing.Queue()
process = multiprocessing.Process(target=do_transaction,
args=(self._base, queue))
process.start()
(token, msg) = queue.get()
while token not in ('post', 'quit'):
if token == 'install':
msg = _("Installing %s") % msg
progressQ.send_message(msg)
(token, msg) = queue.get()
if token == 'quit':
_failure_limbo()
post_msg = _("Performing post-installation setup tasks")
progressQ.send_message(post_msg)
process.join()
self._base.close()
if os.path.exists(self._download_location):
log.info("Cleaning up downloaded packages: %s", self._download_location)
shutil.rmtree(self._download_location)
else:
# Some installation sources, such as NFS, don't need to download packages to
# local storage, so the download location might not always exist. So for now
# warn about this, at least until the RFE in bug 1193121 is implemented and
# we don't have to care about clearing the download location ourselves.
log.warning("Can't delete nonexistent download location: %s", self._download_location)
def getRepo(self, repo_id):
""" Return the yum repo object. """
return self._base.repos[repo_id]
def isRepoEnabled(self, repo_id):
try:
return self._base.repos[repo_id].enabled
except (dnf.exceptions.RepoError, KeyError):
return super(DNFPayload, self).isRepoEnabled(repo_id)
def languageGroups(self):
locales = [self.data.lang.lang] + self.data.lang.addsupport
match_fn = pyanaconda.localization.langcode_matches_locale
gids = set()
gl_tuples = ((g.id, g.lang_only) for g in self._base.comps.groups_iter())
for (gid, lang) in gl_tuples:
for locale in locales:
if match_fn(lang, locale):
gids.add(gid)
log.info('languageGroups: %s', gids)
return list(gids)
def preInstall(self, packages=None, groups=None):
super(DNFPayload, self).preInstall(packages, groups)
self.requiredPackages = ["dnf"]
if packages:
self.requiredPackages += packages
self.requiredGroups = groups
self.addDriverRepos()
def reset(self):
super(DNFPayload, self).reset()
shutil.rmtree(DNF_CACHE_DIR, ignore_errors=True)
self.txID = None
self._base.reset(sack=True, repos=True)
def updateBaseRepo(self, fallback=True, checkmount=True):
log.info('configuring base repo')
self.reset()
url, mirrorlist, sslverify = self._setupInstallDevice(self.storage,
checkmount)
method = self.data.method
# 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()
enabled = []
for repo in self._base.repos.iter_enabled():
enabled.append(repo.id)
repo.disable()
# If askmethod was specified on the command-line, leave all the repos
# disabled and return
if flags.askmethod:
return
if method.method:
try:
self._base.conf.releasever = self._getReleaseVersion(url)
log.debug("releasever from %s is %s", url, self._base.conf.releasever)
except ConfigParser.MissingSectionHeaderError as e:
log.error("couldn't set releasever from base repo (%s): %s",
method.method, e)
try:
proxy = getattr(method, "proxy", None)
base_ksrepo = self.data.RepoData(
name=constants.BASE_REPO_NAME, baseurl=url,
mirrorlist=mirrorlist, noverifyssl=not sslverify, proxy=proxy)
self._add_repo(base_ksrepo)
except (packaging.MetadataError, packaging.PayloadError) as e:
log.error("base repo (%s/%s) not valid -- removing it",
method.method, url)
self._base.repos.pop(constants.BASE_REPO_NAME, None)
if not fallback:
for repo in self._base.repos.iter_enabled():
self.disableRepo(repo.id)
return
# this preserves the method details while disabling it
method.method = None
self.install_device = None
# We need to check this again separately in case method.method was unset above.
if not method.method:
# If this is a kickstart install, just return now
if flags.automatedInstall:
return
# Otherwise, fall back to the default repos that we disabled above
for (id_, repo) in self._base.repos.items():
if id_ in enabled:
repo.enable()
for ksrepo in self.data.repo.dataList():
self._add_repo(ksrepo)
ksnames = [r.name for r in self.data.repo.dataList()]
ksnames.append(constants.BASE_REPO_NAME)
for repo in self._base.repos.iter_enabled():
id_ = repo.id
if 'source' in id_ or 'debuginfo' in id_:
self.disableRepo(id_)
elif constants.isFinal and 'rawhide' in id_:
self.disableRepo(id_)
def _writeDNFRepo(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.isRepoEnabled(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 packaging.PayloadSetupError("repo %s has no baseurl, mirrorlist or metalink", repo.id)
# kickstart repo modifiers
ks_repo = self.getAddOnRepo(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)
def postInstall(self):
""" Perform post-installation tasks. """
# Write selected kickstart repos to target system
for ks_repo in (ks for ks in (self.getAddOnRepo(r) for r in self.addOns) if ks.install):
try:
repo = self.getRepo(ks_repo.name)
if not repo:
continue
except (dnf.exceptions.RepoError, KeyError):
continue
repo_path = pyanaconda.iutil.getSysroot() + YUM_REPOS_DIR + "%s.repo" % repo.id
try:
log.info("Writing %s.repo to target system.", repo.id)
self._writeDNFRepo(repo, repo_path)
except packaging.PayloadSetupError as e:
log.error(e)
super(DNFPayload, self).postInstall()