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