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