diff options
Diffstat (limited to 'src/software/lmi/software/yumdb/jobs.py')
-rw-r--r-- | src/software/lmi/software/yumdb/jobs.py | 668 |
1 files changed, 668 insertions, 0 deletions
diff --git a/src/software/lmi/software/yumdb/jobs.py b/src/software/lmi/software/yumdb/jobs.py new file mode 100644 index 0000000..346ff17 --- /dev/null +++ b/src/software/lmi/software/yumdb/jobs.py @@ -0,0 +1,668 @@ +# -*- encoding: utf-8 -*- +# Software Management Providers +# +# Copyright (C) 2012-2013 Red Hat, Inc. All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Authors: Michal Minar <miminar@redhat.com> +# +""" +Define job classes representing kinds of jobs of worker process. +""" + +import os +import threading +import time +import yum + +from lmi.software import util +from lmi.software.yumdb import errors +from lmi.software.yumdb.packageinfo import PackageInfo +from lmi.software.yumdb.repository import Repository + +DEFAULT_JOB_PRIORITY = 10 +# in seconds +DEFAULT_TIME_BEFORE_REMOVAL = 60 * 5 + +class YumJob(object): #pylint: disable=R0903 + """ + Base class for any job, that is processable by YumWorker process. + It contains jobid attribute, that must be unique for + each job, it's counted from zero a incremented after each creation. + + metadata attribute typically contain: + name - name of job, that is modifiable by user + method - identificator of method, that lead to creation of job + """ + __slots__ = ( 'jobid', 'created', 'started', 'finished', 'last_change' + , 'priority', 'result', 'result_data') + + # jobs can be created concurrently from multiple threads, that's + # why we need to make its creation thread safe + _JOB_ID_LOCK = threading.Lock() + _JOB_ID = 0 + + # job state enumeration + NEW, RUNNING, COMPLETED, TERMINATED, EXCEPTION = range(5) + # job result enumeration + RESULT_SUCCESS, RESULT_TERMINATED, RESULT_ERROR = range(3) + + ResultNames = ("success", "terminated", "error") + + @staticmethod + def _get_job_id(): + """ + Generates new job ids. It should be called only from constructor + of YumJob. Ensures, that each job has a unique number. + @return number of jobs created since program start -1 + """ + with YumJob._JOB_ID_LOCK: + val = YumJob._JOB_ID + YumJob._JOB_ID += 1 + return val + + @classmethod + def handle_ignore_job_props(cls): + """ + @return set of job properties, that does not count as job's handler + arguments - job handler does not care fore metadata, jobid, priority, + etc... + """ + return set(YumJob.__slots__) + + def __init__(self, priority=10): + if not isinstance(priority, (int, long)): + raise TypeError("priority must be integer") + self.jobid = self._get_job_id() + self.started = None + self.finished = None + self.priority = priority + self.created = time.time() + self.last_change = self.created + self.result = None + self.result_data = None + + @property + def state(self): + """ + @return integer representing job's state + """ + if not self.started: + return self.NEW + if not self.finished: + return self.RUNNING + if self.result == self.RESULT_ERROR: + return self.EXCEPTION + if self.result == self.RESULT_TERMINATED: + return self.TERMINATED + return self.COMPLETED + + @property + def job_kwargs(self): + """ + Jobs are in worker handled in handlers specific for each subclass. + These handlers are methods of worker. They accepts concrete arguments + that can be obtained from job by invoking this property. + @return dictionary of keyword arguments of job + """ + kwargs = {} + cls = self.__class__ + while not cls in (YumJob, object): + for slot in cls.__slots__: + if ( not slot in kwargs + and not slot in cls.handle_ignore_job_props()): + kwargs[slot] = getattr(self, slot) + cls = cls.__bases__[0] + for prop in YumJob.__slots__: + kwargs.pop(prop, None) + return kwargs + + def start(self): + """Modify the state of job to RUNNING.""" + if self.started: + raise errors.InvalidJobState("can not start already started job") + self.started = time.time() + self.last_change = self.started + + def finish(self, result, data=None): + """ + Modify the state of job to one of {COMPLETED, EXCEPTION, TERMINATED}. + Depending on result parameter. + """ + if not self.started and result != self.RESULT_TERMINATED: + raise errors.InvalidJobState("can not finish not started job") + self.finished = time.time() + if result == self.RESULT_TERMINATED: + self.started = self.finished + self.result = result + self.result_data = data + self.last_change = self.finished + + def update(self, **kwargs): + """Change job's properties.""" + change = False + for key, value in kwargs.items(): + if getattr(self, key) != value: + setattr(self, key, value) + change = True + if change is True: + self.last_change = time.time() + + def __eq__(self, other): + return self.__class__ is other.__class__ and self.jobid == other.jobid + + def __ne__(self, other): + return ( self.__class__ is not other.__class__ + or self.jobid != other.jobid) + + def __lt__(self, other): + """ + JobControl jobs have the highest priority. + """ + return ( ( isinstance(self, YumJobControl) + and not isinstance(other, YumJobControl)) + or ( self.priority < other.priority + or ( self.priority == other.priority + and ( self.jobid < other.jobid + or ( self.jobid == other.jobid + and (self.created < other.created)))))) + + def __cmp__(self, other): + if ( isinstance(self, YumJobControl) + and not isinstance(other, YumJobControl)): + return -1 + if ( not isinstance(self, YumJobControl) + and isinstance(other, YumJobControl)): + return 1 + if self.priority < other.priority: + return -1 + if self.priority > other.priority: + return 1 + if self.jobid < other.jobid: + return -1 + if self.jobid > other.jobid: + return 1 + if self.created < other.created: + return -1 + if self.created > other.created: + return 1 + return 0 + + def __str__(self): + return "%s(id=%d,p=%d)" % ( + self.__class__.__name__, self.jobid, self.priority) + + def __getstate__(self): + ret = self.job_kwargs + for prop in self.handle_ignore_job_props(): + ret[prop] = getattr(self, prop) + return ret + + def __setstate__(self, state): + for k, value in state.items(): + setattr(self, k, value) + +class YumAsyncJob(YumJob): #pylint: disable=R0903 + """ + Base class for jobs, that support asynchronnous execution. + No reply is sent upon job completition or error. The results are + kept on server. + """ + __slots__ = ( 'async' + , 'delete_on_completion' + , 'time_before_removal' + , 'metadata') + + @classmethod + def handle_ignore_job_props(cls): + return YumJob.handle_ignore_job_props().union(YumAsyncJob.__slots__) + + def __init__(self, priority=10, async=False, metadata=None): + YumJob.__init__(self, priority) + self.async = bool(async) + self.delete_on_completion = True + self.time_before_removal = DEFAULT_TIME_BEFORE_REMOVAL + if metadata is None and self.async is True: + metadata = {} + self.metadata = metadata + + def __str__(self): + return "%s(id=%d,p=%d%s%s)" % ( + self.__class__.__name__, self.jobid, + self.priority, + ',async' if self.async else '', + (',name="%s"'%self.metadata['name']) + if self.metadata and 'name' in self.metadata else '') + + def update(self, **kwargs): + if 'metadata' in kwargs: + self.metadata.update(kwargs.pop('metadata')) + return YumJob.update(self, **kwargs) + +# ***************************************************************************** +# Job control funtions +# ***************************************************************************** +class YumJobControl(YumJob): #pylint: disable=R0903 + """Base class for any job used for asynchronous jobs management.""" + pass + +class YumJobGetList(YumJobControl): #pylint: disable=R0903 + """Request for obtaining list of all asynchronous jobs.""" + pass + +class YumJobOnJob(YumJobControl): + """ + Base class for any control job acting upon particular asynchronous job. + """ + __slots__ = ('target', ) + def __init__(self, target): + YumJobControl.__init__(self) + if not isinstance(target, (int, long)): + raise TypeError("target must be an integer") + self.target = target + +class YumJobGet(YumJobOnJob): #pylint: disable=R0903 + """Get job object by its id.""" + pass + +class YumJobGetByName(YumJobOnJob): #pylint: disable=R0903 + """Get job object by its name property.""" + def __init__(self, name): + YumJobOnJob.__init__(self, -1) + self.target = name + +class YumJobSetPriority(YumJobOnJob): #pylint: disable=R0903 + """Change priority of job.""" + __slots__ = ('new_priority', ) + + def __init__(self, target, priority): + YumJobOnJob.__init__(self, target) + self.new_priority = priority + +class YumJobUpdate(YumJobOnJob): #pylint: disable=R0903 + """ + .. _YumJobUpdate: + + Update job's metadata. There are some forbidden properties, that + can not be changed in this way. Those are all affecting job's priority + and its scheduling for deletion. Plus any that store job's state. + All forbidden properties are listed in ``FORBIDDEN_PROPERTIES``. + """ + __slots__ = ('data', ) + FORBIDDEN_PROPERTIES = ( + 'async', 'jobid', 'created', 'started', 'priority', 'finished', + 'delete_on_completion', 'time_before_removal', 'last_change') + + def __init__(self, target, **kwargs): + YumJobOnJob.__init__(self, target) + assert not set.intersection( + set(YumJobUpdate.FORBIDDEN_PROPERTIES), set(kwargs)) + self.data = kwargs + +class YumJobReschedule(YumJobOnJob): #pylint: disable=R0903 + """Change the schedule of job's deletion.""" + __slots__ = ('delete_on_completion', 'time_before_removal') + def __init__(self, target, delete_on_completion, time_before_removal): + YumJobOnJob.__init__(self, target) + if not isinstance(time_before_removal, (int, long, float)): + raise TypeError("time_before_removal must be float") + self.delete_on_completion = bool(delete_on_completion) + self.time_before_removal = time_before_removal + +class YumJobDelete(YumJobOnJob): #pylint: disable=R0903 + """Delete job - can only be called on finished job.""" + pass + +class YumJobTerminate(YumJobOnJob): #pylint: disable=R0903 + """ + Can only be called on not yet started job. + Running job can not be terminated. + """ + pass + +# ***************************************************************************** +# Yum API functions +# ***************************************************************************** +class YumBeginSession(YumJob): #pylint: disable=R0903 + """ + Begin session on YumWorker which ensures that yum database is locked + during its lifetime. Sessions can be nested, but the number of + YumEndSession jobs must be processed to make the database unlocked. + """ + pass +class YumEndSession(YumJob): #pylint: disable=R0903 + """ + End the session started with YumBeginSession. If the last active session + is ended, database will be unlocked. + """ + pass + +class YumGetPackageList(YumJob): #pylint: disable=R0903 + """ + Job requesing a list of packages. + Arguments: + kind - supported values are in SUPPORTED_KINDS tuple + * installed lists all installed packages; more packages with + the same name can be installed varying in their architecture + * avail_notinst lists all available, not installed packages; + allow_duplicates must be True to include older packages (but still + available) + * avail_reinst lists all installed packages, that are available; + package can be installed, but not available anymore due to updates + of repository, where only the newest packages are kept + * available lists a union of avail_notinst and avail_reinst + * all lists union of installed and avail_notinst + + allow_duplicates - whether multiple packages can be present + in result for single (name, arch) of package differing + in their version + + sort - whether to sort packages by nevra + + include_repos - either a string passable to RepoStorage.enableRepo() + or a list of repository names, that will be temporared enabled before + listing packages; this is applied after disabling of repositories + + exclude_repos - either a string passable to RepoStorage.disableRepo() + or a list of repository names, that will be temporared disabled before + listing packages; this is applied before enabling of repositories + + Worker replies with [pkg1, pkg2, ...]. + """ + __slots__ = ('kind', 'allow_duplicates', 'sort', 'include_repos', + 'exclude_repos') + + SUPPORTED_KINDS = ( 'installed', 'available', 'avail_reinst' + , 'avail_notinst', 'all') + + def __init__(self, kind, allow_duplicates, sort=False, + include_repos=None, exclude_repos=None): + YumJob.__init__(self) + if not isinstance(kind, basestring): + raise TypeError("kind must be a string") + if not kind in self.SUPPORTED_KINDS: + raise ValueError("kind must be one of {%s}" % + ", ".join(self.SUPPORTED_KINDS)) + for arg in ('include_repos', 'exclude_repos'): + val = locals()[arg] + if ( not val is None + and not isinstance(arg, (tuple, list, basestring))): + raise TypeError("expected list or string for %s" % arg) + self.kind = kind + self.allow_duplicates = bool(allow_duplicates) + self.sort = bool(sort) + self.include_repos = include_repos + self.exclude_repos = exclude_repos + +class YumFilterPackages(YumGetPackageList): #pylint: disable=R0903 + """ + Job similar to YumGetPackageList, but allowing to specify + filter on packages. + Arguments (plus those in YumGetPackageList): + name, epoch, version, release, arch, nevra, envra, evra + + Some of those are redundant, but filtering is optimized for + speed, so supplying all of them won't affect performance. + + Worker replies with [pkg1, pkg2, ...]. + """ + __slots__ = ( + 'name', 'epoch', 'version', 'release', 'arch', + 'nevra', 'envra', 'evra', 'repoid') + + def __init__(self, kind, allow_duplicates, + sort=False, include_repos=None, exclude_repos=None, + name=None, epoch=None, version=None, + release=None, arch=None, + nevra=None, evra=None, + envra=None, + repoid=None): + if nevra is not None and not util.RE_NEVRA.match(nevra): + raise ValueError("Invalid nevra: %s" % nevra) + if evra is not None and not util.RE_EVRA.match(evra): + raise ValueError("Invalid evra: %s" % evra) + if envra is not None and not util.RE_ENVRA.match(evra): + raise ValueError("Invalid envra: %s" % envra) + YumGetPackageList.__init__(self, kind, allow_duplicates, sort, + include_repos=include_repos, exclude_repos=exclude_repos) + self.name = name + self.epoch = None if epoch is None else str(epoch) + self.version = version + self.release = release + self.arch = arch + self.nevra = nevra + self.evra = evra + self.envra = envra + self.repoid = repoid + +class YumSpecificPackageJob(YumAsyncJob): #pylint: disable=R0903 + """ + Abstract job taking instance of yumdb.PackageInfo as argument or + package's nevra. + Arguments: + pkg - plays different role depending on job subclass; + can also be a nevra + """ + __slots__ = ('pkg', ) + def __init__(self, pkg, async=False, metadata=None): + if isinstance(pkg, basestring): + if not util.RE_NEVRA_OPT_EPOCH.match(pkg): + raise errors.InvalidNevra('not a valid nevra "%s"' % pkg) + elif not isinstance(pkg, PackageInfo): + raise TypeError("pkg must be either string or instance" + " of PackageInfo") + YumAsyncJob.__init__(self, async=async, metadata=metadata) + self.pkg = pkg + +class YumInstallPackage(YumSpecificPackageJob): #pylint: disable=R0903 + """ + Job requesting installation of specific package. + pkg argument should be available. + Arguments: + pkg - same as in YumSpecificPackageJob + force is a boolean saying: + True -> reinstall the package if it's already installed + False -> fail if the package is already installed + + Worker replies with new instance of package. + """ + __slots__ = ('force', ) + def __init__(self, pkg, async=False, force=False, metadata=None): + YumSpecificPackageJob.__init__( + self, pkg, async=async, metadata=metadata) + self.force = bool(force) + +class YumRemovePackage(YumSpecificPackageJob): #pylint: disable=R0903 + """ + Job requesting removal of specific package. + pkg argument should be installed. + """ + pass + +class YumUpdateToPackage(YumSpecificPackageJob): #pylint: disable=R0903 + """ + Job requesting update to provided specific package. + Package is updated to epoch, version and release of this + provided available package. + + Worker replies with new instance of package. + """ + pass + +class YumUpdatePackage(YumSpecificPackageJob): #pylint: disable=R0903 + """ + Job requesting update of package, optionally reducing possible + candidate packages to ones with specific evr. + Arguments: + to_epoch, to_version, to_release + force is a boolean, that has meaning only when update_only is False: + True -> reinstall the package if it's already installed + False -> fail if the package is already installed + + The arguments more given, the more complete filter of candidates. + + Worker replies with new instance of package. + """ + __slots__ = ('to_epoch', 'to_version', 'to_release', 'force') + + def __init__(self, pkg, async=False, + to_epoch=None, to_version=None, to_release=None, force=False, + metadata=None): + if not isinstance(pkg, PackageInfo): + raise TypeError("pkg must be instance of yumdb.PackageInfo") + YumSpecificPackageJob.__init__( + self, pkg, async=async, metadata=metadata) + self.to_epoch = to_epoch + self.to_version = to_version + self.to_release = to_release + self.force = bool(force) + +class YumCheckPackage(YumSpecificPackageJob): #pylint: disable=R0903 + """ + Request verification information for instaled package and its files. + + Arguments: + pkg - either instance of PackageInfo or nevra string. + In latter case it will be replaced for YumWorker with instance + of PackageInfo. + + Worker replies with ``(pkg_info, pkg_check)``. + where: + ``pkg_info`` - is instance of PackageInfo + ``pkg_check`` - new instance of yumdb.PackageCheck + """ + def __init__(self, pkg, async=False, metadata=None): + YumSpecificPackageJob.__init__(self, pkg, async=async, + metadata=metadata) + if isinstance(pkg, PackageInfo) and not pkg.installed: + raise ValueError("package must be installed to check it") + +class YumCheckPackageFile(YumCheckPackage): #pylint: disable=R0903 + """ + Request verification information for particular file of installed + package. + + Worker replies with ``(pkg_info, pkg_check)``. + where: + ``pkg_info`` - is instance of PackageInfo + ``pkg_check`` - new instance of yumdb.PackageCheck containing only + requested file. + """ + __slots__ = ('file_name', ) + def __init__(self, pkg, file_name, *args, **kwargs): + YumCheckPackage.__init__(self, pkg, *args, **kwargs) + if not isinstance(file_name, basestring): + raise TypeError("file_name must be string") + self.file_name = file_name + +class YumInstallPackageFromURI(YumAsyncJob): #pylint: disable=R0903 + """ + Job requesting installation of specific package from URI. + Arguments: + uri is either a path to rpm package on local filesystem or url + of rpm stored on remote host + update_only is a boolean: + True -> install the package only if the older version is installed + False -> install the package if it's not already installed + force is a boolean, that has meaning only when update_only is False: + True -> reinstall the package if it's already installed + False -> fail if the package is already installed + + Worker replies with new instance of package. + """ + __slots__ = ('uri', 'update_only', "force") + def __init__(self, uri, async=False, update_only=False, force=False, + metadata=None): + if not isinstance(uri, basestring): + raise TypeError("uri must be a string") + if uri.startswith('file://'): + uri = uri[len('file://'):] + if not yum.misc.re_remote_url(uri) and not os.path.exists(uri): + raise errors.InvalidURI(uri) + YumAsyncJob.__init__(self, async=async, metadata=metadata) + self.uri = uri + self.update_only = bool(update_only) + self.force = bool(force) + +class YumGetRepositoryList(YumJob): #pylint: disable=R0903 + """ + Job requesing a list of repositories. + Arguments: + kind - supported values are in SUPPORTED_KINDS tuple + + Worker replies with [repo1, repo2, ...]. + """ + __slots__ = ('kind', ) + + SUPPORTED_KINDS = ('all', 'enabled', 'disabled') + + def __init__(self, kind): + YumJob.__init__(self) + if not isinstance(kind, basestring): + raise TypeError("kind must be a string") + if not kind in self.SUPPORTED_KINDS: + raise ValueError("kind must be one of {%s}" % + ", ".join(self.SUPPORTED_KINDS)) + self.kind = kind + +class YumFilterRepositories(YumGetRepositoryList): #pylint: disable=R0903 + """ + Job similar to YumGetRepositoryList, but allowing to specify + filter on packages. + Arguments (plus those in YumGetRepositoryList): + name, gpg_check, repo_gpg_check + + Some of those are redundant, but filtering is optimized for + speed, so supplying all of them won't affect performance. + + Worker replies with [repo1, repo2, ...]. + """ + __slots__ = ('repoid', 'gpg_check', 'repo_gpg_check') + + def __init__(self, kind, + repoid=None, gpg_check=None, repo_gpg_check=None): + YumGetRepositoryList.__init__(self, kind) + self.repoid = repoid + self.gpg_check = None if gpg_check is None else bool(gpg_check) + self.repo_gpg_check = ( + None if repo_gpg_check is None else bool(repo_gpg_check)) + +class YumSpecificRepositoryJob(YumJob): #pylint: disable=R0903 + """ + Abstract job taking instance of yumdb.Repository as argument. + Arguments: + repo - (``Repository`` or ``str``) plays different role depending + on job subclass + """ + __slots__ = ('repo', ) + def __init__(self, repo): + if not isinstance(repo, (Repository, basestring)): + raise TypeError("repoid must be either instance of" + " yumdb.Repository or string") + YumJob.__init__(self) + self.repo = repo + +class YumSetRepositoryEnabled(YumSpecificRepositoryJob):#pylint: disable=R0903 + """ + Job allowing to enable or disable repository. + Arguments: + enable - (``boolean``) representing next state + """ + __slots__ = ('enable', ) + def __init__(self, repo, enable): + YumSpecificRepositoryJob.__init__(self, repo) + self.enable = bool(enable) + |