moving towards reservable nodes
[nodemanager.git] / nodemanager.py
1 #!/usr/bin/python
2 #
3 # $Id$
4 # $URL$
5 #
6 # Useful information can be found at https://svn.planet-lab.org/wiki/NodeManager
7 #
8
9 # Faiyaz Ahmed <faiyaza at cs dot princeton dot edu>
10 # Copyright (C) 2008 The Trustees of Princeton University
11
12
13 """Node Manager"""
14
15 import optparse
16 import time
17 import xmlrpclib
18 import socket
19 import os
20 import sys
21 import resource
22 import glob
23 import pickle
24
25 import logger
26 import tools
27
28 from config import Config
29 from plcapi import PLCAPI 
30 import random
31
32
33 class NodeManager:
34
35     PLUGIN_PATH = "/usr/share/NodeManager/plugins"
36
37     DB_FILE = "/var/lib/nodemanager/getslivers.pickle"
38
39     # the modules in this directory that need to be run
40     # NOTE: modules listed here will also be loaded in this order
41     # once loaded, they get re-ordered after their priority (lower comes first) 
42     # for determining the runtime order
43     core_modules=['net', 'conf_files', 'slivermanager', 'bwmon']
44
45     default_period=600
46     default_random=301
47     default_priority=100
48
49     def __init__ (self):
50
51         parser = optparse.OptionParser()
52         parser.add_option('-d', '--daemon', action='store_true', dest='daemon', default=False, help='run daemonized')
53         parser.add_option('-s', '--startup', action='store_true', dest='startup', default=False, help='run all sliver startup scripts')
54         parser.add_option('-f', '--config', action='store', dest='config', default='/etc/planetlab/plc_config', help='PLC configuration file')
55         parser.add_option('-k', '--session', action='store', dest='session', default='/etc/planetlab/session', help='API session key (or file)')
56         parser.add_option('-p', '--period', action='store', dest='period', default=NodeManager.default_period, 
57                           help='Polling interval (sec) - default %d'%NodeManager.default_period)
58         parser.add_option('-r', '--random', action='store', dest='random', default=NodeManager.default_random, 
59                           help='Range for additional random polling interval (sec) -- default %d'%NodeManager.default_random)
60         parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False, help='more verbose log')
61         parser.add_option('-P', '--path', action='store', dest='path', default=NodeManager.PLUGIN_PATH, help='Path to plugins directory')
62
63         # NOTE: BUG the 'help' for this parser.add_option() wont list plugins from the --path argument
64         parser.add_option('-m', '--module', action='store', dest='user_module', default='', help='run a single module')
65         (self.options, args) = parser.parse_args()
66
67         if len(args) != 0:
68             parser.print_help()
69             sys.exit(1)
70
71         # determine the modules to be run
72         self.modules = NodeManager.core_modules
73         # Deal with plugins directory
74         if os.path.exists(self.options.path):
75             sys.path.append(self.options.path)
76             plugins = [ os.path.split(os.path.splitext(x)[0])[1] for x in glob.glob( os.path.join(self.options.path,'*.py') ) ]
77             self.modules += plugins
78         if self.options.user_module:
79             assert self.options.user_module in self.modules
80             self.modules=[self.options.user_module]
81             logger.verbose('nodemanager: Running single module %s'%self.options.user_module)
82
83
84     def GetSlivers(self, config, plc):
85         """Retrieves GetSlivers at PLC and triggers callbacks defined in modules/plugins"""
86         try: 
87             logger.log("nodemanager: Syncing w/ PLC")
88             # retrieve GetSlivers from PLC
89             data = plc.GetSlivers()
90             # use the magic 'default' slice to retrieve system-wide defaults
91             self.getPLCDefaults(data, config)
92             # tweak the 'vref' attribute from GetSliceFamily
93             self.setSliversVref (data)
94             # dump it too, so it can be retrieved later in case of comm. failure
95             self.dumpSlivers(data)
96             # log it for debug purposes, no matter what verbose is
97             logger.log_slivers(data)
98             logger.verbose("nodemanager: Sync w/ PLC done")
99             last_data=data
100         except: 
101             logger.log_exc("nodemanager: failed in GetSlivers")
102             #  XXX So some modules can at least boostrap.
103             logger.log("nodemanager:  Can't contact PLC to GetSlivers().  Continuing.")
104             data = {}
105             # for modules that request it though the 'persistent_data' property
106             last_data=self.loadSlivers()
107         #  Invoke GetSlivers() functions from the callback modules
108         for module in self.loaded_modules:
109             logger.verbose('nodemanager: triggering %s.GetSlivers'%module.__name__)
110             try:        
111                 callback = getattr(module, 'GetSlivers')
112                 module_data=data
113                 if getattr(module,'persistent_data',False):
114                     module_data=last_data
115                 callback(data, config, plc)
116             except: 
117                 logger.log_exc("nodemanager: GetSlivers failed to run callback for module %r"%module)
118
119
120     def getPLCDefaults(self, data, config):
121         """
122         Get PLC wide defaults from _default system slice.  Adds them to config class.
123         """
124         for slice in data.get('slivers'): 
125             if slice['name'] == config.PLC_SLICE_PREFIX+"_default":
126                 attr_dict = {}
127                 for attr in slice.get('attributes'): attr_dict[attr['tagname']] = attr['value'] 
128                 if len(attr_dict):
129                     logger.verbose("nodemanager: Found default slice overrides.\n %s" % attr_dict)
130                     config.OVERRIDES = attr_dict
131                     return
132         # NOTE: if an _default slice existed, it would have been found above and
133         #           the routine would return.  Thus, if we've gotten here, then no default
134         #           slice is bound to this node.
135         if 'OVERRIDES' in dir(config): del config.OVERRIDES
136
137
138     def setSliversVref (self, data):
139         """
140         Tweak the 'vref' attribute in all slivers based on the 'GetSliceFamily' key
141         """
142         # GetSlivers exposes the result of GetSliceFamily() as an separate key in data
143         # It is safe to override the attributes with this, as this method has the right logic
144         for sliver in data.get('slivers'): 
145             try:
146                 slicefamily=sliver.get('GetSliceFamily')
147                 for att in sliver['attributes']:
148                     if att['tagname']=='vref': 
149                         att['value']=slicefamily
150                         continue
151                 sliver['attributes'].append({ 'tagname':'vref','value':slicefamily})
152             except:
153                 logger.log_exc("nodemanager: Could not overwrite 'vref' attribute from 'GetSliceFamily'",name=sliver['name'])
154     
155     def dumpSlivers (self, slivers):
156         f = open(NodeManager.DB_FILE, "w")
157         logger.log ("nodemanager: saving successfully fetched GetSlivers in %s" % NodeManager.DB_FILE)
158         pickle.dump(slivers, f)
159         f.close()
160     
161     def loadSlivers (self):
162         try:
163             f = open(NodeManager.DB_FILE, "r+")
164             logger.log("nodemanager: restoring latest known GetSlivers from %s" % NodeManager.DB_FILE)
165             slivers = pickle.load(f)
166             f.close()
167             return slivers
168         except:
169             logger.log("Could not restore GetSlivers from %s" % NodeManager.DB_FILE)
170             return {}
171     
172     def run(self):
173         try:
174             if self.options.daemon: tools.daemon()
175     
176             # set log level
177             if (self.options.verbose):
178                 logger.set_level(logger.LOG_VERBOSE)
179     
180             # Load /etc/planetlab/plc_config
181             config = Config(self.options.config)
182     
183             try:
184                 other_pid = tools.pid_file()
185                 if other_pid != None:
186                     print """There might be another instance of the node manager running as pid %d.  If this is not the case, please remove the pid file %s""" % (other_pid, tools.PID_FILE)
187                     return
188             except OSError, err:
189                 print "Warning while writing PID file:", err
190     
191             # load modules
192             self.loaded_modules = []
193             for module in self.modules:
194                 try:
195                     m = __import__(module)
196                     logger.verbose("nodemanager: triggering %s.start"%m.__name__)
197                     m.start(self.options, config)
198                     self.loaded_modules.append(m)
199                 except ImportError, err:
200                     print "Warning while loading module %s:" % module, err
201     
202             # sort on priority (lower first)
203             def sort_module_priority (m1,m2):
204                 return getattr(m1,'priority',NodeManager.default_priority) - getattr(m2,'priority',NodeManager.default_priority)
205             self.loaded_modules.sort(sort_module_priority)
206     
207             logger.log('ordered modules:')
208             for module in self.loaded_modules: 
209                 logger.log ('%s: %s'%(getattr(module,'priority',NodeManager.default_priority),module.__name__))
210     
211             # Load /etc/planetlab/session
212             if os.path.exists(self.options.session):
213                 session = file(self.options.session).read().strip()
214             else:
215                 session = None
216     
217     
218             # get random periods
219             iperiod=int(self.options.period)
220             irandom=int(self.options.random)
221     
222             # Initialize XML-RPC client
223             plc = PLCAPI(config.plc_api_uri, config.cacert, session, timeout=iperiod/2)
224     
225             #check auth
226             logger.log("nodemanager: Checking Auth.")
227             while plc.check_authentication() != True:
228                 try:
229                     plc.update_session()
230                     logger.log("nodemanager: Authentication Failure. Retrying")
231                 except Exception,e:
232                     logger.log("nodemanager: Retry Failed. (%r); Waiting.."%e)
233                 time.sleep(iperiod)
234             logger.log("nodemanager: Authentication Succeeded!")
235     
236     
237             while True:
238             # Main nodemanager Loop
239                 logger.log('nodemanager: mainloop - calling GetSlivers - period=%d random=%d'%(iperiod,irandom))
240                 self.GetSlivers(config, plc)
241                 delay=iperiod + random.randrange(0,irandom)
242                 logger.log('nodemanager: mainloop - sleeping for %d s'%delay)
243                 time.sleep(delay)
244         except: logger.log_exc("nodemanager: failed in run")
245
246 def run():
247     logger.log("======================================== Entering nodemanager.py")
248     NodeManager().run()
249     
250 if __name__ == '__main__':
251     run()
252 else:
253     # This is for debugging purposes.  Open a copy of Python and import nm
254     tools.as_daemon_thread(run)