diff options
author | Michal Minar <miminar@redhat.com> | 2013-02-12 15:54:14 +0100 |
---|---|---|
committer | Michal Minar <miminar@redhat.com> | 2013-02-13 20:02:50 +0100 |
commit | 46aacac8e2aabed5582f5c7da70a5acb5b679569 (patch) | |
tree | 8190c55b05f02108620732232ef5c080d5529f35 /src/software/test/rpmcache.py | |
parent | 1d60a267af3eb94f8f158b4cf8abf3cc254611a1 (diff) | |
download | openlmi-providers-46aacac8e2aabed5582f5c7da70a5acb5b679569.tar.gz openlmi-providers-46aacac8e2aabed5582f5c7da70a5acb5b679569.tar.xz openlmi-providers-46aacac8e2aabed5582f5c7da70a5acb5b679569.zip |
initial phase of rewrite for SMASH profile
currently only a subset of Software Inventory profile is supported
* listing available packages
Diffstat (limited to 'src/software/test/rpmcache.py')
-rw-r--r-- | src/software/test/rpmcache.py | 422 |
1 files changed, 234 insertions, 188 deletions
diff --git a/src/software/test/rpmcache.py b/src/software/test/rpmcache.py index 9a0e98a..c2dde24 100644 --- a/src/software/test/rpmcache.py +++ b/src/software/test/rpmcache.py @@ -18,49 +18,59 @@ # # Authors: Radek Novacek <rnovacek@redhat.com> # Authors: Michal Minar <miminar@redhat.com> + """ Creation and manipulation utilities with rpm cache for software tests. """ + import copy import datetime import os import pickle +import random import re from collections import defaultdict from subprocess import call, check_output, CalledProcessError +from package import Package +import util + DB_BACKUP_FILE = 'lmi_software_test_cache' RE_AVAIL_PKG = re.compile( r'^(?P<name>[^\s]+)\.(?P<arch>[a-zA-Z0-9_]+)' - r'\s+(?P<epoch>([0-9]+:)?)(?P<version>[a-zA-Z0-9._+-]+)' - r'-(?P<release>[a-zA-Z0-9_.]+)\s+' - r'(?P<repository>[a-zA-Z0-9_-]+)\s*$', re.MULTILINE) + r'\s+((?P<epoch>[0-9]+):)?(?P<ver>[a-zA-Z0-9._+-]+)' + r'-(?P<rel>[a-zA-Z0-9_.]+)\s+' + r'(?P<repo>[a-zA-Z0-9_-]+)\s*$', re.MULTILINE) # this won't match the last entry, unless "package\n" is not appended # at the end of the string RE_PKG_DEPS = re.compile( r'^package:\s*(?P<name>[^\s]+)\.(?P<arch>[a-zA-Z0-9_]+)' - r'\s+(?P<epoch>([0-9]+:)?)(?P<version>[a-zA-Z0-9._+-]+)' - r'-(?P<release>[a-zA-Z0-9_.]+)\s+(?P<dep_list>.*?)' + r'\s+((?P<epoch>[0-9]+):)?(?P<ver>[a-zA-Z0-9._+-]+)' + r'-(?P<rel>[a-zA-Z0-9_.]+)\s+(?P<dep_list>.*?)' r'(?=^package|\Z)', re.MULTILINE | re.DOTALL) RE_DEPS_PROVIDERS = re.compile( r'^\s*dependency:\s*(?P<dependency>.+?)\s*' - r'(?P<providers>(^\s+provider:.+?$)+)', re.MULTILINE | re.IGNORECASE) + r'(?P<providers>(^\s+unsatisfied\s+dependency$)|(^\s+provider:.+?$)+)', + re.MULTILINE | re.IGNORECASE) +RE_DEPS_UNSATISFIED = re.compile(r'^\s+unsatisfied\s+dep.*$', re.IGNORECASE) RE_PROVIDER = re.compile( r'^\s+provider:\s*(?P<name>[^\s]+)\.(?P<arch>[a-zA-Z0-9_]+)' - r'\s+(?P<epoch>([0-9]+:)?)(?P<version>[a-zA-Z0-9._+-]+)' - r'-(?P<release>[a-zA-Z0-9_.]+)\s*$', re.IGNORECASE | re.MULTILINE) + r'\s+((?P<epoch>[0-9]+):)?(?P<ver>[a-zA-Z0-9._+-]+)' + r'-(?P<rel>[a-zA-Z0-9_.]+)\s*$', re.IGNORECASE | re.MULTILINE) RE_PKG_INFO = re.compile( r'^Name\s*:\s*(?P<name>[^\s]+).*?' r'^(Epoch\s*:\s*(?P<epoch>[0-9]+)\s+)?' - r'^Version\s*:\s*(?P<version>[a-zA-Z0-9._+-]+)\s+' - r'^Release\s*:\s*(?P<release>[^\s]+)\s+.*?' + r'^Version\s*:\s*(?P<ver>[a-zA-Z0-9._+-]+)\s+' + r'^Release\s*:\s*(?P<rel>[^\s]+)\s+.*?' r'^Size\s*:\s*(?P<size>\d+(\.\d+)?)( *(?P<units>[kMG]))?', re.MULTILINE | re.DOTALL | re.IGNORECASE) RE_REPO = re.compile( r'(?:^\*?)(?P<name>[^\s/]+\b)(?!\s+id)', re.MULTILINE | re.IGNORECASE) -# maximum number of packages, that will be selected for testing +# Maximum number of packages, that will be selected for testing / 2 +# There are 2 sets of test packages (safe and dangerous). When running +# in dangerous mode, the resulting number will be twice as much. MAX_PKG_DB_SIZE = 10 # step used to iterate over package names used to check for thery dependencies # it's a number of packages, that will be passed to yum command at once @@ -78,104 +88,6 @@ class MissingRPM(InvalidTestCache): InvalidTestCache.__init__(self, "Missing package '%s' in test cache!"%pkg_name) -def make_nevra(name, epoch, ver, rel, arch, with_epoch='NOT_ZERO'): - """ - @param with_epoch may be one of: - "NOT_ZERO" - include epoch only if it's not zero - "ALWAYS" - include epoch always - "NEVER" - do not include epoch at all - """ - estr = '' - if with_epoch.lower() == "always": - estr = epoch - elif with_epoch.lower() == "not_zero": - if epoch != "0": - estr = epoch - if len(estr): - estr += ":" - return "%s-%s%s-%s.%s" % (name, estr, ver, rel, arch) - -def run_yum(*params, **kwargs): - """ - Runs yum with params and returns its output - It's here especially to allow pass a repolist argument, that - specifies list of repositories, to run the command on. - """ - cmd = ['yum'] + list(params) - repolist = kwargs.get('repolist', []) - if repolist: - cmd += ['--disablerepo=*'] - cmd += ['--enablerepo='+r for r in repolist] - try: - return check_output(cmd) - except Exception: - import pdb;pdb.set_trace() - -class Package(object): #pylint: disable=R0902 - """ - Element of test package database. It's a container for package - informations. It contains two sets of versions for single package. - That's meant for updating tests. - """ - def __init__(self, name, epoch, ver, rel, arch, repo, - up_epoch, up_ver, up_rel, up_repo): - """ - Arguments prefixed with 'up_' are for newer package. - """ - self._name = name - self._epoch = epoch - self._ver = ver - self._rel = rel - self._arch = arch - self._repo = repo - self._up_epoch = up_epoch - self._up_ver = up_ver - self._up_rel = up_rel - self._up_repo = up_repo - - def __str__(self): - return self.get_nevra() - - @property - def name(self): return self._name #pylint: disable=C0111,C0321 - @property - def epoch(self): return self._epoch #pylint: disable=C0111,C0321 - @property - def ver(self): return self._ver #pylint: disable=C0111,C0321 - @property - def rel(self): return self._rel #pylint: disable=C0111,C0321 - @property - def arch(self): return self._arch #pylint: disable=C0111,C0321 - @property - def repo(self): return self._repo #pylint: disable=C0111,C0321 - @property - def nevra(self): #pylint: disable=C0111,C0321 - return self.get_nevra(True) - - @property - def up_epoch(self): return self._up_epoch #pylint: disable=C0111,C0321 - @property - def up_ver(self): return self._up_ver #pylint: disable=C0111,C0321 - @property - def up_rel(self): return self._up_rel #pylint: disable=C0111,C0321 - @property - def up_repo(self): return self._up_repo #pylint: disable=C0111,C0321 - @property - def up_nevra(self): #pylint: disable=C0111,C0321 - return self.get_nevra(True) - - def get_nevra(self, newer=True, with_epoch='NOT_ZERO'): - """ - @newer if True, evr part is made from properties prefixed with 'up_' - @return pkg nevra string - """ - if newer: - attrs = ['name', 'up_epoch', 'up_ver', 'up_rel', 'arch'] - else: - attrs = ['name', 'epoch', 'ver', 'rel', 'arch'] - return make_nevra(*[getattr(self, '_'+a) for a in attrs], - with_epoch=with_epoch) - def _match_nevr(match): """ @param match is a regexp match object with parsed rpm package @@ -183,22 +95,33 @@ def _match_nevr(match): """ epoch = match.group('epoch') return ( match.group('name') - , epoch[:-1] if epoch else "0" - , match.group('version') - , match.group('release')) + , epoch if epoch and epoch.lower() != "(none)" else "0" + , match.group('ver') + , match.group('rel')) -def _match2pkg(match): +def _match2pkg(match, safe=False): """ @param match is a regexp match object with attributes: name, epoch, version, release, arch + @param safe if True, the packe will have up_* properties equal to + non-prefixed ones, otherwise they will be set to None @return instance of Package """ + kwargs = {} + if safe is True: + kwargs['safe'] = True + else: + for attr in ('epoch', 'ver', 'rel', 'repo'): + kwargs['up_'+attr] = None + epoch = match.group('epoch') + if not epoch or epoch.lower() == "(none)": + epoch = '0' return Package( match.group('name'), - match.group('epoch')[:-1] if match.group('epoch') else '0', - match.group('version'), match.group('release'), - match.group('arch'), match.groupdict().get('repository', None), - None, None, None, None) + epoch, + match.group('ver'), match.group('rel'), + match.group('arch'), match.groupdict().get('repo', None), + **kwargs) def _filter_duplicates(installed, avail_str): """ @@ -213,7 +136,7 @@ def _filter_duplicates(installed, avail_str): dups_list = [] cur_package_matches = [] prev_match = None - system_arch = get_system_architecture() + system_arch = util.get_system_architecture() for match in RE_AVAIL_PKG.finditer(avail_str): if ( _match_nevr(match) in [ _match_nevr(m) for m in cur_package_matches] @@ -246,12 +169,14 @@ def _check_single_pkg_deps( """ for match_deps in RE_DEPS_PROVIDERS.finditer(dependencies_str): providers = [] + if RE_DEPS_UNSATISFIED.search(match_deps.group('providers')): + return False for match_dep in RE_PROVIDER.finditer(match_deps.group('providers')): if match_dep.group('name') not in installed: continue providers.append(_match2pkg(match_dep)) for provider in providers: - if is_installed(provider, False): + if is_pkg_installed(provider, False): break else: # no provider is installed return False @@ -261,7 +186,7 @@ def _check_pkg_dependencies( installed, dup_list, number_of_packages=MAX_PKG_DB_SIZE, - repolist=[]): + repolist=None): """ Finds packages from dup_list with satisfied (installed) dependencies. @param installed is a set of installed package names @@ -274,7 +199,7 @@ def _check_pkg_dependencies( yum_params = yum_params[:1] for dups in dups_part: yum_params.extend([d.get_nevra(newer=False) for d in dups]) - deplist_str = run_yum(*yum_params, repolist=repolist) + deplist_str = util.run_yum(*yum_params, repolist=repolist) matches = RE_PKG_DEPS.finditer(deplist_str) prev_match = None for pkgs in dups_part: @@ -300,14 +225,14 @@ def _check_pkg_dependencies( break return dups_no_deps -def _sorted_db_by_size(pkgdb, repolist=[]): +def _sorted_db_by_size(pkgdb, repolist=None): """ @param pkgdb is a list of lists of packages with common name @return sorted instances of Package according to their size """ yum_params = ['info', '--showduplicates'] yum_params.extend([ps[0].name for ps in pkgdb]) - info_str = run_yum(*yum_params, repolist=repolist) + info_str = util.run_yum(*yum_params, repolist=repolist) pkg_sizes = {} # to get correct ordering from "yum info" command # { pkg_name : [(epoch, version, release), ... ] } @@ -329,7 +254,7 @@ def _sorted_db_by_size(pkgdb, repolist=[]): if not epoch: epoch = "0" pkg_version_order[pkg_name].append(( - epoch, info_match.group('version'), info_match.group('release'))) + epoch, info_match.group('ver'), info_match.group('rel'))) pkgdb = sorted(pkgdb, key=lambda pkgs: pkg_sizes[pkgs[0].name])[ :MAX_PKG_DB_SIZE] @@ -350,7 +275,7 @@ def _get_repo_list(): repos_str = check_output(['yum', 'repolist', '-q']) return RE_REPO.findall(repos_str) -def _download_pkgdb(repolist, pkgdb, cache_dir=None): +def _download_dangerous(repolist, pkgdb, cache_dir=None): """ Downloads all rpm packages (old and newer versions) from package database to current directory. @@ -385,32 +310,6 @@ def _make_rpm_path(pkg, cache_dir='', newer=True, without_epoch=False): with_epoch='NEVER' if without_epoch else 'NOT_ZERO') return os.path.join(cache_dir, nevra) + '.rpm' -def is_installed(pkg, newer=True): - """ - Check, whether package is installed. - """ - if not isinstance(pkg, Package): - return call(["rpm", "--quiet", "-q", pkg]) == 0 - else: - try: - cmd = [ "rpm", "-q", "--qf", "%{EPOCH}:%{NVRA}", pkg.get_nevra( - newer, with_epoch='NEVER') ] - out = check_output(cmd) - epoch, nvra = out.split(':') #pylint: disable=E1103 - if not epoch or epoch == "(none)": - epoch = "0" - return ( epoch == pkg.epoch - and nvra == pkg.get_nevra(newer=newer, with_epoch="NEVER")) - except CalledProcessError: - return False - -def rpm_exists(pkg, cache_dir='', newer=True): - """ - @return True, when rpm package is in cache. - """ - return ( os.path.exists(_make_rpm_path(pkg, cache_dir, newer)) - or os.path.exists(_make_rpm_path(pkg, cache_dir, newer, True))) - def get_rpm_name(pkg, cache_dir='', newer=True): """ Some packages do not have epoch in their name, even if it's higher than @@ -425,56 +324,126 @@ def get_rpm_name(pkg, cache_dir='', newer=True): return path raise MissingRPM(pkg.name) -def get_system_architecture(): +def rpm_exists(pkg, cache_dir='', newer=True): + """ + @return True, when rpm package is in cache. + """ + return ( os.path.exists(_make_rpm_path(pkg, cache_dir, newer)) + or os.path.exists(_make_rpm_path(pkg, cache_dir, newer, True))) + + +def remove_pkg(pkg, *args): + """ + Remove package with rpm command. + @param pkg is either instance of Package or package name + @param args is a list of parameters for rpm command + """ + if isinstance(pkg, Package): + pkg = pkg.name + call(["rpm", "--quiet"] + list(args) + ["-e", pkg]) + +def install_pkg(pkg, newer=True, repolist=None): """ - @return the system architecture name as seen by rpm + Install a specific package. + @param pkg is either package name or instance of Package + In latter case, a specific version is installed. + @param repolist is a list of repositories, that should be + used for downloading, if using yum + when empty, all enabled repositories are used """ - return check_output(['rpm', '-q', '--qf', '%{ARCH}\n', 'rpm']) + if isinstance(pkg, Package): + try: + rpm_name = get_rpm_name(pkg, newer=newer) + call(["rpm", "--quiet", "-i", rpm_name]) + return + except MissingRPM: + pass + pkg = pkg.name + util.run_yum('-q', '-y', 'install', pkg, repolist=repolist) + +def ensure_pkg_installed(pkg, newer=True, repolist=None): + """ + Ensures, that specific version of package is installed. If other + version is installed, it is removed and reinstalled. + """ + if not isinstance(pkg, Package): + raise TypeError("pkg must be a Package instance") + if not is_pkg_installed(pkg, newer): + if is_pkg_installed(pkg.name): + remove_pkg(pkg.name) + install_pkg(pkg, newer, repolist) -def write_pkgdb(pkgdb, cache_dir=''): +def verify_pkg(pkg): + """ + @return output of command rpm, with verification output for package + """ + if isinstance(pkg, basestring): + name = pkg + elif isinstance(pkg, Package): + name = pkg.name + else: + raise TypeError("pkg must be either package name or Package instance") + return call(["rpm", "--quiet", "-Va", name]) == 0 + +def has_pkg_config_file(pkg, file_path): + """ + @return True, if file_path is a configuration file of package pkg. + """ + cmd = ['rpm', '-qc', pkg.name] + out = check_output(cmd) + return file_path in set(out.splitlines()) #pylint: disable=E1103 + +def has_pkg_doc_file(pkg, file_path): + """ + @return True, if file_path is a documentation file of package pkg. + """ + cmd = ['rpm', '-qd', pkg.name] + out = check_output(cmd) + return file_path in set(out.splitlines()) #pylint: disable=E1103 + +def is_pkg_installed(pkg, newer=True): + """ + Check, whether package is installed. + """ + if not isinstance(pkg, Package): + return call(["rpm", "--quiet", "-q", pkg]) == 0 + else: + cmd = [ "rpm", "-q", "--qf", "%{EPOCH}:%{NVRA}\n", pkg.get_nevra( + newer, with_epoch='NEVER') ] + try: + out = check_output(cmd).splitlines()[0] + epoch, nvra = out.split(':') #pylint: disable=E1103 + if not epoch or epoch.lower() == "(none)": + epoch = "0" + return ( epoch == getattr(pkg, 'up_epoch' if newer else 'epoch') + and nvra == pkg.get_nevra(newer=newer, with_epoch="NEVER")) + except CalledProcessError: + return False + +def write_pkgdb(safe_pkgs, dangerous_pkgs, cache_dir=''): """ Writes package database into a file named DB_BACKUP_FILE. """ with open(os.path.join(cache_dir, DB_BACKUP_FILE), 'w') as db_file: - pickle.dump((datetime.datetime.now(), pkgdb), db_file) + data = (datetime.datetime.now(), safe_pkgs, dangerous_pkgs) + pickle.dump(data, db_file) def load_pkgdb(cache_dir=''): """ This is inverse function to _write_pkgdb(). - @return package database loaded from file + @return (safe, dangerous) package lists loaded from file """ with open(os.path.join(cache_dir, DB_BACKUP_FILE), 'r') as db_file: - date_time, pkgdb = pickle.load(db_file) + _, safe, dangerous = pickle.load(db_file) #print "Loaded package database from: %s" % date_time - return pkgdb + return safe, dangerous -def get_pkg_database(force_update=False, use_cache=True, - cache_dir='', - repolist=[]): +def make_dangerous_list(installed, repolist=None): """ - Checks yum database for available packages, that have at least two - different versions in repositories. Only not installed ones with - all of their dependencies intalled are selected. - And from those, few of the smallest are downloaded as rpms. - @return list of instances of Package of selected packages + This makes a list of instances of Package for dangerous tests. """ - if ( use_cache and not force_update - and os.path.exists(os.path.join(cache_dir, DB_BACKUP_FILE))): - pkgdb = load_pkgdb(cache_dir) - valid_db = True - for pkg in pkgdb: - if ( not rpm_exists(pkg, cache_dir, False) - or not rpm_exists(pkg, cache_dir, True)): - valid_db = False - #print "Old package database is not valid" - break - if valid_db: - return pkgdb - #print "Getting installed packages" - installed = set(check_output( #pylint: disable=E1103 - ['rpm', '-qa', '--qf=%{NAME}\n']).splitlines()) #print "Getting all available packages" - avail_str = run_yum('list', 'available', '--showduplicates', + avail_str = util.run_yum('list', 'available', '--showduplicates', repolist=repolist) # list of lists of packages with the same name, longer than 2 #print "Finding duplicates" @@ -485,11 +454,88 @@ def get_pkg_database(force_update=False, use_cache=True, number_of_packages=MAX_PKG_DB_SIZE*5, repolist=repolist) #print "Selecting the smallest ones" - pkgdb = _sorted_db_by_size(selected, repolist=repolist) + return _sorted_db_by_size(selected, repolist=repolist) + +def make_safe_list(installed, exclude=None): + """ + Makes list of installed packages for non-dangerous tests. + @param installed is a list of installed packages names + @param exclude is a list of package names, that won't appear in result + """ + if exclude is None: + exclude = set() + base = list(installed) + random.shuffle(base) + res = [] + i = 0 + while len(res) < MAX_PKG_DB_SIZE and i < len(base): + name = base[i] + i += 1 + if name in exclude: + continue + cmd = [ "rpm", "-q", "--qf", "%{EPOCH}:%{NVRA}\n", name ] + envra = check_output(cmd).splitlines()[0] + res.append(_match2pkg(util.RE_ENVRA.match(envra), safe=True)) + return res + +def get_pkg_database( + force_update=False, + use_cache=True, + dangerous=False, + cache_dir='', + repolist=None): + """ + Checks yum database for available packages, that have at least two + different versions in repositories. Only not installed ones with + all of their dependencies intalled are selected. + And from those, few of the smallest are downloaded as rpms. + @param dangerous whether to load available, not installed + packages for dangerous tests + @return (safe, dangerous) + where + safe is a list of instances of Package, representing installed + software, these should be used for not-dangerous tests; + both older + dangerous is a list of instances of Package of selected packages, + that are not installed, but available; instances contain + both newer and older version of package + """ + dangerous_pkgs = [] + if ( use_cache and not force_update + and os.path.exists(os.path.join(cache_dir, DB_BACKUP_FILE))): + safe_pkgs, dangerous_pkgs = load_pkgdb(cache_dir) + valid_db = True + if len(safe_pkgs) < MAX_PKG_DB_SIZE: + valid_db = False + elif not dangerous and len(dangerous_pkgs) > 0: + dangerous_pkgs = [] + elif dangerous and len(dangerous_pkgs) == 0: + valid_db = False + else: + for pkg in safe_pkgs: + if not is_pkg_installed(pkg): + valid_db = False + break + for pkg in dangerous_pkgs: + if ( not rpm_exists(pkg, cache_dir, False) + or not rpm_exists(pkg, cache_dir, True)): + valid_db = False + #print "Old package database is not valid" + break + if valid_db: + return (safe_pkgs, dangerous_pkgs) + #print "Getting installed packages" + installed = set(check_output( #pylint: disable=E1103 + ['rpm', '-qa', '--qf=%{NAME}\n']).splitlines()) + if dangerous: + dangerous_pkgs = make_dangerous_list(installed) + safe_pkgs = make_safe_list(installed, exclude=set( + pkg.name for pkg in dangerous_pkgs)) + if use_cache: - repolist = _get_repo_list() - _download_pkgdb(repolist, pkgdb, cache_dir) + repolist = _get_repo_list() if repolist in (None, []) else repolist + _download_dangerous(repolist, dangerous_pkgs, cache_dir) #print "Backing up database information" - write_pkgdb(pkgdb, cache_dir) - return pkgdb + write_pkgdb(safe_pkgs, dangerous_pkgs, cache_dir) + return (safe_pkgs, dangerous_pkgs) |