root/tags/v1.0/confloader.py

Revision 29, 31.3 KB (checked in by daedalus, 4 years ago)

* Manual bailout when in authoritarian mode now works correctly.
* Fixed up some of the log level and stats reporting.
* Added 'backout' mode, that will only run the backout portion of changes. More

useful for testing, but can also be used to undo provisioning by repurposing
the same set of templates used for provisioning.

Line 
1##
2
3"""
4The ConfigLoader loads in a particular configuration file that
5specifies the change definition to be run.
6"""
7import re
8import sys
9import os
10
11#from xml.parsers.expat import ParserCreate
12#from xml.dom.minidom import Document, Text
13from lxml import etree
14
15from device import Device
16from change import CHANGE_STATE, ChangeConditionFailure
17from provisioner import UserBailout
18import util
19
20from twisted.internet import defer, reactor
21from twisted.python import log as tlog
22
23import logging
24import debug
25
26log = logging.getLogger('modipy')
27
28xinclude_re = re.compile(r'.*<xi:include href=[\'\"](?P<uri>.*)[\'\"].*')
29
30class Namespace:
31    """
32    A cascading namespace object that can refer to items in
33    parent namespaces to resolve names if they do not exist
34    in the current namespace.
35    """
36    def __init__(self, name, namespace={}, parent=None):
37
38        self.parent = parent
39        self.namespace = namespace
40
41    def __getitem__(self, key):
42        """
43        Implement dictionary interface
44        """
45        try:
46            return self.namespace[key]
47        except KeyError:
48            if self.parent is not None:
49                return self.parent[key]
50            else:
51                raise
52
53    def __setitem__(self, key, value):
54        self.namespace[key] = value
55
56    def keys(self):
57        keys = self.namespace.keys()
58       
59        if self.parent is not None:
60            pkeys = self.parent.keys()
61            for pkey in pkeys:
62                if pkey not in keys:
63                    keys.append(pkey)
64                    pass
65                pass
66            pass
67
68        return keys
69
70    def has_key(self, key):
71        if self.namespace.has_key(key):
72            return True
73        else:
74            if self.parent is not None:
75                return self.parent.has_key(key)
76            else:
77                return False
78            pass
79        pass
80
81    def update(self, dict):
82        for key in dict:
83            self.namespace[key] = dict[key]
84
85    def __iter__(self):
86        log.debug("Creating a Namespace iterator")
87
88        # initialise iterators
89        if self.parent is not None:
90            log.debug("started parent iterator")
91            return self.parent.__iter__()
92
93        self.namespace.__iter__()
94        log.debug("started my iterator")
95        return self
96
97    def next(self):
98        if self.parent is not None:
99            try:
100                return self.parent.next()
101            except StopIteration:
102                return self.namespace.next()
103        else:
104            try:
105                return self.namespace.next()
106            except AttributeError:
107                raise StopIteration
108           
109    def items(self):
110        log.debug("Using namespace.items()!")
111        items = []
112        if self.parent is not None:
113            items.extend( self.parent.items() )
114        items.extend( self.namespace.items() )
115
116        return items
117
118class ConfigLoader:
119    """
120    A ConfigLoader is used to load in a change configuration and
121    set up all the objects that define the change.
122    """
123
124    def __init__(self, options=None, devices=[]):
125
126        self.doc = None
127
128        self.options = options
129
130        self.provisioners = {}
131        self.devices = {}
132        self.change_templates = {}
133        self.changes = {}
134        self.iterators = {}
135
136        self.pending_changes = []
137        self.change_success = []
138        self.change_failure = []
139        self.backout_success = []
140        self.backout_failure = []
141
142        log.debug("created ConfigLoader")
143
144        if options.configfile:
145            self.parse(options.configfile)
146            pass
147
148        # Add devices specified on the commandline to the active configuration
149        if len(devices) != 0:
150            for dev in devices:
151                # If a device given on the commandline is not present in the
152                # configuration file, add it.
153                if dev not in self.devices.keys():
154                    log.info("Adding commandline specified device: '%s'" % dev)
155                    self.devices[dev] = Device(dev)
156                    pass
157                pass
158                # If devices other than those specified on the commandline
159                # ARE present in the config file, remove the extra ones.
160                # This means you can run a configuration on a subset of devices
161                # by specifying the device name on the commandline.
162
163            for dev in self.devices.keys():
164                if dev not in devices:
165                    log.info("Not using configured device '%s' (not on commandline)", dev)
166                    del self.devices[dev]
167                    pass
168           
169    def parse(self, configfile):
170        """
171        Parse my configuration file.
172        """
173        self.tree = etree.parse(configfile)
174
175        # Process xincludes
176        try:
177            self.tree.xinclude()
178        except etree.XIncludeError:
179            log.error("XInclude of a file failed.")
180            log.error("Use external tool such as xmllint to figure out why.")
181            log.error("Sorry, but lxml.etree won't tell me exactly what went wrong.")
182            raise
183           
184        # add the global namespace
185        try:
186            nsnode = self.tree.xpath('/config/namespace')[0]
187            self.add_namespace(nsnode)
188        except IndexError:
189            self.global_namespace = {}
190
191        # Add all my node types, in the order specified in the list
192        for nodename in [
193            'iterator',
194            'provisioner',
195            'device',
196            'changetemplate',
197            'change',
198#            'dependencies',
199            ]:
200
201            for node in self.tree.findall(nodename):
202                # Find the function to call based on the name of
203                # the object by using dynamic lookup
204                # This will call 'add_iterator' for an iterator node, for example.
205                funcname = 'add_%s' % nodename
206                func = getattr(self, funcname)
207                log.debug("calling %s with arg: %s", func, node)
208                func(node)
209
210        # Find all the prereq definitions
211        prereqs = self.tree.findall('prereq')
212        for node in prereqs:
213            prereq_changename = node.text
214
215            try:
216                prereq_for = node.attrib['for']
217            except KeyError:
218                # See if this prereq node is within a change node
219                prereq_for = node.xpath("parent::*/change")[0].attrib['name']
220                log.debug("Using change name of '%s'", prereq_for)
221               
222            # find the change
223            try:
224                prereq_change = self.changes[prereq_changename]
225            except KeyError:
226                log.error("Cannot find change '%s' as prereq for change '%s'", prereq_changename, prereq_for)
227                raise
228
229            try:
230                change_for = self.changes[prereq_for]
231            except KeyError:
232                log.error("Cannot find change '%s' with prereq of '%s'", prereq_for, prereq_changename)
233                raise
234
235            # add change as prereq
236            change_for.pre_requisites.append(prereq_change)
237            log.debug("Added change '%s' as prereq for change '%s'", prereq_changename, prereq_for)
238
239        # once the parse has completed, do some post-parse setup
240        self.process_dependencies()
241
242    def add_iterator(self, node):
243        """
244        Add an iterator, which is a list of dictionary name/value pairs
245        that will be iterated over for a particular change step.
246        """
247        iter_name = node.attrib['name']
248        iterlist = []
249        for ns in node.findall('dict'):
250            iter_ns = {}
251            for entry in ns.findall('entry'):
252                item_name = entry.attrib['name']
253                item_value = entry.text
254                iter_ns[item_name] = item_value
255                pass
256            iterlist.append(iter_ns)
257            pass
258        self.iterators[iter_name] = iterlist
259        log.debug("Added iterator '%s': %s" % (iter_name, iterlist))
260
261    def add_provisioner(self, node):
262        """
263        Add a provisioner to my configuration based on the parsed element.
264        """
265        # Provisioner type is dynamic, as it supports the loading
266        # of plugin modules at runtime, so we look up the type
267        prov_klass = node.attrib['type']
268        prov_name = node.attrib['name']
269
270        # Copy the attribs into a dictionary for use as kwargs
271        kwargs = {}
272        for key in node.attrib:
273            if key not in ['type', 'name']:
274                kwargs[key] = node.attrib[key]
275
276        log.debug("Attempting to create a '%s' provisioner", prov_klass)
277        prov_module = __import__('provisioner')
278        klass = getattr( prov_module, prov_klass )
279        try:
280            provisioner = klass(prov_name, authoritarian=self.options.authoritarian, **kwargs)
281        except TypeError, e:
282            log.error("Incorrect parameter supplied for provisioner of type '%s'", prov_klass)
283            raise e
284
285        log.debug("created provisioner '%s': %s", prov_name, provisioner)
286
287        # If there are further attributes for the provisioner,
288        # handle them with the provisioner itself
289        for subnode in node.xpath('*'):
290            provisioner.parse_config_node(subnode)
291            pass
292
293        self.provisioners[prov_name] = provisioner
294
295    def add_device(self, node):
296        """
297        Add a device to my configuration based on the parsed element.
298        """
299        device_name = node.attrib['name']
300        device = Device(device_name)
301
302        log.debug("created device '%s': %s", device_name, device)
303
304        # add an attribute for each sub-element
305        for subnode in node.xpath('*'):
306            log.debug("processing device subnode: %s", subnode.tag)
307            attrib = subnode.tag
308            value = subnode.text
309            setattr(device, attrib, value)
310            pass
311
312        log.debug("my ip addr is: %s", device.ipaddress)
313        self.devices[device_name] = device
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
341    def add_change(self, node):
342        """
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.
391        """
392        # Change type is dynamic, as it supports the loading
393        # of plugin modules at runtime, so we look up the type
394        change_klass = node.attrib['type']
395        change_name = node.attrib['name']
396
397        log.debug("Creating '%s' change", change_klass)
398
399        try:
400            change_module = sys.modules['change']
401        except KeyError:
402            log.info("change module not yet imported. Importing...")
403            change_module = __import__('change')
404            pass
405
406        klass = getattr(change_module, change_klass)
407        change = klass(change_name)
408
409        log.debug("created change '%s': %s", change_name, change)
410        return change
411
412    def set_change_namespace(self, change, node):
413        """
414        Add a namespace to a change or change template
415        """
416        ns = node.find('namespace')
417        if ns is None:
418            change.namespace = self.global_namespace
419        else:
420            self.add_namespace(ns, change)
421
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        """
440        for subnode in node.xpath('*'):
441            # a target device for the change
442            if subnode.tag == 'target':
443                targetname = subnode.text
444
445                # Parse special targetname ALL_TARGETS
446                if targetname == 'ALL_TARGETS':
447                    change.devices.extend( self.devices.values() )
448                    devicenames = ','.join( [ '%s' % x for x in self.devices.keys() ] )
449                    log.debug("added target devices '%s' for change '%s'", devicenames, change)
450                else:
451                    # perform variable substitution with global namespace
452                    log.debug("my namespace: %s", change.namespace)
453                    targetname = util.substituteVariables(targetname, change.namespace)
454                    dev = self.devices[targetname]
455                    log.debug("current target list: %s", change.devices)
456                    change.devices.append(dev)
457                    log.debug("added target device '%s' for change '%s'", dev, change)
458                pass
459
460            elif subnode.tag == 'depends':
461                changename = subnode.attrib['on']
462                try:
463                    change.pre_requisites.append(self.changes[changename])
464                except KeyError:
465                    if changename == change.name:
466                        log.error("Change '%s' cannot be a pre-requisite of itself.", changename)
467                        pass
468                    else:
469                        log.error("Configuration error: Unknown pre-requisite '%s' for change '%s'", changename, change.name)
470                        pass
471                    raise
472                log.debug("added change pre-requisite: '%s' for change '%s'", changename, change)
473
474            #
475            # The following attributes need to be parsed by the change
476            # implementation itself, as the permitted contents of the
477            # node varies depending on the change type. For example,
478            # the implementation actions for a CommandChange are unix-style
479            # commandline invokations, while a ZAPIChange might specify
480            # NetApp ZAPI documents
481            #
482
483            # pre-implementation actions for the change
484            elif subnode.tag == 'preimpl':
485                log.debug("adding change pre-implementation actions")
486                change.parse_node_preimpl(subnode)
487
488            elif subnode.tag == 'impl':
489                log.debug("adding change implementation actions")
490                change.parse_node_impl(subnode)
491               
492            elif subnode.tag == 'postimpl':
493                log.debug("adding change post-implementation actions")
494                change.parse_node_postimpl(subnode)
495               
496            elif subnode.tag == 'prebackout':
497                log.debug("adding change pre-backout actions")
498                change.parse_node_prebackout(subnode)
499               
500            elif subnode.tag == 'backout':
501                log.debug("adding change backout actions")
502                change.parse_node_backout(subnode)
503               
504            elif subnode.tag == 'postbackout':
505                log.debug("adding change post-backout actions")
506                change.parse_node_postbackout(subnode)
507                pass
508            pass
509        pass
510
511    def set_change_iterator(self, change, node):
512        """
513        Set an optional change iterator
514        """
515        try:
516            itername = node.attrib['iterator']
517        except KeyError:
518            itername = None
519
520        if itername is not None:
521            # Use namespace substitution for the itername to allow templating
522            itername = util.substituteVariables(itername, change.namespace)
523            try:
524                change.iterator = self.iterators[itername]
525                log.debug("change '%s' will iterate with iterator '%s'" % (change.name, itername))
526            except KeyError:
527                log.error("Iterator '%s' is not defined", itername)
528                raise
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        """
536        log.debug("Checking for 'onfail' attribute...")
537        try:
538            onfail = node.attrib['onfail']
539            log.debug("onfail node found.")
540            if onfail == 'continue':
541                change.on_fail_continue = True
542                log.debug("Processing will continue if this change fails")
543
544            elif onfail == 'retry':
545                change.on_fail_retry = True
546                log.debug("This change will retry on failure")
547                try:
548                    max_retries = int(node.attrib['max_retries'])
549                    change.max_retries = max_retries
550                except KeyError:
551                    pass
552                log.debug("  This change will retry at most %d times", change.max_retries)
553        except KeyError:
554            pass
555
556
557    def add_namespace(self, node, item=None):
558        """
559        Add a namespace, defined by nsnode, to an item (such as a change)
560        This is called within an 'add_' method for the other items,
561        such as iterators and changes.
562        """
563        ns = Namespace(node.tag)
564
565        # Find any parent namespaces
566        # FIXME: This is harder in an element tree than if
567        # we're building as we go.
568
569        # Find namespace entries
570        for entry in node.findall('entry'):
571            key = entry.attrib['name']
572            value = entry.text
573            log.debug("Adding namespace entry: %s -> %s", key, value)
574            ns[key] = value
575            pass
576
577        if item is None:
578            # This is the global namespace
579            self.global_namespace = ns
580        else:
581            log.debug("Adding namespace to item: %s", item)
582            ns.parent = self.global_namespace
583            item.namespace = ns
584
585    def get_available_changes(self):
586        """
587        This returns a list of pending changes that can be performed,
588        either because they have no pre-requsites, or all their
589        pre-requisistes have been successfully implemented.
590        """
591        changelist = []
592        log.debug("pending changes: %s", self.pending_changes)
593        for change in self.pending_changes:
594            log.debug("testing change %s", change)
595            if len(change.pre_requisites) == 0:
596                changelist.append(change)
597                log.debug("change '%s' has no pre-reqs. Adding to execution queue.", change)
598                continue
599
600            else:
601                all_prereqs = True
602                for prereq in change.pre_requisites:
603                    log.debug("change '%s' has a pre-req of '%s'", change, prereq)
604                    if prereq not in self.change_success:
605
606                        # If we're in backout mode, a prereq in 'backout_ok' state
607                        # is treated as complete.
608                        if prereq.state in [ CHANGE_STATE['backout_ok'], ]:
609                            continue
610
611                        # If a prereq has failed, doesn't need to retry, and is
612                        # marked as 'onfail:continue', then we treat it as if
613                        # this prereq has been met.
614                        elif prereq.state not in [ CHANGE_STATE['pending'],
615                                                 CHANGE_STATE['retry'],
616                                                 ] and prereq.on_fail_continue:
617                            log.debug("prereq failed, but is marked onfail:continue.")
618                            continue
619
620                        log.debug("pre-req '%s' not complete yet.", prereq)
621                        all_prereqs = False
622                        break
623                    pass
624
625                if all_prereqs:
626                    log.debug("All pre-reqs for '%s' have completed. Adding to execution queue.", change)
627                    changelist.append(change)
628                else:
629                    log.debug("change '%s' has pending pre-reqs", change)
630
631        log.debug("Returning changelist: %s", changelist)
632        return changelist
633
634    def change_complete(self, change):
635        """
636        Move a change from pending to complete.
637        """
638        log.info("Change %s completed.", change.name)
639        if change.state == CHANGE_STATE['success']:
640            self.change_success.append(change)
641            self.pending_changes.remove(change)
642
643        elif change.state == CHANGE_STATE['partial_failure']:
644            self.change_failure.append(change)
645            self.pending_changes.remove(change)
646
647        elif change.state == CHANGE_STATE['total_failure']:
648            self.change_failure.append(change)
649            self.pending_changes.remove(change)
650
651        elif change.state == CHANGE_STATE['backout_ok']:
652            self.change_failure.append(change)
653            self.backout_success.append(change)
654            self.pending_changes.remove(change)
655
656        elif change.state == CHANGE_STATE['backout_failed']:
657            self.change_failure.append(change)
658            self.backout_failure.append(change)
659            self.pending_changes.remove(change)
660
661        elif change.state == CHANGE_STATE['retry']:
662            # Change will be retried
663            pass
664
665        else:
666            log.error("Unknown/unhandled change state '%s'", change.state)
667            self.pending_changes.remove(change)
668            raise ValueError("change_complete() cannot handle change state: %s", change.state)
669
670    def _apply_namespace(self, string, namespace):
671        """
672        Apply a namespace to a string
673        Deprecated
674        """
675        if namespace is not None:
676            string = string % namespace
677
678        return string
679
680    def process_dependencies(self):
681        """
682        Post-process the dependency tree after loading it.
683        This sets up the pre-reqs for each change, as specified
684        in the dependency tree.
685        """
686        # Fetch the list of changes with dependencies
687        for node in self.tree.findall('dependencies'):
688            log.debug("dependency node: %s", node)
689
690class ChangeController:
691    """
692    An overall change controller, to control the changes
693    """
694
695    def __init__(self, config_loader):
696
697        self.cfgldr = config_loader
698
699        self.current_changelist = []
700
701    def do_changes(self):
702        self.alldone = defer.Deferred()
703
704        self.get_next_changes(None)
705
706        # print statistics for what happened, no matter if it's callback or errback
707        self.alldone.addCallbacks(self.print_stats, self.print_stats)
708
709        # deal with errors in stats
710        #self.alldone.addErrback(self.print_stats)
711                               
712        return self.alldone
713
714    def get_next_changes(self, results):
715        """
716        Fetch the next set of changes to run.
717        This collects all changes that don't have any
718        pre-requisites (on the first pass), or that have had all
719        their pre-requisites complete successfully.
720        These changes can therefore all be run together, since they
721        have no interdependencies on one another.
722        """
723        log.debug("Results in get_next_changes are: %s", results)
724        log.debug("Fetching outstanding changes...")
725        self.current_changelist = self.cfgldr.get_available_changes()
726
727        if len(self.current_changelist) == 0:
728            if len(self.cfgldr.pending_changes) > 0:
729                log.error("Pending changes exist that cannot be executed!")
730                self.alldone.errback(ValueError("Pending changes cannot be executed"))
731            else:
732                # No more changes, all changes completed.
733                self.alldone.callback("All changes complete")
734            pass
735        else:
736            # If there are pending changes, add that call to the deferred
737            log.debug("Pending changes to be executed. Scheduling.")
738            return self.do_pending_changes(None)
739
740    def do_pending_changes(self, ignored):
741        """
742        This runs all the pending changes that are currently queued.
743        It runs all of them simultaneously, since they have been identified
744        as changes that can be run together. If you want to run changes
745        sequentially, then they should have sequential dependencies.
746        """
747        log.debug("Running pending changes...")
748        dlist = []
749
750        log.debug("changelist is: %s", self.current_changelist)
751        for change in self.current_changelist:
752
753            log.debug("adding change '%s' to implementation queue", change.name)
754               
755            d = change.provisioner.perform_change(None, change, self.cfgldr.global_namespace, backout=self.cfgldr.options.backout)
756            d.addCallback(self.change_complete, change)
757            d.addErrback(self.change_failure, change)
758            dlist.append(d)
759            pass
760
761        dl = defer.DeferredList(dlist, fireOnOneErrback=True, consumeErrors=True)
762        # After the current crop of changes has been completed, find if there are more
763        dl.addCallback( self.get_next_changes )
764        dl.addErrback( self.bailout )
765        return dl
766
767    def bailout(self, failure):
768        """
769        Bailout detects the first error from the deferred list,
770        and bails out.
771        """
772        log.debug("Running bailout...")
773        if failure.value.subFailure.type == UserBailout:
774            self.alldone.callback('Bailout')
775
776        else:
777            log.debug("Unhandled failure: %s", failure)
778            self.alldone.errback(failure)
779
780    def change_complete(self, ignored, change):
781        """
782        Move change from pending to complete
783        """
784        self.cfgldr.change_complete(change)
785
786    def change_failure(self, failure, change):
787        if failure.type == UserBailout:
788            log.debug("Controller detected UserBailout.")
789            return failure
790
791        elif failure.type == ChangeConditionFailure:
792            pass
793
794        else:
795            log.error("Major change failure!")
796            tlog.err(failure)
797            pass
798       
799        self.change_complete(failure, change)
800
801    def print_stats(self, ignored):
802        """
803        We've finished all our work, and now we summarise what happened.
804        """
805        success_count = len(self.cfgldr.change_success)
806        failure_count = len(self.cfgldr.change_failure)
807        backout_success_count = len(self.cfgldr.backout_success)
808        backout_failure_count = len(self.cfgldr.backout_failure)
809        pending_count = len(self.cfgldr.pending_changes)
810
811        log.info("--- Final results ---")
812
813        # Don't print some stats when in backout mode
814        if not self.cfgldr.options.backout:
815       
816            if not (success_count > 0 or failure_count > 0):
817                log.error("no changes succeeded or failed!")
818            else:
819                log.info("%d ok, %d failed (%d%% success rate)" % (success_count, failure_count, 100 * success_count / (success_count+failure_count) ) )
820
821            if success_count > 0:
822                log.info( "Successful changes: %s" % ', '.join([ x.name for x in self.cfgldr.change_success]))
823
824            if failure_count > 0:
825                log.info( "Failed changes: %s" % ', '.join([ x.name for x in self.cfgldr.change_failure]) )
826                pass
827            pass
828
829        # These stats are always printed
830
831        if backout_success_count > 0 or backout_failure_count > 0:
832            log.info( "%d backed out ok, %d failed (%d%% backout success rate)" % (backout_success_count, backout_failure_count, 100 * backout_success_count / (backout_success_count+backout_failure_count) ))
833           
834        if backout_success_count > 0:
835            log.info( "Changes backed out ok: %s" % ', '.join([ x.name for x in self.cfgldr.backout_success]))
836
837        if backout_failure_count > 0:
838            log.error( "Changes NOT backed out ok: %s" % ', '.join([ x.name for x in self.cfgldr.backout_failure]))
839
840        if pending_count > 0:
841            if pending_count > 1:
842                plural = 's'
843                waswere = 'were'
844            else:
845                plural = ''
846                waswere = 'was'
847                pass
848
849            log.info( "%d change%s %s not attempted: %s" % (pending_count, plural, waswere, ', '.join([ x.name for x in self.cfgldr.pending_changes ]) ) )
850
851
852if __name__ == '__main__':
853
854    #log.setLevel(logging.DEBUG)
855    log.setLevel(logging.INFO)
856
857    configfile = 'etc/test_change.conf'
858
859    try:
860        cfgldr = ConfigLoader(configfile)
861    except:
862        log.error("Cannot load configuration. Aborting.")
863        sys.exit(1)
864
865    log.debug("changes to apply: %s", cfgldr.changes)
866    log.debug("total devices: %s", cfgldr.devices)
867
868    controller = ChangeController(cfgldr)
869
870    def mystop(ignored):
871        log.debug("finished!")
872        reactor.stop()
873        pass
874
875    def errstop(failure):
876        log.error("Changes not implemented ok!")
877        tlog.err(failure)
878        reactor.stop()
879       
880    def go():
881        d = controller.do_changes()       
882        d.addCallbacks(mystop, errstop)
883       
884    # Use callLater(0) syntax to trigger running of changes once
885    # the reactor has actually started, so it can be stopped cleanly.
886    reactor.callLater(0, go)
887    reactor.run()
888       
Note: See TracBrowser for help on using the browser.