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

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