Package CedarBackup2 :: Module cli
[hide private]
[frames] | no frames]

Source Code for Module CedarBackup2.cli

   1  # -*- coding: iso-8859-1 -*- 
   2  # vim: set ft=python ts=3 sw=3 expandtab: 
   3  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
   4  # 
   5  #              C E D A R 
   6  #          S O L U T I O N S       "Software done right." 
   7  #           S O F T W A R E 
   8  # 
   9  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
  10  # 
  11  # Copyright (c) 2004-2007,2010 Kenneth J. Pronovici. 
  12  # All rights reserved. 
  13  # 
  14  # This program is free software; you can redistribute it and/or 
  15  # modify it under the terms of the GNU General Public License, 
  16  # Version 2, as published by the Free Software Foundation. 
  17  # 
  18  # This program is distributed in the hope that it will be useful, 
  19  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  20  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
  21  # 
  22  # Copies of the GNU General Public License are available from 
  23  # the Free Software Foundation website, http://www.gnu.org/. 
  24  # 
  25  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
  26  # 
  27  # Author   : Kenneth J. Pronovici <pronovic@ieee.org> 
  28  # Language : Python (>= 2.5) 
  29  # Project  : Cedar Backup, release 2 
  30  # Purpose  : Provides command-line interface implementation. 
  31  # 
  32  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
  33   
  34  ######################################################################## 
  35  # Module documentation 
  36  ######################################################################## 
  37   
  38  """ 
  39  Provides command-line interface implementation for the cback script. 
  40   
  41  Summary 
  42  ======= 
  43   
  44     The functionality in this module encapsulates the command-line interface for 
  45     the cback script.  The cback script itself is very short, basically just an 
  46     invokation of one function implemented here.  That, in turn, makes it 
  47     simpler to validate the command line interface (for instance, it's easier to 
  48     run pychecker against a module, and unit tests are easier, too). 
  49   
  50     The objects and functions implemented in this module are probably not useful 
  51     to any code external to Cedar Backup.   Anyone else implementing their own 
  52     command-line interface would have to reimplement (or at least enhance) all 
  53     of this anyway. 
  54   
  55  Backwards Compatibility 
  56  ======================= 
  57   
  58     The command line interface has changed between Cedar Backup 1.x and Cedar 
  59     Backup 2.x.  Some new switches have been added, and the actions have become 
  60     simple arguments rather than switches (which is a much more standard command 
  61     line format).  Old 1.x command lines are generally no longer valid. 
  62   
  63  @var DEFAULT_CONFIG: The default configuration file. 
  64  @var DEFAULT_LOGFILE: The default log file path. 
  65  @var DEFAULT_OWNERSHIP: Default ownership for the logfile. 
  66  @var DEFAULT_MODE: Default file permissions mode on the logfile. 
  67  @var VALID_ACTIONS: List of valid actions. 
  68  @var COMBINE_ACTIONS: List of actions which can be combined with other actions. 
  69  @var NONCOMBINE_ACTIONS: List of actions which cannot be combined with other actions. 
  70   
  71  @sort: cli, Options, DEFAULT_CONFIG, DEFAULT_LOGFILE, DEFAULT_OWNERSHIP,  
  72         DEFAULT_MODE, VALID_ACTIONS, COMBINE_ACTIONS, NONCOMBINE_ACTIONS 
  73   
  74  @author: Kenneth J. Pronovici <pronovic@ieee.org> 
  75  """ 
  76   
  77  ######################################################################## 
  78  # Imported modules 
  79  ######################################################################## 
  80   
  81  # System modules 
  82  import sys 
  83  import os 
  84  import logging 
  85  import getopt 
  86   
  87  # Cedar Backup modules 
  88  from CedarBackup2.release import AUTHOR, EMAIL, VERSION, DATE, COPYRIGHT 
  89  from CedarBackup2.customize import customizeOverrides 
  90  from CedarBackup2.util import DirectedGraph, PathResolverSingleton 
  91  from CedarBackup2.util import sortDict, splitCommandLine, executeCommand, getFunctionReference 
  92  from CedarBackup2.util import getUidGid, encodePath, Diagnostics 
  93  from CedarBackup2.config import Config 
  94  from CedarBackup2.peer import RemotePeer 
  95  from CedarBackup2.actions.collect import executeCollect 
  96  from CedarBackup2.actions.stage import executeStage 
  97  from CedarBackup2.actions.store import executeStore 
  98  from CedarBackup2.actions.purge import executePurge 
  99  from CedarBackup2.actions.rebuild import executeRebuild 
 100  from CedarBackup2.actions.validate import executeValidate 
 101  from CedarBackup2.actions.initialize import executeInitialize 
 102   
 103   
 104  ######################################################################## 
 105  # Module-wide constants and variables 
 106  ######################################################################## 
 107   
 108  logger = logging.getLogger("CedarBackup2.log.cli") 
 109   
 110  DISK_LOG_FORMAT    = "%(asctime)s --> [%(levelname)-7s] %(message)s" 
 111  DISK_OUTPUT_FORMAT = "%(message)s" 
 112  SCREEN_LOG_FORMAT  = "%(message)s" 
 113  SCREEN_LOG_STREAM  = sys.stdout 
 114  DATE_FORMAT        = "%Y-%m-%dT%H:%M:%S %Z" 
 115   
 116  DEFAULT_CONFIG     = "/etc/cback.conf" 
 117  DEFAULT_LOGFILE    = "/var/log/cback.log" 
 118  DEFAULT_OWNERSHIP  = [ "root", "adm", ] 
 119  DEFAULT_MODE       = 0640 
 120   
 121  REBUILD_INDEX      = 0        # can't run with anything else, anyway 
 122  VALIDATE_INDEX     = 0        # can't run with anything else, anyway 
 123  INITIALIZE_INDEX   = 0        # can't run with anything else, anyway 
 124  COLLECT_INDEX      = 100 
 125  STAGE_INDEX        = 200 
 126  STORE_INDEX        = 300 
 127  PURGE_INDEX        = 400 
 128   
 129  VALID_ACTIONS      = [ "collect", "stage", "store", "purge", "rebuild", "validate", "initialize", "all", ] 
 130  COMBINE_ACTIONS    = [ "collect", "stage", "store", "purge", ] 
 131  NONCOMBINE_ACTIONS = [ "rebuild", "validate", "initialize", "all", ] 
 132   
 133  SHORT_SWITCHES     = "hVbqc:fMNl:o:m:OdsD" 
 134  LONG_SWITCHES      = [ 'help', 'version', 'verbose', 'quiet',  
 135                         'config=', 'full', 'managed', 'managed-only', 
 136                         'logfile=', 'owner=', 'mode=',  
 137                         'output', 'debug', 'stack', 'diagnostics', ] 
138 139 140 ####################################################################### 141 # Public functions 142 ####################################################################### 143 144 ################# 145 # cli() function 146 ################# 147 148 -def cli():
149 """ 150 Implements the command-line interface for the C{cback} script. 151 152 Essentially, this is the "main routine" for the cback script. It does all 153 of the argument processing for the script, and then sets about executing the 154 indicated actions. 155 156 As a general rule, only the actions indicated on the command line will be 157 executed. We will accept any of the built-in actions and any of the 158 configured extended actions (which makes action list verification a two- 159 step process). 160 161 The C{'all'} action has a special meaning: it means that the built-in set of 162 actions (collect, stage, store, purge) will all be executed, in that order. 163 Extended actions will be ignored as part of the C{'all'} action. 164 165 Raised exceptions always result in an immediate return. Otherwise, we 166 generally return when all specified actions have been completed. Actions 167 are ignored if the help, version or validate flags are set. 168 169 A different error code is returned for each type of failure: 170 171 - C{1}: The Python interpreter version is < 2.5 172 - C{2}: Error processing command-line arguments 173 - C{3}: Error configuring logging 174 - C{4}: Error parsing indicated configuration file 175 - C{5}: Backup was interrupted with a CTRL-C or similar 176 - C{6}: Error executing specified backup actions 177 178 @note: This function contains a good amount of logging at the INFO level, 179 because this is the right place to document high-level flow of control (i.e. 180 what the command-line options were, what config file was being used, etc.) 181 182 @note: We assume that anything that I{must} be seen on the screen is logged 183 at the ERROR level. Errors that occur before logging can be configured are 184 written to C{sys.stderr}. 185 186 @return: Error code as described above. 187 """ 188 try: 189 if map(int, [sys.version_info[0], sys.version_info[1]]) < [2, 5]: 190 sys.stderr.write("Python version 2.5 or greater required.\n") 191 return 1 192 except: 193 # sys.version_info isn't available before 2.0 194 sys.stderr.write("Python version 2.5 or greater required.\n") 195 return 1 196 197 try: 198 options = Options(argumentList=sys.argv[1:]) 199 logger.info("Specified command-line actions: " % options.actions) 200 except Exception, e: 201 _usage() 202 sys.stderr.write(" *** Error: %s\n" % e) 203 return 2 204 205 if options.help: 206 _usage() 207 return 0 208 if options.version: 209 _version() 210 return 0 211 if options.diagnostics: 212 _diagnostics() 213 return 0 214 215 try: 216 logfile = setupLogging(options) 217 except Exception, e: 218 sys.stderr.write("Error setting up logging: %s\n" % e) 219 return 3 220 221 logger.info("Cedar Backup run started.") 222 logger.info("Options were [%s]" % options) 223 logger.info("Logfile is [%s]" % logfile) 224 Diagnostics().logDiagnostics(method=logger.info) 225 226 if options.config is None: 227 logger.debug("Using default configuration file.") 228 configPath = DEFAULT_CONFIG 229 else: 230 logger.debug("Using user-supplied configuration file.") 231 configPath = options.config 232 233 executeLocal = True 234 executeManaged = False 235 if options.managedOnly: 236 executeLocal = False 237 executeManaged = True 238 if options.managed: 239 executeManaged = True 240 logger.debug("Execute local actions: %s" % executeLocal) 241 logger.debug("Execute managed actions: %s" % executeManaged) 242 243 try: 244 logger.info("Configuration path is [%s]" % configPath) 245 config = Config(xmlPath=configPath) 246 customizeOverrides(config) 247 setupPathResolver(config) 248 actionSet = _ActionSet(options.actions, config.extensions, config.options, 249 config.peers, executeManaged, executeLocal) 250 except Exception, e: 251 logger.error("Error reading or handling configuration: %s" % e) 252 logger.info("Cedar Backup run completed with status 4.") 253 return 4 254 255 if options.stacktrace: 256 actionSet.executeActions(configPath, options, config) 257 else: 258 try: 259 actionSet.executeActions(configPath, options, config) 260 except KeyboardInterrupt: 261 logger.error("Backup interrupted.") 262 logger.info("Cedar Backup run completed with status 5.") 263 return 5 264 except Exception, e: 265 logger.error("Error executing backup: %s" % e) 266 logger.info("Cedar Backup run completed with status 6.") 267 return 6 268 269 logger.info("Cedar Backup run completed with status 0.") 270 return 0
271
272 273 ######################################################################## 274 # Action-related class definition 275 ######################################################################## 276 277 #################### 278 # _ActionItem class 279 #################### 280 281 -class _ActionItem(object):
282 283 """ 284 Class representing a single action to be executed. 285 286 This class represents a single named action to be executed, and understands 287 how to execute that action. 288 289 The built-in actions will use only the options and config values. We also 290 pass in the config path so that extension modules can re-parse configuration 291 if they want to, to add in extra information. 292 293 This class is also where pre-action and post-action hooks are executed. An 294 action item is instantiated in terms of optional pre- and post-action hook 295 objects (config.ActionHook), which are then executed at the appropriate time 296 (if set). 297 298 @note: The comparison operators for this class have been implemented to only 299 compare based on the index and SORT_ORDER value, and ignore all other 300 values. This is so that the action set list can be easily sorted first by 301 type (_ActionItem before _ManagedActionItem) and then by index within type. 302 303 @cvar SORT_ORDER: Defines a sort order to order properly between types. 304 """ 305 306 SORT_ORDER = 0 307
308 - def __init__(self, index, name, preHook, postHook, function):
309 """ 310 Default constructor. 311 312 It's OK to pass C{None} for C{index}, C{preHook} or C{postHook}, but not 313 for C{name}. 314 315 @param index: Index of the item (or C{None}). 316 @param name: Name of the action that is being executed. 317 @param preHook: Pre-action hook in terms of an C{ActionHook} object, or C{None}. 318 @param postHook: Post-action hook in terms of an C{ActionHook} object, or C{None}. 319 @param function: Reference to function associated with item. 320 """ 321 self.index = index 322 self.name = name 323 self.preHook = preHook 324 self.postHook = postHook 325 self.function = function
326
327 - def __cmp__(self, other):
328 """ 329 Definition of equals operator for this class. 330 The only thing we compare is the item's index. 331 @param other: Other object to compare to. 332 @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other. 333 """ 334 if other is None: 335 return 1 336 if self.index != other.index: 337 if self.index < other.index: 338 return -1 339 else: 340 return 1 341 else: 342 if self.SORT_ORDER != other.SORT_ORDER: 343 if self.SORT_ORDER < other.SORT_ORDER: 344 return -1 345 else: 346 return 1 347 return 0
348
349 - def executeAction(self, configPath, options, config):
350 """ 351 Executes the action associated with an item, including hooks. 352 353 See class notes for more details on how the action is executed. 354 355 @param configPath: Path to configuration file on disk. 356 @param options: Command-line options to be passed to action. 357 @param config: Parsed configuration to be passed to action. 358 359 @raise Exception: If there is a problem executing the action. 360 """ 361 logger.debug("Executing [%s] action." % self.name) 362 if self.preHook is not None: 363 self._executeHook("pre-action", self.preHook) 364 self._executeAction(configPath, options, config) 365 if self.postHook is not None: 366 self._executeHook("post-action", self.postHook)
367
368 - def _executeAction(self, configPath, options, config):
369 """ 370 Executes the action, specifically the function associated with the action. 371 @param configPath: Path to configuration file on disk. 372 @param options: Command-line options to be passed to action. 373 @param config: Parsed configuration to be passed to action. 374 """ 375 name = "%s.%s" % (self.function.__module__, self.function.__name__) 376 logger.debug("Calling action function [%s], execution index [%d]" % (name, self.index)) 377 self.function(configPath, options, config)
378
379 - def _executeHook(self, type, hook): # pylint: disable=W0622,R0201
380 """ 381 Executes a hook command via L{util.executeCommand()}. 382 @param type: String describing the type of hook, for logging. 383 @param hook: Hook, in terms of a C{ActionHook} object. 384 """ 385 logger.debug("Executing %s hook for action [%s]." % (type, hook.action)) 386 fields = splitCommandLine(hook.command) 387 executeCommand(command=fields[0:1], args=fields[1:])
388
389 390 ########################### 391 # _ManagedActionItem class 392 ########################### 393 394 -class _ManagedActionItem(object):
395 396 """ 397 Class representing a single action to be executed on a managed peer. 398 399 This class represents a single named action to be executed, and understands 400 how to execute that action. 401 402 Actions to be executed on a managed peer rely on peer configuration and 403 on the full-backup flag. All other configuration takes place on the remote 404 peer itself. 405 406 @note: The comparison operators for this class have been implemented to only 407 compare based on the index and SORT_ORDER value, and ignore all other 408 values. This is so that the action set list can be easily sorted first by 409 type (_ActionItem before _ManagedActionItem) and then by index within type. 410 411 @cvar SORT_ORDER: Defines a sort order to order properly between types. 412 """ 413 414 SORT_ORDER = 1 415
416 - def __init__(self, index, name, remotePeers):
417 """ 418 Default constructor. 419 420 @param index: Index of the item (or C{None}). 421 @param name: Name of the action that is being executed. 422 @param remotePeers: List of remote peers on which to execute the action. 423 """ 424 self.index = index 425 self.name = name 426 self.remotePeers = remotePeers
427
428 - def __cmp__(self, other):
429 """ 430 Definition of equals operator for this class. 431 The only thing we compare is the item's index. 432 @param other: Other object to compare to. 433 @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other. 434 """ 435 if other is None: 436 return 1 437 if self.index != other.index: 438 if self.index < other.index: 439 return -1 440 else: 441 return 1 442 else: 443 if self.SORT_ORDER != other.SORT_ORDER: 444 if self.SORT_ORDER < other.SORT_ORDER: 445 return -1 446 else: 447 return 1 448 return 0
449
450 - def executeAction(self, configPath, options, config):
451 """ 452 Executes the managed action associated with an item. 453 454 @note: Only options.full is actually used. The rest of the arguments 455 exist to satisfy the ActionItem iterface. 456 457 @note: Errors here result in a message logged to ERROR, but no thrown 458 exception. The analogy is the stage action where a problem with one host 459 should not kill the entire backup. Since we're logging an error, the 460 administrator will get an email. 461 462 @param configPath: Path to configuration file on disk. 463 @param options: Command-line options to be passed to action. 464 @param config: Parsed configuration to be passed to action. 465 466 @raise Exception: If there is a problem executing the action. 467 """ 468 for peer in self.remotePeers: 469 logger.debug("Executing managed action [%s] on peer [%s]." % (self.name, peer.name)) 470 try: 471 peer.executeManagedAction(self.name, options.full) 472 except IOError, e: 473 logger.error(e) # log the message and go on, so we don't kill the backup
474
475 476 ################### 477 # _ActionSet class 478 ################### 479 480 -class _ActionSet(object):
481 482 """ 483 Class representing a set of local actions to be executed. 484 485 This class does four different things. First, it ensures that the actions 486 specified on the command-line are sensible. The command-line can only list 487 either built-in actions or extended actions specified in configuration. 488 Also, certain actions (in L{NONCOMBINE_ACTIONS}) cannot be combined with 489 other actions. 490 491 Second, the class enforces an execution order on the specified actions. Any 492 time actions are combined on the command line (either built-in actions or 493 extended actions), we must make sure they get executed in a sensible order. 494 495 Third, the class ensures that any pre-action or post-action hooks are 496 scheduled and executed appropriately. Hooks are configured by building a 497 dictionary mapping between hook action name and command. Pre-action hooks 498 are executed immediately before their associated action, and post-action 499 hooks are executed immediately after their associated action. 500 501 Finally, the class properly interleaves local and managed actions so that 502 the same action gets executed first locally and then on managed peers. 503 504 @sort: __init__, executeActions 505 """ 506
507 - def __init__(self, actions, extensions, options, peers, managed, local):
508 """ 509 Constructor for the C{_ActionSet} class. 510 511 This is kind of ugly, because the constructor has to set up a lot of data 512 before being able to do anything useful. The following data structures 513 are initialized based on the input: 514 515 - C{extensionNames}: List of extensions available in configuration 516 - C{preHookMap}: Mapping from action name to pre C{ActionHook} 517 - C{preHookMap}: Mapping from action name to post C{ActionHook} 518 - C{functionMap}: Mapping from action name to Python function 519 - C{indexMap}: Mapping from action name to execution index 520 - C{peerMap}: Mapping from action name to set of C{RemotePeer} 521 - C{actionMap}: Mapping from action name to C{_ActionItem} 522 523 Once these data structures are set up, the command line is validated to 524 make sure only valid actions have been requested, and in a sensible 525 combination. Then, all of the data is used to build C{self.actionSet}, 526 the set action items to be executed by C{executeActions()}. This list 527 might contain either C{_ActionItem} or C{_ManagedActionItem}. 528 529 @param actions: Names of actions specified on the command-line. 530 @param extensions: Extended action configuration (i.e. config.extensions) 531 @param options: Options configuration (i.e. config.options) 532 @param peers: Peers configuration (i.e. config.peers) 533 @param managed: Whether to include managed actions in the set 534 @param local: Whether to include local actions in the set 535 536 @raise ValueError: If one of the specified actions is invalid. 537 """ 538 extensionNames = _ActionSet._deriveExtensionNames(extensions) 539 (preHookMap, postHookMap) = _ActionSet._buildHookMaps(options.hooks) 540 functionMap = _ActionSet._buildFunctionMap(extensions) 541 indexMap = _ActionSet._buildIndexMap(extensions) 542 peerMap = _ActionSet._buildPeerMap(options, peers) 543 actionMap = _ActionSet._buildActionMap(managed, local, extensionNames, functionMap, 544 indexMap, preHookMap, postHookMap, peerMap) 545 _ActionSet._validateActions(actions, extensionNames) 546 self.actionSet = _ActionSet._buildActionSet(actions, actionMap)
547 548 @staticmethod
549 - def _deriveExtensionNames(extensions):
550 """ 551 Builds a list of extended actions that are available in configuration. 552 @param extensions: Extended action configuration (i.e. config.extensions) 553 @return: List of extended action names. 554 """ 555 extensionNames = [] 556 if extensions is not None and extensions.actions is not None: 557 for action in extensions.actions: 558 extensionNames.append(action.name) 559 return extensionNames
560 561 @staticmethod
562 - def _buildHookMaps(hooks):
563 """ 564 Build two mappings from action name to configured C{ActionHook}. 565 @param hooks: List of pre- and post-action hooks (i.e. config.options.hooks) 566 @return: Tuple of (pre hook dictionary, post hook dictionary). 567 """ 568 preHookMap = {} 569 postHookMap = {} 570 if hooks is not None: 571 for hook in hooks: 572 if hook.before: 573 preHookMap[hook.action] = hook 574 elif hook.after: 575 postHookMap[hook.action] = hook 576 return (preHookMap, postHookMap)
577 578 @staticmethod
579 - def _buildFunctionMap(extensions):
580 """ 581 Builds a mapping from named action to action function. 582 @param extensions: Extended action configuration (i.e. config.extensions) 583 @return: Dictionary mapping action to function. 584 """ 585 functionMap = {} 586 functionMap['rebuild'] = executeRebuild 587 functionMap['validate'] = executeValidate 588 functionMap['initialize'] = executeInitialize 589 functionMap['collect'] = executeCollect 590 functionMap['stage'] = executeStage 591 functionMap['store'] = executeStore 592 functionMap['purge'] = executePurge 593 if extensions is not None and extensions.actions is not None: 594 for action in extensions.actions: 595 functionMap[action.name] = getFunctionReference(action.module, action.function) 596 return functionMap
597 598 @staticmethod
599 - def _buildIndexMap(extensions):
600 """ 601 Builds a mapping from action name to proper execution index. 602 603 If extensions configuration is C{None}, or there are no configured 604 extended actions, the ordering dictionary will only include the built-in 605 actions and their standard indices. 606 607 Otherwise, if the extensions order mode is C{None} or C{"index"}, actions 608 will scheduled by explicit index; and if the extensions order mode is 609 C{"dependency"}, actions will be scheduled using a dependency graph. 610 611 @param extensions: Extended action configuration (i.e. config.extensions) 612 613 @return: Dictionary mapping action name to integer execution index. 614 """ 615 indexMap = {} 616 if extensions is None or extensions.actions is None or extensions.actions == []: 617 logger.info("Action ordering will use 'index' order mode.") 618 indexMap['rebuild'] = REBUILD_INDEX 619 indexMap['validate'] = VALIDATE_INDEX 620 indexMap['initialize'] = INITIALIZE_INDEX 621 indexMap['collect'] = COLLECT_INDEX 622 indexMap['stage'] = STAGE_INDEX 623 indexMap['store'] = STORE_INDEX 624 indexMap['purge'] = PURGE_INDEX 625 logger.debug("Completed filling in action indices for built-in actions.") 626 logger.info("Action order will be: %s" % sortDict(indexMap)) 627 else: 628 if extensions.orderMode is None or extensions.orderMode == "index": 629 logger.info("Action ordering will use 'index' order mode.") 630 indexMap['rebuild'] = REBUILD_INDEX 631 indexMap['validate'] = VALIDATE_INDEX 632 indexMap['initialize'] = INITIALIZE_INDEX 633 indexMap['collect'] = COLLECT_INDEX 634 indexMap['stage'] = STAGE_INDEX 635 indexMap['store'] = STORE_INDEX 636 indexMap['purge'] = PURGE_INDEX 637 logger.debug("Completed filling in action indices for built-in actions.") 638 for action in extensions.actions: 639 indexMap[action.name] = action.index 640 logger.debug("Completed filling in action indices for extended actions.") 641 logger.info("Action order will be: %s" % sortDict(indexMap)) 642 else: 643 logger.info("Action ordering will use 'dependency' order mode.") 644 graph = DirectedGraph("dependencies") 645 graph.createVertex("rebuild") 646 graph.createVertex("validate") 647 graph.createVertex("initialize") 648 graph.createVertex("collect") 649 graph.createVertex("stage") 650 graph.createVertex("store") 651 graph.createVertex("purge") 652 for action in extensions.actions: 653 graph.createVertex(action.name) 654 graph.createEdge("collect", "stage") # Collect must run before stage, store or purge 655 graph.createEdge("collect", "store") 656 graph.createEdge("collect", "purge") 657 graph.createEdge("stage", "store") # Stage must run before store or purge 658 graph.createEdge("stage", "purge") 659 graph.createEdge("store", "purge") # Store must run before purge 660 for action in extensions.actions: 661 if action.dependencies.beforeList is not None: 662 for vertex in action.dependencies.beforeList: 663 try: 664 graph.createEdge(action.name, vertex) # actions that this action must be run before 665 except ValueError: 666 logger.error("Dependency [%s] on extension [%s] is unknown." % (vertex, action.name)) 667 raise ValueError("Unable to determine proper action order due to invalid dependency.") 668 if action.dependencies.afterList is not None: 669 for vertex in action.dependencies.afterList: 670 try: 671 graph.createEdge(vertex, action.name) # actions that this action must be run after 672 except ValueError: 673 logger.error("Dependency [%s] on extension [%s] is unknown." % (vertex, action.name)) 674 raise ValueError("Unable to determine proper action order due to invalid dependency.") 675 try: 676 ordering = graph.topologicalSort() 677 indexMap = dict([(ordering[i], i+1) for i in range(0, len(ordering))]) 678 logger.info("Action order will be: %s" % ordering) 679 except ValueError: 680 logger.error("Unable to determine proper action order due to dependency recursion.") 681 logger.error("Extensions configuration is invalid (check for loops).") 682 raise ValueError("Unable to determine proper action order due to dependency recursion.") 683 return indexMap
684 685 @staticmethod
686 - def _buildActionMap(managed, local, extensionNames, functionMap, indexMap, preHookMap, postHookMap, peerMap):
687 """ 688 Builds a mapping from action name to list of action items. 689 690 We build either C{_ActionItem} or C{_ManagedActionItem} objects here. 691 692 In most cases, the mapping from action name to C{_ActionItem} is 1:1. 693 The exception is the "all" action, which is a special case. However, a 694 list is returned in all cases, just for consistency later. Each 695 C{_ActionItem} will be created with a proper function reference and index 696 value for execution ordering. 697 698 The mapping from action name to C{_ManagedActionItem} is always 1:1. 699 Each managed action item contains a list of peers which the action should 700 be executed. 701 702 @param managed: Whether to include managed actions in the set 703 @param local: Whether to include local actions in the set 704 @param extensionNames: List of valid extended action names 705 @param functionMap: Dictionary mapping action name to Python function 706 @param indexMap: Dictionary mapping action name to integer execution index 707 @param preHookMap: Dictionary mapping action name to pre hooks (if any) for the action 708 @param postHookMap: Dictionary mapping action name to post hooks (if any) for the action 709 @param peerMap: Dictionary mapping action name to list of remote peers on which to execute the action 710 711 @return: Dictionary mapping action name to list of C{_ActionItem} objects. 712 """ 713 actionMap = {} 714 for name in extensionNames + VALID_ACTIONS: 715 if name != 'all': # do this one later 716 function = functionMap[name] 717 index = indexMap[name] 718 actionMap[name] = [] 719 if local: 720 (preHook, postHook) = _ActionSet._deriveHooks(name, preHookMap, postHookMap) 721 actionMap[name].append(_ActionItem(index, name, preHook, postHook, function)) 722 if managed: 723 if name in peerMap: 724 actionMap[name].append(_ManagedActionItem(index, name, peerMap[name])) 725 actionMap['all'] = actionMap['collect'] + actionMap['stage'] + actionMap['store'] + actionMap['purge'] 726 return actionMap
727 728 @staticmethod
729 - def _buildPeerMap(options, peers):
730 """ 731 Build a mapping from action name to list of remote peers. 732 733 There will be one entry in the mapping for each managed action. If there 734 are no managed peers, the mapping will be empty. Only managed actions 735 will be listed in the mapping. 736 737 @param options: Option configuration (i.e. config.options) 738 @param peers: Peers configuration (i.e. config.peers) 739 """ 740 peerMap = {} 741 if peers is not None: 742 if peers.remotePeers is not None: 743 for peer in peers.remotePeers: 744 if peer.managed: 745 remoteUser = _ActionSet._getRemoteUser(options, peer) 746 rshCommand = _ActionSet._getRshCommand(options, peer) 747 cbackCommand = _ActionSet._getCbackCommand(options, peer) 748 managedActions = _ActionSet._getManagedActions(options, peer) 749 remotePeer = RemotePeer(peer.name, None, options.workingDir, remoteUser, None, 750 options.backupUser, rshCommand, cbackCommand) 751 if managedActions is not None: 752 for managedAction in managedActions: 753 if managedAction in peerMap: 754 if remotePeer not in peerMap[managedAction]: 755 peerMap[managedAction].append(remotePeer) 756 else: 757 peerMap[managedAction] = [ remotePeer, ] 758 return peerMap
759 760 @staticmethod
761 - def _deriveHooks(action, preHookDict, postHookDict):
762 """ 763 Derive pre- and post-action hooks, if any, associated with named action. 764 @param action: Name of action to look up 765 @param preHookDict: Dictionary mapping pre-action hooks to action name 766 @param postHookDict: Dictionary mapping post-action hooks to action name 767 @return Tuple (preHook, postHook) per mapping, with None values if there is no hook. 768 """ 769 preHook = None 770 postHook = None 771 if preHookDict.has_key(action): 772 preHook = preHookDict[action] 773 if postHookDict.has_key(action): 774 postHook = postHookDict[action] 775 return (preHook, postHook)
776 777 @staticmethod
778 - def _validateActions(actions, extensionNames):
779 """ 780 Validate that the set of specified actions is sensible. 781 782 Any specified action must either be a built-in action or must be among 783 the extended actions defined in configuration. The actions from within 784 L{NONCOMBINE_ACTIONS} may not be combined with other actions. 785 786 @param actions: Names of actions specified on the command-line. 787 @param extensionNames: Names of extensions specified in configuration. 788 789 @raise ValueError: If one or more configured actions are not valid. 790 """ 791 if actions is None or actions == []: 792 raise ValueError("No actions specified.") 793 for action in actions: 794 if action not in VALID_ACTIONS and action not in extensionNames: 795 raise ValueError("Action [%s] is not a valid action or extended action." % action) 796 for action in NONCOMBINE_ACTIONS: 797 if action in actions and actions != [ action, ]: 798 raise ValueError("Action [%s] may not be combined with other actions." % action)
799 800 @staticmethod
801 - def _buildActionSet(actions, actionMap):
802 """ 803 Build set of actions to be executed. 804 805 The set of actions is built in the proper order, so C{executeActions} can 806 spin through the set without thinking about it. Since we've already validated 807 that the set of actions is sensible, we don't take any precautions here to 808 make sure things are combined properly. If the action is listed, it will 809 be "scheduled" for execution. 810 811 @param actions: Names of actions specified on the command-line. 812 @param actionMap: Dictionary mapping action name to C{_ActionItem} object. 813 814 @return: Set of action items in proper order. 815 """ 816 actionSet = [] 817 for action in actions: 818 actionSet.extend(actionMap[action]) 819 actionSet.sort() # sort the actions in order by index 820 return actionSet
821
822 - def executeActions(self, configPath, options, config):
823 """ 824 Executes all actions and extended actions, in the proper order. 825 826 Each action (whether built-in or extension) is executed in an identical 827 manner. The built-in actions will use only the options and config 828 values. We also pass in the config path so that extension modules can 829 re-parse configuration if they want to, to add in extra information. 830 831 @param configPath: Path to configuration file on disk. 832 @param options: Command-line options to be passed to action functions. 833 @param config: Parsed configuration to be passed to action functions. 834 835 @raise Exception: If there is a problem executing the actions. 836 """ 837 logger.debug("Executing local actions.") 838 for actionItem in self.actionSet: 839 actionItem.executeAction(configPath, options, config)
840 841 @staticmethod
842 - def _getRemoteUser(options, remotePeer):
843 """ 844 Gets the remote user associated with a remote peer. 845 Use peer's if possible, otherwise take from options section. 846 @param options: OptionsConfig object, as from config.options 847 @param remotePeer: Configuration-style remote peer object. 848 @return: Name of remote user associated with remote peer. 849 """ 850 if remotePeer.remoteUser is None: 851 return options.backupUser 852 return remotePeer.remoteUser
853 854 @staticmethod
855 - def _getRshCommand(options, remotePeer):
856 """ 857 Gets the RSH command associated with a remote peer. 858 Use peer's if possible, otherwise take from options section. 859 @param options: OptionsConfig object, as from config.options 860 @param remotePeer: Configuration-style remote peer object. 861 @return: RSH command associated with remote peer. 862 """ 863 if remotePeer.rshCommand is None: 864 return options.rshCommand 865 return remotePeer.rshCommand
866 867 @staticmethod
868 - def _getCbackCommand(options, remotePeer):
869 """ 870 Gets the cback command associated with a remote peer. 871 Use peer's if possible, otherwise take from options section. 872 @param options: OptionsConfig object, as from config.options 873 @param remotePeer: Configuration-style remote peer object. 874 @return: cback command associated with remote peer. 875 """ 876 if remotePeer.cbackCommand is None: 877 return options.cbackCommand 878 return remotePeer.cbackCommand
879 880 @staticmethod
881 - def _getManagedActions(options, remotePeer):
882 """ 883 Gets the managed actions list associated with a remote peer. 884 Use peer's if possible, otherwise take from options section. 885 @param options: OptionsConfig object, as from config.options 886 @param remotePeer: Configuration-style remote peer object. 887 @return: Set of managed actions associated with remote peer. 888 """ 889 if remotePeer.managedActions is None: 890 return options.managedActions 891 return remotePeer.managedActions
892
893 894 ####################################################################### 895 # Utility functions 896 ####################################################################### 897 898 #################### 899 # _usage() function 900 #################### 901 902 -def _usage(fd=sys.stderr):
903 """ 904 Prints usage information for the cback script. 905 @param fd: File descriptor used to print information. 906 @note: The C{fd} is used rather than C{print} to facilitate unit testing. 907 """ 908 fd.write("\n") 909 fd.write(" Usage: cback [switches] action(s)\n") 910 fd.write("\n") 911 fd.write(" The following switches are accepted:\n") 912 fd.write("\n") 913 fd.write(" -h, --help Display this usage/help listing\n") 914 fd.write(" -V, --version Display version information\n") 915 fd.write(" -b, --verbose Print verbose output as well as logging to disk\n") 916 fd.write(" -q, --quiet Run quietly (display no output to the screen)\n") 917 fd.write(" -c, --config Path to config file (default: %s)\n" % DEFAULT_CONFIG) 918 fd.write(" -f, --full Perform a full backup, regardless of configuration\n") 919 fd.write(" -M, --managed Include managed clients when executing actions\n") 920 fd.write(" -N, --managed-only Include ONLY managed clients when executing actions\n") 921 fd.write(" -l, --logfile Path to logfile (default: %s)\n" % DEFAULT_LOGFILE) 922 fd.write(" -o, --owner Logfile ownership, user:group (default: %s:%s)\n" % (DEFAULT_OWNERSHIP[0], DEFAULT_OWNERSHIP[1])) 923 fd.write(" -m, --mode Octal logfile permissions mode (default: %o)\n" % DEFAULT_MODE) 924 fd.write(" -O, --output Record some sub-command (i.e. cdrecord) output to the log\n") 925 fd.write(" -d, --debug Write debugging information to the log (implies --output)\n") 926 fd.write(" -s, --stack Dump a Python stack trace instead of swallowing exceptions\n") # exactly 80 characters in width! 927 fd.write(" -D, --diagnostics Print runtime diagnostics to the screen and exit\n") 928 fd.write("\n") 929 fd.write(" The following actions may be specified:\n") 930 fd.write("\n") 931 fd.write(" all Take all normal actions (collect, stage, store, purge)\n") 932 fd.write(" collect Take the collect action\n") 933 fd.write(" stage Take the stage action\n") 934 fd.write(" store Take the store action\n") 935 fd.write(" purge Take the purge action\n") 936 fd.write(" rebuild Rebuild \"this week's\" disc if possible\n") 937 fd.write(" validate Validate configuration only\n") 938 fd.write(" initialize Initialize media for use with Cedar Backup\n") 939 fd.write("\n") 940 fd.write(" You may also specify extended actions that have been defined in\n") 941 fd.write(" configuration.\n") 942 fd.write("\n") 943 fd.write(" You must specify at least one action to take. More than one of\n") 944 fd.write(" the \"collect\", \"stage\", \"store\" or \"purge\" actions and/or\n") 945 fd.write(" extended actions may be specified in any arbitrary order; they\n") 946 fd.write(" will be executed in a sensible order. The \"all\", \"rebuild\",\n") 947 fd.write(" \"validate\", and \"initialize\" actions may not be combined with\n") 948 fd.write(" other actions.\n") 949 fd.write("\n")
950
951 952 ###################### 953 # _version() function 954 ###################### 955 956 -def _version(fd=sys.stdout):
957 """ 958 Prints version information for the cback script. 959 @param fd: File descriptor used to print information. 960 @note: The C{fd} is used rather than C{print} to facilitate unit testing. 961 """ 962 fd.write("\n") 963 fd.write(" Cedar Backup version %s, released %s.\n" % (VERSION, DATE)) 964 fd.write("\n") 965 fd.write(" Copyright (c) %s %s <%s>.\n" % (COPYRIGHT, AUTHOR, EMAIL)) 966 fd.write(" See CREDITS for a list of included code and other contributors.\n") 967 fd.write(" This is free software; there is NO warranty. See the\n") 968 fd.write(" GNU General Public License version 2 for copying conditions.\n") 969 fd.write("\n") 970 fd.write(" Use the --help option for usage information.\n") 971 fd.write("\n")
972
973 974 ########################## 975 # _diagnostics() function 976 ########################## 977 978 -def _diagnostics(fd=sys.stdout):
979 """ 980 Prints runtime diagnostics information. 981 @param fd: File descriptor used to print information. 982 @note: The C{fd} is used rather than C{print} to facilitate unit testing. 983 """ 984 fd.write("\n") 985 fd.write("Diagnostics:\n") 986 fd.write("\n") 987 Diagnostics().printDiagnostics(fd=fd, prefix=" ") 988 fd.write("\n")
989
990 991 ########################## 992 # setupLogging() function 993 ########################## 994 995 -def setupLogging(options):
996 """ 997 Set up logging based on command-line options. 998 999 There are two kinds of logging: flow logging and output logging. Output 1000 logging contains information about system commands executed by Cedar Backup, 1001 for instance the calls to C{mkisofs} or C{mount}, etc. Flow logging 1002 contains error and informational messages used to understand program flow. 1003 Flow log messages and output log messages are written to two different 1004 loggers target (C{CedarBackup2.log} and C{CedarBackup2.output}). Flow log 1005 messages are written at the ERROR, INFO and DEBUG log levels, while output 1006 log messages are generally only written at the INFO log level. 1007 1008 By default, output logging is disabled. When the C{options.output} or 1009 C{options.debug} flags are set, output logging will be written to the 1010 configured logfile. Output logging is never written to the screen. 1011 1012 By default, flow logging is enabled at the ERROR level to the screen and at 1013 the INFO level to the configured logfile. If the C{options.quiet} flag is 1014 set, flow logging is enabled at the INFO level to the configured logfile 1015 only (i.e. no output will be sent to the screen). If the C{options.verbose} 1016 flag is set, flow logging is enabled at the INFO level to both the screen 1017 and the configured logfile. If the C{options.debug} flag is set, flow 1018 logging is enabled at the DEBUG level to both the screen and the configured 1019 logfile. 1020 1021 @param options: Command-line options. 1022 @type options: L{Options} object 1023 1024 @return: Path to logfile on disk. 1025 """ 1026 logfile = _setupLogfile(options) 1027 _setupFlowLogging(logfile, options) 1028 _setupOutputLogging(logfile, options) 1029 return logfile
1030
1031 -def _setupLogfile(options):
1032 """ 1033 Sets up and creates logfile as needed. 1034 1035 If the logfile already exists on disk, it will be left as-is, under the 1036 assumption that it was created with appropriate ownership and permissions. 1037 If the logfile does not exist on disk, it will be created as an empty file. 1038 Ownership and permissions will remain at their defaults unless user/group 1039 and/or mode are set in the options. We ignore errors setting the indicated 1040 user and group. 1041 1042 @note: This function is vulnerable to a race condition. If the log file 1043 does not exist when the function is run, it will attempt to create the file 1044 as safely as possible (using C{O_CREAT}). If two processes attempt to 1045 create the file at the same time, then one of them will fail. In practice, 1046 this shouldn't really be a problem, but it might happen occassionally if two 1047 instances of cback run concurrently or if cback collides with logrotate or 1048 something. 1049 1050 @param options: Command-line options. 1051 1052 @return: Path to logfile on disk. 1053 """ 1054 if options.logfile is None: 1055 logfile = DEFAULT_LOGFILE 1056 else: 1057 logfile = options.logfile 1058 if not os.path.exists(logfile): 1059 if options.mode is None: 1060 os.fdopen(os.open(logfile, os.O_RDWR|os.O_CREAT|os.O_APPEND, DEFAULT_MODE), "a+").write("") 1061 else: 1062 os.fdopen(os.open(logfile, os.O_RDWR|os.O_CREAT|os.O_APPEND, options.mode), "a+").write("") 1063 try: 1064 if options.owner is None or len(options.owner) < 2: 1065 (uid, gid) = getUidGid(DEFAULT_OWNERSHIP[0], DEFAULT_OWNERSHIP[1]) 1066 else: 1067 (uid, gid) = getUidGid(options.owner[0], options.owner[1]) 1068 os.chown(logfile, uid, gid) 1069 except: pass 1070 return logfile
1071
1072 -def _setupFlowLogging(logfile, options):
1073 """ 1074 Sets up flow logging. 1075 @param logfile: Path to logfile on disk. 1076 @param options: Command-line options. 1077 """ 1078 flowLogger = logging.getLogger("CedarBackup2.log") 1079 flowLogger.setLevel(logging.DEBUG) # let the logger see all messages 1080 _setupDiskFlowLogging(flowLogger, logfile, options) 1081 _setupScreenFlowLogging(flowLogger, options)
1082
1083 -def _setupOutputLogging(logfile, options):
1084 """ 1085 Sets up command output logging. 1086 @param logfile: Path to logfile on disk. 1087 @param options: Command-line options. 1088 """ 1089 outputLogger = logging.getLogger("CedarBackup2.output") 1090 outputLogger.setLevel(logging.DEBUG) # let the logger see all messages 1091 _setupDiskOutputLogging(outputLogger, logfile, options)
1092
1093 -def _setupDiskFlowLogging(flowLogger, logfile, options):
1094 """ 1095 Sets up on-disk flow logging. 1096 @param flowLogger: Python flow logger object. 1097 @param logfile: Path to logfile on disk. 1098 @param options: Command-line options. 1099 """ 1100 formatter = logging.Formatter(fmt=DISK_LOG_FORMAT, datefmt=DATE_FORMAT) 1101 handler = logging.FileHandler(logfile, mode="a") 1102 handler.setFormatter(formatter) 1103 if options.debug: 1104 handler.setLevel(logging.DEBUG) 1105 else: 1106 handler.setLevel(logging.INFO) 1107 flowLogger.addHandler(handler)
1108
1109 -def _setupScreenFlowLogging(flowLogger, options):
1110 """ 1111 Sets up on-screen flow logging. 1112 @param flowLogger: Python flow logger object. 1113 @param options: Command-line options. 1114 """ 1115 formatter = logging.Formatter(fmt=SCREEN_LOG_FORMAT) 1116 handler = logging.StreamHandler(SCREEN_LOG_STREAM) 1117 handler.setFormatter(formatter) 1118 if options.quiet: 1119 handler.setLevel(logging.CRITICAL) # effectively turn it off 1120 elif options.verbose: 1121 if options.debug: 1122 handler.setLevel(logging.DEBUG) 1123 else: 1124 handler.setLevel(logging.INFO) 1125 else: 1126 handler.setLevel(logging.ERROR) 1127 flowLogger.addHandler(handler)
1128
1129 -def _setupDiskOutputLogging(outputLogger, logfile, options):
1130 """ 1131 Sets up on-disk command output logging. 1132 @param outputLogger: Python command output logger object. 1133 @param logfile: Path to logfile on disk. 1134 @param options: Command-line options. 1135 """ 1136 formatter = logging.Formatter(fmt=DISK_OUTPUT_FORMAT, datefmt=DATE_FORMAT) 1137 handler = logging.FileHandler(logfile, mode="a") 1138 handler.setFormatter(formatter) 1139 if options.debug or options.output: 1140 handler.setLevel(logging.DEBUG) 1141 else: 1142 handler.setLevel(logging.CRITICAL) # effectively turn it off 1143 outputLogger.addHandler(handler)
1144
1145 1146 ############################### 1147 # setupPathResolver() function 1148 ############################### 1149 1150 -def setupPathResolver(config):
1151 """ 1152 Set up the path resolver singleton based on configuration. 1153 1154 Cedar Backup's path resolver is implemented in terms of a singleton, the 1155 L{PathResolverSingleton} class. This function takes options configuration, 1156 converts it into the dictionary form needed by the singleton, and then 1157 initializes the singleton. After that, any function that needs to resolve 1158 the path of a command can use the singleton. 1159 1160 @param config: Configuration 1161 @type config: L{Config} object 1162 """ 1163 mapping = {} 1164 if config.options.overrides is not None: 1165 for override in config.options.overrides: 1166 mapping[override.command] = override.absolutePath 1167 singleton = PathResolverSingleton() 1168 singleton.fill(mapping)
1169
1170 1171 ######################################################################### 1172 # Options class definition 1173 ######################################################################## 1174 1175 -class Options(object):
1176 1177 ###################### 1178 # Class documentation 1179 ###################### 1180 1181 """ 1182 Class representing command-line options for the cback script. 1183 1184 The C{Options} class is a Python object representation of the command-line 1185 options of the cback script. 1186 1187 The object representation is two-way: a command line string or a list of 1188 command line arguments can be used to create an C{Options} object, and then 1189 changes to the object can be propogated back to a list of command-line 1190 arguments or to a command-line string. An C{Options} object can even be 1191 created from scratch programmatically (if you have a need for that). 1192 1193 There are two main levels of validation in the C{Options} class. The first 1194 is field-level validation. Field-level validation comes into play when a 1195 given field in an object is assigned to or updated. We use Python's 1196 C{property} functionality to enforce specific validations on field values, 1197 and in some places we even use customized list classes to enforce 1198 validations on list members. You should expect to catch a C{ValueError} 1199 exception when making assignments to fields if you are programmatically 1200 filling an object. 1201 1202 The second level of validation is post-completion validation. Certain 1203 validations don't make sense until an object representation of options is 1204 fully "complete". We don't want these validations to apply all of the time, 1205 because it would make building up a valid object from scratch a real pain. 1206 For instance, we might have to do things in the right order to keep from 1207 throwing exceptions, etc. 1208 1209 All of these post-completion validations are encapsulated in the 1210 L{Options.validate} method. This method can be called at any time by a 1211 client, and will always be called immediately after creating a C{Options} 1212 object from a command line and before exporting a C{Options} object back to 1213 a command line. This way, we get acceptable ease-of-use but we also don't 1214 accept or emit invalid command lines. 1215 1216 @note: Lists within this class are "unordered" for equality comparisons. 1217 1218 @sort: __init__, __repr__, __str__, __cmp__ 1219 """ 1220 1221 ############## 1222 # Constructor 1223 ############## 1224
1225 - def __init__(self, argumentList=None, argumentString=None, validate=True):
1226 """ 1227 Initializes an options object. 1228 1229 If you initialize the object without passing either C{argumentList} or 1230 C{argumentString}, the object will be empty and will be invalid until it 1231 is filled in properly. 1232 1233 No reference to the original arguments is saved off by this class. Once 1234 the data has been parsed (successfully or not) this original information 1235 is discarded. 1236 1237 The argument list is assumed to be a list of arguments, not including the 1238 name of the command, something like C{sys.argv[1:]}. If you pass 1239 C{sys.argv} instead, things are not going to work. 1240 1241 The argument string will be parsed into an argument list by the 1242 L{util.splitCommandLine} function (see the documentation for that 1243 function for some important notes about its limitations). There is an 1244 assumption that the resulting list will be equivalent to C{sys.argv[1:]}, 1245 just like C{argumentList}. 1246 1247 Unless the C{validate} argument is C{False}, the L{Options.validate} 1248 method will be called (with its default arguments) after successfully 1249 parsing any passed-in command line. This validation ensures that 1250 appropriate actions, etc. have been specified. Keep in mind that even if 1251 C{validate} is C{False}, it might not be possible to parse the passed-in 1252 command line, so an exception might still be raised. 1253 1254 @note: The command line format is specified by the L{_usage} function. 1255 Call L{_usage} to see a usage statement for the cback script. 1256 1257 @note: It is strongly suggested that the C{validate} option always be set 1258 to C{True} (the default) unless there is a specific need to read in 1259 invalid command line arguments. 1260 1261 @param argumentList: Command line for a program. 1262 @type argumentList: List of arguments, i.e. C{sys.argv} 1263 1264 @param argumentString: Command line for a program. 1265 @type argumentString: String, i.e. "cback --verbose stage store" 1266 1267 @param validate: Validate the command line after parsing it. 1268 @type validate: Boolean true/false. 1269 1270 @raise getopt.GetoptError: If the command-line arguments could not be parsed. 1271 @raise ValueError: If the command-line arguments are invalid. 1272 """ 1273 self._help = False 1274 self._version = False 1275 self._verbose = False 1276 self._quiet = False 1277 self._config = None 1278 self._full = False 1279 self._managed = False 1280 self._managedOnly = False 1281 self._logfile = None 1282 self._owner = None 1283 self._mode = None 1284 self._output = False 1285 self._debug = False 1286 self._stacktrace = False 1287 self._diagnostics = False 1288 self._actions = None 1289 self.actions = [] # initialize to an empty list; remainder are OK 1290 if argumentList is not None and argumentString is not None: 1291 raise ValueError("Use either argumentList or argumentString, but not both.") 1292 if argumentString is not None: 1293 argumentList = splitCommandLine(argumentString) 1294 if argumentList is not None: 1295 self._parseArgumentList(argumentList) 1296 if validate: 1297 self.validate()
1298 1299 1300 ######################### 1301 # String representations 1302 ######################### 1303
1304 - def __repr__(self):
1305 """ 1306 Official string representation for class instance. 1307 """ 1308 return self.buildArgumentString(validate=False)
1309
1310 - def __str__(self):
1311 """ 1312 Informal string representation for class instance. 1313 """ 1314 return self.__repr__()
1315 1316 1317 ############################# 1318 # Standard comparison method 1319 ############################# 1320
1321 - def __cmp__(self, other):
1322 """ 1323 Definition of equals operator for this class. 1324 Lists within this class are "unordered" for equality comparisons. 1325 @param other: Other object to compare to. 1326 @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other. 1327 """ 1328 if other is None: 1329 return 1 1330 if self.help != other.help: 1331 if self.help < other.help: 1332 return -1 1333 else: 1334 return 1 1335 if self.version != other.version: 1336 if self.version < other.version: 1337 return -1 1338 else: 1339 return 1 1340 if self.verbose != other.verbose: 1341 if self.verbose < other.verbose: 1342 return -1 1343 else: 1344 return 1 1345 if self.quiet != other.quiet: 1346 if self.quiet < other.quiet: 1347 return -1 1348 else: 1349 return 1 1350 if self.config != other.config: 1351 if self.config < other.config: 1352 return -1 1353 else: 1354 return 1 1355 if self.full != other.full: 1356 if self.full < other.full: 1357 return -1 1358 else: 1359 return 1 1360 if self.managed != other.managed: 1361 if self.managed < other.managed: 1362 return -1 1363 else: 1364 return 1 1365 if self.managedOnly != other.managedOnly: 1366 if self.managedOnly < other.managedOnly: 1367 return -1 1368 else: 1369 return 1 1370 if self.logfile != other.logfile: 1371 if self.logfile < other.logfile: 1372 return -1 1373 else: 1374 return 1 1375 if self.owner != other.owner: 1376 if self.owner < other.owner: 1377 return -1 1378 else: 1379 return 1 1380 if self.mode != other.mode: 1381 if self.mode < other.mode: 1382 return -1 1383 else: 1384 return 1 1385 if self.output != other.output: 1386 if self.output < other.output: 1387 return -1 1388 else: 1389 return 1 1390 if self.debug != other.debug: 1391 if self.debug < other.debug: 1392 return -1 1393 else: 1394 return 1 1395 if self.stacktrace != other.stacktrace: 1396 if self.stacktrace < other.stacktrace: 1397 return -1 1398 else: 1399 return 1 1400 if self.diagnostics != other.diagnostics: 1401 if self.diagnostics < other.diagnostics: 1402 return -1 1403 else: 1404 return 1 1405 if self.actions != other.actions: 1406 if self.actions < other.actions: 1407 return -1 1408 else: 1409 return 1 1410 return 0
1411 1412 1413 ############# 1414 # Properties 1415 ############# 1416
1417 - def _setHelp(self, value):
1418 """ 1419 Property target used to set the help flag. 1420 No validations, but we normalize the value to C{True} or C{False}. 1421 """ 1422 if value: 1423 self._help = True 1424 else: 1425 self._help = False
1426
1427 - def _getHelp(self):
1428 """ 1429 Property target used to get the help flag. 1430 """ 1431 return self._help
1432
1433 - def _setVersion(self, value):
1434 """ 1435 Property target used to set the version flag. 1436 No validations, but we normalize the value to C{True} or C{False}. 1437 """ 1438 if value: 1439 self._version = True 1440 else: 1441 self._version = False
1442
1443 - def _getVersion(self):
1444 """ 1445 Property target used to get the version flag. 1446 """ 1447 return self._version
1448
1449 - def _setVerbose(self, value):
1450 """ 1451 Property target used to set the verbose flag. 1452 No validations, but we normalize the value to C{True} or C{False}. 1453 """ 1454 if value: 1455 self._verbose = True 1456 else: 1457 self._verbose = False
1458
1459 - def _getVerbose(self):
1460 """ 1461 Property target used to get the verbose flag. 1462 """ 1463 return self._verbose
1464
1465 - def _setQuiet(self, value):
1466 """ 1467 Property target used to set the quiet flag. 1468 No validations, but we normalize the value to C{True} or C{False}. 1469 """ 1470 if value: 1471 self._quiet = True 1472 else: 1473 self._quiet = False
1474
1475 - def _getQuiet(self):
1476 """ 1477 Property target used to get the quiet flag. 1478 """ 1479 return self._quiet
1480
1481 - def _setConfig(self, value):
1482 """ 1483 Property target used to set the config parameter. 1484 """ 1485 if value is not None: 1486 if len(value) < 1: 1487 raise ValueError("The config parameter must be a non-empty string.") 1488 self._config = value
1489
1490 - def _getConfig(self):
1491 """ 1492 Property target used to get the config parameter. 1493 """ 1494 return self._config
1495
1496 - def _setFull(self, value):
1497 """ 1498 Property target used to set the full flag. 1499 No validations, but we normalize the value to C{True} or C{False}. 1500 """ 1501 if value: 1502 self._full = True 1503 else: 1504 self._full = False
1505
1506 - def _getFull(self):
1507 """ 1508 Property target used to get the full flag. 1509 """ 1510 return self._full
1511
1512 - def _setManaged(self, value):
1513 """ 1514 Property target used to set the managed flag. 1515 No validations, but we normalize the value to C{True} or C{False}. 1516 """ 1517 if value: 1518 self._managed = True 1519 else: 1520 self._managed = False
1521
1522 - def _getManaged(self):
1523 """ 1524 Property target used to get the managed flag. 1525 """ 1526 return self._managed
1527
1528 - def _setManagedOnly(self, value):
1529 """ 1530 Property target used to set the managedOnly flag. 1531 No validations, but we normalize the value to C{True} or C{False}. 1532 """ 1533 if value: 1534 self._managedOnly = True 1535 else: 1536 self._managedOnly = False
1537
1538 - def _getManagedOnly(self):
1539 """ 1540 Property target used to get the managedOnly flag. 1541 """ 1542 return self._managedOnly
1543
1544 - def _setLogfile(self, value):
1545 """ 1546 Property target used to set the logfile parameter. 1547 @raise ValueError: If the value cannot be encoded properly. 1548 """ 1549 if value is not None: 1550 if len(value) < 1: 1551 raise ValueError("The logfile parameter must be a non-empty string.") 1552 self._logfile = encodePath(value)
1553
1554 - def _getLogfile(self):
1555 """ 1556 Property target used to get the logfile parameter. 1557 """ 1558 return self._logfile
1559
1560 - def _setOwner(self, value):
1561 """ 1562 Property target used to set the owner parameter. 1563 If not C{None}, the owner must be a C{(user,group)} tuple or list. 1564 Strings (and inherited children of strings) are explicitly disallowed. 1565 The value will be normalized to a tuple. 1566 @raise ValueError: If the value is not valid. 1567 """ 1568 if value is None: 1569 self._owner = None 1570 else: 1571 if isinstance(value, str): 1572 raise ValueError("Must specify user and group tuple for owner parameter.") 1573 if len(value) != 2: 1574 raise ValueError("Must specify user and group tuple for owner parameter.") 1575 if len(value[0]) < 1 or len(value[1]) < 1: 1576 raise ValueError("User and group tuple values must be non-empty strings.") 1577 self._owner = (value[0], value[1])
1578
1579 - def _getOwner(self):
1580 """ 1581 Property target used to get the owner parameter. 1582 The parameter is a tuple of C{(user, group)}. 1583 """ 1584 return self._owner
1585
1586 - def _setMode(self, value):
1587 """ 1588 Property target used to set the mode parameter. 1589 """ 1590 if value is None: 1591 self._mode = None 1592 else: 1593 try: 1594 if isinstance(value, str): 1595 value = int(value, 8) 1596 else: 1597 value = int(value) 1598 except TypeError: 1599 raise ValueError("Mode must be an octal integer >= 0, i.e. 644.") 1600 if value < 0: 1601 raise ValueError("Mode must be an octal integer >= 0. i.e. 644.") 1602 self._mode = value
1603
1604 - def _getMode(self):
1605 """ 1606 Property target used to get the mode parameter. 1607 """ 1608 return self._mode
1609
1610 - def _setOutput(self, value):
1611 """ 1612 Property target used to set the output flag. 1613 No validations, but we normalize the value to C{True} or C{False}. 1614 """ 1615 if value: 1616 self._output = True 1617 else: 1618 self._output = False
1619
1620 - def _getOutput(self):
1621 """ 1622 Property target used to get the output flag. 1623 """ 1624 return self._output
1625
1626 - def _setDebug(self, value):
1627 """ 1628 Property target used to set the debug flag. 1629 No validations, but we normalize the value to C{True} or C{False}. 1630 """ 1631 if value: 1632 self._debug = True 1633 else: 1634 self._debug = False
1635
1636 - def _getDebug(self):
1637 """ 1638 Property target used to get the debug flag. 1639 """ 1640 return self._debug
1641
1642 - def _setStacktrace(self, value):
1643 """ 1644 Property target used to set the stacktrace flag. 1645 No validations, but we normalize the value to C{True} or C{False}. 1646 """ 1647 if value: 1648 self._stacktrace = True 1649 else: 1650 self._stacktrace = False
1651
1652 - def _getStacktrace(self):
1653 """ 1654 Property target used to get the stacktrace flag. 1655 """ 1656 return self._stacktrace
1657
1658 - def _setDiagnostics(self, value):
1659 """ 1660 Property target used to set the diagnostics flag. 1661 No validations, but we normalize the value to C{True} or C{False}. 1662 """ 1663 if value: 1664 self._diagnostics = True 1665 else: 1666 self._diagnostics = False
1667
1668 - def _getDiagnostics(self):
1669 """ 1670 Property target used to get the diagnostics flag. 1671 """ 1672 return self._diagnostics
1673
1674 - def _setActions(self, value):
1675 """ 1676 Property target used to set the actions list. 1677 We don't restrict the contents of actions. They're validated somewhere else. 1678 @raise ValueError: If the value is not valid. 1679 """ 1680 if value is None: 1681 self._actions = None 1682 else: 1683 try: 1684 saved = self._actions 1685 self._actions = [] 1686 self._actions.extend(value) 1687 except Exception, e: 1688 self._actions = saved 1689 raise e
1690
1691 - def _getActions(self):
1692 """ 1693 Property target used to get the actions list. 1694 """ 1695 return self._actions
1696 1697 help = property(_getHelp, _setHelp, None, "Command-line help (C{-h,--help}) flag.") 1698 version = property(_getVersion, _setVersion, None, "Command-line version (C{-V,--version}) flag.") 1699 verbose = property(_getVerbose, _setVerbose, None, "Command-line verbose (C{-b,--verbose}) flag.") 1700 quiet = property(_getQuiet, _setQuiet, None, "Command-line quiet (C{-q,--quiet}) flag.") 1701 config = property(_getConfig, _setConfig, None, "Command-line configuration file (C{-c,--config}) parameter.") 1702 full = property(_getFull, _setFull, None, "Command-line full-backup (C{-f,--full}) flag.") 1703 managed = property(_getManaged, _setManaged, None, "Command-line managed (C{-M,--managed}) flag.") 1704 managedOnly = property(_getManagedOnly, _setManagedOnly, None, "Command-line managed-only (C{-N,--managed-only}) flag.") 1705 logfile = property(_getLogfile, _setLogfile, None, "Command-line logfile (C{-l,--logfile}) parameter.") 1706 owner = property(_getOwner, _setOwner, None, "Command-line owner (C{-o,--owner}) parameter, as tuple C{(user,group)}.") 1707 mode = property(_getMode, _setMode, None, "Command-line mode (C{-m,--mode}) parameter.") 1708 output = property(_getOutput, _setOutput, None, "Command-line output (C{-O,--output}) flag.") 1709 debug = property(_getDebug, _setDebug, None, "Command-line debug (C{-d,--debug}) flag.") 1710 stacktrace = property(_getStacktrace, _setStacktrace, None, "Command-line stacktrace (C{-s,--stack}) flag.") 1711 diagnostics = property(_getDiagnostics, _setDiagnostics, None, "Command-line diagnostics (C{-D,--diagnostics}) flag.") 1712 actions = property(_getActions, _setActions, None, "Command-line actions list.") 1713 1714 1715 ################## 1716 # Utility methods 1717 ################## 1718
1719 - def validate(self):
1720 """ 1721 Validates command-line options represented by the object. 1722 1723 Unless C{--help} or C{--version} are supplied, at least one action must 1724 be specified. Other validations (as for allowed values for particular 1725 options) will be taken care of at assignment time by the properties 1726 functionality. 1727 1728 @note: The command line format is specified by the L{_usage} function. 1729 Call L{_usage} to see a usage statement for the cback script. 1730 1731 @raise ValueError: If one of the validations fails. 1732 """ 1733 if not self.help and not self.version and not self.diagnostics: 1734 if self.actions is None or len(self.actions) == 0: 1735 raise ValueError("At least one action must be specified.") 1736 if self.managed and self.managedOnly: 1737 raise ValueError("The --managed and --managed-only options may not be combined.")
1738
1739 - def buildArgumentList(self, validate=True):
1740 """ 1741 Extracts options into a list of command line arguments. 1742 1743 The original order of the various arguments (if, indeed, the object was 1744 initialized with a command-line) is not preserved in this generated 1745 argument list. Besides that, the argument list is normalized to use the 1746 long option names (i.e. --version rather than -V). The resulting list 1747 will be suitable for passing back to the constructor in the 1748 C{argumentList} parameter. Unlike L{buildArgumentString}, string 1749 arguments are not quoted here, because there is no need for it. 1750 1751 Unless the C{validate} parameter is C{False}, the L{Options.validate} 1752 method will be called (with its default arguments) against the 1753 options before extracting the command line. If the options are not valid, 1754 then an argument list will not be extracted. 1755 1756 @note: It is strongly suggested that the C{validate} option always be set 1757 to C{True} (the default) unless there is a specific need to extract an 1758 invalid command line. 1759 1760 @param validate: Validate the options before extracting the command line. 1761 @type validate: Boolean true/false. 1762 1763 @return: List representation of command-line arguments. 1764 @raise ValueError: If options within the object are invalid. 1765 """ 1766 if validate: 1767 self.validate() 1768 argumentList = [] 1769 if self._help: 1770 argumentList.append("--help") 1771 if self.version: 1772 argumentList.append("--version") 1773 if self.verbose: 1774 argumentList.append("--verbose") 1775 if self.quiet: 1776 argumentList.append("--quiet") 1777 if self.config is not None: 1778 argumentList.append("--config") 1779 argumentList.append(self.config) 1780 if self.full: 1781 argumentList.append("--full") 1782 if self.managed: 1783 argumentList.append("--managed") 1784 if self.managedOnly: 1785 argumentList.append("--managed-only") 1786 if self.logfile is not None: 1787 argumentList.append("--logfile") 1788 argumentList.append(self.logfile) 1789 if self.owner is not None: 1790 argumentList.append("--owner") 1791 argumentList.append("%s:%s" % (self.owner[0], self.owner[1])) 1792 if self.mode is not None: 1793 argumentList.append("--mode") 1794 argumentList.append("%o" % self.mode) 1795 if self.output: 1796 argumentList.append("--output") 1797 if self.debug: 1798 argumentList.append("--debug") 1799 if self.stacktrace: 1800 argumentList.append("--stack") 1801 if self.diagnostics: 1802 argumentList.append("--diagnostics") 1803 if self.actions is not None: 1804 for action in self.actions: 1805 argumentList.append(action) 1806 return argumentList
1807
1808 - def buildArgumentString(self, validate=True):
1809 """ 1810 Extracts options into a string of command-line arguments. 1811 1812 The original order of the various arguments (if, indeed, the object was 1813 initialized with a command-line) is not preserved in this generated 1814 argument string. Besides that, the argument string is normalized to use 1815 the long option names (i.e. --version rather than -V) and to quote all 1816 string arguments with double quotes (C{"}). The resulting string will be 1817 suitable for passing back to the constructor in the C{argumentString} 1818 parameter. 1819 1820 Unless the C{validate} parameter is C{False}, the L{Options.validate} 1821 method will be called (with its default arguments) against the options 1822 before extracting the command line. If the options are not valid, then 1823 an argument string will not be extracted. 1824 1825 @note: It is strongly suggested that the C{validate} option always be set 1826 to C{True} (the default) unless there is a specific need to extract an 1827 invalid command line. 1828 1829 @param validate: Validate the options before extracting the command line. 1830 @type validate: Boolean true/false. 1831 1832 @return: String representation of command-line arguments. 1833 @raise ValueError: If options within the object are invalid. 1834 """ 1835 if validate: 1836 self.validate() 1837 argumentString = "" 1838 if self._help: 1839 argumentString += "--help " 1840 if self.version: 1841 argumentString += "--version " 1842 if self.verbose: 1843 argumentString += "--verbose " 1844 if self.quiet: 1845 argumentString += "--quiet " 1846 if self.config is not None: 1847 argumentString += "--config \"%s\" " % self.config 1848 if self.full: 1849 argumentString += "--full " 1850 if self.managed: 1851 argumentString += "--managed " 1852 if self.managedOnly: 1853 argumentString += "--managed-only " 1854 if self.logfile is not None: 1855 argumentString += "--logfile \"%s\" " % self.logfile 1856 if self.owner is not None: 1857 argumentString += "--owner \"%s:%s\" " % (self.owner[0], self.owner[1]) 1858 if self.mode is not None: 1859 argumentString += "--mode %o " % self.mode 1860 if self.output: 1861 argumentString += "--output " 1862 if self.debug: 1863 argumentString += "--debug " 1864 if self.stacktrace: 1865 argumentString += "--stack " 1866 if self.diagnostics: 1867 argumentString += "--diagnostics " 1868 if self.actions is not None: 1869 for action in self.actions: 1870 argumentString += "\"%s\" " % action 1871 return argumentString
1872
1873 - def _parseArgumentList(self, argumentList):
1874 """ 1875 Internal method to parse a list of command-line arguments. 1876 1877 Most of the validation we do here has to do with whether the arguments 1878 can be parsed and whether any values which exist are valid. We don't do 1879 any validation as to whether required elements exist or whether elements 1880 exist in the proper combination (instead, that's the job of the 1881 L{validate} method). 1882 1883 For any of the options which supply parameters, if the option is 1884 duplicated with long and short switches (i.e. C{-l} and a C{--logfile}) 1885 then the long switch is used. If the same option is duplicated with the 1886 same switch (long or short), then the last entry on the command line is 1887 used. 1888 1889 @param argumentList: List of arguments to a command. 1890 @type argumentList: List of arguments to a command, i.e. C{sys.argv[1:]} 1891 1892 @raise ValueError: If the argument list cannot be successfully parsed. 1893 """ 1894 switches = { } 1895 opts, self.actions = getopt.getopt(argumentList, SHORT_SWITCHES, LONG_SWITCHES) 1896 for o, a in opts: # push the switches into a hash 1897 switches[o] = a 1898 if switches.has_key("-h") or switches.has_key("--help"): 1899 self.help = True 1900 if switches.has_key("-V") or switches.has_key("--version"): 1901 self.version = True 1902 if switches.has_key("-b") or switches.has_key("--verbose"): 1903 self.verbose = True 1904 if switches.has_key("-q") or switches.has_key("--quiet"): 1905 self.quiet = True 1906 if switches.has_key("-c"): 1907 self.config = switches["-c"] 1908 if switches.has_key("--config"): 1909 self.config = switches["--config"] 1910 if switches.has_key("-f") or switches.has_key("--full"): 1911 self.full = True 1912 if switches.has_key("-M") or switches.has_key("--managed"): 1913 self.managed = True 1914 if switches.has_key("-N") or switches.has_key("--managed-only"): 1915 self.managedOnly = True 1916 if switches.has_key("-l"): 1917 self.logfile = switches["-l"] 1918 if switches.has_key("--logfile"): 1919 self.logfile = switches["--logfile"] 1920 if switches.has_key("-o"): 1921 self.owner = switches["-o"].split(":", 1) 1922 if switches.has_key("--owner"): 1923 self.owner = switches["--owner"].split(":", 1) 1924 if switches.has_key("-m"): 1925 self.mode = switches["-m"] 1926 if switches.has_key("--mode"): 1927 self.mode = switches["--mode"] 1928 if switches.has_key("-O") or switches.has_key("--output"): 1929 self.output = True 1930 if switches.has_key("-d") or switches.has_key("--debug"): 1931 self.debug = True 1932 if switches.has_key("-s") or switches.has_key("--stack"): 1933 self.stacktrace = True 1934 if switches.has_key("-D") or switches.has_key("--diagnostics"): 1935 self.diagnostics = True
1936 1937 1938 ######################################################################### 1939 # Main routine 1940 ######################################################################## 1941 1942 if __name__ == "__main__": 1943 result = cli() 1944 sys.exit(result) 1945