Changeset 28
- Timestamp:
- 26/11/07 20:14:17 (2 years ago)
- Location:
- trunk
- Files:
-
- 4 modified
-
change.py (modified) (2 diffs)
-
confloader.py (modified) (17 diffs)
-
etc/netapp-provision-demo.xml (modified) (5 diffs)
-
provisioner.py (modified) (10 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/change.py
r27 r28 215 215 return '<%s: %s>' % ( self.__class__, self.name ) 216 216 217 def copy(self): 218 """ 219 Create a copy of myself. 220 """ 221 newchange = CommandChange('Copy of %s' % self.name, 222 self.devices, 223 self.serial_mode, 224 self.backout_all, 225 self.on_fail_continue, 226 self.on_fail_retry, 227 self.max_retries, 228 self.pre_impl, 229 self.impl, 230 self.post_impl, 231 self.pre_backout, 232 self.backoutset, 233 self.post_backout, 234 self.pre_requisites, 235 ) 236 237 # set the namespace as a copy of mine 238 if self.namespace is not None: 239 newchange.namespace = self.namespace.copy() 240 pass 241 242 return newchange 243 217 244 def set_state(self, state_string): 218 245 self.state = CHANGE_STATE[state_string] … … 236 263 for ns in self.iterator: 237 264 log.debug("Adding namespace entry of: %s", ns) 238 # FIXME: This permanently updates the namespace. 239 # Should we be updating a local copy? 265 266 # Make sure we only update a copy, so that this 267 # update isn't permanent 240 268 newnamespace = ns.copy() 241 269 newnamespace.update(namespace) -
trunk/confloader.py
r27 r28 15 15 from device import Device 16 16 from change import CHANGE_STATE 17 import util 17 18 18 19 from twisted.internet import defer, reactor … … 128 129 self.provisioners = {} 129 130 self.devices = {} 131 self.change_templates = {} 130 132 self.changes = {} 131 133 self.iterators = {} … … 163 165 del self.devices[dev] 164 166 pass 165 166 def oldloader(self):167 # A new provisioner168 if name == 'provisioner':169 log.debug("Creating new provisioner")170 self.add_provisioner(element)171 172 elif name == 'device':173 log.debug("Creating new device")174 self.add_device(element)175 176 elif name == 'change':177 log.debug("Creating new change")178 self.add_change(element)179 180 elif name == 'namespace':181 log.debug("Creating new namespace")182 self.add_namespace(element)183 184 elif name == 'config':185 # We've finished the config186 #self.global_namespace = self.namespace_stack.pop()187 188 # Add some global elements to the namespace189 element.namespace['ALL_TARGETS'] = [ x for x in self.devices.keys() ]190 191 self.global_namespace = element.namespace192 193 194 ## elif name == 'dependencies':195 ## # We've finish a list of dependency elements196 ## log.info("Finished loaded dependencies.")197 ## self.dependency_tree = element198 167 199 168 def parse(self, configfile): … … 203 172 self.tree = etree.parse(configfile) 204 173 174 # Process xincludes 175 try: 176 self.tree.xinclude() 177 except etree.XIncludeError: 178 log.error("XInclude of a file failed.") 179 log.error("Use external tool such as xmllint to figure out why.") 180 log.error("Sorry, but lxml.etree won't tell me exactly what went wrong.") 181 raise 182 205 183 # add the global namespace 206 184 try: … … 209 187 except IndexError: 210 188 log.warn("Cannot find global namespace") 211 self.global_namespace = None189 self.global_namespace = {} 212 190 213 191 # Add all my node types, in the order specified in the list … … 216 194 'provisioner', 217 195 'device', 196 'changetemplate', 218 197 'change', 219 'dependencies',198 # 'dependencies', 220 199 ]: 221 200 … … 333 312 log.debug("my ip addr is: %s", device.ipaddress) 334 313 self.devices[device_name] = device 335 314 315 def add_changetemplate(self, node): 316 """ 317 Add a change template to my list of available templates. 318 A change template is a change object that may not have 319 all of its parameters set yet. 320 """ 321 log.debug("Adding change template...") 322 # Create the change template object 323 change_tmpl = self.create_change_object(node) 324 325 # Set any namespaces 326 self.set_change_namespace(change_tmpl, node) 327 328 # Set change parameters that are common to all changes 329 self.set_change_params(change_tmpl, node) 330 331 # Set the optional change iterator 332 self.set_change_iterator(change_tmpl, node) 333 334 # Add an optional onfail mode 335 self.set_change_onfail(change_tmpl, node) 336 337 self.change_templates[change_tmpl.name] = change_tmpl 338 339 return change_tmpl 340 336 341 def add_change(self, node): 337 342 """ 338 Add a change to my configuration based on the parsed element. 343 When adding a change, check for some extra options such as a 344 change template, which will cause this change to be based on 345 an existing change template. 346 """ 347 log.debug("Adding change...") 348 try: 349 tmpl_name = node.attrib['template'] 350 log.debug("Change is based on a template: '%s'", tmpl_name) 351 # Do template based processing 352 try: 353 template = self.change_templates[tmpl_name] 354 except KeyError: 355 raise ValueError("Change template '%s' is not defined" % tmpl_name) 356 357 change = template.copy() 358 # Make sure we set the change name to the actual change, 359 # not the name of the template 360 change.name = node.attrib['name'] 361 362 # Now do non-templated specific processing 363 self.set_change_namespace(change, node) 364 self.set_change_params(change, node) 365 self.set_change_iterator(change, node) 366 self.set_change_onfail(change, node) 367 368 except KeyError: 369 # Use the same processing stream as a template, with the 370 # assumption that all the required parameters have been 371 # defined. If they haven't, the change will fail. 372 change = self.add_change_template(node) 373 pass 374 375 # If the change doesn't have a provisioner, use the first one 376 # in our config by default. 377 if getattr(change, 'provisioner', None) is None: 378 log.info("Change '%s' has no provisioner. Using default.", change.name ) 379 # FIXME: This should find the first *compatible* provisioner, 380 # rather than just the first one. 381 change.provisioner = self.provisioners.values()[0] 382 pass 383 384 self.changes[change.name] = change 385 self.pending_changes.append( change ) 386 387 def create_change_object(self, node): 388 """ 389 Create a change object from a node. 390 This may be used as a change template, or a regular change. 339 391 """ 340 392 # Change type is dynamic, as it supports the loading … … 343 395 change_name = node.attrib['name'] 344 396 345 log.debug(" Attempting to create a'%s' change", change_klass)397 log.debug("Creating '%s' change", change_klass) 346 398 347 399 try: … … 356 408 357 409 log.debug("created change '%s': %s", change_name, change) 358 359 # Add any namespaces 410 return change 411 412 def set_change_namespace(self, change, node): 413 """ 414 Add a namespace to a change or change template 415 """ 360 416 ns = node.find('namespace') 361 417 if ns is None: … … 364 420 self.add_namespace(ns, change) 365 421 366 # Add change attributes that are common to all changes 422 def set_change_provisioner(self, change, node): 423 # which provisioner to use for the change 424 try: 425 provname = node.attrib['provisioner'] 426 except KeyError: 427 return 428 429 provname = util.substituteVariables(provname, change.namespace) 430 try: 431 change.provisioner = self.provisioners[provname] 432 log.debug("change '%s' will use provisioner '%s'", change, provname) 433 except KeyError: 434 raise KeyError("Provisioner named '%s' is not defined" % provname) 435 436 def set_change_params(self, change, node): 437 """ 438 Set common change parameters, defined in subnodes. 439 """ 367 440 for subnode in node.xpath('*'): 368 # which provisioner to use for the change369 if subnode.tag == 'useprov':370 provname = subnode.text371 provname = provname % change.namespace372 change.provisioner = self.provisioners[provname]373 log.debug("change '%s' will use provisioner '%s'", change, provname)374 pass375 376 441 # a target device for the change 377 elif subnode.tag == 'target':442 if subnode.tag == 'target': 378 443 targetname = subnode.text 379 444 … … 385 450 else: 386 451 # perform variable substitution with global namespace 387 #targetname = self.apply_namespaces(targetname)388 452 log.debug("my namespace: %s", change.namespace) 389 targetname = targetname % change.namespace 390 453 targetname = util.substituteVariables(targetname, change.namespace) 391 454 dev = self.devices[targetname] 392 455 log.debug("current target list: %s", change.devices) … … 395 458 pass 396 459 397 elif subnode.tag == ' prereq':398 changename = subnode. text460 elif subnode.tag == 'depends': 461 changename = subnode.attrib['on'] 399 462 try: 400 463 change.pre_requisites.append(self.changes[changename]) … … 444 507 pass 445 508 pass 446 447 # Set the optional change iterator 509 pass 510 511 def set_change_iterator(self, change, node): 512 """ 513 Set an optional change iterator 514 """ 448 515 try: 449 516 itername = node.attrib['iterator'] … … 453 520 if itername is not None: 454 521 # Use namespace substitution for the itername to allow templating 455 itername = itername % change.namespace522 itername = util.substituteVariables(itername, change.namespace) 456 523 try: 457 524 change.iterator = self.iterators[itername] … … 460 527 log.error("Iterator '%s' is not defined", itername) 461 528 raise 462 463 # Add an optional onfail mode 529 pass 530 pass 531 532 def set_change_onfail(self, change, node): 533 """ 534 Attempt to add an 'onfail' mode for the change, if defined 535 """ 464 536 log.debug("Checking for 'onfail' attribute...") 465 537 try: … … 482 554 pass 483 555 484 # If the change doesn't have a provisioner, use the first one485 # in our config by default.486 if getattr(change, 'provisioner', None) is None:487 change.provisioner = self.provisioners.values()[0]488 489 self.changes[change_name] = change490 self.pending_changes.append( change )491 556 492 557 def add_namespace(self, node, item=None): … … 598 663 raise ValueError("change_complete() cannot handle change state: %s", change.state) 599 664 600 def apply_namespaces(self, string): 601 """ 602 Apply namespaces mid parse. 603 Deprecated. 604 """ 605 # Find the first namespace in the element stack, and use it, 606 # which will implicitly apply its parents. 607 ## tempstack = self.element_stack 608 ## tempstack.reverse() 609 ## for elem in tempstack: 610 ## if getattr(elem, 'namespace', None) is not None: 611 ## log.debug("applying namespace: %s", elem.namespace) 612 ## string = string % elem.namespace 613 ## break 614 ## pass 615 log.warn("Inline namespace usage not yet supported.") 665 def _apply_namespace(self, string, namespace): 666 """ 667 Apply a namespace to a string 668 Deprecated 669 """ 670 if namespace is not None: 671 string = string % namespace 672 616 673 return string 617 674 -
trunk/etc/netapp-provision-demo.xml
r27 r28 12 12 13 13 --> 14 <!-- include change templates I want to use --> 15 <xi:include href="netapp-create-volume.change-template.xml"/> 14 16 17 <!-- define my provisioner --> 15 18 <provisioner 16 19 name='netapp_provisioner' 17 20 type='MultiConnectingProvisioner' 18 command_timeout=' 5'>21 command_timeout='30'> 19 22 20 23 <command>ssh -i /home/daedalus/.ssh/configulator -o BatchMode=yes -o ServerAliveInterval=0 root@%(device.ipaddress)s "%(command.send)s"</command> … … 26 29 </provisioner> 27 30 31 <!-- define some devices I want to provision to --> 28 32 <device name='wibble'> 29 33 <ipaddress>10.232.8.71</ipaddress> … … 34 38 </device> 35 39 36 <!-- define some namespace entities that I need for the changes --> 37 <namespace> 38 <entry name='primary_filer'>wibble</entry> 39 <entry name='secondary_filer'>wibble</entry> 40 41 <entry name='vfiler_name'>vf_demo</entry> 42 43 <entry name='root_aggr'>aggr01</entry> 44 <entry name='root_vol_size'>20m</entry> 45 46 </namespace> 47 48 <iterator name="project_volumes"> 40 <!-- Define iterators used by changes --> 41 <iterator name="primary_volumes"> 49 42 <dict> 50 <entry name="voln um">00</entry>43 <entry name="volname">prim_root</entry> 51 44 <entry name="volaggr">aggr01</entry> 52 45 <entry name="volsize">25m</entry> … … 54 47 55 48 <dict> 56 <entry name="voln um">01</entry>49 <entry name="volname">prim_vol01</entry> 57 50 <entry name="volaggr">aggr01</entry> 58 51 <entry name="volsize">25m</entry> … … 61 54 </iterator> 62 55 63 <change name="create_root_volume_primary" type="CommandChange" onfail='continue'> 56 <iterator name="secondary_volumes"> 57 <dict> 58 <entry name="volname">sec_root</entry> 59 <entry name="volaggr">aggr01</entry> 60 <entry name="volsize">25m</entry> 61 </dict> 64 62 65 <target>%(primary_filer)s</target> 63 <dict> 64 <entry name="volname">sec_vol01</entry> 65 <entry name="volaggr">aggr01</entry> 66 <entry name="volsize">25m</entry> 67 </dict> 66 68 67 <preimpl> 68 <command> 69 <send>vol status %(vfiler_name)s_root</send> 70 </command> 69 </iterator> 71 70 72 <condition>cmdoutput.find("No volume named '%(vfiler_name)s_root' exists.") >= 0</condition> 73 74 </preimpl> 75 76 <impl> 77 <command> 78 <send>vol create %(vfiler_name)s_root %(root_aggr)s %(root_vol_size)s</send> 79 </command> 80 81 <condition>cmdoutput.find("Creation of volume") >= 0</condition> 82 <condition>cmdoutput.find("has completed") >= 0</condition> 83 84 </impl> 85 86 <backout> 87 <command> 88 <send>vol offline %(vfiler_name)s_root</send> 89 </command> 90 91 <command> 92 <expect>Volume '%(vfiler_name)s_root' has been set temporarily offline</expect> 93 <send>vol destroy %(vfiler_name)s_root -f</send> 94 </command> 95 96 <condition>cmdoutput.find("Volume '%(vfiler_name)s_root' destroyed") >= 0</condition> 97 98 </backout> 99 71 <!-- Set up the change list I want to apply --> 72 <change name="create_primary_volumes" template='create_netapp_volume' iterator='primary_volumes'> 73 <target>wibble</target> 100 74 </change> 101 75 102 <change name="create_project_volume_primary" type="CommandChange" iterator="project_volumes" 103 onfail='retry' max_retries='2'> 104 105 <prereq>create_root_volume_primary</prereq> 106 107 <target>%(primary_filer)s</target> 108 109 <preimpl> 110 <command> 111 <send>vol status %(vfiler_name)s_vol%(volnum)s</send> 112 </command> 113 114 <condition>cmdoutput.find("No volume named '%(vfiler_name)s_vol%(volnum)s' exists.") >= 0</condition> 115 116 </preimpl> 117 118 <impl> 119 <command> 120 <send>vol create %(vfiler_name)s_vol%(volnum)s %(volaggr)s %(volsize)s</send> 121 </command> 122 123 <condition>cmdoutput.find("Creation of volume") >= 0</condition> 124 <condition>cmdoutput.find("has completed") >= 0</condition> 125 126 </impl> 127 128 <backout> 129 <command> 130 <send>vol offline %(vfiler_name)s_vol%(volnum)s</send> 131 </command> 132 133 <command> 134 <expect>Volume '%(vfiler_name)s_vol%(volnum)s' has been set temporarily offline</expect> 135 <send>vol destroy %(vfiler_name)s_vol%(volnum)s -f</send> 136 </command> 137 138 <condition>cmdoutput.find("Volume '%(vfiler_name)s_vol%(volnum)s' destroyed") >= 0</condition> 139 140 </backout> 141 76 <change name="create_secondary_volumes" template='create_netapp_volume' iterator='secondary_volumes'> 77 <target>wibble</target> 78 <depends on='create_primary_volumes'/> 142 79 </change> 143 80 144 145 <!--146 <change name="test_iterator" type="CommandChange" iterator="iter1">147 148 <prereq>None</prereq>149 150 <target>wibble</target>151 152 <preimpl>153 <command>154 <send>vol status %(volname)s</send>155 </command>156 157 <condition>cmdoutput.find("No volume named '%(volname)s' exists.") >= 0</condition>158 159 </preimpl>160 161 </change>162 -->163 164 81 </config> -
trunk/provisioner.py
r27 r28 54 54 Back out a change that was applied to a specific device. 55 55 """ 56 57 class UserBailout(Exception): 58 """ 59 User denied a change command from executing in Authoritarian mode. 60 This causes the program to bail out complete at exactly that point. 61 """ 56 62 57 63 class Provisioner: … … 68 74 self.namespace = namespace 69 75 self.authoritarian = authoritarian 76 77 log.debug("My namespace is: %s", self.namespace) 70 78 71 79 def parse_config_node(self, node): … … 148 156 149 157 log.error("Change '%s' failed to apply", change.name) 150 e = failure.check( ChangeConditionFailure )158 e = failure.check( ChangeConditionFailure, UserBailout ) 151 159 if e: 152 160 log.error(" failure was: %s", failure.value ) 161 if isinstance(e, UserBailout): 162 log.info("User bailout detected.") 163 return defer.succeed('bailout') 153 164 else: 154 165 tlog.err(failure) … … 504 515 else: 505 516 log.info(" Bailing out at your command.") 506 self.waitingForCommand.errback("User requested manual bailout.") 517 raise UserBailout("Bailing out at your command") 518 #self.waitingForCommand.errback("User requested manual bailout.") 507 519 return 508 520 pass … … 572 584 for (expr, cmdstring) in expectset: 573 585 574 # Perform variable substitution on the command string 575 log.debug("determining commandstring from template: %s", cmdstring) 576 cmdstring = str(util.substituteVariables(cmdstring, namespace)) 577 log.debug("commandstring is: %s", cmdstring) 578 579 # add the cmdstring to the namespace 580 namespace['command.send'] = cmdstring 581 582 log.debug("Determining command base from template: %s", self.command) 583 command = str(util.substituteVariables(self.command, namespace)) 584 586 try: 587 # Perform variable substitution on the command string 588 log.debug("determining commandstring from template: %s", cmdstring) 589 cmdstring = str(util.substituteVariables(cmdstring, namespace)) 590 log.debug("commandstring is: %s", cmdstring) 591 592 # add the cmdstring to the namespace 593 namespace['command.send'] = cmdstring 594 595 log.debug("Determining command base from template: %s", self.command) 596 command = str(util.substituteVariables(self.command, namespace)) 597 except KeyError, e: 598 log.error("KeyError in commands: %s" % e) 599 self.all_commands_defer.errback( e ) 600 return self.all_commands_defer 601 585 602 #log.debug("checking expr: %s" % expr) 586 603 log.debug("cmdstring is: %s" % command) … … 605 622 log.debug("spawning command...") 606 623 self.waitingForCommand = defer.Deferred() 624 607 625 # If we're in authoritarian mode, wait for confirmation 608 626 # that we should execute the command. … … 617 635 self.exitcode = -1 618 636 self.cmdoutput = 'User requested manual bailout' 619 self.waitingForCommand.errback( Exception("User requested manual bailout.") ) 637 log.critical("User requested manual bailout") 638 self.waitingForCommand.errback( UserBailout("User requested manual bailout.") ) 620 639 return self.waitingForCommand 621 640 pass … … 638 657 self.cmdoutput += result[1] 639 658 log.debug("current results: exit '%d', output: %s", self.exitcode, self.cmdoutput) 640 log.error("Process: %s", self.p)641 659 642 660 def command_failed(self, failure): 643 661 errorstr = "%s: %s" % (self.exitcode, self.cmdoutput) 644 662 log.error("Command failed: %s", errorstr) 645 log.error("Process: %s", self.p)646 self.exitcode = result[0]647 self.cmdoutput += result[1]648 663 #self.all_commands_defer.errback( Exception(errorstr) ) 649 664 … … 704 719 self.parent.waitingForCommand.callback( (self.exitCode, self.databuf) ) 705 720 pass 721 722 except AlreadyCalled, AlreadyCancelled: 723 pass 706 724 707 725 except Exception, e: … … 713 731 714 732 def timedOut(self): 733 # FIXME: If the command times out, we need to ignore anything that comes 734 # back from the process, or we'll notify of error conditions more than once 735 # I'm hoping the AlreadyCalled an AlreadyCancelled catch above will take care of this. 715 736 log.error("Timed out waiting for command to exit") 716 737 self.transport.loseConnection()
