Changeset 28

Show
Ignore:
Timestamp:
26/11/07 20:14:17 (2 years ago)
Author:
daedalus
Message:

* Refactored code to permit more flexible templating of changes. You can now

define a <changetemplate/> in 95% the same way as a <change/>, and then
refer to a <changetemplate/> when defining a <change/> to allow you to
combine change templates as a change flow, using namespaces and iterators
for each one.

* Some work on manual bailout in authoritarian mode, but it isn't completely

bailing out immediately yet, due to some complexities in the deferred chains
that I haven't quite unravelled.

Location:
trunk
Files:
4 modified

Legend:

Unmodified
Added
Removed
  • trunk/change.py

    r27 r28  
    215215        return '<%s: %s>' % ( self.__class__, self.name ) 
    216216 
     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         
    217244    def set_state(self, state_string): 
    218245        self.state = CHANGE_STATE[state_string] 
     
    236263                for ns in self.iterator: 
    237264                    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 
    240268                    newnamespace = ns.copy() 
    241269                    newnamespace.update(namespace) 
  • trunk/confloader.py

    r27 r28  
    1515from device import Device 
    1616from change import CHANGE_STATE 
     17import util 
    1718 
    1819from twisted.internet import defer, reactor 
     
    128129        self.provisioners = {} 
    129130        self.devices = {} 
     131        self.change_templates = {} 
    130132        self.changes = {} 
    131133        self.iterators = {} 
     
    163165                    del self.devices[dev] 
    164166                    pass 
    165  
    166     def oldloader(self): 
    167         # A new provisioner 
    168         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 config 
    186             #self.global_namespace = self.namespace_stack.pop() 
    187  
    188             # Add some global elements to the namespace 
    189             element.namespace['ALL_TARGETS'] = [ x for x in self.devices.keys() ] 
    190  
    191             self.global_namespace = element.namespace 
    192  
    193  
    194 ##         elif name == 'dependencies': 
    195 ##             # We've finish a list of dependency elements 
    196 ##             log.info("Finished loaded dependencies.") 
    197 ##             self.dependency_tree = element 
    198167             
    199168    def parse(self, configfile): 
     
    203172        self.tree = etree.parse(configfile) 
    204173 
     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             
    205183        # add the global namespace 
    206184        try: 
     
    209187        except IndexError: 
    210188            log.warn("Cannot find global namespace") 
    211             self.global_namespace = None 
     189            self.global_namespace = {} 
    212190 
    213191        # Add all my node types, in the order specified in the list 
     
    216194            'provisioner', 
    217195            'device', 
     196            'changetemplate', 
    218197            'change', 
    219             'dependencies', 
     198#            'dependencies', 
    220199            ]: 
    221200 
     
    333312        log.debug("my ip addr is: %s", device.ipaddress) 
    334313        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 
    336341    def add_change(self, node): 
    337342        """ 
    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. 
    339391        """ 
    340392        # Change type is dynamic, as it supports the loading 
     
    343395        change_name = node.attrib['name'] 
    344396 
    345         log.debug("Attempting to create a '%s' change", change_klass) 
     397        log.debug("Creating '%s' change", change_klass) 
    346398 
    347399        try: 
     
    356408 
    357409        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        """ 
    360416        ns = node.find('namespace') 
    361417        if ns is None: 
     
    364420            self.add_namespace(ns, change) 
    365421 
    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        """ 
    367440        for subnode in node.xpath('*'): 
    368             # which provisioner to use for the change 
    369             if subnode.tag == 'useprov': 
    370                 provname = subnode.text 
    371                 provname = provname % change.namespace 
    372                 change.provisioner = self.provisioners[provname] 
    373                 log.debug("change '%s' will use provisioner '%s'", change, provname) 
    374                 pass 
    375  
    376441            # a target device for the change 
    377             elif subnode.tag == 'target': 
     442            if subnode.tag == 'target': 
    378443                targetname = subnode.text 
    379444 
     
    385450                else: 
    386451                    # perform variable substitution with global namespace 
    387                     #targetname = self.apply_namespaces(targetname) 
    388452                    log.debug("my namespace: %s", change.namespace) 
    389                     targetname = targetname % change.namespace 
    390                      
     453                    targetname = util.substituteVariables(targetname, change.namespace) 
    391454                    dev = self.devices[targetname] 
    392455                    log.debug("current target list: %s", change.devices) 
     
    395458                pass 
    396459 
    397             elif subnode.tag == 'prereq': 
    398                 changename = subnode.text 
     460            elif subnode.tag == 'depends': 
     461                changename = subnode.attrib['on'] 
    399462                try: 
    400463                    change.pre_requisites.append(self.changes[changename]) 
     
    444507                pass 
    445508            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        """ 
    448515        try: 
    449516            itername = node.attrib['iterator'] 
     
    453520        if itername is not None: 
    454521            # Use namespace substitution for the itername to allow templating 
    455             itername = itername % change.namespace 
     522            itername = util.substituteVariables(itername, change.namespace) 
    456523            try: 
    457524                change.iterator = self.iterators[itername] 
     
    460527                log.error("Iterator '%s' is not defined", itername) 
    461528                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        """ 
    464536        log.debug("Checking for 'onfail' attribute...") 
    465537        try: 
     
    482554            pass 
    483555 
    484         # If the change doesn't have a provisioner, use the first one 
    485         # 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] = change 
    490         self.pending_changes.append( change ) 
    491556 
    492557    def add_namespace(self, node, item=None): 
     
    598663            raise ValueError("change_complete() cannot handle change state: %s", change.state) 
    599664 
    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 
    616673        return string 
    617674 
  • trunk/etc/netapp-provision-demo.xml

    r27 r28  
    1212   
    1313--> 
     14<!-- include change templates I want to use --> 
     15<xi:include href="netapp-create-volume.change-template.xml"/> 
    1416 
     17<!-- define my provisioner --> 
    1518<provisioner 
    1619  name='netapp_provisioner' 
    1720  type='MultiConnectingProvisioner' 
    18   command_timeout='5'> 
     21  command_timeout='30'> 
    1922 
    2023  <command>ssh -i /home/daedalus/.ssh/configulator -o BatchMode=yes -o ServerAliveInterval=0 root@%(device.ipaddress)s "%(command.send)s"</command> 
     
    2629</provisioner> 
    2730 
     31<!-- define some devices I want to provision to --> 
    2832<device name='wibble'> 
    2933  <ipaddress>10.232.8.71</ipaddress> 
     
    3438</device> 
    3539 
    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"> 
    4942  <dict> 
    50     <entry name="volnum">00</entry> 
     43    <entry name="volname">prim_root</entry> 
    5144    <entry name="volaggr">aggr01</entry> 
    5245    <entry name="volsize">25m</entry> 
     
    5447 
    5548  <dict> 
    56     <entry name="volnum">01</entry> 
     49    <entry name="volname">prim_vol01</entry> 
    5750    <entry name="volaggr">aggr01</entry> 
    5851    <entry name="volsize">25m</entry> 
     
    6154</iterator> 
    6255 
    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> 
    6462 
    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> 
    6668 
    67   <preimpl> 
    68     <command> 
    69       <send>vol status %(vfiler_name)s_root</send> 
    70     </command> 
     69</iterator> 
    7170 
    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> 
    10074</change> 
    10175 
    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'/> 
    14279</change> 
    14380 
    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  
    16481</config> 
  • trunk/provisioner.py

    r27 r28  
    5454        Back out a change that was applied to a specific device. 
    5555        """ 
     56 
     57class 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    """ 
    5662 
    5763class Provisioner: 
     
    6874        self.namespace = namespace 
    6975        self.authoritarian = authoritarian 
     76 
     77        log.debug("My namespace is: %s", self.namespace) 
    7078 
    7179    def parse_config_node(self, node): 
     
    148156 
    149157        log.error("Change '%s' failed to apply", change.name) 
    150         e = failure.check( ChangeConditionFailure ) 
     158        e = failure.check( ChangeConditionFailure, UserBailout ) 
    151159        if e: 
    152160            log.error("  failure was: %s", failure.value ) 
     161            if isinstance(e, UserBailout): 
     162                log.info("User bailout detected.") 
     163                return defer.succeed('bailout') 
    153164        else: 
    154165            tlog.err(failure) 
     
    504515            else: 
    505516                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.") 
    507519                return 
    508520            pass 
     
    572584        for (expr, cmdstring) in expectset: 
    573585 
    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             
    585602            #log.debug("checking expr: %s" % expr) 
    586603            log.debug("cmdstring is: %s" % command) 
     
    605622        log.debug("spawning command...") 
    606623        self.waitingForCommand = defer.Deferred() 
     624 
    607625        # If we're in authoritarian mode, wait for confirmation 
    608626        # that we should execute the command. 
     
    617635                self.exitcode = -1 
    618636                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.") ) 
    620639                return self.waitingForCommand 
    621640            pass 
     
    638657        self.cmdoutput += result[1] 
    639658        log.debug("current results: exit '%d', output: %s", self.exitcode, self.cmdoutput) 
    640         log.error("Process: %s", self.p) 
    641659         
    642660    def command_failed(self, failure): 
    643661        errorstr = "%s: %s" % (self.exitcode, self.cmdoutput) 
    644662        log.error("Command failed: %s", errorstr) 
    645         log.error("Process: %s", self.p) 
    646         self.exitcode = result[0] 
    647         self.cmdoutput += result[1] 
    648663        #self.all_commands_defer.errback( Exception(errorstr) ) 
    649664 
     
    704719                self.parent.waitingForCommand.callback( (self.exitCode, self.databuf) ) 
    705720                pass 
     721 
     722        except AlreadyCalled, AlreadyCancelled: 
     723            pass 
    706724                 
    707725        except Exception, e: 
     
    713731 
    714732    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. 
    715736        log.error("Timed out waiting for command to exit") 
    716737        self.transport.loseConnection()