d32a3dd9949f8275cbf0c42f0c919c936e818f25
[nepi.git] / src / nepi / execution / resource.py
1 #
2 #    NEPI, a framework to manage network experiments
3 #    Copyright (C) 2013 INRIA
4 #
5 #    This program is free software: you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation, either version 3 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14 #
15 #    You should have received a copy of the GNU General Public License
16 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18 # Author: Alina Quereilhac <alina.quereilhac@inria.fr>
19
20 from nepi.util.timefuncs import tnow, tdiff, tdiffsec, stabsformat
21 from nepi.util.logger import Logger
22 from nepi.execution.attribute import Attribute, Flags, Types
23 from nepi.execution.trace import TraceAttr
24
25 import copy
26 import functools
27 import logging
28 import os
29 import pkgutil
30 import sys
31 import threading
32 import weakref
33
34 reschedule_delay = "1s"
35
36 class ResourceAction:
37     """ Action that a user can order to a Resource Manager
38    
39     """
40     DEPLOY = 0
41     START = 1
42     STOP = 2
43
44 class ResourceState:
45     """ State of a Resource Manager
46    
47     """
48     NEW = 0
49     DISCOVERED = 1
50     PROVISIONED = 2
51     READY = 3
52     STARTED = 4
53     STOPPED = 5
54     FAILED = 6
55     RELEASED = 7
56
57 ResourceState2str = dict({
58     ResourceState.NEW : "NEW",
59     ResourceState.DISCOVERED : "DISCOVERED",
60     ResourceState.PROVISIONED : "PROVISIONED",
61     ResourceState.READY : "READY",
62     ResourceState.STARTED : "STARTED",
63     ResourceState.STOPPED : "STOPPED",
64     ResourceState.FAILED : "FAILED",
65     ResourceState.RELEASED : "RELEASED",
66     })
67
68 def clsinit(cls):
69     """ Initializes template information (i.e. attributes and traces)
70     on classes derived from the ResourceManager class.
71
72     It is used as a decorator in the class declaration as follows:
73
74         @clsinit
75         class MyResourceManager(ResourceManager):
76         
77             ...
78
79      """
80
81     cls._clsinit()
82     return cls
83
84 def clsinit_copy(cls):
85     """ Initializes template information (i.e. attributes and traces)
86     on classes direved from the ResourceManager class.
87     It differs from the clsinit method in that it forces inheritance
88     of attributes and traces from the parent class.
89
90     It is used as a decorator in the class declaration as follows:
91
92         @clsinit
93         class MyResourceManager(ResourceManager):
94         
95             ...
96
97
98     clsinit_copy should be prefered to clsinit when creating new
99     ResourceManager child classes.
100
101     """
102     
103     cls._clsinit_copy()
104     return cls
105
106 def failtrap(func):
107     """ Decorator function for instance methods that should set the 
108     RM state to FAILED when an error is raised. The methods that must be
109     decorated are: discover, provision, deploy, start, stop.
110
111     """
112     def wrapped(self, *args, **kwargs):
113         try:
114             return func(self, *args, **kwargs)
115         except:
116             import traceback
117             err = traceback.format_exc()
118             self.error(err)
119             self.debug("SETTING guid %d to state FAILED" % self.guid)
120             self.fail()
121             raise
122     
123     return wrapped
124
125 @clsinit
126 class ResourceManager(Logger):
127     """ Base clase for all ResourceManagers. 
128     
129     A ResourceManger is specific to a resource type (e.g. Node, 
130     Switch, Application, etc) on a specific backend (e.g. PlanetLab, 
131     OMF, etc).
132
133     The ResourceManager instances are responsible for interacting with
134     and controlling concrete (physical or virtual) resources in the 
135     experimental backends.
136     
137     """
138     _rtype = "Resource"
139     _attributes = None
140     _traces = None
141     _help = None
142     _backend = None
143
144     @classmethod
145     def _register_attribute(cls, attr):
146         """ Resource subclasses will invoke this method to add a 
147         resource attribute
148
149         """
150         
151         cls._attributes[attr.name] = attr
152
153     @classmethod
154     def _remove_attribute(cls, name):
155         """ Resource subclasses will invoke this method to remove a 
156         resource attribute
157
158         """
159         
160         del cls._attributes[name]
161
162     @classmethod
163     def _register_trace(cls, trace):
164         """ Resource subclasses will invoke this method to add a 
165         resource trace
166
167         """
168         
169         cls._traces[trace.name] = trace
170
171     @classmethod
172     def _remove_trace(cls, name):
173         """ Resource subclasses will invoke this method to remove a 
174         resource trace
175
176         """
177         
178         del cls._traces[name]
179
180     @classmethod
181     def _register_attributes(cls):
182         """ Resource subclasses will invoke this method to register
183         resource attributes.
184
185         This method should be overriden in the RMs that define
186         attributes.
187
188         """
189         critical = Attribute("critical", 
190                 "Defines whether the resource is critical. "
191                 "A failure on a critical resource will interrupt "
192                 "the experiment. ",
193                 type = Types.Bool,
194                 default = True,
195                 flags = Flags.Design)
196
197         cls._register_attribute(critical)
198         
199     @classmethod
200     def _register_traces(cls):
201         """ Resource subclasses will invoke this method to register
202         resource traces
203
204         This method should be overriden in the RMs that define traces.
205         
206         """
207         
208         pass
209
210     @classmethod
211     def _clsinit(cls):
212         """ ResourceManager classes have different attributes and traces.
213         Attribute and traces are stored in 'class attribute' dictionaries.
214         When a new ResourceManager class is created, the _clsinit method is 
215         called to create a new instance of those dictionaries and initialize 
216         them.
217         
218         The _clsinit method is called by the clsinit decorator method.
219         
220         """
221         
222         # static template for resource attributes
223         cls._attributes = dict()
224         cls._register_attributes()
225
226         # static template for resource traces
227         cls._traces = dict()
228         cls._register_traces()
229
230     @classmethod
231     def _clsinit_copy(cls):
232         """ Same as _clsinit, except that after creating new instances of the
233         dictionaries it copies all the attributes and traces from the parent 
234         class.
235         
236         The _clsinit_copy method is called by the clsinit_copy decorator method.
237         
238         """
239         # static template for resource attributes
240         cls._attributes = copy.deepcopy(cls._attributes)
241         cls._register_attributes()
242
243         # static template for resource traces
244         cls._traces = copy.deepcopy(cls._traces)
245         cls._register_traces()
246
247     @classmethod
248     def get_rtype(cls):
249         """ Returns the type of the Resource Manager
250
251         """
252         return cls._rtype
253
254     @classmethod
255     def get_attributes(cls):
256         """ Returns a copy of the attributes
257
258         """
259         return copy.deepcopy(cls._attributes.values())
260
261     @classmethod
262     def get_attribute(cls, name):
263         """ Returns a copy of the attribute with name 'name'
264
265         """
266         return copy.deepcopy(cls._attributes[name])
267
268
269     @classmethod
270     def get_traces(cls):
271         """ Returns a copy of the traces
272
273         """
274         return copy.deepcopy(cls._traces.values())
275
276     @classmethod
277     def get_help(cls):
278         """ Returns the description of the type of Resource
279
280         """
281         return cls._help
282
283     @classmethod
284     def get_backend(cls):
285         """ Returns the identified of the backend (i.e. testbed, environment)
286         for the Resource
287
288         """
289         return cls._backend
290
291     @classmethod
292     def get_global(cls, name):
293         """ Returns the value of a global attribute
294             Global attribute meaning an attribute for 
295             all the resources from a rtype
296
297         :param name: Name of the attribute
298         :type name: str
299         :rtype: str
300         """
301         global_attr = cls._attributes[name]
302         return global_attr.value
303
304     @classmethod
305     def set_global(cls, name, value):
306         """ Set value for a global attribute
307
308         :param name: Name of the attribute
309         :type name: str
310         :param name: Value of the attribute
311         :type name: str
312         """
313         global_attr = cls._attributes[name]
314         global_attr.value = value
315         return value
316
317     def __init__(self, ec, guid):
318         super(ResourceManager, self).__init__(self.get_rtype())
319         
320         self._guid = guid
321         self._ec = weakref.ref(ec)
322         self._connections = set()
323         self._conditions = dict() 
324
325         # the resource instance gets a copy of all attributes
326         self._attrs = copy.deepcopy(self._attributes)
327
328         # the resource instance gets a copy of all traces
329         self._trcs = copy.deepcopy(self._traces)
330
331         # Each resource is placed on a deployment group by the EC
332         # during deployment
333         self.deployment_group = None
334
335         self._start_time = None
336         self._stop_time = None
337         self._discover_time = None
338         self._provision_time = None
339         self._ready_time = None
340         self._release_time = None
341         self._failed_time = None
342
343         self._state = ResourceState.NEW
344
345         # instance lock to synchronize exclusive state change methods (such
346         # as deploy and release methods), in order to prevent them from being 
347         # executed at the same time
348         self._release_lock = threading.Lock()
349
350     @property
351     def guid(self):
352         """ Returns the global unique identifier of the RM """
353         return self._guid
354
355     @property
356     def ec(self):
357         """ Returns the Experiment Controller of the RM """
358         return self._ec()
359
360     @property
361     def connections(self):
362         """ Returns the set of guids of connected RMs """
363         return self._connections
364
365     @property
366     def conditions(self):
367         """ Returns the conditions to which the RM is subjected to.
368         
369         This method returns a dictionary of conditions lists indexed by
370         a ResourceAction.
371         
372         """
373         return self._conditions
374
375     @property
376     def start_time(self):
377         """ Returns the start time of the RM as a timestamp """
378         return self._start_time
379
380     @property
381     def stop_time(self):
382         """ Returns the stop time of the RM as a timestamp """
383         return self._stop_time
384
385     @property
386     def discover_time(self):
387         """ Returns the discover time of the RM as a timestamp """
388         return self._discover_time
389
390     @property
391     def provision_time(self):
392         """ Returns the provision time of the RM as a timestamp """
393         return self._provision_time
394
395     @property
396     def ready_time(self):
397         """ Returns the deployment time of the RM as a timestamp """
398         return self._ready_time
399
400     @property
401     def release_time(self):
402         """ Returns the release time of the RM as a timestamp """
403         return self._release_time
404
405     @property
406     def failed_time(self):
407         """ Returns the time failure occured for the RM as a timestamp """
408         return self._failed_time
409
410     @property
411     def state(self):
412         """ Get the current state of the RM """
413         return self._state
414
415     def log_message(self, msg):
416         """ Returns the log message formatted with added information.
417
418         :param msg: text message
419         :type msg: str
420         :rtype: str
421
422         """
423         return " %s guid: %d - %s " % (self._rtype, self.guid, msg)
424
425     def register_connection(self, guid):
426         """ Registers a connection to the RM identified by guid
427
428         This method should not be overriden. Specific functionality
429         should be added in the do_connect method.
430
431         :param guid: Global unique identified of the RM to connect to
432         :type guid: int
433
434         """
435         if self.valid_connection(guid):
436             self.do_connect(guid)
437             self._connections.add(guid)
438
439     def unregister_connection(self, guid):
440         """ Removes a registered connection to the RM identified by guid
441         
442         This method should not be overriden. Specific functionality
443         should be added in the do_disconnect method.
444
445         :param guid: Global unique identified of the RM to connect to
446         :type guid: int
447
448         """
449         if guid in self._connections:
450             self.do_disconnect(guid)
451             self._connections.remove(guid)
452
453     @failtrap
454     def discover(self):
455         """ Performs resource discovery.
456         
457         This  method is responsible for selecting an individual resource
458         matching user requirements.
459
460         This method should not be overriden directly. Specific functionality
461         should be added in the do_discover method.
462
463         """
464         with self._release_lock:
465             if self._state != ResourceState.RELEASED:
466                 self.do_discover()
467
468     @failtrap
469     def provision(self):
470         """ Performs resource provisioning.
471
472         This  method is responsible for provisioning one resource.
473         After this method has been successfully invoked, the resource
474         should be accessible/controllable by the RM.
475
476         This method should not be overriden directly. Specific functionality
477         should be added in the do_provision method.
478
479         """
480         with self._release_lock:
481             if self._state != ResourceState.RELEASED:
482                 self.do_provision()
483
484     @failtrap
485     def start(self):
486         """ Starts the RM (e.g. launch remote process).
487     
488         There is no standard start behavior. Some RMs will not need to perform
489         any actions upon start.
490
491         This method should not be overriden directly. Specific functionality
492         should be added in the do_start method.
493
494         """
495
496         if not self.state in [ResourceState.READY, ResourceState.STOPPED]:
497             self.error("Wrong state %s for start" % self.state)
498             return
499
500         with self._release_lock:
501             if self._state != ResourceState.RELEASED:
502                 self.do_start()
503
504     @failtrap
505     def stop(self):
506         """ Interrupts the RM, stopping any tasks the RM was performing.
507      
508         There is no standard stop behavior. Some RMs will not need to perform
509         any actions upon stop.
510     
511         This method should not be overriden directly. Specific functionality
512         should be added in the do_stop method.
513       
514         """
515         if not self.state in [ResourceState.STARTED]:
516             self.error("Wrong state %s for stop" % self.state)
517             return
518         
519         with self._release_lock:
520             self.do_stop()
521
522     @failtrap
523     def deploy(self):
524         """ Execute all steps required for the RM to reach the state READY.
525
526         This method is responsible for deploying the resource (and invoking 
527         the discover and provision methods).
528  
529         This method should not be overriden directly. Specific functionality
530         should be added in the do_deploy method.
531        
532         """
533         if self.state > ResourceState.READY:
534             self.error("Wrong state %s for deploy" % self.state)
535             return
536
537         with self._release_lock:
538             if self._state != ResourceState.RELEASED:
539                 self.do_deploy()
540
541     def release(self):
542         """ Perform actions to free resources used by the RM.
543   
544         This  method is responsible for releasing resources that were
545         used during the experiment by the RM.
546
547         This method should not be overriden directly. Specific functionality
548         should be added in the do_release method.
549       
550         """
551         with self._release_lock:
552             try:
553                 self.do_release()
554             except:
555                 import traceback
556                 err = traceback.format_exc()
557                 self.error(err)
558
559                 self.set_released()
560
561     def fail(self):
562         """ Sets the RM to state FAILED.
563
564         This method should not be overriden directly. Specific functionality
565         should be added in the do_fail method.
566
567         """
568         with self._release_lock:
569             if self._state != ResourceState.RELEASED:
570                 self.do_fail()
571
572     def set(self, name, value):
573         """ Set the value of the attribute
574
575         :param name: Name of the attribute
576         :type name: str
577         :param name: Value of the attribute
578         :type name: str
579         """
580         attr = self._attrs[name]
581         attr.value = value
582         return value
583
584     def get(self, name):
585         """ Returns the value of the attribute
586
587         :param name: Name of the attribute
588         :type name: str
589         :rtype: str
590         """
591         attr = self._attrs[name]
592         if attr.has_flag(Flags.Global):
593             self.warning( "Attribute %s is global. Use get_global instead." % name)
594             
595         return attr.value
596
597     def has_changed(self, name):
598         """ Returns the True is the value of the attribute
599             has been modified by the user.
600
601         :param name: Name of the attribute
602         :type name: str
603         :rtype: str
604         """
605         attr = self._attrs[name]
606         return attr.has_changed()
607
608     def has_flag(self, name, flag):
609         """ Returns true if the attribute has the flag 'flag'
610
611         :param flag: Flag to be checked
612         :type flag: Flags
613         """
614         attr = self._attrs[name]
615         return attr.has_flag(flag)
616
617     def enable_trace(self, name):
618         """ Explicitly enable trace generation
619
620         :param name: Name of the trace
621         :type name: str
622         """
623         trace = self._trcs[name]
624         trace.enabled = True
625     
626     def trace_enabled(self, name):
627         """Returns True if trace is enables 
628
629         :param name: Name of the trace
630         :type name: str
631         """
632         trace = self._trcs[name]
633         return trace.enabled
634  
635     def trace(self, name, attr = TraceAttr.ALL, block = 512, offset = 0):
636         """ Get information on collected trace
637
638         :param name: Name of the trace
639         :type name: str
640
641         :param attr: Can be one of:
642                          - TraceAttr.ALL (complete trace content), 
643                          - TraceAttr.STREAM (block in bytes to read starting at offset), 
644                          - TraceAttr.PATH (full path to the trace file),
645                          - TraceAttr.SIZE (size of trace file). 
646         :type attr: str
647
648         :param block: Number of bytes to retrieve from trace, when attr is TraceAttr.STREAM 
649         :type name: int
650
651         :param offset: Number of 'blocks' to skip, when attr is TraceAttr.STREAM 
652         :type name: int
653
654         :rtype: str
655         """
656         pass
657
658     def register_condition(self, action, group, state, time = None):
659         """ Registers a condition on the resource manager to allow execution 
660         of 'action' only after 'time' has elapsed from the moment all resources 
661         in 'group' reached state 'state'
662
663         :param action: Action to restrict to condition (either 'START' or 'STOP')
664         :type action: str
665         :param group: Group of RMs to wait for (list of guids)
666         :type group: int or list of int
667         :param state: State to wait for on all RM in group. (either 'STARTED', 'STOPPED' or 'READY')
668         :type state: str
669         :param time: Time to wait after 'state' is reached on all RMs in group. (e.g. '2s')
670         :type time: str
671
672         """
673
674         if not action in self.conditions:
675             self._conditions[action] = list()
676         
677         conditions = self.conditions.get(action)
678
679         # For each condition to register a tuple of (group, state, time) is 
680         # added to the 'action' list
681         if not isinstance(group, list):
682             group = [group]
683
684         conditions.append((group, state, time))
685
686     def unregister_condition(self, group, action = None):
687         """ Removed conditions for a certain group of guids
688
689         :param action: Action to restrict to condition (either 'START', 'STOP' or 'READY')
690         :type action: str
691
692         :param group: Group of RMs to wait for (list of guids)
693         :type group: int or list of int
694
695         """
696         # For each condition a tuple of (group, state, time) is 
697         # added to the 'action' list
698         if not isinstance(group, list):
699             group = [group]
700
701         for act, conditions in self.conditions.iteritems():
702             if action and act != action:
703                 continue
704
705             for condition in list(conditions):
706                 (grp, state, time) = condition
707
708                 # If there is an intersection between grp and group,
709                 # then remove intersected elements
710                 intsec = set(group).intersection(set(grp))
711                 if intsec:
712                     idx = conditions.index(condition)
713                     newgrp = set(grp)
714                     newgrp.difference_update(intsec)
715                     conditions[idx] = (newgrp, state, time)
716                  
717     def get_connected(self, rtype = None):
718         """ Returns the list of RM with the type 'rtype'
719
720         :param rtype: Type of the RM we look for
721         :type rtype: str
722         :return: list of guid
723         """
724         connected = []
725         rclass = ResourceFactory.get_resource_type(rtype)
726         for guid in self.connections:
727
728             rm = self.ec.get_resource(guid)
729             if not rtype or isinstance(rm, rclass):
730                 connected.append(rm)
731         return connected
732
733     @failtrap
734     def _needs_reschedule(self, group, state, time):
735         """ Internal method that verify if 'time' has elapsed since 
736         all elements in 'group' have reached state 'state'.
737
738         :param group: Group of RMs to wait for (list of guids)
739         :type group: int or list of int
740         :param state: State to wait for on all RM in group. (either 'STARTED', 'STOPPED' or 'READY')
741         :type state: str
742         :param time: Time to wait after 'state' is reached on all RMs in group. (e.g. '2s')
743         :type time: str
744
745         .. note : time should be written like "2s" or "3m" with s for seconds, m for minutes, h for hours, ...
746         If for example, you need to wait 2min 30sec, time could be "150s" or "2.5m".
747         For the moment, 2m30s is not a correct syntax.
748
749         """
750         reschedule = False
751         delay = reschedule_delay 
752
753         # check state and time elapsed on all RMs
754         for guid in group:
755             rm = self.ec.get_resource(guid)
756             
757             # If one of the RMs this resource needs to wait for has FAILED
758             # and is critical we raise an exception
759             if rm.state == ResourceState.FAILED:
760                 if not rm.get('critical'):
761                     continue
762                 msg = "Resource can not wait for FAILED RM %d. Setting Resource to FAILED"
763                 raise RuntimeError, msg
764
765             # If the RM state is lower than the requested state we must
766             # reschedule (e.g. if RM is READY but we required STARTED).
767             if rm.state < state:
768                 reschedule = True
769                 break
770
771             # If there is a time restriction, we must verify the
772             # restriction is satisfied 
773             if time:
774                 if state == ResourceState.DISCOVERED:
775                     t = rm.discover_time
776                 if state == ResourceState.PROVISIONED:
777                     t = rm.provision_time
778                 elif state == ResourceState.READY:
779                     t = rm.ready_time
780                 elif state == ResourceState.STARTED:
781                     t = rm.start_time
782                 elif state == ResourceState.STOPPED:
783                     t = rm.stop_time
784                 elif state == ResourceState.RELEASED:
785                     t = rm.release_time
786                 else:
787                     break
788
789                 # time already elapsed since RM changed state
790                 waited = "%fs" % tdiffsec(tnow(), t)
791
792                 # time still to wait
793                 wait = tdiffsec(stabsformat(time), stabsformat(waited))
794
795                 if wait > 0.001:
796                     reschedule = True
797                     delay = "%fs" % wait
798                     break
799
800         return reschedule, delay
801
802     def set_with_conditions(self, name, value, group, state, time):
803         """ Set value 'value' on attribute with name 'name' when 'time' 
804         has elapsed since all elements in 'group' have reached state
805         'state'
806
807         :param name: Name of the attribute to set
808         :type name: str
809         :param name: Value of the attribute to set
810         :type name: str
811         :param group: Group of RMs to wait for (list of guids)
812         :type group: int or list of int
813         :param state: State to wait for on all RM in group. (either 'STARTED', 'STOPPED' or 'READY')
814         :type state: str
815         :param time: Time to wait after 'state' is reached on all RMs in group. (e.g. '2s')
816         :type time: str
817         """
818
819         reschedule = False
820         delay = reschedule_delay 
821
822         ## evaluate if set conditions are met
823
824         # only can set with conditions after the RM is started
825         if self.state != ResourceState.STARTED:
826             reschedule = True
827         else:
828             reschedule, delay = self._needs_reschedule(group, state, time)
829
830         if reschedule:
831             callback = functools.partial(self.set_with_conditions, 
832                     name, value, group, state, time)
833             self.ec.schedule(delay, callback)
834         else:
835             self.set(name, value)
836
837     def start_with_conditions(self):
838         """ Starts RM when all the conditions in self.conditions for
839         action 'START' are satisfied.
840
841         """
842         #import pdb;pdb.set_trace()
843
844         reschedule = False
845         delay = reschedule_delay 
846
847
848         ## evaluate if conditions to start are met
849         if self.ec.abort:
850             return 
851
852         # Can only start when RM is either STOPPED or READY
853         if self.state not in [ResourceState.STOPPED, ResourceState.READY]:
854             reschedule = True
855             self.debug("---- RESCHEDULING START ---- state %s " % self.state )
856         else:
857             start_conditions = self.conditions.get(ResourceAction.START, [])
858             
859             self.debug("---- START CONDITIONS ---- %s" % start_conditions) 
860             
861             # Verify all start conditions are met
862             for (group, state, time) in start_conditions:
863                 # Uncomment for debug
864                 unmet = []
865                 for guid in group:
866                     rm = self.ec.get_resource(guid)
867                     unmet.append((guid, rm._state))
868                 
869                 self.debug("---- WAITED STATES ---- %s" % unmet )
870
871                 reschedule, delay = self._needs_reschedule(group, state, time)
872                 if reschedule:
873                     break
874
875         if reschedule:
876             self.ec.schedule(delay, self.start_with_conditions)
877         else:
878             self.debug("----- STARTING ---- ")
879             self.start()
880
881     def stop_with_conditions(self):
882         """ Stops RM when all the conditions in self.conditions for
883         action 'STOP' are satisfied.
884
885         """
886         reschedule = False
887         delay = reschedule_delay 
888
889         ## evaluate if conditions to stop are met
890         if self.ec.abort:
891             return 
892
893         # only can stop when RM is STARTED
894         if self.state != ResourceState.STARTED:
895             reschedule = True
896             self.debug("---- RESCHEDULING STOP ---- state %s " % self.state )
897         else:
898             self.debug(" ---- STOP CONDITIONS ---- %s" % 
899                     self.conditions.get(ResourceAction.STOP))
900
901             stop_conditions = self.conditions.get(ResourceAction.STOP, []) 
902             for (group, state, time) in stop_conditions:
903                 reschedule, delay = self._needs_reschedule(group, state, time)
904                 if reschedule:
905                     break
906
907         if reschedule:
908             callback = functools.partial(self.stop_with_conditions)
909             self.ec.schedule(delay, callback)
910         else:
911             self.debug(" ----- STOPPING ---- ") 
912             self.stop()
913
914     def deploy_with_conditions(self):
915         """ Deploy RM when all the conditions in self.conditions for
916         action 'READY' are satisfied.
917
918         """
919         reschedule = False
920         delay = reschedule_delay 
921
922         ## evaluate if conditions to deploy are met
923         if self.ec.abort:
924             return 
925
926         # only can deploy when RM is either NEW, DISCOVERED or PROVISIONED 
927         if self.state not in [ResourceState.NEW, ResourceState.DISCOVERED, 
928                 ResourceState.PROVISIONED]:
929             reschedule = True
930             self.debug("---- RESCHEDULING DEPLOY ---- state %s " % self.state )
931         else:
932             deploy_conditions = self.conditions.get(ResourceAction.DEPLOY, [])
933             
934             self.debug("---- DEPLOY CONDITIONS ---- %s" % deploy_conditions) 
935             
936             # Verify all start conditions are met
937             for (group, state, time) in deploy_conditions:
938                 # Uncomment for debug
939                 #unmet = []
940                 #for guid in group:
941                 #    rm = self.ec.get_resource(guid)
942                 #    unmet.append((guid, rm._state))
943                 
944                 #self.debug("---- WAITED STATES ---- %s" % unmet )
945
946                 reschedule, delay = self._needs_reschedule(group, state, time)
947                 if reschedule:
948                     break
949
950         if reschedule:
951             self.ec.schedule(delay, self.deploy_with_conditions)
952         else:
953             self.debug("----- DEPLOYING ---- ")
954             self.deploy()
955
956     def do_connect(self, guid):
957         """ Performs actions that need to be taken upon associating RMs.
958         This method should be redefined when necessary in child classes.
959         """
960         pass
961
962     def do_disconnect(self, guid):
963         """ Performs actions that need to be taken upon disassociating RMs.
964         This method should be redefined when necessary in child classes.
965         """
966         pass
967
968     def valid_connection(self, guid):
969         """Checks whether a connection with the other RM
970         is valid.
971         This method need to be redefined by each new Resource Manager.
972
973         :param guid: Guid of the current Resource Manager
974         :type guid: int
975         :rtype:  Boolean
976
977         """
978         # TODO: Validate!
979         return True
980
981     def do_discover(self):
982         self.set_discovered()
983
984     def do_provision(self):
985         self.set_provisioned()
986
987     def do_start(self):
988         self.set_started()
989
990     def do_stop(self):
991         self.set_stopped()
992
993     def do_deploy(self):
994         self.set_ready()
995
996     def do_release(self):
997         self.set_released()
998
999     def do_fail(self):
1000         self.set_failed()
1001
1002     def set_started(self):
1003         """ Mark ResourceManager as STARTED """
1004         self.set_state(ResourceState.STARTED, "_start_time")
1005         self.debug("----- STARTED ---- ")
1006
1007     def set_stopped(self):
1008         """ Mark ResourceManager as STOPPED """
1009         self.set_state(ResourceState.STOPPED, "_stop_time")
1010         self.debug("----- STOPPED ---- ")
1011
1012     def set_ready(self):
1013         """ Mark ResourceManager as READY """
1014         self.set_state(ResourceState.READY, "_ready_time")
1015         self.debug("----- READY ---- ")
1016
1017     def set_released(self):
1018         """ Mark ResourceManager as REALEASED """
1019         self.set_state(ResourceState.RELEASED, "_release_time")
1020         self.debug("----- RELEASED ---- ")
1021
1022     def set_failed(self):
1023         """ Mark ResourceManager as FAILED """
1024         self.set_state(ResourceState.FAILED, "_failed_time")
1025         self.debug("----- FAILED ---- ")
1026
1027     def set_discovered(self):
1028         """ Mark ResourceManager as DISCOVERED """
1029         self.set_state(ResourceState.DISCOVERED, "_discover_time")
1030         self.debug("----- DISCOVERED ---- ")
1031
1032     def set_provisioned(self):
1033         """ Mark ResourceManager as PROVISIONED """
1034         self.set_state(ResourceState.PROVISIONED, "_provision_time")
1035         self.debug("----- PROVISIONED ---- ")
1036
1037     def set_state(self, state, state_time_attr):
1038         """ Set the state of the RM while keeping a trace of the time """
1039
1040         # Ensure that RM state will not change after released
1041         if self._state == ResourceState.RELEASED:
1042             return 
1043    
1044         setattr(self, state_time_attr, tnow())
1045         self._state = state
1046
1047 class ResourceFactory(object):
1048     _resource_types = dict()
1049
1050     @classmethod
1051     def resource_types(cls):
1052         """Return the type of the Class"""
1053         return cls._resource_types
1054
1055     @classmethod
1056     def get_resource_type(cls, rtype):
1057         """Return the type of the Class"""
1058         return cls._resource_types.get(rtype)
1059
1060     @classmethod
1061     def register_type(cls, rclass):
1062         """Register a new Ressource Manager"""
1063         cls._resource_types[rclass.get_rtype()] = rclass
1064
1065     @classmethod
1066     def create(cls, rtype, ec, guid):
1067         """Create a new instance of a Ressource Manager"""
1068         rclass = cls._resource_types[rtype]
1069         return rclass(ec, guid)
1070
1071 def populate_factory():
1072     """Register all the possible RM that exists in the current version of Nepi.
1073     """
1074     # Once the factory is populated, don't repopulate
1075     if not ResourceFactory.resource_types():
1076         for rclass in find_types():
1077             ResourceFactory.register_type(rclass)
1078
1079 def find_types():
1080     """Look into the different folders to find all the 
1081     availables Resources Managers
1082     """
1083     search_path = os.environ.get("NEPI_SEARCH_PATH", "")
1084     search_path = set(search_path.split(" "))
1085    
1086     import inspect
1087     import nepi.resources 
1088     path = os.path.dirname(nepi.resources.__file__)
1089     search_path.add(path)
1090
1091     types = set()
1092
1093     for importer, modname, ispkg in pkgutil.walk_packages(search_path, 
1094             prefix = "nepi.resources."):
1095
1096         loader = importer.find_module(modname)
1097         
1098         try:
1099             # Notice: Repeated calls to load_module will act as a reload of the module
1100             if modname in sys.modules:
1101                 module = sys.modules.get(modname)
1102             else:
1103                 module = loader.load_module(modname)
1104
1105             for attrname in dir(module):
1106                 if attrname.startswith("_"):
1107                     continue
1108
1109                 attr = getattr(module, attrname)
1110
1111                 if attr == ResourceManager:
1112                     continue
1113
1114                 if not inspect.isclass(attr):
1115                     continue
1116
1117                 if issubclass(attr, ResourceManager):
1118                     types.add(attr)
1119
1120                     if not modname in sys.modules:
1121                         sys.modules[modname] = module
1122
1123         except:
1124             import traceback
1125             import logging
1126             err = traceback.format_exc()
1127             logger = logging.getLogger("Resource.find_types()")
1128             logger.error("Error while loading Resource Managers %s" % err)
1129
1130     return types
1131