Changeset 27
- Timestamp:
- 25/11/07 20:50:23 (4 years ago)
- Location:
- trunk
- Files:
-
- 9 modified
-
change.py (modified) (8 diffs)
-
confloader.py (modified) (15 diffs)
-
debug.py (modified) (1 diff)
-
etc/netapp-provision-demo.xml (modified) (3 diffs)
-
modipy.py (modified) (2 diffs)
-
options.py (modified) (3 diffs)
-
provisioner.py (modified) (19 diffs)
-
test_ssh.py (modified) (1 diff)
-
util.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
trunk/change.py
r25 r27 61 61 import debug 62 62 63 log = logging.getLogger(' configulator')63 log = logging.getLogger('modipy') 64 64 65 65 from zope.interface import Interface, implements … … 74 74 'backout_ok': 4, 75 75 'backout_failed': 5, 76 'retry': 6, 76 77 } 77 78 … … 86 87 """ 87 88 88 def __init__(self, name, devices=[], serial_mode=True, backout_all=False, continue_on_fail=False, **kwargs):89 def __init__(self, name, devices=[], serial_mode=True, backout_all=False, on_fail_continue=False, **kwargs): 89 90 """ 90 91 @param devices: a list of devices that this change should be applied to. … … 95 96 L{Change} to a device fails, the L{Change} will be backed out for all other 96 97 devices that have so far succeeded. This makes a L{Change} atomic. 97 @param continue_on_fail: Boolean. If set to True, if application of a98 @param on_fail_continue: Boolean. If set to True, if application of a 98 99 L{Change} to a device fails, the change will be backed out from that device, 99 100 but the system will continue attempting to apply the change to the rest … … 175 176 implements( IChange, ) 176 177 177 def __init__(self, name, devices=[], serial_mode=True, backout_all=False, continue_on_fail=False, 178 def __init__(self, name, devices=[], serial_mode=True, backout_all=False, 179 on_fail_continue=False, 180 on_fail_retry=False, 181 max_retries=3, 178 182 pre_impl=None, 179 183 impl=None, … … 191 195 self.serial_mode = serial_mode 192 196 self.backout_all = backout_all 193 self.continue_on_fail = continue_on_fail 194 197 self.on_fail_continue = on_fail_continue 198 self.on_fail_retry = on_fail_retry 199 self.max_retries = max_retries 200 self.retries = 0 195 201 self.pre_impl = pre_impl 196 202 self.impl = impl … … 206 212 pass 207 213 208 def __ str__(self):214 def __repr__(self): 209 215 return '<%s: %s>' % ( self.__class__, self.name ) 210 216 … … 427 433 428 434 namespace = self.namespace 435 436 def can_retry(self): 437 """ 438 Check to see if this change is allowed to retry after failing. 439 """ 440 if self.on_fail_retry: 441 log.debug("Onfail-retry is enabled") 442 if self.retries < self.max_retries: 443 log.debug("Retries: %d is less than retry max: %s. Will retry change.", self.retries, self.max_retries) 444 self.retries += 1 445 return True 446 else: 447 log.debug("Too many retries for change. Will not retry again.") 448 pass 449 return False 429 450 430 451 if __name__ == '__main__': -
trunk/confloader.py
r26 r27 22 22 import debug 23 23 24 log = logging.getLogger(' configulator')24 log = logging.getLogger('modipy') 25 25 26 26 xinclude_re = re.compile(r'.*<xi:include href=[\'\"](?P<uri>.*)[\'\"].*') … … 78 78 79 79 def update(self, dict): 80 log.debug("Updating a namespace!") 80 for key in dict: 81 self.namespace[key] = dict[key] 81 82 82 83 def __iter__(self): … … 102 103 return self.namespace.next() 103 104 except AttributeError: 104 log.debug("i have no parent!")105 105 raise StopIteration 106 106 … … 114 114 return items 115 115 116 117 116 class ConfigLoader: 118 117 """ … … 121 120 """ 122 121 123 def __init__(self, configfile=None, devices=[]):122 def __init__(self, options=None, devices=[]): 124 123 125 124 self.doc = None 125 126 self.options = options 126 127 127 128 self.provisioners = {} … … 138 139 log.debug("created ConfigLoader") 139 140 140 if configfile:141 self.parse( configfile)141 if options.configfile: 142 self.parse(options.configfile) 142 143 pass 143 144 … … 196 197 ## self.dependency_tree = element 197 198 198 def parse(self, configfile =None):199 def parse(self, configfile): 199 200 """ 200 201 Parse my configuration file. 201 202 """ 202 if configfile is None:203 configfile = self.configfile204 pass205 206 203 self.tree = etree.parse(configfile) 207 204 … … 291 288 prov_klass = node.attrib['type'] 292 289 prov_name = node.attrib['name'] 290 291 # Copy the attribs into a dictionary for use as kwargs 292 kwargs = {} 293 for key in node.attrib: 294 if key not in ['type', 'name']: 295 kwargs[key] = node.attrib[key] 296 293 297 log.debug("Attempting to create a '%s' provisioner", prov_klass) 294 298 prov_module = __import__('provisioner') 295 299 klass = getattr( prov_module, prov_klass ) 296 provisioner = klass() 300 try: 301 provisioner = klass(prov_name, authoritarian=self.options.authoritarian, **kwargs) 302 except TypeError, e: 303 log.error("Incorrect parameter supplied for provisioner of type '%s'", prov_klass) 304 raise e 297 305 298 306 log.debug("created provisioner '%s': %s", prov_name, provisioner) … … 334 342 change_klass = node.attrib['type'] 335 343 change_name = node.attrib['name'] 344 336 345 log.debug("Attempting to create a '%s' change", change_klass) 337 346 … … 341 350 log.info("change module not yet imported. Importing...") 342 351 change_module = __import__('change') 343 344 klass = getattr( change_module, change_klass ) 345 352 pass 353 354 klass = getattr(change_module, change_klass) 346 355 change = klass(change_name) 347 356 … … 451 460 log.error("Iterator '%s' is not defined", itername) 452 461 raise 453 462 463 # Add an optional onfail mode 464 log.debug("Checking for 'onfail' attribute...") 465 try: 466 onfail = node.attrib['onfail'] 467 log.debug("onfail node found.") 468 if onfail == 'continue': 469 change.on_fail_continue = True 470 log.debug("Processing will continue if this change fails") 471 472 elif onfail == 'retry': 473 change.on_fail_retry = True 474 log.debug("This change will retry on failure") 475 try: 476 max_retries = int(node.attrib['max_retries']) 477 change.max_retries = max_retries 478 except KeyError: 479 pass 480 log.debug(" This change will retry at most %d times", change.max_retries) 481 except KeyError: 482 pass 483 454 484 # If the change doesn't have a provisioner, use the first one 455 485 # in our config by default. … … 497 527 log.debug("pending changes: %s", self.pending_changes) 498 528 for change in self.pending_changes: 529 log.debug("testing change %s", change) 499 530 if len(change.pre_requisites) == 0: 500 531 changelist.append(change) 501 log.debug("change '%s' has no pre-reqs. Adding .", change)532 log.debug("change '%s' has no pre-reqs. Adding to execution queue.", change) 502 533 continue 534 503 535 else: 504 536 all_prereqs = True 505 log.debug("checking change pre-requsisites: %s", change.pre_requisites)506 537 for prereq in change.pre_requisites: 507 538 log.debug("change '%s' has a pre-req of '%s'", change, prereq) 508 539 if prereq not in self.change_success: 509 log.debug("pre-req '%s' not complete yet. skipping", prereq) 540 541 # If a prereq has failed, doesn't need to retry, and is 542 # marked as 'onfail:continue', then we treat it as if 543 # this prereq has been met. 544 if prereq.state not in [ CHANGE_STATE['pending'], 545 CHANGE_STATE['retry'], 546 ] and prereq.on_fail_continue: 547 log.debug("prereq failed, but is marked onfail:continue.") 548 continue 549 550 log.debug("pre-req '%s' not complete yet.", prereq) 510 551 all_prereqs = False 511 552 break 512 553 pass 513 554 514 log.debug("All pre-reqs for '%s' have completed. Adding.", change)515 555 if all_prereqs: 556 log.debug("All pre-reqs for '%s' have completed. Adding to execution queue.", change) 516 557 changelist.append(change) 517 558 else: … … 547 588 self.backout_failure.append(change) 548 589 self.pending_changes.remove(change) 549 590 591 elif change.state == CHANGE_STATE['retry']: 592 # Change will be retried 593 pass 594 550 595 else: 551 596 log.error("Unknown/unhandled change state '%s'", change.state) … … 647 692 d = change.provisioner.perform_change(None, change, self.cfgldr.global_namespace) 648 693 d.addCallback(self.change_complete, change) 649 d.addErrback(self.change_ complete, change)694 d.addErrback(self.change_failure, change) 650 695 dlist.append(d) 651 696 pass … … 662 707 self.cfgldr.change_complete(change) 663 708 664 def change_failure(self, failure ):709 def change_failure(self, failure, change): 665 710 log.error("Major change failure!") 666 711 tlog.err(failure) 712 self.change_complete(failure, change) 667 713 668 714 def print_stats(self, ignored): -
trunk/debug.py
r14 r27 32 32 handler = logging.handlers.RotatingFileHandler(filename=filename, maxBytes=10e6, backupCount=10) 33 33 handler.setFormatter(formatter) 34 log = logging.getLogger(' configulator')34 log = logging.getLogger('modipy') 35 35 log.addHandler(handler) 36 36 -
trunk/etc/netapp-provision-demo.xml
r24 r27 15 15 <provisioner 16 16 name='netapp_provisioner' 17 type='MultiConnectingProvisioner'> 17 type='MultiConnectingProvisioner' 18 command_timeout='5'> 18 19 19 20 <command>ssh -i /home/daedalus/.ssh/configulator -o BatchMode=yes -o ServerAliveInterval=0 root@%(device.ipaddress)s "%(command.send)s"</command> … … 60 61 </iterator> 61 62 62 <change name="create_root_volume_primary" type="CommandChange" >63 <change name="create_root_volume_primary" type="CommandChange" onfail='continue'> 63 64 64 65 <target>%(primary_filer)s</target> … … 99 100 </change> 100 101 101 <change name="create_project_volume_primary" type="CommandChange" iterator="project_volumes"> 102 <change name="create_project_volume_primary" type="CommandChange" iterator="project_volumes" 103 onfail='retry' max_retries='2'> 102 104 103 105 <prereq>create_root_volume_primary</prereq> -
trunk/modipy.py
r26 r27 11 11 12 12 import logging 13 log = logging.getLogger(' configulator')13 log = logging.getLogger('modipy') 14 14 15 15 optparser = ChangeOptions() … … 17 17 18 18 try: 19 cfgldr = ConfigLoader(optparser.options .configfile, optparser.args)19 cfgldr = ConfigLoader(optparser.options, optparser.args) 20 20 except Exception, e: 21 21 log.error("Cannot load configuration: %s", e) -
trunk/options.py
r20 r27 18 18 from twisted.python import log as tlog 19 19 20 log = logging.getLogger(' configulator')20 log = logging.getLogger('modipy') 21 21 22 22 class BaseOptions(optparse.OptionParser): … … 32 32 help_license = 'Display the license agreement and exit.' 33 33 help_debug = "Set the output debug level to: debug, info, warn, error, or critical." 34 help_logfile = "Log to specified file instead of default logfile"35 help_no_logfile = "Disable logging to logfile"34 # help_logfile = "Log to specified file instead of default logfile" 35 # help_no_logfile = "Disable logging to logfile" 36 36 37 37 38 self.add_option('', '--license', dest='license', action='store_true', help=help_license) 38 39 self.add_option('', '--debug', dest='debug', type='choice', choices=('debug', 'info', 'warn', 'error', 'critical'), metavar='LEVEL', default='info', help=help_debug) 39 self.add_option('', '--logfile', dest='logfile', type='string', help=help_logfile)40 self.add_option('', '--no-logfile', dest='no-logfile', action='store_true', default=False, help=help_no_logfile)41 40 # self.add_option('', '--logfile', dest='logfile', type='string', help=help_logfile) 41 # self.add_option('', '--no-logfile', dest='no-logfile', action='store_true', default=False, help=help_no_logfile) 42 42 43 self.addOptions() 43 44 … … 75 76 76 77 def addOptions(self): 78 help_authoritarian = "Authoritarian mode. Confirm every change command." 77 79 help_configfile = "The configuration file to load" 78 #help_loadonly = "Load the configuration file and exit. Used to test parsing."80 help_loadonly = "Load the configuration file and exit. Used to test parsing." 79 81 82 83 self.add_option('-a', '--authoritarian', dest='authoritarian', action='store_true', default=False, help=help_authoritarian) 80 84 self.add_option('-c', '--configfile', dest='configfile', type='string', help=help_configfile) 81 #self.add_option('', '--loadonly', dest='loadonly', action='store_true', default=False, help=help_configfile)85 self.add_option('', '--loadonly', dest='loadonly', action='store_true', default=False, help=help_loadonly) 82 86 83 87 def check_values(self, options, args): -
trunk/provisioner.py
r26 r27 17 17 from twisted.internet import protocol 18 18 from twisted.python import log as tlog 19 from twisted.internet.error import ProcessDone, ProcessTerminated 19 from twisted.internet.error import ProcessDone, ProcessTerminated, AlreadyCancelled, AlreadyCalled 20 20 21 21 from change import ChangeFailed, ChangeConditionFailure, CHANGE_STATE … … 26 26 27 27 import debug 28 log = logging.getLogger(' configulator')28 log = logging.getLogger('modipy') 29 29 30 30 from twisted.internet.base import DelayedCall … … 63 63 implements( IProvisioner, ) 64 64 65 def __init__(self, name='', namespace={} ):65 def __init__(self, name='', namespace={}, authoritarian=False, **kwargs): 66 66 self.name = name 67 67 self.connectedDevice = None 68 68 self.namespace = namespace 69 self.authoritarian = authoritarian 70 71 def parse_config_node(self, node): 72 pass 69 73 70 74 def perform_change(self, ignored, change, namespace={}): … … 109 113 110 114 log.debug("applying change with namespace: %s", namespace) 111 112 ## for key in self.namespace: 113 ## if namespace.has_key(key): 114 ## log.warning("Conflicting namespace keys: %s from %s conflicts with %s from global namespace" % (key, self, key) ) 115 ## pass 116 ## namespace[key] = self.namespace[key] 117 118 ## # Then the change namespace overrides 119 ## for key in change.namespace: 120 ## if namespace.has_key(key): 121 ## log.warning("Conflicting namespace keys: %s from %s conflicts with %s from global namespace" % (key, change, key) ) 122 ## pass 123 ## namespace[key] = change.namespace[key] 124 125 ## for key in device.namespace: 126 ## if namespace.has_key[key]: 127 ## log.warning("Conflicting namespace keys: %s from %s conflicts with %s from global namespace" % (key, device, key) ) 128 ## pass 129 ## namespace[key] = device.namespace[key] 130 131 d.addCallback( self.apply_change, device, change, namespace) 115 d.addCallback(self.apply_change, device, change, namespace) 132 116 133 117 d.addCallback(self.change_complete_success, change, namespace) … … 175 159 d = self.backout_change(None, device, change, namespace) 176 160 177 if change. continue_on_fail:161 if change.on_fail_continue: 178 162 log.info("Attempting to continue, despite failure...") 179 163 change.set_state('partial_failure') … … 191 175 def change_failure(self, failure, change, namespace): 192 176 log.debug("Change failure for %s: %s", change.name, failure.value) 193 if change.state == CHANGE_STATE['pending']: 194 change.state = CHANGE_STATE['total_failure'] 177 if change.state in [ CHANGE_STATE['pending'], CHANGE_STATE['retry'] ]: 178 # If the change is marked as 'on_fail: retry', let it retry 179 if change.can_retry(): 180 change.state = CHANGE_STATE['retry'] 181 else: 182 change.state = CHANGE_STATE['total_failure'] 195 183 else: 196 184 change.state = CHANGE_STATE['backout_failed'] … … 199 187 200 188 def change_complete_success(self, result, change, namespace): 201 if change.state == CHANGE_STATE['pending']:189 if change.state in [ CHANGE_STATE['pending'], CHANGE_STATE['retry'] ]: 202 190 change.state = CHANGE_STATE['success'] 203 191 log.info("Change '%s' was a success!", change.name) … … 258 246 command_re = re.compile(r'^(?P<start>.*)"(?P<quoted>.*)"(?P<end>.*)$') 259 247 248 def parse_config_node(self, node): 249 Provisioner.parse_config_node(self, node) 250 pass 251 252 def __init__(self, name='', namespace={}, authoritarian=False, command_timeout=300): 253 Provisioner.__init__(self, name, namespace, authoritarian) 254 self.command_timeout = int(command_timeout) 255 260 256 def split_command(self, cmdstring): 261 257 """ … … 298 294 device via STDIN, and receives the output from STDOUT and STDERR. 299 295 """ 296 def parse_config_node(self, node): 297 CommandProvisioner.parse_config_node(self, node) 300 298 301 299 def connect(self, device): … … 327 325 log.error("Connection to child timed out!") 328 326 self.ep.transport.loseConnection() 329 self.childConnectingDefer.errback( ValueError("Connection timed out") )327 self.childConnectingDefer.errback( Exception("Connection timed out") ) 330 328 331 329 def childConnectSuccess(self): … … 454 452 # check the expr to see if we should process it 455 453 if expr is None: 456 log.debug("No expression to wait for. Running command '%s' immediately." % cmdstring)457 454 if cmdstring is not None: 458 self.transport.writeToChild(0, '%s\n' % cmdstring) 459 self.data_wait_timeout = reactor.callLater(10, self.data_wait_too_long) 455 log.debug("No expression to wait for. Running command '%s' immediately." % cmdstring) 456 self.issue_command(cmdstring) 457 else: 458 log.debug("cmdstring is 'None', so nothing to do") 460 459 461 460 else: … … 477 476 478 477 if cmdstring is not None: 479 log.debug("running command: %s" % cmdstring) 480 self.transport.writeToChild(0, '%s\n' % cmdstring) 481 self.data_wait_timeout = reactor.callLater(10, self.data_wait_too_long) 478 self.issue_command(cmdstring) 482 479 pass 483 480 else: … … 494 491 pass 495 492 493 def issue_command(self, cmdstring): 494 """ 495 Write the command to the remote device. 496 """ 497 # If we're in authoritarian mode, wait for confirmation 498 # that we should execute the command. 499 if self.parent.authoritarian: 500 log.debug("Authoritarian mode. Waiting for ok to proceed...") 501 isok = raw_input("Issue command (y/n)[n]?> ") 502 if isok.startswith('y'): 503 log.debug("Ok! Let's continue!") 504 else: 505 log.info(" Bailing out at your command.") 506 self.waitingForCommand.errback("User requested manual bailout.") 507 return 508 pass 509 log.debug("running command: %s" % cmdstring) 510 self.transport.writeToChild(0, '%s\n' % cmdstring) 511 self.data_wait_timeout = reactor.callLater(10, self.data_wait_too_long) 496 512 497 513 def data_wait_too_long(self): … … 516 532 Deals with additional configuration that I expect to be provided with. 517 533 """ 534 ConnectingProvisioner.parse_config_node(self, node) 518 535 log.debug("Parsing config node: %s", node) 519 536 # parse command definition … … 572 589 cmd = self.split_command(command) 573 590 log.debug("command is: %s", cmd) 574 d.addCallback(self.spawn_command, cmd )591 d.addCallback(self.spawn_command, cmd, self.command_timeout) 575 592 d.addCallbacks(self.command_completed, self.command_failed) 576 593 pass … … 586 603 will fire based on the results. 587 604 """ 605 log.debug("spawning command...") 606 self.waitingForCommand = defer.Deferred() 607 # If we're in authoritarian mode, wait for confirmation 608 # that we should execute the command. 609 log.debug("Checking for authoritarian mode...") 610 if self.authoritarian: 611 log.debug("Authoritarian mode. Waiting for ok to proceed...") 612 isok = raw_input("Issue command: %s\n(y/n)[n]?> " % ' '.join(cmd) ) 613 if isok.startswith('y'): 614 log.debug("Ok! Let's continue!") 615 else: 616 log.info("Bailing out at your command.") 617 self.exitcode = -1 618 self.cmdoutput = 'User requested manual bailout' 619 self.waitingForCommand.errback( Exception("User requested manual bailout.") ) 620 return self.waitingForCommand 621 pass 622 588 623 log.debug("running command remotely: %s" % cmd) 589 self.waitingForCommand = defer.Deferred()590 624 self.ep = SingleCommandProtocol(self, timeout) 591 625 self.p = reactor.spawnProcess(self.ep, cmd[0], cmd) 626 log.debug("Spawned process: %s", self.p) 627 592 628 return self.waitingForCommand 593 629 … … 602 638 self.cmdoutput += result[1] 603 639 log.debug("current results: exit '%d', output: %s", self.exitcode, self.cmdoutput) 604 640 log.error("Process: %s", self.p) 641 605 642 def command_failed(self, failure): 606 643 errorstr = "%s: %s" % (self.exitcode, self.cmdoutput) 607 644 log.error("Command failed: %s", errorstr) 608 self.all_commands_defer.errback( Exception(errorstr) ) 645 log.error("Process: %s", self.p) 646 self.exitcode = result[0] 647 self.cmdoutput += result[1] 648 #self.all_commands_defer.errback( Exception(errorstr) ) 609 649 610 650 def all_commands_done(self, result): … … 650 690 651 691 def processEnded(self, status_object): 652 #log.debug("process ended: %s" % status_object)692 log.debug("process ended: %s" % status_object) 653 693 try: 654 694 if isinstance(status_object.value, ProcessDone): … … 659 699 660 700 elif isinstance(status_object.value, ProcessTerminated): 661 log.debug("process did not exit ok: %s: %s" % (status_object.value.exitCode, self.databuf))701 log.debug("process did not exit ok: %s: [%s: %s] %s" % (status_object.value.exitCode, status_object.value.signal, status_object.value.status, self.databuf)) 662 702 self.exitCode = status_object.value.exitCode 663 703 self.timeout.cancel() 664 704 self.parent.waitingForCommand.callback( (self.exitCode, self.databuf) ) 705 pass 665 706 666 707 except Exception, e: 667 log.error("erk: not good") 668 traceback.print_exc(e) 669 self.timeout.cancel() 670 self.parent.waitingForCommand.errback( e ) 708 log.error("Exception raised during end of process processing. That's quite bad.") 709 raise 710 ## traceback.print_exc(e) 711 ## self.timeout.cancel() 712 ## self.parent.waitingForCommand.errback( e ) 671 713 672 714 def timedOut(self): -
trunk/test_ssh.py
r8 r27 11 11 12 12 import logging 13 log = logging.getLogger(' configulator')13 log = logging.getLogger('modipy') 14 14 log.setLevel(logging.DEBUG) 15 15 -
trunk/util.py
r18 r27 3 3 # 4 4 import logging 5 log = logging.getLogger(' configulator')5 log = logging.getLogger('modipy') 6 6 7 7 def substituteVariables(str, namespace={}):
