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