Files
entropy/libraries/entropy/client/interfaces/package.py
T

4271 lines
158 KiB
Python

# -*- coding: utf-8 -*-
"""
@author: Fabio Erculiani <lxnay@sabayon.org>
@contact: lxnay@sabayon.org
@copyright: Fabio Erculiani
@license: GPL-2
B{Entropy Package Manager Client Package Interface}.
"""
import sys
import os
import errno
import stat
import shutil
import tempfile
import time
from entropy.const import etpConst, etpUi, const_setup_perms, \
const_isunicode, const_convert_to_unicode, const_debug_write
from entropy.exceptions import PermissionDenied, SPMError
from entropy.i18n import _
from entropy.output import brown, blue, bold, darkgreen, \
darkblue, red, purple, darkred, teal
from entropy.client.interfaces.client import Client
from entropy.client.mirrors import StatusInterface
from entropy.core.settings.base import SystemSettings
from entropy.security import Repository as RepositorySecurity
import entropy.dep
import entropy.tools
class Package:
def __init__(self, entropy_client):
if not isinstance(entropy_client, Client):
mytxt = "A valid Client instance or subclass is needed"
raise AttributeError(mytxt)
self._entropy = entropy_client
self._settings = SystemSettings()
self.pkgmeta = {}
self.__prepared = False
self._package_match = ()
self._valid_actions = ("source", "fetch", "multi_fetch", "remove",
"remove_conflict", "install", "config"
)
self._action = None
self._xterm_title = ''
def __repr__(self):
return "<%s.Package at %s | metadata: %s | action: %s, prepared: %s>" \
% (__name__, hex(id(self)), self.pkgmeta, self._action,
self.__prepared)
def __str__(self):
return repr(self)
def __unicode__(self):
return unicode(repr(self))
def kill(self):
self.pkgmeta.clear()
self._package_match = ()
self._valid_actions = ()
self._action = None
self.__prepared = False
def _error_on_prepared(self):
if self.__prepared:
mytxt = _("Already prepared")
raise PermissionDenied("PermissionDenied: %s" % (mytxt,))
def _error_on_not_prepared(self):
if not self.__prepared:
mytxt = _("Not yet prepared")
raise PermissionDenied("PermissionDenied: %s" % (mytxt,))
def _check_action_validity(self, action):
if action not in self._valid_actions:
raise AttributeError("Action must be in %s" % (
self._valid_actions,))
@staticmethod
def get_standard_fetch_disk_path(download):
"""
Return standard path where package is going to be downloaded.
"download" argument passed must come from
EntropyRepository.retrieveDownloadURL()
"""
return os.path.join(etpConst['entropypackagesworkdir'], download)
def __get_fetch_disk_path(self, download):
"""
Return proper Entropy package store path
"""
if 'fetch_path' in self.pkgmeta:
# only supported by fetch action, multifetch also unsupported
pkg_disk_path = os.path.join(self.pkgmeta['fetch_path'],
os.path.basename(download))
else:
pkg_disk_path = Package.get_standard_fetch_disk_path(download)
return pkg_disk_path
def __escape_path(self, path):
"""
Some applications (like ld) don't like ":" in path, others just don't
escape paths at all. So, it's better to avoid to use field separators
in path.
"""
path = path.replace(":", "_")
path = path.replace("~", "_")
return path
def __check_pkg_path_download(self, download, checksum = None):
# is the file available
pkg_path = self.__get_fetch_disk_path(download)
if os.path.isfile(pkg_path):
if checksum is None:
return 0
# check digest
md5res = entropy.tools.compare_md5(pkg_path, checksum)
if md5res:
return 0
return -2
return -1
def __fetch_files(self, url_data_list, checksum = True, resume = True):
def _generate_checksum_map(url_data):
if not checksum:
return {}
ck_map = {}
ck_map_id = 0
for pkg_id, repo, url, dest_path, cksum in url_data:
ck_map_id += 1
if cksum is not None:
ck_map[ck_map_id] = cksum
return ck_map
fetch_abort_function = self.pkgmeta.get('fetch_abort_function')
# avoid tainting data pointed by url_data_list
url_data = url_data_list[:]
diff_map = {}
# setup directories
for pkg_id, repo, url, dest_path, cksum in url_data:
dest_dir = os.path.dirname(dest_path)
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir, 0o775)
const_setup_perms(dest_dir, etpConst['entropygid'])
checksum_map = _generate_checksum_map(url_data)
fetched_url_data, data_transfer, abort = self.__try_edelta_multifetch(
url_data, resume)
if abort:
return -100, {}, 0
for url_data_item in fetched_url_data:
url_data.remove(url_data_item)
# some packages haven't been downloaded using edelta
if url_data:
url_path_list = []
for pkg_id, repo, url, dest_path, cksum in url_data:
url_path_list.append((url, dest_path,))
self._setup_differential_download(
self._entropy._multiple_url_fetcher, url, resume, dest_path,
repo, pkg_id)
# load class
fetch_intf = self._entropy._multiple_url_fetcher(url_path_list,
resume = resume, abort_check_func = fetch_abort_function,
url_fetcher_class = self._entropy._url_fetcher,
checksum = checksum)
try:
data = fetch_intf.download()
except KeyboardInterrupt:
return -100, {}, 0
# update transfer rate information
data_transfer = fetch_intf.get_transfer_rate()
checksum_map = _generate_checksum_map(url_data)
# if checksum_map is empty, it means that checksum == False
for ck_id in checksum_map:
orig_checksum = checksum_map.get(ck_id)
if orig_checksum != data.get(ck_id):
diff_map[url_path_list[ck_id-1][0]] = orig_checksum
if diff_map:
defval = -1
for key, val in tuple(diff_map.items()):
if val == "-1": # general error
diff_map[key] = -1
elif val == "-2":
diff_map[key] = -2
elif val == "-4": # timeout
diff_map[key] = -4
elif val == "-3": # not found
diff_map[key] = -3
elif val == -100:
defval = -100
return defval, diff_map, data_transfer
return 0, diff_map, data_transfer
def _get_url_name(self, url):
"""
Given a mirror URL, returns a smaller string representing the URL name.
@param url: URL string
@type url: string
@return: representative URL string
@rtype: string
"""
url_data = entropy.tools.spliturl(url)
url_name = url_data.netloc
url_scheme = url_data.scheme
if not url_scheme:
url_scheme = "unknown"
return "%s://%s" % (url_scheme, url_name,)
def _download_packages(self, download_list, checksum = False):
avail_data = self._settings['repositories']['available']
excluded_data = self._settings['repositories']['excluded']
repo_uris = {}
for pkg_id, repo, fname, cksum, signatures in download_list:
repo_db = self._entropy.open_repository(repo)
# grab original repo, if any and use it if available
# this is done in order to support "equo repo merge" feature
# allowing client-side repository package metadata moves.
original_repo = repo_db.getInstalledPackageRepository(pkg_id)
if (original_repo != repo) and (original_repo not in avail_data) \
and (original_repo is not None):
# build up a new uris list, at least try, hoping that
# repository is just shadowing original_repo
# for example: original_repo got copied to repository, without
# copying packages, which would be useless. like it happens
# with sabayon-weekly
uris = self.__build_uris_list(original_repo, repo)
else:
if original_repo in avail_data:
uris = avail_data[original_repo]['packages'][::-1]
uris += avail_data[repo]['packages'][::-1]
elif original_repo in excluded_data:
uris = excluded_data[original_repo]['packages'][::-1]
uris += avail_data[repo]['packages'][::-1]
else:
uris = avail_data[repo]['packages'][::-1]
obj = repo_uris.setdefault(repo, [])
# append at the beginning
new_ones = [x for x in uris if x not in obj][::-1]
for new_obj in new_ones:
obj.insert(0, new_obj)
remaining = repo_uris.copy()
my_download_list = download_list[:]
mirror_status = StatusInterface()
def get_best_mirror(repository):
try:
return remaining[repository][0]
except IndexError:
return None
def update_download_list(down_list, failed_down):
newlist = []
for pkg_id, repo, fname, cksum, signatures in down_list:
myuri = get_best_mirror(repo)
myuri = os.path.join(myuri, fname)
if myuri not in failed_down:
continue
newlist.append((pkg_id, repo, fname, cksum, signatures,))
return newlist
# return True: for failing, return False: for fine
def mirror_fail_check(repository, best_mirror):
# check if uri is sane
if not mirror_status.get_failing_mirror_status(best_mirror) >= 30:
return False
# set to 30 for convenience
mirror_status.set_failing_mirror_status(best_mirror, 30)
mirrorcount = repo_uris[repository].index(best_mirror)+1
mytxt = "( mirror #%s ) " % (mirrorcount,)
mytxt += blue(" %s: ") % (_("Mirror"),)
mytxt += red(self._get_url_name(best_mirror))
mytxt += " - %s." % (_("maximum failure threshold reached"),)
self._entropy.output(
mytxt,
importance = 1,
level = "warning",
header = red(" ## ")
)
if mirror_status.get_failing_mirror_status(best_mirror) == 30:
mirror_status.add_failing_mirror(best_mirror, 45)
elif mirror_status.get_failing_mirror_status(best_mirror) > 31:
mirror_status.add_failing_mirror(best_mirror, -4)
else:
mirror_status.set_failing_mirror_status(best_mirror, 0)
try:
remaining[repository].remove(best_mirror)
except ValueError:
# ignore
pass
return True
def show_download_summary(down_list):
for pkg_id, repo, fname, cksum, signatures in down_list:
best_mirror = get_best_mirror(repo)
mirrorcount = repo_uris[repo].index(best_mirror)+1
mytxt = "( mirror #%s ) " % (mirrorcount,)
basef = os.path.basename(fname)
mytxt += "[%s] %s " % (brown(basef), blue("@"),)
mytxt += red(self._get_url_name(best_mirror))
# now fetch the new one
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
def show_successful_download(down_list, data_transfer):
for pkg_id, repo, fname, cksum, signatures in down_list:
best_mirror = get_best_mirror(repo)
mirrorcount = repo_uris[repo].index(best_mirror)+1
mytxt = "( mirror #%s ) " % (mirrorcount,)
basef = os.path.basename(fname)
mytxt += "[%s] %s %s " % (brown(basef),
darkred(_("success")), blue("@"),)
mytxt += red(self._get_url_name(best_mirror))
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
mytxt = " %s: %s%s%s" % (
blue(_("Aggregated transfer rate")),
bold(entropy.tools.bytes_into_human(data_transfer)),
darkred("/"),
darkblue(_("second")),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
def show_download_error(down_list, rc):
for pkg_id, repo, fname, cksum, signatures in down_list:
best_mirror = get_best_mirror(repo)
mirrorcount = repo_uris[repo].index(best_mirror)+1
mytxt = "( mirror #%s ) " % (mirrorcount,)
mytxt += blue("%s: %s") % (
_("Error downloading from"),
red(self._get_url_name(best_mirror)),
)
if rc == -1:
mytxt += " - %s." % (_("data not available on this mirror"),)
elif rc == -2:
mirror_status.add_failing_mirror(best_mirror, 1)
mytxt += " - %s." % (_("wrong checksum"),)
elif rc == -3:
mytxt += " - %s." % (_("not found"),)
elif rc == -4: # timeout!
mytxt += " - %s." % (_("timeout error"),)
elif rc == -100:
mytxt += " - %s." % (_("discarded download"),)
else:
mirror_status.add_failing_mirror(best_mirror, 5)
mytxt += " - %s." % (_("unknown reason"),)
self._entropy.output(
mytxt,
importance = 1,
level = "warning",
header = red(" ## ")
)
def remove_failing_mirrors(repos):
for repo in repos:
best_mirror = get_best_mirror(repo)
if remaining[repo]:
remaining[repo].pop(0)
def check_remaining_mirror_failure(repos):
return [x for x in repos if not remaining.get(x)]
while True:
do_resume = True
timeout_try_count = 50
while True:
fetch_files_list = []
for pkg_id, repo, fname, cksum, signatures in my_download_list:
best_mirror = get_best_mirror(repo)
# set working mirror, dont care if its None
mirror_status.set_working_mirror(best_mirror)
if best_mirror is not None:
mirror_fail_check(repo, best_mirror)
best_mirror = get_best_mirror(repo)
if best_mirror is None:
# at least one package failed to download
# properly, give up with everything
return 3, my_download_list
myuri = os.path.join(best_mirror, fname)
pkg_path = Package.get_standard_fetch_disk_path(fname)
fetch_files_list.append(
(pkg_id, repo, myuri, pkg_path, cksum,))
try:
show_download_summary(my_download_list)
rc, failed_downloads, data_transfer = self.__fetch_files(
fetch_files_list, checksum = checksum,
resume = do_resume
)
if rc == 0:
show_successful_download(my_download_list,
data_transfer)
return 0, []
# update my_download_list
my_download_list = update_download_list(my_download_list,
failed_downloads)
if rc not in (-3, -4, -100,) and failed_downloads and \
do_resume:
# disable resume
do_resume = False
continue
else:
show_download_error(my_download_list, rc)
if rc == -4: # timeout
timeout_try_count -= 1
if timeout_try_count > 0:
continue
elif rc == -100: # user discarded fetch
return 1, []
myrepos = set([x[1] for x in my_download_list])
remove_failing_mirrors(myrepos)
# make sure we don't have nasty issues
remaining_failure = check_remaining_mirror_failure(
myrepos)
if remaining_failure:
return 3, my_download_list
break
except KeyboardInterrupt:
return 1, []
return 0, []
def _setup_differential_download(self, fetcher, url, resume,
fetch_path, repository, package_id):
"""
Setup differential download in case of URL supporting it.
Internal function.
@param fetcher: UrlFetcher or MultipleUrlFetcher class
@param url: URL to check differential download against
@type url: string
@param resume: resume support
@type resume: bool
@param fetch_path: path where package file will be saved
@type fetch_path: string
@param repository: repository identifier belonging to package file
@type repository: string
@param package_id: package identifier belonging to repository identifier
@type package_id: int
"""
# no resume? no party?
if not resume:
const_debug_write(__name__,
"_setup_differential_download(%s) %s" % (
url, "resume disabled"))
return
if not fetcher.supports_differential_download(url):
# no differential download support
const_debug_write(__name__,
"_setup_differential_download(%s) %s" % (
url, "no differential download support"))
return
# this is the fetch path of the file that is going to be downloaded
# not going to overwrite it if a file is located there because
# user would want a resume for sure in first place
if os.path.isfile(fetch_path):
const_debug_write(__name__,
"_setup_differential_download(%s) %s" % (
url, "fetch path already exists, not overwriting"))
return
pkg_repo = self._entropy.open_repository(repository)
keyslot = pkg_repo.retrieveKeySlot(package_id)
if keyslot is None:
# fucked up entry, not dealing with it here
const_debug_write(__name__,
"_setup_differential_download(%s) %s" % (
url, "key, slot fucked up"))
return
key, slot = keyslot
inst_repo = self._entropy.installed_repository()
pkg_ids = inst_repo.searchKeySlot(key, slot)
if not pkg_ids:
# not installed, nothing to use as diff download
const_debug_write(__name__,
"_setup_differential_download(%s) %s" % (
url, "no installed packages"))
return
# grab the highest, we don't know if user was able to mess up
# its installed packages repository
pkg_id = max(pkg_ids)
download_url = inst_repo.retrieveDownloadURL(pkg_id)
installed_fetch_path = self.__get_fetch_disk_path(download_url)
if os.path.isfile(installed_fetch_path) and \
os.access(installed_fetch_path, os.R_OK | os.F_OK):
copied = False
try:
shutil.copyfile(installed_fetch_path, fetch_path)
user = os.stat(installed_fetch_path)[stat.ST_UID]
group = os.stat(installed_fetch_path)[stat.ST_GID]
os.chown(fetch_path, user, group)
shutil.copystat(installed_fetch_path, fetch_path)
copied = True
except (OSError, IOError, shutil.Error):
try:
os.remove(fetch_path)
except OSError:
pass
const_debug_write(__name__,
"_setup_differential_download(%s) %s %s => %s" % (
url, "copied", copied, fetch_path))
else:
const_debug_write(__name__,
"_setup_differential_download(%s) %s" % (
url, "no installed package file found"))
def __approve_edelta(self, url, installed_package_id, package_digest):
"""
Approve Entropy package delta support for given url, checking if
a previously fetched package is available.
@return: edelta URL to download and previously downloaded package path
or None if edelta is not available
@rtype: tuple of strings or None
"""
inst_repo = self._entropy.installed_repository()
download_url = inst_repo.retrieveDownloadURL(installed_package_id)
installed_digest = inst_repo.retrieveDigest(installed_package_id)
installed_fetch_path = self.__get_fetch_disk_path(download_url)
if os.path.isfile(installed_fetch_path) and \
os.access(installed_fetch_path, os.R_OK | os.F_OK):
edelta_local_approved = entropy.tools.compare_md5(
installed_fetch_path, installed_digest)
if edelta_local_approved:
hash_tag = installed_digest + package_digest
edelta_file_name = \
entropy.tools.generate_entropy_delta_file_name(
os.path.basename(download_url),
os.path.basename(url), hash_tag)
edelta_url = os.path.join(os.path.dirname(url),
etpConst['packagesdeltasubdir'], edelta_file_name)
return edelta_url, installed_fetch_path
def __try_edelta_multifetch(self, url_data, resume):
# no edelta support enabled
if not self.pkgmeta.get('edelta_support'):
return [], 0.0, False
# edelta enabled?
if not entropy.tools.is_entropy_delta_available():
return [], 0.0, False
url_path_list = []
url_data_map = {}
url_data_map_idx = 0
for pkg_id, repo, url, dest_path, cksum in url_data:
repo_db = self._entropy.open_repository(repo)
if cksum is None:
# cannot setup edelta without checksum, get from repository
cksum = repo_db.retrieveDigest(pkg_id)
if cksum is None:
# still nothing
continue
key_slot = repo_db.retrieveKeySlot(pkg_id)
if key_slot is None:
# wtf corrupted entry, skip
continue
pkg_key, pkg_slot = key_slot
installed_package_id = self.__setup_package_to_remove(pkg_key,
pkg_slot)
if installed_package_id == -1:
# package is not installed
continue
edelta_approve = self.__approve_edelta(url, installed_package_id,
cksum)
if edelta_approve is None:
# no edelta support
continue
edelta_url, installed_fetch_path = edelta_approve
edelta_save_path = dest_path + etpConst['packagesdeltaext']
key = (edelta_url, edelta_save_path)
url_path_list.append(key)
url_data_map_idx += 1
url_data_map[url_data_map_idx] = (pkg_id, repo, url, dest_path,
cksum, edelta_url, edelta_save_path, installed_fetch_path)
if not url_path_list:
# no martini, no party!
return [], 0.0, False
fetch_abort_function = self.pkgmeta.get('fetch_abort_function')
fetch_intf = self._entropy._multiple_url_fetcher(url_path_list,
resume = resume, abort_check_func = fetch_abort_function,
url_fetcher_class = self._entropy._url_fetcher)
try:
data = fetch_intf.download()
except KeyboardInterrupt:
return [], 0.0, True
data_transfer = fetch_intf.get_transfer_rate()
valid_idxs = []
for url_data_map_idx, cksum in tuple(data.items()):
if cksum.startswith("-"):
# download failed => "-1", "-2", "-3", etc
continue
pkg_id, repo, url, dest_path, orig_cksum, edelta_url, \
edelta_save_path, installed_fetch_path = \
url_data_map[url_data_map_idx]
# now check
tmp_dest_path = dest_path + ".edelta_pkg_tmp"
# yay, we can apply the delta and cook the new package file!
try:
entropy.tools.apply_entropy_delta(installed_fetch_path,
edelta_save_path, tmp_dest_path)
except IOError:
# give up with this edelta
try:
os.remove(tmp_dest_path)
except (OSError, IOError):
pass
continue
os.rename(tmp_dest_path, dest_path)
valid_idxs.append(url_data_map_idx)
# now check md5
fetched_url_data = []
for url_data_map_idx in valid_idxs:
pkg_id, repo, url, dest_path, orig_cksum, edelta_url, \
edelta_save_path, installed_fetch_path = \
url_data_map[url_data_map_idx]
try:
valid = entropy.tools.compare_md5(dest_path, orig_cksum)
except (IOError, OSError):
valid = False
if valid:
url_data_item = (pkg_id, repo, url, dest_path, orig_cksum)
fetched_url_data.append(url_data_item)
return fetched_url_data, data_transfer, False
def __try_edelta_fetch(self, url, save_path, resume):
# no edelta support enabled
if not self.pkgmeta.get('edelta_support'):
return 1, 0.0
# edelta enabled?
if not entropy.tools.is_entropy_delta_available():
return 1, 0.0
# when called by __fetch_file, which is called by _download_package
# which is called by _match_checksum, which is called by
# multi_match_checksum, removeidpackage metadatum is not available
# So, be fault tolerant.
installed_package_id = self.pkgmeta.get('removeidpackage', -1)
# fresh install, cannot fetch edelta, edelta only works for installed
# packages, by design.
if installed_package_id == -1:
return 1, 0.0
edelta_approve = self.__approve_edelta(url, installed_package_id,
self.pkgmeta['checksum'])
if edelta_approve is None:
# edelta not available, give up
return 1, 0.0
edelta_url, installed_fetch_path = edelta_approve
# check if edelta file is available online
edelta_save_path = save_path + etpConst['packagesdeltaext']
max_tries = 2
edelta_approved = False
data_transfer = 0
download_plan = [(edelta_url, edelta_save_path) for x in \
range(max_tries)]
delta_resume = resume
fetch_abort_function = self.pkgmeta.get('fetch_abort_function')
for delta_url, delta_save in download_plan:
delta_fetcher = self._entropy._url_fetcher(delta_url,
delta_save, resume = delta_resume,
abort_check_func = fetch_abort_function)
try:
delta_checksum = delta_fetcher.download()
data_transfer = delta_fetcher.get_transfer_rate()
del delta_fetcher
except KeyboardInterrupt:
return -100, data_transfer
except NameError:
raise
except Exception:
return -1, data_transfer
# "-3", "-4", etc are considered errors, so give up.
if delta_checksum.startswith("-"):
# make sure this points to the hell
delta_resume = False
# retry
continue
# now check
tmp_save_path = save_path + ".edelta_pkg_tmp"
# yay, we can apply the delta and cook the new package file!
try:
entropy.tools.apply_entropy_delta(installed_fetch_path,
delta_save, tmp_save_path)
except IOError:
# make sure this points to the hell
delta_resume = False
# retry
try:
os.remove(tmp_save_path)
except (OSError, IOError):
pass
continue
os.rename(tmp_save_path, save_path)
edelta_approved = True
break
if edelta_approved:
# we can happily return
return 0, data_transfer
# error, give up with the edelta stuff
return 1, data_transfer
def __fetch_file(self, url, save_path, digest = None, resume = True,
download = None, package_id = None, repository = None):
def do_stfu_rm(xpath):
try:
os.remove(xpath)
except OSError:
pass
fetch_abort_function = self.pkgmeta.get('fetch_abort_function')
filepath_dir = os.path.dirname(save_path)
# symlink support
if not os.path.isdir(os.path.realpath(filepath_dir)):
try:
os.remove(filepath_dir)
except OSError as err:
const_debug_write(__name__,
"__fetch_file.remove, %s, error: %s" % (
filepath_dir, err))
try:
os.makedirs(filepath_dir, 0o755)
except OSError as err:
const_debug_write(__name__,
"__fetch_file.makedirs, %s, error: %s" % (
filepath_dir, err))
return -1, 0, False
rc, data_transfer = self.__try_edelta_fetch(url, save_path, resume)
if rc == 0:
return rc, data_transfer, False
elif rc < 0: # < 0 errors are unrecoverable
return rc, data_transfer, False
# otherwise, just fallback to package download
existed_before = False
if os.path.isfile(save_path) and os.path.exists(save_path):
existed_before = True
fetch_intf = self._entropy._url_fetcher(url, save_path, resume = resume,
abort_check_func = fetch_abort_function)
if (download is not None) and (package_id is not None) and \
(repository is not None):
fetch_path = self.__get_fetch_disk_path(download)
self._setup_differential_download(self._entropy._url_fetcher, url,
resume, fetch_path, repository, package_id)
# start to download
data_transfer = 0
resumed = False
try:
fetch_checksum = fetch_intf.download()
data_transfer = fetch_intf.get_transfer_rate()
resumed = fetch_intf.is_resumed()
except KeyboardInterrupt:
return -100, data_transfer, resumed
except NameError:
raise
except:
if etpUi['debug']:
self._entropy.output(
"fetch_file:",
importance = 1,
level = "warning",
header = red(" ## ")
)
entropy.tools.print_traceback()
if (not existed_before) or (not resume):
do_stfu_rm(save_path)
return -1, data_transfer, resumed
if fetch_checksum == "-3":
# not found
return -3, data_transfer, resumed
elif fetch_checksum == "-4":
# timeout
return -4, data_transfer, resumed
del fetch_intf
if digest and (fetch_checksum != digest):
# not properly downloaded
if (not existed_before) or (not resume):
do_stfu_rm(save_path)
return -2, data_transfer, resumed
return 0, data_transfer, resumed
def __build_uris_list(self, original_repo, repository):
avail_data = self._settings['repositories']['available']
product = self._settings['repositories']['product']
uris = []
plain_packages = avail_data[repository]['plain_packages']
for uri in plain_packages:
expanded_uri = entropy.tools.expand_plain_package_mirror(
uri, product, original_repo)
uris.append(expanded_uri)
uris.reverse()
uris.extend(avail_data[repository]['packages'][::-1])
return uris
def _download_package(self, package_id, repository, download, save_path,
digest = False, resume = True):
avail_data = self._settings['repositories']['available']
excluded_data = self._settings['repositories']['excluded']
repo_db = self._entropy.open_repository(repository)
# grab original repo, if any and use it if available
# this is done in order to support "equo repo merge" feature
# allowing client-side repository package metadata moves.
original_repo = repo_db.getInstalledPackageRepository(package_id)
if (original_repo != repository) and (original_repo not in avail_data) \
and (original_repo is not None):
# build up a new uris list, at least try, hoping that
# repository is just shadowing original_repo
# for example: original_repo got copied to repository, without
# copying packages, which would be useless. like it happens
# with sabayon-weekly
uris = self.__build_uris_list(original_repo, repository)
else:
if original_repo in avail_data:
uris = avail_data[original_repo]['packages'][::-1]
if repository in avail_data:
uris += avail_data[repository]['packages'][::-1]
elif original_repo in excluded_data:
uris = excluded_data[original_repo]['packages'][::-1]
if repository in avail_data:
uris += avail_data[repository]['packages'][::-1]
else:
uris = avail_data[repository]['packages'][::-1]
remaining = set(uris)
mirror_status = StatusInterface()
mirrorcount = 0
for uri in uris:
if not remaining:
# tried all the mirrors, quitting for error
mirror_status.set_working_mirror(None)
return 3
mirror_status.set_working_mirror(uri)
mirrorcount += 1
mirror_count_txt = "( mirror #%s ) " % (mirrorcount,)
url = uri + "/" + download
# check if uri is sane
if mirror_status.get_failing_mirror_status(uri) >= 30:
# ohohoh!
# set to 30 for convenience
mirror_status.set_failing_mirror_status(uri, 30)
mytxt = mirror_count_txt
mytxt += blue(" %s: ") % (_("Mirror"),)
mytxt += red(self._get_url_name(uri))
mytxt += " - %s." % (_("maximum failure threshold reached"),)
self._entropy.output(
mytxt,
importance = 1,
level = "warning",
header = red(" ## ")
)
if mirror_status.get_failing_mirror_status(uri) == 30:
# put to 75 then decrement by 4 so we
# won't reach 30 anytime soon ahahaha
mirror_status.add_failing_mirror(uri, 45)
elif mirror_status.get_failing_mirror_status(uri) > 31:
# now decrement each time this point is reached,
# if will be back < 30, then equo will try to use it again
mirror_status.add_failing_mirror(uri, -4)
else:
# put to 0 - reenable mirror, welcome back uri!
mirror_status.set_failing_mirror_status(uri, 0)
remaining.discard(uri)
continue
do_resume = resume
timeout_try_count = 50
while True:
try:
mytxt = mirror_count_txt
mytxt += blue("%s: ") % (_("Downloading from"),)
mytxt += red(self._get_url_name(uri))
# now fetch the new one
self._entropy.output(
mytxt,
importance = 1,
level = "warning",
header = red(" ## ")
)
rc, data_transfer, resumed = \
self.__fetch_file(
url,
save_path,
download = download,
package_id = package_id,
repository = repository,
digest = digest,
resume = do_resume
)
if rc == 0:
mytxt = mirror_count_txt
mytxt += "%s: " % (
blue(_("Successfully downloaded from")),
)
mytxt += red(self._get_url_name(uri))
human_bytes = entropy.tools.bytes_into_human(
data_transfer)
mytxt += " %s %s/%s" % (_("at"),
human_bytes, _("second"),)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
mirror_status.set_working_mirror(None)
return 0
elif resumed and (rc not in (-3, -4, -100,)):
do_resume = False
continue
else:
error_message = mirror_count_txt
error_message += blue("%s: %s") % (
_("Error downloading from"),
red(self._get_url_name(uri)),
)
# something bad happened
if rc == -1:
error_message += " - %s." % (
_("file not available on this mirror"),)
elif rc == -2:
mirror_status.add_failing_mirror(uri, 1)
error_message += " - %s." % (_("wrong checksum"),)
# If file is fetched (with no resume) and its complete
# better to enforce resume to False.
if (data_transfer < 1) and do_resume:
error_message += " %s." % (
_("Disabling resume"),)
do_resume = False
continue
elif rc == -3:
mirror_status.add_failing_mirror(uri, 3)
error_message += " - %s." % (_("not found"),)
elif rc == -4: # timeout!
timeout_try_count -= 1
if timeout_try_count > 0:
error_message += " - %s." % (
_("timeout, retrying on this mirror"),)
else:
error_message += " - %s." % (
_("timeout, giving up"),)
elif rc == -100:
error_message += " - %s." % (
_("discarded download"),)
else:
mirror_status.add_failing_mirror(uri, 5)
error_message += " - %s." % (_("unknown reason"),)
self._entropy.output(
error_message,
importance = 1,
level = "warning",
header = red(" ## ")
)
if rc == -4: # timeout
if timeout_try_count > 0:
continue
elif rc == -100: # user discarded fetch
mirror_status.set_working_mirror(None)
return 1
remaining.discard(uri)
# make sure we don't have nasty issues
if not remaining:
mirror_status.set_working_mirror(None)
return 3
break
except KeyboardInterrupt:
mirror_status.set_working_mirror(None)
return 1
mirror_status.set_working_mirror(None)
return 0
def _match_checksum(self, package_id, repository, checksum, download,
signatures):
sys_settings = self._settings
sys_set_plg_id = \
etpConst['system_settings_plugins_ids']['client_plugin']
enabled_hashes = sys_settings[sys_set_plg_id]['misc']['packagehashes']
pkg_disk_path = self.__get_fetch_disk_path(download)
pkg_disk_path_mtime = pkg_disk_path + etpConst['packagemtimefileext']
def do_mtime_validation():
if not (os.path.isfile(pkg_disk_path_mtime) and \
os.access(pkg_disk_path_mtime, os.R_OK)):
return 1
if not (os.path.isfile(pkg_disk_path) and \
os.access(pkg_disk_path, os.R_OK)):
return 2
with open(pkg_disk_path_mtime, "r") as mt_f:
stored_mtime = mt_f.read().strip()
# get pkg mtime
cur_mtime = str(os.path.getmtime(pkg_disk_path))
if cur_mtime == stored_mtime:
return 0
return 1
def do_store_mtime():
if not (os.path.isfile(pkg_disk_path) and \
os.access(pkg_disk_path, os.R_OK)):
return
with open(pkg_disk_path_mtime, "w") as mt_f:
cur_mtime = str(os.path.getmtime(pkg_disk_path))
mt_f.write(cur_mtime)
mt_f.flush()
def do_compare_gpg(pkg_path, hash_val):
try:
repo_sec = self._entropy.RepositorySecurity()
except RepositorySecurity.GPGServiceNotAvailable:
return None
# check if we have repository pubkey
try:
if not repo_sec.is_pubkey_available(repository):
return None
except repo_sec.KeyExpired:
# key is expired
return None
# write gpg signature to disk for verification
tmp_fd, tmp_path = tempfile.mkstemp()
with os.fdopen(tmp_fd, "w") as tmp_f:
tmp_f.write(hash_val)
tmp_f.flush()
try:
# actually verify
valid, err_msg = repo_sec.verify_file(repository, pkg_path,
tmp_path)
finally:
os.remove(tmp_path)
if valid:
return True
if err_msg:
self._entropy.output(
"%s: %s, %s" % (
darkred(_("Package signature verification error for")),
purple("GPG"),
err_msg,
),
importance = 0,
level = "error",
header = darkred(" ## ")
)
return False
signature_vry_map = {
'sha1': entropy.tools.compare_sha1,
'sha256': entropy.tools.compare_sha256,
'sha512': entropy.tools.compare_sha512,
'gpg': do_compare_gpg,
}
def do_signatures_validation(signatures):
# check signatures, if available
if isinstance(signatures, dict):
for hash_type in sorted(signatures):
hash_val = signatures[hash_type]
# NOTE: workaround bug on unreleased
# entropy versions
if hash_val in signatures:
continue
if hash_val is None:
continue
if hash_type not in enabled_hashes:
self._entropy.output(
"%s %s" % (
purple(hash_type.upper()),
darkgreen(_("disabled")),
),
importance = 0,
level = "info",
header = " : "
)
continue
cmp_func = signature_vry_map.get(hash_type)
if cmp_func is None:
continue
self._entropy.output(
"%s: %s" % (blue(_("Checking package signature")),
purple(hash_type.upper()),),
importance = 0,
level = "info",
header = red(" ## "),
back = True
)
valid = cmp_func(pkg_disk_path, hash_val)
if valid is None:
self._entropy.output(
"%s '%s' %s" % (
darkred(_("Package signature verification")),
purple(hash_type.upper()),
darkred(_("temporarily unavailable")),
),
importance = 0,
level = "warning",
header = darkred(" ## ")
)
continue
if not valid:
self._entropy.output(
"%s: %s %s" % (
darkred(_("Package signature")),
purple(hash_type.upper()),
darkred(_("does not match the recorded one")),
),
importance = 0,
level = "error",
header = darkred(" ## ")
)
return 1
self._entropy.output(
"%s %s" % (
purple(hash_type.upper()),
darkgreen(_("matches")),
),
importance = 0,
level = "info",
header = " : "
)
return 0
dlcount = 0
match = False
max_dlcount = 5
while dlcount <= max_dlcount:
self._entropy.output(
blue(_("Checking package checksum...")),
importance = 0,
level = "info",
header = red(" ## "),
back = True
)
dlcheck = self.__check_pkg_path_download(download,
checksum = checksum)
if dlcheck == 0:
basef = os.path.basename(download)
self._entropy.output(
"%s: %s" % (
blue(_("Package checksum matches")),
darkgreen(basef),
),
importance = 0,
level = "info",
header = red(" ## ")
)
# check if package has been already checked
dlcheck = do_mtime_validation()
if dlcheck != 0:
dlcheck = do_signatures_validation(signatures)
if dlcheck == 0:
do_store_mtime()
match = True
break # file downloaded successfully
if dlcheck != 0:
dlcount += 1
mytxt = _("Checksum does not match. Download attempt #%s") % (
dlcount,
)
self._entropy.output(
darkred(mytxt),
importance = 0,
level = "warning",
header = darkred(" ## ")
)
# Unfortunately, disabling resume makes possible to recover
# from bad download data. trying to resume would do more harm
# than good in the majority of cases.
fetch = self._download_package(
package_id,
repository,
download,
pkg_disk_path,
checksum,
resume = False
)
if fetch != 0:
self._entropy.output(
blue(_("Cannot properly fetch package! Quitting.")),
importance = 0,
level = "error",
header = darkred(" ## ")
)
return fetch
# package is fetched, let's loop one more time
# to make sure to run all the checksum checks
continue
if not match:
mytxt = _("Cannot fetch package or checksum does not match")
mytxt2 = _("Try to download latest repositories")
for txt in (mytxt, mytxt2,):
self._entropy.output(
"%s." % (blue(txt),),
importance = 0,
level = "info",
header = red(" ## ")
)
return 1
return 0
def multi_match_checksum(self):
rc = 0
for pkg_id, repository, download, digest, signatures in \
self.pkgmeta['multi_checksum_list']:
rc = self._match_checksum(pkg_id, repository, digest, download,
signatures)
if rc != 0:
break
if rc == 0:
self.pkgmeta['verified'] = True
return rc
def __unpack_package(self, download, package_path, image_dir, pkg_dbpath):
mytxt = "%s: %s" % (
blue(_("Unpacking")),
red(os.path.basename(download)),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Unpacking package: %s" % (download,)
)
# removed in the meantime? at least try to cope
if not os.path.isfile(package_path):
# must be fault-tolerant
if os.path.isdir(package_path):
shutil.rmtree(package_path)
if os.path.islink(package_path):
os.remove(package_path)
self.pkgmeta['verified'] = False
rc = self._fetch_step()
if rc != 0:
return rc
# pkg_dbpath is only non-None for the base package file
# extra package files don't carry any other edb information
if pkg_dbpath is not None:
# extract entropy database from package file
# in order to avoid having to read content data
# from the repository database, which, in future
# is allowed to not provide such info.
pkg_dbdir = os.path.dirname(pkg_dbpath)
if not os.path.isdir(pkg_dbdir):
os.makedirs(pkg_dbdir, 0o755)
# extract edb
entropy.tools.dump_entropy_metadata(package_path, pkg_dbpath)
unpack_tries = 3
while True:
if unpack_tries <= 0:
return 1
unpack_tries -= 1
try:
rc = entropy.tools.uncompress_tarball(
package_path,
extract_path = image_dir,
catch_empty = True
)
except EOFError as err:
self._entropy.logger.log(
"[Package]", etpConst['logging']['normal_loglevel_id'],
"EOFError on " + package_path + " " + \
repr(err)
)
entropy.tools.print_traceback()
# try again until unpack_tries goes to 0
rc = 1
except Exception as err:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Ouch! error while unpacking " + \
package_path + " " + repr(err)
)
entropy.tools.print_traceback()
# try again until unpack_tries goes to 0
rc = 1
if rc == 0:
break
# try to download it again
self.pkgmeta['verified'] = False
f_rc = self._fetch_step()
if f_rc != 0:
return f_rc
return 0
def __configure_package(self):
try:
Spm = self._entropy.Spm()
except Exception as err:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Source Package Manager not available: %s | %s" % (
type(Exception), err,
)
)
return 1
self._entropy.output(
"SPM: %s" % (brown(_("configuration phase")),),
importance = 0,
header = red(" ## ")
)
return Spm.execute_package_phase(self.pkgmeta, self.pkgmeta,
self._action, "configure")
def __remove_package(self):
self._entropy.clear_cache()
self._entropy.logger.log("[Package]",
etpConst['logging']['normal_loglevel_id'],
"Removing package: %s" % (self.pkgmeta['removeatom'],))
mytxt = "%s: %s" % (
blue(_("Removing from Entropy")),
red(self.pkgmeta['removeatom']),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
automerge_metadata = \
self._entropy.installed_repository().retrieveAutomergefiles(
self.pkgmeta['removeidpackage'], get_dict = True
)
self._entropy.installed_repository().removePackage(
self.pkgmeta['removeidpackage'], do_commit = False,
do_cleanup = False)
# commit changes, to avoid users pressing CTRL+C and still having
# all the db entries in, so we need to commit at every iteration
self._entropy.installed_repository().commit()
self._remove_content_from_system(self.pkgmeta['removeidpackage'],
automerge_metadata)
return 0
def _remove_content_from_system(self, installed_package_id,
automerge_metadata = None):
"""
Remove installed package content (files/directories) from live system.
@param installed_package_id: Entropy Repository package identifier
@type installed_package_id: int
@keyword automerge_metadata: Entropy "automerge metadata"
@type automerge_metadata: dict
"""
if automerge_metadata is None:
automerge_metadata = {}
sys_root = etpConst['systemroot']
# load CONFIG_PROTECT and CONFIG_PROTECT_MASK
sys_settings = self._settings
protect = self.__get_installed_package_config_protect(
installed_package_id)
mask = self.__get_installed_package_config_protect(installed_package_id,
mask = True)
sys_set_plg_id = \
etpConst['system_settings_plugins_ids']['client_plugin']
col_protect = sys_settings[sys_set_plg_id]['misc']['collisionprotect']
# remove files from system
directories = set()
directories_cache = set()
not_removed_due_to_collisions = set()
colliding_path_messages = set()
remove_content = sorted(self.pkgmeta['removecontent'], reverse = True)
for item in remove_content:
if not item:
continue # empty element??
sys_root_item = sys_root + item
# collision check
if col_protect > 0:
if self._entropy.installed_repository().isFileAvailable(item) \
and os.path.isfile(sys_root_item):
# in this way we filter out directories
colliding_path_messages.add(sys_root_item)
not_removed_due_to_collisions.add(item)
continue
protected = False
in_mask = False
if not self.pkgmeta['removeconfig']:
protected_item_test = sys_root_item
if const_isunicode(protected_item_test):
protected_item_test = protected_item_test.encode('utf-8')
in_mask, protected, x, do_continue = \
self._handle_config_protect(
protect, mask, None, protected_item_test,
do_allocation_check = False, do_quiet = True
)
if do_continue:
protected = True
# when files have not been modified by the user
# and they are inside a config protect directory
# we could even remove them directly
if in_mask:
oldprot_md5 = automerge_metadata.get(item)
if oldprot_md5 and os.path.exists(protected_item_test) and \
os.access(protected_item_test, os.R_OK):
in_system_md5 = entropy.tools.md5sum(
protected_item_test)
if oldprot_md5 == in_system_md5:
prot_msg = _("Removing config file, never modified")
mytxt = "%s: %s" % (
darkgreen(prot_msg),
blue(item),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
protected = False
do_continue = False
# Is file or directory a protected item?
if protected:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['verbose_loglevel_id'],
"[remove] Protecting config file: %s" % (sys_root_item,)
)
mytxt = "[%s] %s: %s" % (
red(_("remove")),
brown(_("Protecting config file")),
sys_root_item,
)
self._entropy.output(
mytxt,
importance = 1,
level = "warning",
header = red(" ## ")
)
continue
try:
os.lstat(sys_root_item)
except OSError:
continue # skip file, does not exist
except UnicodeEncodeError:
msg = _("This package contains a badly encoded file !!!")
mytxt = brown(msg)
self._entropy.output(
red("QA: ")+mytxt,
importance = 1,
level = "warning",
header = darkred(" ## ")
)
continue # file has a really bad encoding
if os.path.isdir(sys_root_item) and \
os.path.islink(sys_root_item):
# S_ISDIR returns False for directory symlinks,
# so using os.path.isdir valid directory symlink
if sys_root_item not in directories_cache:
directories.add((sys_root_item, "link"))
directories_cache.add(sys_root_item)
continue
if os.path.isdir(sys_root_item):
# plain directory
if sys_root_item not in directories_cache:
directories.add((sys_root_item, "dir"))
directories_cache.add(sys_root_item)
continue
# files, symlinks or not
# just a file or symlink or broken
# directory symlink (remove now)
try:
os.remove(sys_root_item)
except OSError as err:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"[remove] Unable to remove %s, error: %s" % (
sys_root_item, err,)
)
continue
# add its parent directory
dirobj = os.path.dirname(sys_root_item)
if dirobj not in directories_cache:
if os.path.isdir(dirobj) and os.path.islink(dirobj):
directories.add((dirobj, "link"))
elif os.path.isdir(dirobj):
directories.add((dirobj, "dir"))
directories_cache.add(dirobj)
if colliding_path_messages:
self._entropy.output(
"%s:" % (_("Collision found during removal of"),),
importance = 1,
level = "warning",
header = red(" ## ")
)
for path in sorted(colliding_path_messages):
self._entropy.output(
purple(path),
importance = 0,
level = "warning",
header = red(" ## ")
)
self._entropy.logger.log("[Package]", etpConst['logging']['normal_loglevel_id'],
"Collision found during removal of %s - cannot overwrite" % (
path,)
)
# removing files not removed from removecontent.
# it happened that boot services not removed due to
# collisions got removed from their belonging runlevels
# by postremove step.
# since this is a set, it is a mapped type, so every
# other instance around will feature this update
self.pkgmeta['removecontent'] -= not_removed_due_to_collisions
# now handle directories
directories = sorted(directories, reverse = True)
while True:
taint = False
for directory, dirtype in directories:
mydir = "%s%s" % (sys_root, directory,)
if dirtype == "link":
try:
mylist = os.listdir(mydir)
if not mylist:
try:
os.remove(mydir)
taint = True
except OSError:
pass
except OSError:
pass
elif dirtype == "dir":
try:
mylist = os.listdir(mydir)
if not mylist:
try:
os.rmdir(mydir)
taint = True
except OSError:
pass
except OSError:
pass
if not taint:
break
del directories_cache
del directories
def _cleanup_package(self, unpack_dir):
# shutil.rmtree wants raw strings... otherwise it will explode
if const_isunicode(unpack_dir):
unpack_dir = unpack_dir.encode('raw_unicode_escape')
# remove unpack dir
try:
shutil.rmtree(unpack_dir, True)
except Exception as err:
# fault tolerance here dude
self._entropy.logger.log(
"[Package]", etpConst['logging']['normal_loglevel_id'],
"WARNING!!! Failed to cleanup directory %s," \
" error: %s" % (unpack_dir, err,))
try:
os.rmdir(unpack_dir)
except OSError:
pass
return 0
def __install_package(self):
# clear on-disk cache
self._entropy.clear_cache()
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Installing package: %s" % (self.pkgmeta['atom'],)
)
if self.pkgmeta['removeidpackage'] != -1:
self.pkgmeta['already_protected_config_files'] = \
self._entropy.installed_repository().retrieveAutomergefiles(
self.pkgmeta['removeidpackage'], get_dict = True
)
# copy files over - install
# use fork? (in this case all the changed structures
# need to be pushed back)
rc = self._move_image_to_system()
if rc != 0:
return rc
# inject into database
mytxt = "%s: %s" % (
blue(_("Updating database")),
red(self.pkgmeta['atom']),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
self.pkgmeta['installed_package_id'] = self._add_installed_package()
return 0
def _spm_install_package(self, installed_package_id):
"""
Call Source Package Manager interface and tell it to register our
newly installed package.
@param installed_package_id: Entropy repository package identifier
@type installed_package_id: int
@return: execution status
@rtype: int
"""
try:
Spm = self._entropy.Spm()
except Exception as err:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Source Package Manager not available: %s | %s" % (
type(Exception), err,
)
)
return -1
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Installing new SPM entry: %s" % (self.pkgmeta['atom'],)
)
spm_uid = Spm.add_installed_package(self.pkgmeta)
if spm_uid != -1:
self._entropy.installed_repository().insertSpmUid(
installed_package_id, spm_uid)
return 0
def _spm_update_package_uid(self, installed_package_id, entropy_atom):
"""
Update Source Package Manager <-> Entropy package identifiers coupling.
Entropy can handle multiple packages in the same scope from a SPM POV
(see the "package tag" feature to provide linux kernel module packages
for different kernel versions). This method just reassigns a new SPM
unique package identifier to Entropy.
@param installed_package_id: Entropy package identifier bound to
given entropy_atom
@type installed_package_id: int
@param entropy_atom: Entropy package atom, must be converted to a valid
SPM package atom.
@type entropy_atom: string
@return: execution status
@rtype: int
"""
try:
Spm = self._entropy.Spm()
except Exception as err:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Source Package Manager not available: %s | %s" % (
type(Exception), err,
)
)
return -1
spm_package = Spm.convert_from_entropy_package_name(entropy_atom)
try:
spm_uid = Spm.assign_uid_to_installed_package(spm_package)
except (SPMError, KeyError,):
# installed package not available, we must ignore it
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Spm uid not available for Spm package: %s (pkg not avail?)" % (
spm_package,
)
)
return 0
if spm_uid != -1:
self._entropy.installed_repository().insertSpmUid(
installed_package_id, spm_uid)
return 0
def _spm_remove_package(self):
"""
Call Source Package Manager interface and tell it to remove our
just removed package.
@return: execution status
@rtype: int
"""
try:
Spm = self._entropy.Spm()
except Exception as err:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Source Package Manager not available: %s | %s" % (
type(Exception), err,
)
)
return -1
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Removing from SPM: %s" % (self.pkgmeta['removeatom'],)
)
return Spm.remove_installed_package(self.pkgmeta)
def _add_installed_package(self):
"""
For internal use only.
Copy package from repository to installed packages one.
"""
# fetch info
smart_pkg = self.pkgmeta['smartpackage']
dbconn = self._entropy.open_repository(self.pkgmeta['repository'])
if smart_pkg or self.pkgmeta['merge_from']:
data = dbconn.getPackageData(self.pkgmeta['idpackage'],
content_insert_formatted = True, get_changelog = False)
if self.pkgmeta['removeidpackage'] != -1:
self.pkgmeta['removecontent'].update(
self._entropy.installed_repository().contentDiff(
self.pkgmeta['removeidpackage'],
dbconn,
self.pkgmeta['idpackage']
)
)
else:
# normal repositories
data = dbconn.getPackageData(self.pkgmeta['idpackage'],
get_content = False, get_changelog = False)
# indexing_override = False : no need to index tables
# xcache = False : no need to use on-disk cache
# skipChecks = False : creating missing tables is unwanted,
# and also no foreign keys update
# readOnly = True: no need to open in write mode
pkg_dbconn = self._entropy.open_generic_repository(
self.pkgmeta['pkgdbpath'], skip_checks = True,
indexing_override = False, read_only = True,
xcache = False)
# it is safe to consider that package dbs coming from repos
# contain only one entry
pkg_idpackage = sorted(pkg_dbconn.listAllPackageIds(),
reverse = True)[0]
content = pkg_dbconn.retrieveContent(
pkg_idpackage, extended = True,
formatted = True, insert_formatted = True
)
real_idpk = self.pkgmeta['idpackage']
content = [(real_idpk, x, y,) for orig_idpk, x, y in content]
data['content'] = content
# setup content safety metadata, get from package
data['content_safety'] = pkg_dbconn.retrieveContentSafety(
pkg_idpackage)
if self.pkgmeta['removeidpackage'] != -1:
self.pkgmeta['removecontent'].update(
self._entropy.installed_repository().contentDiff(
self.pkgmeta['removeidpackage'],
pkg_dbconn,
pkg_idpackage
)
)
pkg_dbconn.close()
# fix removecontent, need to check if we just installed files
# that resolves at the same directory path (different symlink)
my_remove_content = self.__filter_out_files_installed_on_diff_path(
self.pkgmeta['removecontent'], self.pkgmeta['__items_installed'])
self.pkgmeta['removecontent'] -= my_remove_content
# filter out files not installed from content metadata
self.__filter_out_items_not_installed_from_content(data)
# this is needed to make postinstall trigger to work properly
trigger_content = set((x[1] for x in data['content']))
self.pkgmeta['triggers']['install']['content'] = trigger_content
# open client db
# always set data['injected'] to False
# installed packages database SHOULD never have more
# than one package for scope (key+slot)
data['injected'] = False
# spm counter will be set in self._install_package_into_spm_database()
data['counter'] = -1
# branch must be always set properly, it could happen it's not
# when installing packages through their .tbz2s
data['branch'] = self._settings['repositories']['branch']
# there is no need to store needed paths into db
if "needed_paths" in data:
del data['needed_paths']
# there is no need to store changelog data into db
if "changelog" in data:
del data['changelog']
# we don't want it to be added now, we want to add install source
# info too.
if "original_repository" in data:
del data['original_repository']
# rewrite extra_download metadata with the currently provided,
# and accepted extra_download items (in case of splitdebug being
# disable, we're not going to add those entries, for example)
data['extra_download'] = self.pkgmeta['extra_download']
inst_repo = self._entropy.installed_repository()
idpackage = inst_repo.handlePackage(data,
forcedRevision = data['revision'], formattedContent = True)
# update datecreation
ctime = time.time()
inst_repo.setCreationDate(idpackage, str(ctime))
# add idpk to the installedtable
inst_repo.dropInstalledPackageFromStore(idpackage)
inst_repo.storeInstalledPackage(idpackage,
self.pkgmeta['repository'], self.pkgmeta['install_source'])
automerge_data = self.pkgmeta.get('configprotect_data')
if automerge_data:
inst_repo.insertAutomergefiles(idpackage, automerge_data)
return idpackage
def __filter_out_items_not_installed_from_content(self, pkg_data):
items_not_installed = self.pkgmeta.get('items_not_installed', set())
# CONTENT metadata getting here is always already formatted
# for EntropyRepository insertion
def filter_content(content_item):
pkg_id, pkg_path, path_type = content_item
return pkg_path not in items_not_installed
pkg_data['content'] = filter(filter_content, pkg_data['content'])
def __fill_image_dir(self, merge_from, image_dir):
dbconn = self._entropy.open_repository(self.pkgmeta['repository'])
# this is triggered by merge_from pkgmeta metadata
# even if repositories are allowed to not have content
# metadata, in this particular case, it is mandatory
package_content = dbconn.retrieveContent(
self.pkgmeta['idpackage'], extended = True, formatted = True)
contents = sorted(package_content)
# collect files
for path in contents:
# convert back to filesystem str
encoded_path = path
path = os.path.join(merge_from, encoded_path[1:])
topath = os.path.join(image_dir, encoded_path[1:])
path = path.encode('raw_unicode_escape')
topath = topath.encode('raw_unicode_escape')
try:
exist = os.lstat(path)
except OSError:
continue # skip file
ftype = package_content[encoded_path]
if 'dir' == ftype and \
not stat.S_ISDIR(exist.st_mode) and \
os.path.isdir(path):
# workaround for directory symlink issues
path = os.path.realpath(path)
copystat = False
# if our directory is a symlink instead, then copy the symlink
if os.path.islink(path):
tolink = os.readlink(path)
if os.path.islink(topath):
os.remove(topath)
os.symlink(tolink, topath)
elif os.path.isdir(path):
if not os.path.isdir(topath):
os.makedirs(topath)
copystat = True
elif os.path.isfile(path):
if os.path.isfile(topath):
os.remove(topath) # should never happen
shutil.copy2(path, topath)
copystat = True
if copystat:
user = os.stat(path)[stat.ST_UID]
group = os.stat(path)[stat.ST_GID]
os.chown(topath, user, group)
shutil.copystat(path, topath)
del package_content, contents
def __get_package_match_config_protect(self, mask = False):
idpackage, repoid = self._package_match
dbconn = self._entropy.open_repository(repoid)
cl_id = etpConst['system_settings_plugins_ids']['client_plugin']
misc_data = self._settings[cl_id]['misc']
if mask:
config_protect = set(dbconn.retrieveProtectMask(idpackage).split())
config_protect |= set(misc_data['configprotectmask'])
else:
config_protect = set(dbconn.retrieveProtect(idpackage).split())
config_protect |= set(misc_data['configprotect'])
config_protect = [etpConst['systemroot']+x for x in config_protect]
return sorted(config_protect)
def __get_installed_package_config_protect(self, installed_package_id,
mask = False):
inst_repo = self._entropy.installed_repository()
if inst_repo is None:
return []
cl_id = etpConst['system_settings_plugins_ids']['client_plugin']
misc_data = self._settings[cl_id]['misc']
if mask:
_pmask = inst_repo.retrieveProtectMask(installed_package_id).split()
config_protect = set(_pmask)
config_protect |= set(misc_data['configprotectmask'])
else:
_protect = inst_repo.retrieveProtect(installed_package_id).split()
config_protect = set(_protect)
config_protect |= set(misc_data['configprotect'])
config_protect = [etpConst['systemroot']+x for x in config_protect]
return sorted(config_protect)
def __get_sys_root(self):
return self.pkgmeta.get('unittest_root', '') + \
etpConst['systemroot']
def _move_image_to_system(self):
# load CONFIG_PROTECT and its mask
protect = self.__get_package_match_config_protect()
mask = self.__get_package_match_config_protect(mask = True)
# support for unit testing settings
sys_root = self.__get_sys_root()
sys_set_plg_id = \
etpConst['system_settings_plugins_ids']['client_plugin']
misc_data = self._settings[sys_set_plg_id]['misc']
col_protect = misc_data['collisionprotect']
splitdebug, splitdebug_dirs = self.pkgmeta['splitdebug'], \
self.pkgmeta['splitdebug_dirs']
items_installed = set()
file_items_installed = set()
self.pkgmeta['items_not_installed'] = set()
# setup image_dir properly
image_dir = self.pkgmeta['imagedir'][:]
if sys.hexversion < 0x3000000:
image_dir = image_dir.encode('utf-8')
movefile = entropy.tools.movefile
def workout_subdir(currentdir, subdir):
imagepath_dir = os.path.join(currentdir, subdir)
rootdir = sys_root + imagepath_dir[len(image_dir):]
# splitdebug (.debug files) support
# If splitdebug is not enabled, do not create splitdebug directories
# and move on instead (return)
if not splitdebug:
for split_dir in splitdebug_dirs:
if rootdir.startswith(split_dir):
# also drop item from content metadata. In this way
# SPM has in sync information on what the package
# content really is.
self.pkgmeta['items_not_installed'].add(rootdir)
return
# handle broken symlinks
if os.path.islink(rootdir) and not os.path.exists(rootdir):
# broken symlink
os.remove(rootdir)
# if our directory is a file on the live system
elif os.path.isfile(rootdir): # really weird...!
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! %s is a file when it should be " \
"a directory !! Removing in 20 seconds..." % (rootdir,)
)
mytxt = darkred(_("%s is a file when should be a " \
"directory !! Removing in 20 seconds...") % (rootdir,))
self._entropy.output(
red("QA: ")+mytxt,
importance = 1,
level = "warning",
header = red(" !!! ")
)
os.remove(rootdir)
# if our directory is a symlink instead, then copy the symlink
if os.path.islink(imagepath_dir):
# if our live system features a directory instead of
# a symlink, we should consider removing the directory
if not os.path.islink(rootdir) and os.path.isdir(rootdir):
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! %s is a directory when it should be " \
"a symlink !! Removing in 20 seconds..." % (
rootdir,)
)
mytxt = "%s: %s" % (
_("directory expected, symlink found"),
rootdir,
)
mytxt2 = _("Removing in 20 seconds !!")
for txt in (mytxt, mytxt2,):
self._entropy.output(
darkred("QA: ") + darkred(txt),
importance = 1,
level = "warning",
header = red(" !!! ")
)
# fucking kill it in any case!
# rootdir must die! die die die die!
# /me brings chainsaw
try:
shutil.rmtree(rootdir, True)
except (shutil.Error, OSError,) as err:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! Failed to rm %s " \
"directory ! [workout_subdir/1]: %s" % (
rootdir, err,
)
)
tolink = os.readlink(imagepath_dir)
live_tolink = None
if os.path.islink(rootdir):
live_tolink = os.readlink(rootdir)
if tolink != live_tolink:
_symfail = False
if os.path.lexists(rootdir):
# at this point, it must be a file
try:
os.remove(rootdir)
except OSError as err:
_symfail = True
# must be atomic, too bad if it fails
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! Failed to remove %s " \
"file ! [workout_file/0]: %s" % (
rootdir, err,
)
)
msg = _("Cannot remove symlink")
mytxt = "%s: %s => %s" % (
purple(msg),
blue(rootdir),
repr(err),
)
self._entropy.output(
mytxt,
importance = 1,
level = "warning",
header = brown(" ## ")
)
if not _symfail:
os.symlink(tolink, rootdir)
elif not os.path.isdir(rootdir) and not \
os.access(rootdir, os.R_OK):
# directory not found, we need to create it
try:
# really force a simple mkdir first of all
os.mkdir(rootdir)
except OSError:
os.makedirs(rootdir)
if not os.path.islink(rootdir) and os.access(rootdir, os.W_OK):
# symlink doesn't need permissions, also
# until os.walk ends they might be broken
# NOTE: also, added os.access() check because
# there might be directories/files unwritable
# what to do otherwise?
user = os.stat(imagepath_dir)[stat.ST_UID]
group = os.stat(imagepath_dir)[stat.ST_GID]
os.chown(rootdir, user, group)
shutil.copystat(imagepath_dir, rootdir)
item_dir, item_base = os.path.split(rootdir)
item_dir = os.path.realpath(item_dir)
item_inst = os.path.join(item_dir, item_base)
items_installed.add(item_inst)
def workout_file(currentdir, item):
fromfile = os.path.join(currentdir, item)
tofile = sys_root + fromfile[len(image_dir):]
# splitdebug (.debug files) support
# If splitdebug is not enabled, do not create
# splitdebug directories and move on instead (return)
if not splitdebug:
for split_dir in splitdebug_dirs:
if tofile.startswith(split_dir):
# also drop item from content metadata. In this way
# SPM has in sync information on what the package
# content really is.
self.pkgmeta['items_not_installed'].add(tofile)
return 0
if col_protect > 1:
todbfile = fromfile[len(image_dir):]
myrc = self._handle_install_collision_protect(tofile,
todbfile)
if not myrc:
return 0
prot_old_tofile = tofile[len(sys_root):]
pre_tofile = tofile[:]
in_mask, protected, tofile, do_return = \
self._handle_config_protect(protect, mask, fromfile, tofile)
# collect new config automerge data
if in_mask and os.path.exists(fromfile):
try:
prot_md5 = entropy.tools.md5sum(fromfile)
self.pkgmeta['configprotect_data'].append(
(prot_old_tofile, prot_md5,))
except (IOError,) as err:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! Failed to get md5 of %s " \
"file ! [workout_file/1]: %s" % (
fromfile, err,
)
)
# check if it's really necessary to protect file
if protected:
# second task
oldprot_md5 = self.pkgmeta['already_protected_config_files'].get(
prot_old_tofile)
if oldprot_md5 and os.path.exists(pre_tofile) and \
os.access(pre_tofile, os.R_OK):
try:
in_system_md5 = entropy.tools.md5sum(pre_tofile)
except (IOError,):
# which is a clearly invalid value
in_system_md5 = "0000"
if oldprot_md5 == in_system_md5:
# we can merge it, files, even if
# contains changes have not been modified
# by the user
msg = _("Automerging config file, never modified")
mytxt = "%s: %s" % (
darkgreen(msg),
blue(pre_tofile),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
protected = False
do_return = False
tofile = pre_tofile
if do_return:
return 0
try:
from_r_path = os.path.realpath(fromfile)
except RuntimeError:
# circular symlink, fuck!
# really weird...!
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! %s is a circular symlink !!!" % (fromfile,)
)
mytxt = "%s: %s" % (
_("Circular symlink issue"),
const_convert_to_unicode(fromfile),
)
self._entropy.output(
darkred("QA: ") + darkred(mytxt),
importance = 1,
level = "warning",
header = red(" !!! ")
)
from_r_path = fromfile
try:
to_r_path = os.path.realpath(tofile)
except RuntimeError:
# circular symlink, fuck!
# really weird...!
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! %s is a circular symlink !!!" % (tofile,)
)
mytxt = "%s: %s" % (
_("Circular symlink issue"),
const_convert_to_unicode(tofile),
)
self._entropy.output(
darkred("QA: ") + darkred(mytxt),
importance = 1,
level = "warning",
header = red(" !!! ")
)
to_r_path = tofile
if from_r_path == to_r_path and os.path.islink(tofile):
# there is a serious issue here, better removing tofile,
# happened to someone.
try:
# try to cope...
os.remove(tofile)
except (OSError, IOError,) as err:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! Failed to cope to oddity of %s " \
"file ! [workout_file/2]: %s" % (
tofile, err,
)
)
# if our file is a dir on the live system
if os.path.isdir(tofile) and not os.path.islink(tofile):
# really weird...!
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! %s is a directory when it should " \
"be a file !! Removing in 20 seconds..." % (tofile,)
)
mytxt = "%s: %s" % (
_("file expected, directory found"),
const_convert_to_unicode(tofile),
)
mytxt2 = _("Removing in 20 seconds !!")
for txt in (mytxt, mytxt2,):
self._entropy.output(
darkred("QA: ") + darkred(txt),
importance = 1,
level = "warning",
header = red(" !!! ")
)
try:
shutil.rmtree(tofile, True)
except (shutil.Error, IOError,) as err:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! Failed to cope to oddity of %s " \
"file ! [workout_file/3]: %s" % (
tofile, err,
)
)
# moving file using the raw format
try:
done = movefile(fromfile, tofile, src_basedir = image_dir)
except (IOError,) as err:
# try to move forward, sometimes packages might be
# fucked up and contain broken things
if err.errno not in (errno.ENOENT, errno.EACCES,):
raise
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! Error during file move" \
" to system: %s => %s | IGNORED: %s" % (
const_convert_to_unicode(fromfile),
const_convert_to_unicode(tofile),
err,
)
)
done = True
if not done:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! Error during file move" \
" to system: %s => %s" % (fromfile, tofile,)
)
mytxt = "%s: %s => %s, %s" % (
_("File move error"),
const_convert_to_unicode(fromfile),
const_convert_to_unicode(tofile),
_("please report"),
)
self._entropy.output(
red("QA: ")+darkred(mytxt),
importance = 1,
level = "warning",
header = red(" !!! ")
)
return 4
item_dir = os.path.realpath(os.path.dirname(tofile))
item_inst = os.path.join(item_dir, os.path.basename(tofile))
items_installed.add(item_inst)
file_items_installed.add(item_inst)
if protected:
# add to disk cache
file_updates = self._entropy.PackageFileUpdates()
file_updates.add(tofile, quiet = True)
return 0
# merge data into system
for currentdir, subdirs, files in os.walk(image_dir):
# create subdirs
for subdir in subdirs:
workout_subdir(currentdir, subdir)
for item in files:
move_st = workout_file(currentdir, item)
if move_st != 0:
return move_st
# this is useful to avoid the removal of installed
# files by __remove_package just because
# there's a difference in the directory path, perhaps,
# which is not handled correctly by
# EntropyRepository.contentDiff for obvious reasons
# (think about stuff in /usr/lib and /usr/lib64,
# where the latter is just a symlink to the former)
self.pkgmeta['__items_installed'] = items_installed
if self.pkgmeta.get('removecontent'):
my_remove_content = self.__filter_out_files_installed_on_diff_path(
self.pkgmeta['removecontent'],
self.pkgmeta['__items_installed'])
self.pkgmeta['removecontent'] -= my_remove_content
return 0
def __filter_out_files_installed_on_diff_path(self, remove_content,
installed_content):
"""
Use case: if a package provided files in /lib then, a new version
of that package moved the same files under /lib64, we need to check
if both directory paths solve to the same inode and if so, add to our
set that we're going to return.
"""
sys_root = self.__get_sys_root()
my_remove_content = set()
for mypath in remove_content:
if not mypath:
continue # empty?
item_dir = os.path.dirname("%s%s" % (sys_root, mypath,))
item = os.path.join(os.path.realpath(item_dir),
os.path.basename(mypath))
if item in installed_content:
my_remove_content.add(item)
my_remove_content.add(mypath)
return my_remove_content
def _handle_config_protect(self, protect, mask, fromfile, tofile,
do_allocation_check = True, do_quiet = False):
"""
Handle configuration file protection. This method contains the logic
for determining if a file should be protected from overwrite.
"""
protected = False
do_continue = False
in_mask = False
encoded_protect = [x.encode('raw_unicode_escape') for x in protect]
if tofile in encoded_protect:
protected = True
in_mask = True
elif os.path.dirname(tofile) in encoded_protect:
protected = True
in_mask = True
else:
tofile_testdir = os.path.dirname(tofile)
old_tofile_testdir = None
while tofile_testdir != old_tofile_testdir:
if tofile_testdir in encoded_protect:
protected = True
in_mask = True
break
old_tofile_testdir = tofile_testdir
tofile_testdir = os.path.dirname(tofile_testdir)
if protected: # check if perhaps, file is masked, so unprotected
newmask = [x.encode('raw_unicode_escape') for x in mask]
if tofile in newmask:
protected = False
in_mask = False
elif os.path.dirname(tofile) in newmask:
protected = False
in_mask = False
else:
tofile_testdir = os.path.dirname(tofile)
old_tofile_testdir = None
while tofile_testdir != old_tofile_testdir:
if tofile_testdir in newmask:
protected = False
in_mask = False
break
old_tofile_testdir = tofile_testdir
tofile_testdir = os.path.dirname(tofile_testdir)
if not os.path.lexists(tofile):
protected = False # file doesn't exist
# check if it's a text file
if protected and os.path.isfile(tofile) and os.access(tofile, os.R_OK):
protected = entropy.tools.istextfile(tofile)
in_mask = protected
else:
protected = False # it's not a file
if fromfile is not None:
if protected and os.path.lexists(fromfile) and \
(not os.path.exists(fromfile)) and os.path.islink(fromfile):
# broken symlink, don't protect
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! Failed to handle file protection for: " \
"%s, broken symlink in package" % (
fromfile,
)
)
msg = _("Cannot protect broken symlink")
mytxt = "%s:" % (
purple(msg),
)
self._entropy.output(
mytxt,
importance = 1,
level = "warning",
header = brown(" ## ")
)
self._entropy.output(
fromfile,
level = "warning",
header = brown(" ## ")
)
protected = False
if not protected:
return in_mask, protected, tofile, do_continue
## ##
# file is protected #
##__________________##
sys_set_plg_id = \
etpConst['system_settings_plugins_ids']['client_plugin']
client_settings = self._settings[sys_set_plg_id]
misc_settings = client_settings['misc']
# check if protection is disabled for this element
if tofile in misc_settings['configprotectskip']:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Skipping config file installation/removal, " \
"as stated in client.conf: %s" % (tofile,)
)
if not do_quiet:
mytxt = "%s: %s" % (
_("Skipping file installation/removal"),
tofile,
)
self._entropy.output(
mytxt,
importance = 1,
level = "warning",
header = darkred(" ## ")
)
do_continue = True
return in_mask, protected, tofile, do_continue
## ##
# file is protected (2) #
##______________________##
prot_status = True
if do_allocation_check:
spm_class = self._entropy.Spm_class()
tofile, prot_status = spm_class.allocate_protected_file(fromfile,
tofile)
if not prot_status:
# a protected file with the same content
# is already in place, so not going to protect
# the same file twice
protected = False
return in_mask, protected, tofile, do_continue
## ##
# file is protected (3) #
##______________________##
oldtofile = tofile
if oldtofile.find("._cfg") != -1:
oldtofile = os.path.join(os.path.dirname(oldtofile),
os.path.basename(oldtofile)[10:])
if not do_quiet:
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Protecting config file: %s" % (oldtofile,)
)
mytxt = red("%s: %s") % (_("Protecting config file"), oldtofile,)
self._entropy.output(
mytxt,
importance = 1,
level = "warning",
header = darkred(" ## ")
)
return in_mask, protected, tofile, do_continue
def _handle_install_collision_protect(self, tofile, todbfile):
avail = self._entropy.installed_repository().isFileAvailable(
const_convert_to_unicode(todbfile), get_id = True)
if (self.pkgmeta['removeidpackage'] not in avail) and avail:
mytxt = darkred(_("Collision found during install for"))
mytxt += " %s - %s" % (
blue(tofile),
darkred(_("cannot overwrite")),
)
self._entropy.output(
red("QA: ")+mytxt,
importance = 1,
level = "warning",
header = darkred(" ## ")
)
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"WARNING!!! Collision found during install " \
"for %s - cannot overwrite" % (tofile,)
)
return False
return True
def _sources_fetch_step(self):
down_data = self.pkgmeta['download']
down_keys = list(down_data.keys())
d_cache = set()
rc = 0
key_cache = [os.path.basename(x) for x in down_keys]
for key in sorted(down_keys):
key_name = os.path.basename(key)
if key_name in d_cache:
continue
# first fine wins
keyboard_interrupt = False
for url in down_data[key]:
file_name = os.path.basename(url)
if self.pkgmeta.get('fetch_path'):
dest_file = os.path.join(self.pkgmeta['fetch_path'],
file_name)
else:
dest_file = os.path.join(self.pkgmeta['unpackdir'],
file_name)
try:
rc = self._fetch_source(url, dest_file)
except KeyboardInterrupt:
keyboard_interrupt = True
break
if rc == -100:
keyboard_interrupt = True
break
if rc == 0:
d_cache.add(key_name)
break
if keyboard_interrupt:
rc = 1
break
key_cache.remove(key_name)
if rc != 0 and key_name not in key_cache:
break
rc = 0
return rc
def _fetch_source(self, url, dest_file):
rc = 1
mytxt = "%s: %s" % (blue(_("Downloading")), brown(url),)
# now fetch the new one
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
rc, data_transfer, resumed = self.__fetch_file(
url,
dest_file,
digest = None,
resume = False
)
if rc == 0:
mytxt = blue("%s: ") % (_("Successfully downloaded from"),)
mytxt += red(self._get_url_name(url))
human_bytes = entropy.tools.bytes_into_human(data_transfer)
mytxt += " %s %s/%s" % (_("at"), human_bytes, _("second"),)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
self._entropy.output(
"%s: %s" % (blue(_("Local path")), brown(dest_file),),
importance = 1,
level = "info",
header = red(" # ")
)
else:
error_message = blue("%s: %s") % (
_("Error downloading from"),
red(self._get_url_name(url)),
)
# something bad happened
if rc == -1:
error_message += " - %s." % (
_("file not available on this mirror"),
)
elif rc == -3:
error_message += " - not found."
elif rc == -100:
error_message += " - %s." % (_("discarded download"),)
else:
error_message += " - %s: %s" % (_("unknown reason"), rc,)
self._entropy.output(
error_message,
importance = 1,
level = "warning",
header = red(" ## ")
)
return rc
def _fetch_step(self):
if self.pkgmeta['verified']:
return 0
def _fetch(download, checksum):
mytxt = "%s: %s" % (blue(_("Downloading")),
red(os.path.basename(download)),)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
pkg_disk_path = self.__get_fetch_disk_path(download)
return self._download_package(
self.pkgmeta['idpackage'],
self.pkgmeta['repository'],
download,
pkg_disk_path,
checksum
)
rc = _fetch(self.pkgmeta['download'], self.pkgmeta['checksum'])
if rc == 0:
# go ahead with extra_download
for extra_download in self.pkgmeta['extra_download']:
rc = _fetch(extra_download['download'],
extra_download['md5'])
if rc != 0:
break
if rc == 0:
return 0
mytxt = "%s. %s: %s" % (
red(_("Package cannot be fetched. Try to update repositories")),
blue(_("Error")),
rc,
)
self._entropy.output(
mytxt,
importance = 1,
level = "error",
header = darkred(" ## ")
)
return rc
def _multi_fetch_step(self):
m_fetch_len = len(self.pkgmeta['multi_fetch_list'])
mytxt = "%s: %s %s" % (
blue(_("Downloading")),
darkred(str(m_fetch_len)),
_("archives"),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
rc, err_list = self._download_packages(
self.pkgmeta['multi_fetch_list'],
self.pkgmeta['checksum']
)
if rc == 0:
return 0
mytxt = _("Some packages cannot be fetched")
mytxt2 = _("Try to update your repositories and retry")
mytxt3 = "%s: %s" % (brown(_("Error")), bold(str(rc)),)
for txt in (mytxt, mytxt2,):
self._entropy.output(
"%s." % (darkred(txt),),
importance = 0,
level = "info",
header = red(" ## ")
)
self._entropy.output(
mytxt3,
importance = 0,
level = "info",
header = red(" ## ")
)
for pkg_id, repo, fname, cksum, signatures in err_list:
self._entropy.output(
"[%s|%s] %s" % (blue(repo), darkgreen(cksum), darkred(fname),),
importance = 1,
level = "error",
header = darkred(" # ")
)
return rc
def _fetch_not_available_step(self):
self._entropy.output(
blue(_("Package cannot be downloaded, unknown error.")),
importance = 1,
level = "info",
header = red(" ## ")
)
return 0
def _vanished_step(self):
self._entropy.output(
blue(_("Installed package in queue vanished, skipping.")),
importance = 1,
level = "info",
header = red(" ## ")
)
return 0
def _checksum_step(self):
base_pkg_rc = self._match_checksum(self.pkgmeta['idpackage'],
self.pkgmeta['repository'], self.pkgmeta['checksum'],
self.pkgmeta['download'], self.pkgmeta['signatures'])
if base_pkg_rc != 0:
return base_pkg_rc
# now go with extra_download
for extra_download in self.pkgmeta['extra_download']:
download = extra_download['download']
checksum = extra_download['md5']
signatures = {
'sha1': extra_download['sha1'],
'sha256': extra_download['sha256'],
'sha512': extra_download['sha512'],
'gpg': extra_download['gpg'],
}
extra_rc = self._match_checksum(self.pkgmeta['idpackage'],
self.pkgmeta['repository'], checksum, download, signatures)
if extra_rc != 0:
return extra_rc
self.pkgmeta['verified'] = True
return 0
def _multi_checksum_step(self):
return self.multi_match_checksum()
def _merge_from_unpack_step(self):
mytxt = "%s: %s" % (
blue(_("Merging package")),
red(os.path.basename(self.pkgmeta['atom'])),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Merging package: %s" % (self.pkgmeta['atom'],)
)
self.__fill_image_dir(self.pkgmeta['merge_from'],
self.pkgmeta['imagedir'])
spm_class = self._entropy.Spm_class()
# call Spm unpack hook
return spm_class.entropy_install_unpack_hook(self._entropy,
self.pkgmeta)
def _unpack_step(self):
unpack_dir = self.pkgmeta['unpackdir']
unpack_dir_raw = unpack_dir.encode('raw_unicode_escape')
if os.path.isdir(unpack_dir):
shutil.rmtree(unpack_dir)
elif os.path.isfile(unpack_dir):
os.remove(unpack_dir)
os.makedirs(unpack_dir)
rc = self.__unpack_package(self.pkgmeta['download'],
self.pkgmeta['pkgpath'], self.pkgmeta['imagedir'],
self.pkgmeta['pkgdbpath'])
if rc == 0:
for extra_download in self.pkgmeta['extra_download']:
download = extra_download['download']
pkgpath = self.__get_fetch_disk_path(download)
rc = self.__unpack_package(download, pkgpath,
self.pkgmeta['imagedir'], None)
if rc != 0:
break
if rc != 0:
if rc == 512:
errormsg = "%s. %s. %s: 512" % (
red(_("You are running out of disk space")),
red(_("I bet, you're probably Michele")),
blue(_("Error")),
)
else:
msg = _("An error occured while trying to unpack the package")
errormsg = "%s. %s. %s: %s" % (
red(msg),
red(_("Check if your system is healthy")),
blue(_("Error")),
rc,
)
self._entropy.output(
errormsg,
importance = 1,
level = "error",
header = red(" ## ")
)
return rc
spm_class = self._entropy.Spm_class()
# call Spm unpack hook
return spm_class.entropy_install_unpack_hook(self._entropy,
self.pkgmeta)
def _install_step(self):
mytxt = "%s: %s" % (
blue(_("Installing package")),
red(self.pkgmeta['atom']),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
if self.pkgmeta.get('description'):
mytxt = "[%s]" % (purple(self.pkgmeta.get('description')),)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
if self.pkgmeta['splitdebug']:
if self.pkgmeta.get('splitdebug_pkgfile'):
mytxt = "[%s]" % (
teal(_("unsupported splitdebug usage (package files)")),)
level = "warning"
else:
mytxt = "[%s]" % (
teal(_("<3 debug files installation enabled <3")),)
level = "info"
self._entropy.output(
mytxt,
importance = 1,
level = level,
header = red(" ## ")
)
rc = self.__install_package()
if rc != 0:
mytxt = "%s. %s. %s: %s" % (
red(_("An error occured while trying to install the package")),
red(_("Check if your system is healthy")),
blue(_("Error")),
rc,
)
self._entropy.output(
mytxt,
importance = 1,
level = "error",
header = red(" ## ")
)
return rc
def _remove_step(self):
mytxt = "%s: %s" % (
blue(_("Removing data")),
red(self.pkgmeta['removeatom']),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
rc = self.__remove_package()
if rc != 0:
mytxt = "%s. %s. %s: %s" % (
red(_("An error occured while trying to remove the package")),
red(_("Check if you have enough disk space on your hard disk")),
blue(_("Error")),
rc,
)
self._entropy.output(
mytxt,
importance = 1,
level = "error",
header = red(" ## ")
)
return rc
def _cleanup_step(self):
mytxt = "%s: %s" % (
blue(_("Cleaning")),
red(self.pkgmeta['atom']),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
self._cleanup_package(self.pkgmeta['unpackdir'])
# we don't care if cleanupPackage fails since it's not critical
return 0
def _post_install_step(self):
pkgdata = self.pkgmeta['triggers'].get('install')
action_data = self.pkgmeta['triggers'].get('install')
if pkgdata:
trigger = self._entropy.Triggers(self._action, 'postinstall',
pkgdata, action_data)
do = trigger.prepare()
if do:
trigger.run()
trigger.kill()
del pkgdata
return 0
def _pre_install_step(self):
pkgdata = self.pkgmeta['triggers'].get('install')
action_data = self.pkgmeta['triggers'].get('install')
if pkgdata:
trigger = self._entropy.Triggers(self._action, 'preinstall',
pkgdata, action_data)
do = trigger.prepare()
if do:
trigger.run()
trigger.kill()
del pkgdata
return 0
def _setup_step(self):
pkgdata = self.pkgmeta['triggers'].get('install')
action_data = self.pkgmeta['triggers'].get('install')
if pkgdata:
trigger = self._entropy.Triggers(self._action, 'setup',
pkgdata, action_data)
do = trigger.prepare()
if do:
trigger.run()
trigger.kill()
del pkgdata
# NOTE: fixup permissions in the image directory
# the setup phase could have created additional users and groups
package_path = self.pkgmeta['pkgpath']
prefix_dir = self.pkgmeta['imagedir']
entropy.tools.apply_tarball_ownership(package_path, prefix_dir)
return 0
def _pre_remove_step(self):
remdata = self.pkgmeta['triggers'].get('remove')
action_data = self.pkgmeta['triggers'].get('install')
if remdata:
trigger = self._entropy.Triggers(self._action, 'preremove', remdata,
action_data)
do = trigger.prepare()
if do:
trigger.run()
trigger.kill()
del remdata
return 0
def _package_install_clean(self):
"""
Cleanup package files not used anymore by newly installed version.
This is part of the atomic install, which overwrites the live fs with
new files and removes old afterwards.
"""
self._entropy.output(
blue(_("Cleaning previously installed application data.")),
importance = 1,
level = "info",
header = red(" ## ")
)
self._remove_content_from_system(self.pkgmeta['removeidpackage'],
automerge_metadata = self.pkgmeta['already_protected_config_files'])
return 0
def _post_remove_step_install(self):
"""
Post-remove phase of package install, this step removes older SPM
package entries from SPM db.
"""
self._entropy.logger.log(
"[Package]",
etpConst['logging']['normal_loglevel_id'],
"Remove old package (spm data): %s" % (self.pkgmeta['removeatom'],)
)
return self._spm_remove_package()
def _post_remove_step_remove(self):
"""
Post-remove phase of package remove action, this step removes SPM
package entries if there are no other Entropy-tagged packages installed.
"""
# remove pkg
# -- now it's possible to remove SPM package entry.
# if another package with the same atom is installed in
# Entropy db, do not call SPM at all because it would cause
# to get that package removed from there resulting in matching
# inconsistencies.
# -- of course, we need to drop versiontag before being able to look
# for other pkgs with same atom but different tag (which is an
# entropy-only metadatum)
test_atom = entropy.dep.remove_tag(self.pkgmeta['removeatom'])
others_installed = self._entropy.installed_repository().getPackageIds(
test_atom)
# It's obvious that clientdb cannot have more than one idpackage
# featuring the same "atom" value, but still, let's be fault-tolerant.
spm_rc = 0
if not others_installed:
spm_rc = self._spm_remove_package()
for other_installed in others_installed:
# we have one installed, we need to update SPM uid
spm_rc = self._spm_update_package_uid(other_installed,
self.pkgmeta['removeatom'])
if spm_rc != 0:
break # ohi ohi ohi
return spm_rc
def _post_remove_step(self):
remdata = self.pkgmeta['triggers'].get('remove')
action_data = self.pkgmeta['triggers'].get('install')
if remdata:
trigger = self._entropy.Triggers(self._action, 'postremove',
remdata, action_data)
do = trigger.prepare()
if do:
trigger.run()
trigger.kill()
del remdata
return 0
def _removeconflict_step(self):
inst_repo = self._entropy.installed_repository()
confl_package_ids = [x for x in self.pkgmeta['conflicts'] if \
inst_repo.isPackageIdAvailable(x)]
if not confl_package_ids:
return 0
# calculate removal dependencies
# system_packages must be False because we should not exclude
# them from the dependency tree in any case. Also, we cannot trigger
# DependenciesNotRemovable() exception, too.
proposed_pkg_ids = self._entropy.get_removal_queue(confl_package_ids,
system_packages = False)
# we don't want to remove the whole inverse dependencies of course,
# but just the conflicting ones, in a proper order
package_ids = [x for x in proposed_pkg_ids if x in confl_package_ids]
# make sure that every package is listed in package_ids before
# proceeding, cannot keep packages behind anyway, and must be fault
# tolerant. Besides, having missing packages here should never happen.
package_ids += [x for x in confl_package_ids if x not in \
package_ids]
for package_id in package_ids:
pkg = self._entropy.Package()
pkg.prepare((package_id,), "remove_conflict",
self.pkgmeta['remove_metaopts'])
rc = pkg.run(xterm_header = self._xterm_title)
pkg.kill()
if rc != 0:
return rc
return 0
def _config_step(self):
mytxt = "%s: %s" % (
blue(_("Configuring package")),
red(self.pkgmeta['atom']),
)
self._entropy.output(
mytxt,
importance = 1,
level = "info",
header = red(" ## ")
)
conf_rc = self.__configure_package()
if conf_rc == 1:
mytxt = _("An error occured while trying to configure the package")
mytxt2 = "%s. %s: %s" % (
red(_("Make sure that your system is healthy")),
blue(_("Error")),
conf_rc,
)
self._entropy.output(
darkred(mytxt),
importance = 1,
level = "error",
header = red(" ## ")
)
self._entropy.output(
mytxt2,
importance = 1,
level = "error",
header = red(" ## ")
)
elif conf_rc == 2:
mytxt = _("An error occured while trying to configure the package")
mytxt2 = "%s. %s: %s" % (
red(_("It seems that Source Package Manager entry is missing")),
blue(_("Error")),
conf_rc,
)
self._entropy.output(
darkred(mytxt),
importance = 1,
level = "error",
header = red(" ## ")
)
self._entropy.output(
mytxt2,
importance = 1,
level = "error",
header = red(" ## ")
)
return conf_rc
def _stepper(self, xterm_header):
if xterm_header is None:
xterm_header = ""
if 'remove_installed_vanished' in self.pkgmeta:
self._xterm_title += ' %s' % (_("Installed package vanished"),)
self._entropy.set_title(self._xterm_title)
rc = self._vanished_step()
return rc
if 'fetch_not_available' in self.pkgmeta:
self._xterm_title += ' %s' % (_("Fetch not available"),)
self._entropy.set_title(self._xterm_title)
rc = self._fetch_not_available_step()
return rc
def do_fetch():
self._xterm_title += ' %s: %s' % (
_("Fetching"),
os.path.basename(self.pkgmeta['download']),
)
self._entropy.set_title(self._xterm_title)
return self._fetch_step()
def do_multi_fetch():
self._xterm_title += ' %s: %s %s' % (_("Multi Fetching"),
len(self.pkgmeta['multi_fetch_list']) / 2, _("packages"),)
self._entropy.set_title(self._xterm_title)
return self._multi_fetch_step()
def do_sources_fetch():
self._xterm_title += ' %s: %s' % (
_("Fetching sources"),
os.path.basename(self.pkgmeta['atom']),)
self._entropy.set_title(self._xterm_title)
return self._sources_fetch_step()
def do_checksum():
self._xterm_title += ' %s: %s' % (_("Verifying"),
os.path.basename(self.pkgmeta['download']),)
self._entropy.set_title(self._xterm_title)
return self._checksum_step()
def do_multi_checksum():
self._xterm_title += ' %s: %s %s' % (_("Multi Verification"),
len(self.pkgmeta['multi_checksum_list']), _("packages"),)
self._entropy.set_title(self._xterm_title)
return self._multi_checksum_step()
def do_unpack():
if self.pkgmeta['merge_from']:
mytxt = _("Merging")
self._xterm_title += ' %s: %s' % (
mytxt,
os.path.basename(self.pkgmeta['atom']),
)
self._entropy.set_title(self._xterm_title)
return self._merge_from_unpack_step()
mytxt = _("Unpacking")
self._xterm_title += ' %s: %s' % (
mytxt,
os.path.basename(self.pkgmeta['download']),
)
self._entropy.set_title(self._xterm_title)
return self._unpack_step()
def do_remove_conflicts():
return self._removeconflict_step()
def do_install():
self._xterm_title += ' %s: %s' % (
_("Installing"),
self.pkgmeta['atom'],
)
self._entropy.set_title(self._xterm_title)
return self._install_step()
def do_install_clean():
return self._package_install_clean()
def do_install_spm():
return self._spm_install_package(
self.pkgmeta['installed_package_id'])
def do_remove():
self._xterm_title += ' %s: %s' % (
_("Removing"),
self.pkgmeta['removeatom'],
)
self._entropy.set_title(self._xterm_title)
return self._remove_step()
def do_cleanup():
self._xterm_title += ' %s: %s' % (
_("Cleaning"),
self.pkgmeta['atom'],
)
self._entropy.set_title(self._xterm_title)
return self._cleanup_step()
def do_postinstall():
self._xterm_title += ' %s: %s' % (
_("Postinstall"),
self.pkgmeta['atom'],
)
self._entropy.set_title(self._xterm_title)
return self._post_install_step()
def do_setup():
self._xterm_title += ' %s: %s' % (
_("Setup"),
self.pkgmeta['atom'],
)
self._entropy.set_title(self._xterm_title)
return self._setup_step()
def do_preinstall():
self._xterm_title += ' %s: %s' % (
_("Preinstall"),
self.pkgmeta['atom'],
)
self._entropy.set_title(self._xterm_title)
return self._pre_install_step()
def do_preremove():
self._xterm_title += ' %s: %s' % (
_("Preremove"),
self.pkgmeta['removeatom'],
)
self._entropy.set_title(self._xterm_title)
return self._pre_remove_step()
def do_postremove():
self._xterm_title += ' %s: %s' % (
_("Postremove"),
self.pkgmeta['removeatom'],
)
self._entropy.set_title(self._xterm_title)
return self._post_remove_step()
def do_postremove_install():
return self._post_remove_step_install()
def do_postremove_remove():
return self._post_remove_step_remove()
def do_config():
self._xterm_title += ' %s: %s' % (
_("Configuring"),
self.pkgmeta['atom'],
)
self._entropy.set_title(self._xterm_title)
return self._config_step()
steps_data = {
"fetch": do_fetch,
"multi_fetch": do_multi_fetch,
"multi_checksum": do_multi_checksum,
"sources_fetch": do_sources_fetch,
"checksum": do_checksum,
"unpack": do_unpack,
"remove_conflicts": do_remove_conflicts,
"install": do_install,
"install_spm": do_install_spm,
"install_clean": do_install_clean,
"remove": do_remove,
"cleanup": do_cleanup,
"postinstall": do_postinstall,
"setup": do_setup,
"preinstall": do_preinstall,
"postremove": do_postremove,
"postremove_install": do_postremove_install,
"postremove_remove": do_postremove_remove,
"preremove": do_preremove,
"config": do_config,
}
rc = 0
for step in self.pkgmeta['steps']:
self._xterm_title = xterm_header
rc = steps_data.get(step)()
if rc != 0:
break
return rc
def run(self, xterm_header = None):
self._error_on_not_prepared()
gave_up = self._entropy.wait_resources()
if gave_up:
return 20
locked = self._entropy.another_entropy_running()
if locked:
self._entropy.output(
red(_("Another Entropy is currently running.")),
importance = 1,
level = "error",
header = darkred(" @@ ")
)
return 21
# lock
acquired = self._entropy.lock_resources()
if not acquired:
self._entropy.output(
blue(_("Cannot acquire Entropy resources lock.")),
importance = 2,
level = "error",
header = darkred(" ## ")
)
return 4 # app locked during lock acquire
try:
rc = self._stepper(xterm_header)
finally:
self._entropy.unlock_resources()
if rc != 0:
self._entropy.output(
blue(_("An error occured. Action aborted.")),
importance = 2,
level = "error",
header = darkred(" ## ")
)
return rc
def prepare(self, package_match, action, metaopts = None):
self._error_on_prepared()
self._check_action_validity(action)
self._action = action
self._package_match = package_match
if metaopts is None:
metaopts = {}
self.metaopts = metaopts
# generate metadata dictionary
self._generate_metadata()
def __get_base_metadata(self, action):
def get_splitdebug_data():
sys_set_plg_id = \
etpConst['system_settings_plugins_ids']['client_plugin']
misc_data = self._settings[sys_set_plg_id]['misc']
splitdebug = misc_data['splitdebug']
splitdebug_dirs = misc_data['splitdebug_dirs']
return splitdebug, splitdebug_dirs
splitdebug, splitdebug_dirs = get_splitdebug_data()
metadata = {
'splitdebug': splitdebug,
'splitdebug_dirs': splitdebug_dirs,
}
return metadata
def _generate_metadata(self):
self._error_on_prepared()
self._check_action_validity(self._action)
self.pkgmeta.clear()
self.pkgmeta.update(self.__get_base_metadata(self._action))
if self._action == "fetch":
self.__generate_fetch_metadata()
elif self._action == "multi_fetch":
self.__generate_multi_fetch_metadata()
elif self._action in ("remove", "remove_conflict"):
self.__generate_remove_metadata()
elif self._action == "install":
self.__generate_install_metadata()
elif self._action == "source":
self.__generate_fetch_metadata(sources = True)
elif self._action == "config":
self.__generate_config_metadata()
self.__prepared = True
def __generate_remove_metadata(self):
idpackage = self._package_match[0]
if not self._entropy.installed_repository().isPackageIdAvailable(
idpackage):
self.pkgmeta['remove_installed_vanished'] = True
return 0
self.pkgmeta['idpackage'] = idpackage
self.pkgmeta['removeidpackage'] = idpackage
self.pkgmeta['configprotect_data'] = []
self.pkgmeta['triggers'] = {}
self.pkgmeta['removeatom'] = \
self._entropy.installed_repository().retrieveAtom(idpackage)
self.pkgmeta['slot'] = \
self._entropy.installed_repository().retrieveSlot(idpackage)
self.pkgmeta['versiontag'] = \
self._entropy.installed_repository().retrieveTag(idpackage)
self.pkgmeta['diffremoval'] = False
remove_config = False
if 'removeconfig' in self.metaopts:
remove_config = self.metaopts.get('removeconfig')
self.pkgmeta['removeconfig'] = remove_config
self.pkgmeta['removecontent'] = \
self._entropy.installed_repository().retrieveContent(idpackage)
self.pkgmeta['triggers']['remove'] = \
self._entropy.installed_repository().getTriggerData(idpackage)
if self.pkgmeta['triggers']['remove'] is None:
self.pkgmeta['remove_installed_vanished'] = True
return 0
self.pkgmeta['triggers']['remove']['removecontent'] = \
self.pkgmeta['removecontent']
self.pkgmeta['triggers']['remove']['spm_repository'] = \
self._entropy.installed_repository().retrieveSpmRepository(
idpackage)
self.pkgmeta['triggers']['remove'].update(
self.__get_base_metadata(self._action))
pkg_license = self._entropy.installed_repository().retrieveLicense(
idpackage)
if pkg_license is None:
pkg_license = set()
else:
pkg_license = set(pkg_license.split())
self.pkgmeta['triggers']['remove']['accept_license'] = pkg_license
self.pkgmeta['steps'] = ["preremove", "remove", "postremove",
"postremove_remove"]
return 0
def __generate_config_metadata(self):
idpackage = self._package_match[0]
self.pkgmeta['atom'] = \
self._entropy.installed_repository().retrieveAtom(idpackage)
key, slot = self._entropy.installed_repository().retrieveKeySlot(
idpackage)
self.pkgmeta['key'], self.pkgmeta['slot'] = key, slot
self.pkgmeta['version'] = \
self._entropy.installed_repository().retrieveVersion(idpackage)
self.pkgmeta['category'] = \
self._entropy.installed_repository().retrieveCategory(idpackage)
self.pkgmeta['name'] = \
self._entropy.installed_repository().retrieveName(idpackage)
self.pkgmeta['spm_repository'] = \
self._entropy.installed_repository().retrieveSpmRepository(
idpackage)
pkg_license = self._entropy.installed_repository().retrieveLicense(
idpackage)
if pkg_license is None:
pkg_license = set()
else:
pkg_license = set(pkg_license.split())
self.pkgmeta['accept_license'] = pkg_license
self.pkgmeta['steps'] = []
self.pkgmeta['steps'].append("config")
return 0
def __get_match_conflicts(self, match):
m_id, m_repo = match
dbconn = self._entropy.open_repository(m_repo)
conflicts = dbconn.retrieveConflicts(m_id)
found_conflicts = set()
inst_repo = self._entropy.installed_repository()
for conflict in conflicts:
my_m_id, my_m_rc = inst_repo.atomMatch(conflict)
if my_m_id != -1:
# check if the package shares the same slot
match_data = dbconn.retrieveKeySlot(m_id)
installed_match_data = inst_repo.retrieveKeySlot(my_m_id)
if match_data != installed_match_data:
found_conflicts.add(my_m_id)
return found_conflicts
def __setup_package_to_remove(self, package_key, slot):
inst_repo = self._entropy.installed_repository()
inst_idpackage, inst_rc = inst_repo.atomMatch(package_key,
matchSlot = slot)
if inst_idpackage != -1:
avail = inst_repo.isPackageIdAvailable(inst_idpackage)
if avail:
inst_atom = inst_repo.retrieveAtom(inst_idpackage)
self.pkgmeta['removeatom'] = inst_atom
else:
inst_idpackage = -1
return inst_idpackage
def __generate_install_metadata(self):
idpackage, repository = self._package_match
inst_repo = self._entropy.installed_repository()
self.pkgmeta['idpackage'] = idpackage
self.pkgmeta['repository'] = repository
cl_id = etpConst['system_settings_plugins_ids']['client_plugin']
edelta_support = self._settings[cl_id]['misc']['edelta_support']
self.pkgmeta['edelta_support'] = edelta_support
is_package_repo = repository.endswith(etpConst['packagesext'])
# fetch abort function
self.pkgmeta['fetch_abort_function'] = \
self.metaopts.get('fetch_abort_function')
install_source = etpConst['install_sources']['unknown']
meta_inst_source = self.metaopts.get('install_source', install_source)
if meta_inst_source in list(etpConst['install_sources'].values()):
install_source = meta_inst_source
self.pkgmeta['install_source'] = install_source
self.pkgmeta['already_protected_config_files'] = {}
self.pkgmeta['configprotect_data'] = []
dbconn = self._entropy.open_repository(repository)
self.pkgmeta['triggers'] = {}
self.pkgmeta['atom'] = dbconn.retrieveAtom(idpackage)
self.pkgmeta['slot'] = dbconn.retrieveSlot(idpackage)
ver, tag, rev = dbconn.getVersioningData(idpackage)
self.pkgmeta['version'] = ver
self.pkgmeta['versiontag'] = tag
self.pkgmeta['revision'] = rev
self.pkgmeta['extra_download'] = []
self.pkgmeta['splitdebug_pkgfile'] = True
if not is_package_repo:
self.pkgmeta['splitdebug_pkgfile'] = False
extra_download = dbconn.retrieveExtraDownload(idpackage)
if not self.pkgmeta['splitdebug']:
extra_download = [x for x in extra_download if \
x['type'] != "debug"]
self.pkgmeta['extra_download'] += extra_download
self.pkgmeta['category'] = dbconn.retrieveCategory(idpackage)
self.pkgmeta['download'] = dbconn.retrieveDownloadURL(idpackage)
self.pkgmeta['name'] = dbconn.retrieveName(idpackage)
self.pkgmeta['checksum'] = dbconn.retrieveDigest(idpackage)
sha1, sha256, sha512, gpg = dbconn.retrieveSignatures(idpackage)
signatures = {
'sha1': sha1,
'sha256': sha256,
'sha512': sha512,
'gpg': gpg,
}
self.pkgmeta['signatures'] = signatures
self.pkgmeta['conflicts'] = self.__get_match_conflicts(
self._package_match)
description = dbconn.retrieveDescription(idpackage)
if description:
if len(description) > 74:
description = description[:74].strip()
description += "..."
self.pkgmeta['description'] = description
# this is set by __install_package() and required by spm_install
# phase
self.pkgmeta['installed_package_id'] = None
# fill action queue
self.pkgmeta['removeidpackage'] = -1
remove_config = False
if 'removeconfig' in self.metaopts:
remove_config = self.metaopts.get('removeconfig')
self.pkgmeta['remove_metaopts'] = {
'removeconfig': True,
}
if 'remove_metaopts' in self.metaopts:
self.pkgmeta['remove_metaopts'] = \
self.metaopts.get('remove_metaopts')
self.pkgmeta['merge_from'] = None
mf = self.metaopts.get('merge_from')
if mf != None:
self.pkgmeta['merge_from'] = const_convert_to_unicode(mf)
self.pkgmeta['removeconfig'] = remove_config
self.pkgmeta['removeidpackage'] = self.__setup_package_to_remove(
entropy.dep.dep_getkey(self.pkgmeta['atom']), self.pkgmeta['slot'])
# filled later...
self.pkgmeta['removecontent'] = set()
# smartpackage ?
self.pkgmeta['smartpackage'] = False
# set unpack dir and image dir
if is_package_repo:
try:
compiled_arch = dbconn.getSetting("arch")
arch_fine = compiled_arch == etpConst['currentarch']
except KeyError:
arch_fine = True # sorry, old db, cannot check
if not arch_fine:
self.__prepared = False
return -1
repo_data = self._settings['repositories']
repo_meta = repo_data['available'][self.pkgmeta['repository']]
self.pkgmeta['smartpackage'] = repo_meta['smartpackage']
self.pkgmeta['pkgpath'] = repo_meta['pkgpath']
else:
self.pkgmeta['pkgpath'] = self.__get_fetch_disk_path(
self.pkgmeta['download'])
self.pkgmeta['unpackdir'] = etpConst['entropyunpackdir'] + \
os.path.sep + self.__escape_path(self.pkgmeta['download'])
self.pkgmeta['imagedir'] = self.pkgmeta['unpackdir'] + os.path.sep + \
etpConst['entropyimagerelativepath']
self.pkgmeta['pkgdbpath'] = os.path.join(self.pkgmeta['unpackdir'],
"edb/pkg.db")
# compare both versions and if they match, disable removeidpackage
if self.pkgmeta['removeidpackage'] != -1:
trigger_data = inst_repo.getTriggerData(
self.pkgmeta['removeidpackage'])
if trigger_data is None:
# installed repository entry is corrupted
self.pkgmeta['removeidpackage'] = -1
else:
# differential remove list
self.pkgmeta['diffremoval'] = True
self.pkgmeta['removeatom'] = inst_repo.retrieveAtom(
self.pkgmeta['removeidpackage'])
self.pkgmeta['triggers']['remove'] = trigger_data
# pass reference, not copy! nevva!
self.pkgmeta['triggers']['remove']['removecontent'] = \
self.pkgmeta['removecontent']
self.pkgmeta['triggers']['remove']['spm_repository'] = \
inst_repo.retrieveSpmRepository(idpackage)
self.pkgmeta['triggers']['remove'].update(
self.__get_base_metadata(self._action))
pkg_rm_license = inst_repo.retrieveLicense(
self.pkgmeta['removeidpackage'])
if pkg_rm_license is None:
pkg_rm_license = set()
else:
pkg_rm_license = set(pkg_rm_license.split())
self.pkgmeta['triggers']['remove']['accept_license'] = \
pkg_rm_license
# set steps
self.pkgmeta['steps'] = []
if self.pkgmeta['conflicts']:
self.pkgmeta['steps'].append("remove_conflicts")
# install
self.pkgmeta['steps'].append("unpack")
# preinstall placed before preremove in order
# to respect Spm order
self.pkgmeta['steps'].append("setup")
self.pkgmeta['steps'].append("preinstall")
self.pkgmeta['steps'].append("install")
if self.pkgmeta['removeidpackage'] != -1:
self.pkgmeta['steps'].append("preremove")
self.pkgmeta['steps'].append("install_clean")
if self.pkgmeta['removeidpackage'] != -1:
self.pkgmeta['steps'].append("postremove")
self.pkgmeta['steps'].append("postremove_install")
self.pkgmeta['steps'].append("install_spm")
self.pkgmeta['steps'].append("postinstall")
self.pkgmeta['steps'].append("cleanup")
self.pkgmeta['triggers']['install'] = dbconn.getTriggerData(idpackage)
if self.pkgmeta['triggers']['install'] is None:
# wtf!?
return 1
pkg_license = dbconn.retrieveLicense(idpackage)
if pkg_license is None:
pkg_license = set()
else:
pkg_license = set(pkg_license.split())
self.pkgmeta['accept_license'] = pkg_license
self.pkgmeta['triggers']['install']['accept_license'] = pkg_license
self.pkgmeta['triggers']['install']['unpackdir'] = \
self.pkgmeta['unpackdir']
self.pkgmeta['triggers']['install']['imagedir'] = \
self.pkgmeta['imagedir']
self.pkgmeta['triggers']['install']['spm_repository'] = \
dbconn.retrieveSpmRepository(idpackage)
self.pkgmeta['triggers']['install'].update(
self.__get_base_metadata(self._action))
spm_class = self._entropy.Spm_class()
# call Spm setup hook
return spm_class.entropy_install_setup_hook(self._entropy, self.pkgmeta)
def __generate_fetch_metadata(self, sources = False):
idpackage, repository = self._package_match
dochecksum = True
# fetch abort function
self.pkgmeta['fetch_abort_function'] = \
self.metaopts.get('fetch_abort_function')
if 'dochecksum' in self.metaopts:
dochecksum = self.metaopts.get('dochecksum')
# NOTE: if you want to implement download-to-dir feature in your
# client, you've found what you were looking for.
# fetch_path is the path where data should be downloaded
# it overrides default path
if 'fetch_path' in self.metaopts:
fetch_path = self.metaopts.get('fetch_path')
if entropy.tools.is_valid_path(fetch_path):
self.pkgmeta['fetch_path'] = fetch_path
self.pkgmeta['repository'] = repository
self.pkgmeta['idpackage'] = idpackage
dbconn = self._entropy.open_repository(repository)
self.pkgmeta['atom'] = dbconn.retrieveAtom(idpackage)
self.pkgmeta['slot'] = dbconn.retrieveSlot(idpackage)
self.pkgmeta['removeidpackage'] = self.__setup_package_to_remove(
entropy.dep.dep_getkey(self.pkgmeta['atom']), self.pkgmeta['slot'])
if sources:
self.pkgmeta['edelta_support'] = False
self.pkgmeta['extra_download'] = tuple()
self.pkgmeta['download'] = dbconn.retrieveSources(idpackage,
extended = True)
else:
cl_id = etpConst['system_settings_plugins_ids']['client_plugin']
edelta_support = self._settings[cl_id]['misc']['edelta_support']
self.pkgmeta['edelta_support'] = edelta_support
self.pkgmeta['checksum'] = dbconn.retrieveDigest(idpackage)
sha1, sha256, sha512, gpg = dbconn.retrieveSignatures(idpackage)
signatures = {
'sha1': sha1,
'sha256': sha256,
'sha512': sha512,
'gpg': gpg,
}
self.pkgmeta['signatures'] = signatures
extra_download = dbconn.retrieveExtraDownload(idpackage)
if not self.pkgmeta['splitdebug']:
extra_download = [x for x in extra_download if \
x['type'] != "debug"]
self.pkgmeta['extra_download'] = extra_download
self.pkgmeta['download'] = dbconn.retrieveDownloadURL(idpackage)
# export main package download path to metadata
# this is actually used by PackageKit backend in order
# to signal downloaded package files
self.pkgmeta['pkgpath'] = self.__get_fetch_disk_path(
self.pkgmeta['download'])
if not self.pkgmeta['download']:
self.pkgmeta['fetch_not_available'] = True
return 0
self.pkgmeta['verified'] = False
self.pkgmeta['steps'] = []
if not repository.endswith(etpConst['packagesext']) and not sources:
dl_check = self.__check_pkg_path_download(self.pkgmeta['download'],
None)
dl_fetch = dl_check < 0
if not dl_fetch:
for extra_download in self.pkgmeta['extra_download']:
dl_check = self.__check_pkg_path_download(
extra_download['download'], None)
if dl_check < 0:
# dl_check checked again right below
break
if dl_check < 0:
self.pkgmeta['steps'].append("fetch")
if dochecksum:
self.pkgmeta['steps'].append("checksum")
elif sources:
self.pkgmeta['steps'].append("sources_fetch")
if sources:
# create sources destination directory
unpack_dir = os.path.join(etpConst['entropyunpackdir'],
"sources", self.pkgmeta['atom'])
self.pkgmeta['unpackdir'] = unpack_dir
if not self.pkgmeta.get('fetch_path'):
if os.path.lexists(unpack_dir):
if os.path.isfile(unpack_dir):
os.remove(unpack_dir)
elif os.path.isdir(unpack_dir):
shutil.rmtree(unpack_dir, True)
if not os.path.lexists(unpack_dir):
os.makedirs(unpack_dir, 0o755)
const_setup_perms(unpack_dir, etpConst['entropygid'],
recursion = False, uid = etpConst['uid'])
return 0
def _check_matching_size(download, size):
d_path = self.__get_fetch_disk_path(download)
if os.access(d_path, os.R_OK) and os.path.isfile(d_path):
# check size first
with open(d_path, "rb") as f:
f.seek(0, os.SEEK_END)
disk_size = f.tell()
return size == disk_size
return False
matching_size = _check_matching_size(self.pkgmeta['download'],
dbconn.retrieveSize(idpackage))
if matching_size:
for extra_download in self.pkgmeta['extra_download']:
matching_size = _check_matching_size(
extra_download['download'],
extra_download['size'])
if not matching_size:
break
# downloading binary package
# if file exists, first checksum then fetch
if matching_size:
self.pkgmeta['steps'].reverse()
return 0
def __generate_multi_fetch_metadata(self):
if not isinstance(self._package_match, list):
raise AttributeError(
"package_match must be a list of tuples, not %s" % (
type(self._package_match,)
)
)
dochecksum = True
# meta options
self.pkgmeta['fetch_abort_function'] = \
self.metaopts.get('fetch_abort_function')
if 'dochecksum' in self.metaopts:
dochecksum = self.metaopts.get('dochecksum')
self.pkgmeta['checksum'] = dochecksum
matches = self._package_match
cl_id = etpConst['system_settings_plugins_ids']['client_plugin']
edelta_support = self._settings[cl_id]['misc']['edelta_support']
self.pkgmeta['edelta_support'] = edelta_support
self.pkgmeta['matches'] = matches
self.pkgmeta['atoms'] = []
self.pkgmeta['repository_atoms'] = {}
temp_fetch_list = []
temp_checksum_list = []
temp_already_downloaded_count = 0
def _setup_download(download, size, idpackage, repository, digest,
signatures):
if dochecksum:
obj = (idpackage, repository, download, digest,
signatures)
temp_checksum_list.append(obj)
if self.__check_pkg_path_download(download, None) < 0:
obj = (idpackage, repository, download, digest, signatures)
temp_fetch_list.append(obj)
else:
down_path = self.__get_fetch_disk_path(download)
if os.path.isfile(down_path):
with open(down_path, "r") as f:
f.seek(0, os.SEEK_END)
disk_size = f.tell()
if size == disk_size:
return 1
return 0
for idpackage, repository in matches:
if repository.endswith(etpConst['packagesext']):
continue
dbconn = self._entropy.open_repository(repository)
myatom = dbconn.retrieveAtom(idpackage)
# general purpose metadata
self.pkgmeta['atoms'].append(myatom)
if repository not in self.pkgmeta['repository_atoms']:
self.pkgmeta['repository_atoms'][repository] = set()
self.pkgmeta['repository_atoms'][repository].add(myatom)
download = dbconn.retrieveDownloadURL(idpackage)
digest = dbconn.retrieveDigest(idpackage)
sha1, sha256, sha512, gpg = dbconn.retrieveSignatures(idpackage)
size = dbconn.retrieveSize(idpackage)
signatures = {
'sha1': sha1,
'sha256': sha256,
'sha512': sha512,
'gpg': gpg,
}
temp_already_downloaded_count += _setup_download(download, size,
idpackage, repository, digest, signatures)
extra_downloads = dbconn.retrieveExtraDownload(idpackage)
if not self.pkgmeta['splitdebug']:
extra_downloads = [x for x in extra_downloads if \
x['type'] != "debug"]
for extra_download in extra_downloads:
download = extra_download['download']
size = extra_download['size']
digest = extra_download['md5']
signatures = {
'sha1': extra_download['sha1'],
'sha256': extra_download['sha256'],
'sha512': extra_download['sha512'],
'gpg': extra_download['gpg'],
}
temp_already_downloaded_count += _setup_download(download,
size, idpackage, repository, digest, signatures)
self.pkgmeta['steps'] = []
self.pkgmeta['multi_fetch_list'] = temp_fetch_list
self.pkgmeta['multi_checksum_list'] = temp_checksum_list
if self.pkgmeta['multi_fetch_list']:
self.pkgmeta['steps'].append("multi_fetch")
if self.pkgmeta['multi_checksum_list']:
self.pkgmeta['steps'].append("multi_checksum")
if temp_already_downloaded_count == len(temp_checksum_list):
self.pkgmeta['steps'].reverse()
return 0