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