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