cosmetic
[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.
187 If this is not the case, please remove the pid file %s. -- exiting""" % (other_pid, tools.PID_FILE)
188                     return
189             except OSError, err:
190                 print "Warning while writing PID file:", err
191     
192             # load modules
193             self.loaded_modules = []
194             for module in self.modules:
195                 try:
196                     m = __import__(module)
197                     logger.verbose("nodemanager: triggering %s.start"%m.__name__)
198                     m.start(self.options, config)
199                     self.loaded_modules.append(m)
200                 except ImportError, err:
201                     print "Warning while loading module %s:" % module, err
202     
203             # sort on priority (lower first)
204             def sort_module_priority (m1,m2):
205                 return getattr(m1,'priority',NodeManager.default_priority) - getattr(m2,'priority',NodeManager.default_priority)
206             self.loaded_modules.sort(sort_module_priority)
207     
208             logger.log('ordered modules:')
209             for module in self.loaded_modules: 
210                 logger.log ('%s: %s'%(getattr(module,'priority',NodeManager.default_priority),module.__name__))
211     
212             # Load /etc/planetlab/session
213             if os.path.exists(self.options.session):
214                 session = file(self.options.session).read().strip()
215             else:
216                 session = None
217     
218     
219             # get random periods
220             iperiod=int(self.options.period)
221             irandom=int(self.options.random)
222     
223             # Initialize XML-RPC client
224             plc = PLCAPI(config.plc_api_uri, config.cacert, session, timeout=iperiod/2)
225     
226             #check auth
227             logger.log("nodemanager: Checking Auth.")
228             while plc.check_authentication() != True:
229                 try:
230                     plc.update_session()
231                     logger.log("nodemanager: Authentication Failure. Retrying")
232                 except Exception,e:
233                     logger.log("nodemanager: Retry Failed. (%r); Waiting.."%e)
234                 time.sleep(iperiod)
235             logger.log("nodemanager: Authentication Succeeded!")
236     
237     
238             while True:
239             # Main nodemanager Loop
240                 logger.log('nodemanager: mainloop - calling GetSlivers - period=%d random=%d'%(iperiod,irandom))
241                 self.GetSlivers(config, plc)
242                 delay=iperiod + random.randrange(0,irandom)
243                 logger.log('nodemanager: mainloop - sleeping for %d s'%delay)
244                 time.sleep(delay)
245         except: logger.log_exc("nodemanager: failed in run")
246
247 def run():
248     logger.log("======================================== Entering nodemanager.py")
249     NodeManager().run()
250     
251 if __name__ == '__main__':
252     run()
253 else:
254     # This is for debugging purposes.  Open a copy of Python and import nodemanager
255     tools.as_daemon_thread(run)