Package yum :: Module plugins
[hide private]
[frames] | no frames]

Source Code for Module yum.plugins

  1  # This program is free software; you can redistribute it and/or modify 
  2  # it under the terms of the GNU General Public License as published by 
  3  # the Free Software Foundation; either version 2 of the License, or 
  4  # (at your option) any later version. 
  5  # 
  6  # This program is distributed in the hope that it will be useful, 
  7  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  8  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  9  # GNU Library General Public License for more details. 
 10  # 
 11  # You should have received a copy of the GNU General Public License 
 12  # along with this program; if not, write to the Free Software 
 13  # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 
 14  # Copyright 2005 Duke University 
 15   
 16  import os 
 17  import glob 
 18  import imp 
 19  import warnings 
 20  import atexit 
 21  import gettext 
 22  import logging 
 23  import logginglevels 
 24  from constants import * 
 25  import config  
 26  from config import ParsingError, ConfigParser 
 27  import Errors 
 28  from parser import ConfigPreProcessor 
 29   
 30  from textwrap import fill 
 31  import fnmatch 
 32   
 33  from weakref import proxy as weakref 
 34   
 35  from yum import _ 
 36   
 37  from yum.i18n import utf8_width 
 38   
 39  # TODO: expose rpm package sack objects to plugins (once finished) 
 40  # TODO: allow plugins to use the existing config stuff to define options for 
 41  # their own configuration files (would replace confString() etc). 
 42  # TODO: expose progress bar interface 
 43  # TODO "log" slot? To allow plugins to do customised logging/history (say to a 
 44  # SQL db) 
 45  # TODO: consistent case of YumPlugins methods 
 46  # TODO: allow plugins to extend shell commands 
 47  # TODO: allow plugins to define new repository types 
 48  # TODO: More developer docs:  use epydoc as API begins to stablise 
 49   
 50   
 51  # The API_VERSION constant defines the current plugin API version. It is used 
 52  # to decided whether or not plugins can be loaded. It is compared against the 
 53  # 'requires_api_version' attribute of each plugin. The version number has the 
 54  # format: "major_version.minor_version". 
 55  #  
 56  # For a plugin to be loaded the major version required by the plugin must match 
 57  # the major version in API_VERSION. Additionally, the minor version in 
 58  # API_VERSION must be greater than or equal the minor version required by the 
 59  # plugin. 
 60  #  
 61  # If a change to yum is made that break backwards compatibility wrt the plugin 
 62  # API, the major version number must be incremented and the minor version number 
 63  # reset to 0. If a change is made that doesn't break backwards compatibility, 
 64  # then the minor number must be incremented. 
 65  API_VERSION = '2.6' 
66 67 -class DeprecatedInt(int):
68 ''' 69 A simple int subclass that used to check when a deprecated constant is used. 70 '''
71 72 # Plugin types 73 TYPE_CORE = 0 74 TYPE_INTERACTIVE = 1 75 TYPE_INTERFACE = DeprecatedInt(1) 76 ALL_TYPES = (TYPE_CORE, TYPE_INTERACTIVE) 77 78 # Mapping of slots to conduit classes 79 SLOT_TO_CONDUIT = { 80 'config': 'ConfigPluginConduit', 81 'postconfig': 'PostConfigPluginConduit', 82 'init': 'InitPluginConduit', 83 'args': 'ArgsPluginConduit', 84 'predownload': 'DownloadPluginConduit', 85 'postdownload': 'DownloadPluginConduit', 86 'prereposetup': 'PreRepoSetupPluginConduit', 87 'postreposetup': 'PostRepoSetupPluginConduit', 88 'close': 'PluginConduit', 89 'clean': 'PluginConduit', 90 'pretrans': 'MainPluginConduit', 91 'posttrans': 'MainPluginConduit', 92 'exclude': 'MainPluginConduit', 93 'preresolve': 'DepsolvePluginConduit', 94 'postresolve': 'DepsolvePluginConduit', 95 } 96 97 # Enumerate all slot names 98 SLOTS = SLOT_TO_CONDUIT.keys()
99 100 -class PluginYumExit(Exception):
101 '''Used by plugins to signal that yum should stop 102 '''
103 - def __init__(self, value="", translation_domain=""):
104 self.value = value 105 self.translation_domain = translation_domain
106 - def __str__(self):
107 if self.translation_domain: 108 return gettext.dgettext(self.translation_domain, self.value) 109 else: 110 return self.value
111
112 -class YumPlugins:
113 ''' 114 Manager class for Yum plugins. 115 ''' 116
117 - def __init__(self, base, searchpath, optparser=None, types=None, 118 pluginconfpath=None,disabled=None,enabled=None):
119 '''Initialise the instance. 120 121 @param base: The 122 @param searchpath: A list of paths to look for plugin modules. 123 @param optparser: The OptionParser instance for this run (optional). 124 Use to allow plugins to extend command line options. 125 @param types: A sequence specifying the types of plugins to load. 126 This should be sequnce containing one or more of the TYPE_... 127 constants. If None (the default), all plugins will be loaded. 128 @param pluginconfpath: A list of paths to look for plugin configuration 129 files. Defaults to "/etc/yum/pluginconf.d". 130 ''' 131 if not pluginconfpath: 132 pluginconfpath = ['/etc/yum/pluginconf.d'] 133 134 self.searchpath = searchpath 135 self.pluginconfpath = pluginconfpath 136 self.base = weakref(base) 137 self.optparser = optparser 138 self.cmdline = (None, None) 139 self.verbose_logger = logging.getLogger("yum.verbose.YumPlugins") 140 self.disabledPlugins = disabled 141 self.enabledPlugins = enabled 142 if types is None: 143 types = ALL_TYPES 144 if not isinstance(types, (list, tuple)): 145 types = (types,) 146 147 if id(TYPE_INTERFACE) in [id(t) for t in types]: 148 self.verbose_logger.log(logginglevels.INFO_2, 149 'Deprecated constant TYPE_INTERFACE during plugin ' 150 'initialization.\nPlease use TYPE_INTERACTIVE instead.') 151 152 self._importplugins(types) 153 154 self.cmdlines = {} 155 156 # Call close handlers when yum exit's 157 atexit.register(self.run, 'close') 158 159 # Let plugins register custom config file options 160 self.run('config')
161
162 - def run(self, slotname, **kwargs):
163 '''Run all plugin functions for the given slot. 164 ''' 165 # Determine handler class to use 166 conduitcls = SLOT_TO_CONDUIT.get(slotname, None) 167 if conduitcls is None: 168 raise ValueError('unknown slot name "%s"' % slotname) 169 conduitcls = eval(conduitcls) # Convert name to class object 170 171 for modname, func in self._pluginfuncs[slotname]: 172 self.verbose_logger.log(logginglevels.DEBUG_4, 173 'Running "%s" handler for "%s" plugin', 174 slotname, modname) 175 176 _, conf = self._plugins[modname] 177 func(conduitcls(self, self.base, conf, **kwargs))
178
179 - def _importplugins(self, types):
180 '''Load plugins matching the given types. 181 ''' 182 183 # Initialise plugin dict 184 self._plugins = {} 185 self._pluginfuncs = {} 186 for slot in SLOTS: 187 self._pluginfuncs[slot] = [] 188 189 # Import plugins 190 self._used_disable_plugin = set() 191 self._used_enable_plugin = set() 192 for dir in self.searchpath: 193 if not os.path.isdir(dir): 194 continue 195 for modulefile in sorted(glob.glob('%s/*.py' % dir)): 196 self._loadplugin(modulefile, types) 197 198 # If we are in verbose mode we get the full 'Loading "blah" plugin' lines 199 if (self._plugins and 200 not self.verbose_logger.isEnabledFor(logginglevels.DEBUG_3)): 201 # Mostly copied from YumOutput._outKeyValFill() 202 key = _("Loaded plugins: ") 203 val = ", ".join(sorted(self._plugins)) 204 nxt = ' ' * (utf8_width(key) - 2) + ': ' 205 width = 80 206 if hasattr(self.base, 'term'): 207 width = self.base.term.columns 208 self.verbose_logger.log(logginglevels.INFO_2, 209 fill(val, width=width, initial_indent=key, 210 subsequent_indent=nxt)) 211 212 if self.disabledPlugins: 213 for wc in self.disabledPlugins: 214 if wc not in self._used_disable_plugin: 215 self.verbose_logger.log(logginglevels.INFO_2, 216 _("No plugin match for: %s") % wc) 217 del self._used_disable_plugin 218 if self.enabledPlugins: 219 for wc in self.enabledPlugins: 220 if wc not in self._used_enable_plugin: 221 self.verbose_logger.log(logginglevels.INFO_2, 222 _("No plugin match for: %s") % wc) 223 del self._used_enable_plugin
224 225 @staticmethod
226 - def _plugin_cmdline_match(modname, plugins, used):
227 """ Check if this plugin has been temporary enabled/disabled. """ 228 if plugins is None: 229 return False 230 231 for wc in plugins: 232 if fnmatch.fnmatch(modname, wc): 233 used.add(wc) 234 return True 235 236 return False
237 238
239 - def _loadplugin(self, modulefile, types):
240 '''Attempt to import a plugin module and register the hook methods it 241 uses. 242 ''' 243 dir, modname = os.path.split(modulefile) 244 modname = modname.split('.py')[0] 245 246 conf = self._getpluginconf(modname) 247 if (not conf or 248 (not config.getOption(conf, 'main', 'enabled', 249 config.BoolOption(False)) and 250 not self._plugin_cmdline_match(modname, self.enabledPlugins, 251 self._used_enable_plugin))): 252 self.verbose_logger.debug(_('Not loading "%s" plugin, as it is disabled'), modname) 253 return 254 255 try: 256 fp, pathname, description = imp.find_module(modname, [dir]) 257 try: 258 module = imp.load_module(modname, fp, pathname, description) 259 finally: 260 fp.close() 261 except: 262 if self.verbose_logger.isEnabledFor(logginglevels.DEBUG_4): 263 raise # Give full backtrace: 264 self.verbose_logger.error(_('Plugin "%s" can\'t be imported') % 265 modname) 266 return 267 268 # Check API version required by the plugin 269 if not hasattr(module, 'requires_api_version'): 270 self.verbose_logger.error( 271 _('Plugin "%s" doesn\'t specify required API version') % 272 modname) 273 return 274 if not apiverok(API_VERSION, module.requires_api_version): 275 self.verbose_logger.error( 276 _('Plugin "%s" requires API %s. Supported API is %s.') % ( 277 modname, 278 module.requires_api_version, 279 API_VERSION, 280 )) 281 return 282 283 # Check plugin type against filter 284 plugintypes = getattr(module, 'plugin_type', ALL_TYPES) 285 if not isinstance(plugintypes, (list, tuple)): 286 plugintypes = (plugintypes,) 287 288 if len(plugintypes) < 1: 289 return 290 for plugintype in plugintypes: 291 if id(plugintype) == id(TYPE_INTERFACE): 292 self.verbose_logger.log(logginglevels.INFO_2, 293 'Plugin "%s" uses deprecated constant ' 294 'TYPE_INTERFACE.\nPlease use TYPE_INTERACTIVE ' 295 'instead.', modname) 296 297 if plugintype not in types: 298 return 299 300 # This should really work like enable/disable repo. and be based on the 301 # cmd line order ... but the API doesn't really allow that easily. 302 # FIXME: Fix for 4.* 303 if (self._plugin_cmdline_match(modname, self.disabledPlugins, 304 self._used_disable_plugin) and 305 not self._plugin_cmdline_match(modname, self.enabledPlugins, 306 self._used_enable_plugin)): 307 return 308 309 self.verbose_logger.log(logginglevels.DEBUG_3, _('Loading "%s" plugin'), 310 modname) 311 312 # Store the plugin module and its configuration file 313 if modname not in self._plugins: 314 self._plugins[modname] = (module, conf) 315 else: 316 raise Errors.ConfigError(_('Two or more plugins with the name "%s" ' \ 317 'exist in the plugin search path') % modname) 318 319 for slot in SLOTS: 320 funcname = slot+'_hook' 321 if hasattr(module, funcname): 322 self._pluginfuncs[slot].append( 323 (modname, getattr(module, funcname)) 324 )
325
326 - def _getpluginconf(self, modname):
327 '''Parse the plugin specific configuration file and return a 328 IncludingConfigParser instance representing it. Returns None if there 329 was an error reading or parsing the configuration file. 330 ''' 331 for dir in self.pluginconfpath: 332 conffilename = os.path.join(dir, modname + ".conf") 333 if os.access(conffilename, os.R_OK): 334 # Found configuration file 335 break 336 self.verbose_logger.log(logginglevels.INFO_2, _("Configuration file %s not found") % conffilename) 337 else: # for 338 # Configuration files for the plugin not found 339 self.verbose_logger.log(logginglevels.INFO_2, _("Unable to find configuration file for plugin %s") 340 % modname) 341 return None 342 parser = ConfigParser() 343 confpp_obj = ConfigPreProcessor(conffilename) 344 try: 345 parser.readfp(confpp_obj) 346 except ParsingError, e: 347 raise Errors.ConfigError("Couldn't parse %s: %s" % (conffilename, 348 str(e))) 349 return parser
350
351 - def setCmdLine(self, opts, commands):
352 '''Set the parsed command line options so that plugins can access them 353 ''' 354 self.cmdline = (opts, commands)
355
356 357 -class DummyYumPlugins:
358 ''' 359 This class provides basic emulation of the YumPlugins class. It exists so 360 that calls to plugins.run() don't fail if plugins aren't in use. 361 '''
362 - def run(self, *args, **kwargs):
363 pass
364
365 - def setCmdLine(self, *args, **kwargs):
366 pass
367
368 -class PluginConduit:
369 - def __init__(self, parent, base, conf):
370 self._parent = parent 371 self._base = base 372 self._conf = conf 373 374 self.logger = logging.getLogger("yum.plugin") 375 self.verbose_logger = logging.getLogger("yum.verbose.plugin")
376
377 - def info(self, level, msg):
378 converted_level = logginglevels.logLevelFromDebugLevel(level) 379 self.verbose_logger.log(converted_level, msg)
380
381 - def error(self, level, msg):
382 converted_level = logginglevels.logLevelFromErrorLevel(level) 383 self.logger.log(converted_level, msg)
384
385 - def promptYN(self, msg):
386 self.info(2, msg) 387 if self._base.conf.assumeyes: 388 return 1 389 else: 390 return self._base.userconfirm()
391
392 - def getYumVersion(self):
393 import yum 394 return yum.__version__
395
396 - def getOptParser(self):
397 '''Return the optparse.OptionParser instance for this execution of Yum 398 399 In the "config" and "init" slots a plugin may add extra options to this 400 instance to extend the command line options that Yum exposes. 401 402 In all other slots a plugin may only read the OptionParser instance. 403 Any modification of the instance at this point will have no effect. 404 405 See the getCmdLine() method for details on how to retrieve the parsed 406 values of command line options. 407 408 @return: the global optparse.OptionParser instance used by Yum. May be 409 None if an OptionParser isn't in use. 410 ''' 411 # ' xemacs highlighting hack 412 # This isn't API compatible :( 413 # return self._parent.optparser.plugin_option_group 414 return self._parent.optparser
415
416 - def confString(self, section, opt, default=None):
417 '''Read a string value from the plugin's own configuration file 418 419 @param section: Configuration file section to read. 420 @param opt: Option name to read. 421 @param default: Value to read if option is missing. 422 @return: String option value read, or default if option was missing. 423 ''' 424 # ' xemacs highlighting hack 425 return config.getOption(self._conf, section, opt, config.Option(default))
426
427 - def confInt(self, section, opt, default=None):
428 '''Read an integer value from the plugin's own configuration file 429 430 @param section: Configuration file section to read. 431 @param opt: Option name to read. 432 @param default: Value to read if option is missing. 433 @return: Integer option value read, or default if option was missing or 434 could not be parsed. 435 ''' 436 return config.getOption(self._conf, section, opt, config.IntOption(default))
437
438 - def confFloat(self, section, opt, default=None):
439 '''Read a float value from the plugin's own configuration file 440 441 @param section: Configuration file section to read. 442 @param opt: Option name to read. 443 @param default: Value to read if option is missing. 444 @return: Float option value read, or default if option was missing or 445 could not be parsed. 446 ''' 447 return config.getOption(self._conf, section, opt, config.FloatOption(default))
448
449 - def confBool(self, section, opt, default=None):
450 '''Read a boolean value from the plugin's own configuration file 451 452 @param section: Configuration file section to read. 453 @param opt: Option name to read. 454 @param default: Value to read if option is missing. 455 @return: Boolean option value read, or default if option was missing or 456 could not be parsed. 457 ''' 458 return config.getOption(self._conf, section, opt, config.BoolOption(default))
459
460 - def registerPackageName(self, name):
461 self._base.run_with_package_names.add(name)
462
463 464 -class ConfigPluginConduit(PluginConduit):
465
466 - def registerOpt(self, name, valuetype, where, default):
467 '''Register a yum configuration file option. 468 469 @param name: Name of the new option. 470 @param valuetype: Option type (PLUG_OPT_BOOL, PLUG_OPT_STRING ...) 471 @param where: Where the option should be available in the config file. 472 (PLUG_OPT_WHERE_MAIN, PLUG_OPT_WHERE_REPO, ...) 473 @param default: Default value for the option if not set by the user. 474 ''' 475 warnings.warn('registerOpt() will go away in a future version of Yum.\n' 476 'Please manipulate config.YumConf and config.RepoConf directly.', 477 DeprecationWarning) 478 479 type2opt = { 480 PLUG_OPT_STRING: config.Option, 481 PLUG_OPT_INT: config.IntOption, 482 PLUG_OPT_BOOL: config.BoolOption, 483 PLUG_OPT_FLOAT: config.FloatOption, 484 } 485 486 if where == PLUG_OPT_WHERE_MAIN: 487 setattr(config.YumConf, name, type2opt[valuetype](default)) 488 489 elif where == PLUG_OPT_WHERE_REPO: 490 setattr(config.RepoConf, name, type2opt[valuetype](default)) 491 492 elif where == PLUG_OPT_WHERE_ALL: 493 option = type2opt[valuetype](default) 494 setattr(config.YumConf, name, option) 495 setattr(config.RepoConf, name, config.Inherit(option))
496
497 - def registerCommand(self, command):
498 if hasattr(self._base, 'registerCommand'): 499 self._base.registerCommand(command) 500 else: 501 raise Errors.ConfigError(_('registration of commands not supported'))
502
503 -class PostConfigPluginConduit(ConfigPluginConduit):
504
505 - def getConf(self):
506 return self._base.conf
507
508 -class InitPluginConduit(PluginConduit):
509
510 - def getConf(self):
511 return self._base.conf
512
513 - def getRepos(self):
514 '''Return Yum's container object for all configured repositories. 515 516 @return: Yum's RepoStorage instance 517 ''' 518 return self._base.repos
519
520 -class ArgsPluginConduit(InitPluginConduit):
521
522 - def __init__(self, parent, base, conf, args):
523 InitPluginConduit.__init__(self, parent, base, conf) 524 self._args = args
525
526 - def getArgs(self):
527 return self._args
528
529 -class PreRepoSetupPluginConduit(InitPluginConduit):
530
531 - def getCmdLine(self):
532 '''Return parsed command line options. 533 534 @return: (options, commands) as returned by OptionParser.parse_args() 535 ''' 536 return self._parent.cmdline
537
538 - def getRpmDB(self):
539 '''Return a representation of local RPM database. This allows querying 540 of installed packages. 541 542 @return: rpmUtils.RpmDBHolder instance 543 ''' 544 return self._base.rpmdb
545
546 -class PostRepoSetupPluginConduit(PreRepoSetupPluginConduit):
547
548 - def getGroups(self):
549 '''Return group information. 550 551 @return: yum.comps.Comps instance 552 ''' 553 return self._base.comps
554
555 -class DownloadPluginConduit(PostRepoSetupPluginConduit):
556
557 - def __init__(self, parent, base, conf, pkglist, errors=None):
558 PostRepoSetupPluginConduit.__init__(self, parent, base, conf) 559 self._pkglist = pkglist 560 self._errors = errors
561
562 - def getDownloadPackages(self):
563 '''Return a list of package objects representing packages to be 564 downloaded. 565 ''' 566 return self._pkglist
567
568 - def getErrors(self):
569 '''Return a dictionary of download errors. 570 571 The returned dictionary is indexed by package object. Each element is a 572 list of strings describing the error. 573 ''' 574 if not self._errors: 575 return {} 576 return self._errors
577
578 -class MainPluginConduit(PostRepoSetupPluginConduit):
579
580 - def getPackages(self, repo=None):
581 if repo: 582 arg = repo.id 583 else: 584 arg = None 585 return self._base.pkgSack.returnPackages(arg)
586
587 - def getPackageByNevra(self, nevra):
588 '''Retrieve a package object from the packages loaded by Yum using 589 nevra information 590 591 @param nevra: A tuple holding (name, epoch, version, release, arch) 592 for a package 593 @return: A PackageObject instance (or subclass) 594 ''' 595 return self._base.getPackageObject(nevra)
596
597 - def delPackage(self, po):
598 po.repo.sack.delPackage(po)
599
600 - def getTsInfo(self):
601 return self._base.tsInfo
602
603 -class DepsolvePluginConduit(MainPluginConduit):
604 - def __init__(self, parent, base, conf, rescode=None, restring=[]):
605 MainPluginConduit.__init__(self, parent, base, conf) 606 self.resultcode = rescode 607 self.resultstring = restring
608
609 610 -def parsever(apiver):
611 maj, min = apiver.split('.') 612 return int(maj), int(min)
613
614 -def apiverok(a, b):
615 '''Return true if API version "a" supports API version "b" 616 ''' 617 a = parsever(a) 618 b = parsever(b) 619 620 if a[0] != b[0]: 621 return 0 622 623 if a[1] >= b[1]: 624 return 1 625 626 return 0
627