Cleaned up NodeRunStates handling
[bootmanager.git] / source / BootManager.py
1 #!/usr/bin/python2 -u
2
3 # ------------------------------------------------------------------------
4 # THIS file used to be named alpina.py, from the node installer. Since then
5 # the installer has been expanded to include all the functions of the boot
6 # manager as well, hence the new name for this file.
7 # ------------------------------------------------------------------------
8
9 # Copyright (c) 2003 Intel Corporation
10 # All rights reserved.
11
12 # Redistribution and use in source and binary forms, with or without
13 # modification, are permitted provided that the following conditions are
14 # met:
15
16 #     * Redistributions of source code must retain the above copyright
17 #       notice, this list of conditions and the following disclaimer.
18
19 #     * Redistributions in binary form must reproduce the above
20 #       copyright notice, this list of conditions and the following
21 #       disclaimer in the documentation and/or other materials provided
22 #       with the distribution.
23
24 #     * Neither the name of the Intel Corporation nor the names of its
25 #       contributors may be used to endorse or promote products derived
26 #       from this software without specific prior written permission.
27
28 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
31 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE INTEL OR
32 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
33 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
34 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
35 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
36 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
37 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
38 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
40 # EXPORT LAWS: THIS LICENSE ADDS NO RESTRICTIONS TO THE EXPORT LAWS OF
41 # YOUR JURISDICTION. It is licensee's responsibility to comply with any
42 # export regulations applicable in licensee's jurisdiction. Under
43 # CURRENT (May 2000) U.S. export regulations this software is eligible
44 # for export from the U.S. and can be downloaded by or otherwise
45 # exported or reexported worldwide EXCEPT to U.S. embargoed destinations
46 # which include Cuba, Iraq, Libya, North Korea, Iran, Syria, Sudan,
47 # Afghanistan and any other country to which the U.S. has embargoed
48 # goods and services.
49
50
51 import string
52 import sys, os, traceback
53 from time import gmtime, strftime
54 from gzip import GzipFile
55
56 from steps import *
57 from Exceptions import *
58 import notify_messages
59 import BootServerRequest
60
61
62
63 # all output is written to this file
64 LOG_FILE= "/tmp/bm.log"
65 UPLOAD_LOG_PATH = "/alpina-logs/upload.php"
66
67 # the new contents of PATH when the boot manager is running
68 BIN_PATH= ('/usr/local/bin',
69            '/usr/local/sbin',
70            '/bin',
71            '/sbin',
72            '/usr/bin',
73            '/usr/sbin',
74            '/usr/local/planetlab/bin')
75            
76
77 # the set of valid node run states
78 NodeRunStates = {}
79
80 class log:
81
82     def __init__( self, OutputFilePath= None ):
83         if OutputFilePath:
84             try:
85                 self.OutputFilePath= OutputFilePath
86                 self.OutputFile= GzipFile( OutputFilePath, "w", 9 )
87             except:
88                 print( "Unable to open output file for log, continuing" )
89                 self.OutputFile= None
90
91     
92     def LogEntry( self, str, inc_newline= 1, display_screen= 1 ):
93         if self.OutputFile:
94             self.OutputFile.write( str )
95         if display_screen:
96             sys.stdout.write( str )
97             
98         if inc_newline:
99             if display_screen:
100                 sys.stdout.write( "\n" )
101             if self.OutputFile:
102                 self.OutputFile.write( "\n" )
103
104         if self.OutputFile:
105             self.OutputFile.flush()
106
107             
108
109     def write( self, str ):
110         """
111         make log behave like a writable file object (for traceback
112         prints)
113         """
114         self.LogEntry( str, 0, 1 )
115
116
117     
118     def Upload( self ):
119         """
120         upload the contents of the log to the server
121         """
122
123         if self.OutputFile is not None:
124             self.LogEntry( "Uploading logs to %s" % UPLOAD_LOG_PATH )
125             
126             self.OutputFile.close()
127             self.OutputFile= None
128
129             bs_request = BootServerRequest.BootServerRequest()
130             bs_request.MakeRequest(PartialPath = UPLOAD_LOG_PATH,
131                                    GetVars = None, PostVars = None,
132                                    FormData = ["log=@" + self.OutputFilePath],
133                                    DoSSL = True, DoCertCheck = True)
134         
135     
136
137         
138
139
140 class BootManager:
141
142     # file containing initial variables/constants
143     VARS_FILE = "configuration"
144
145     
146     def __init__(self, log, forceState):
147         # override machine's current state from the command line
148         self.forceState = forceState
149
150         # this contains a set of information used and updated
151         # by each step
152         self.VARS= {}
153
154         # the main logging point
155         self.LOG= log
156
157         # set to 1 if we can run after initialization
158         self.CAN_RUN = 0
159              
160         if not self.ReadBMConf():
161             self.LOG.LogEntry( "Unable to read configuration vars." )
162             return
163
164         # find out which directory we are running it, and set a variable
165         # for that. future steps may need to get files out of the bootmanager
166         # directory
167         current_dir= os.getcwd()
168         self.VARS['BM_SOURCE_DIR']= current_dir
169
170         # not sure what the current PATH is set to, replace it with what
171         # we know will work with all the boot cds
172         os.environ['PATH']= string.join(BIN_PATH,":")
173                    
174         self.CAN_RUN= 1
175
176     def ReadBMConf(self):
177         """
178         read in and store all variables in VARS_FILE into
179         self.VARS
180         
181         each line is in the format name=val (any whitespace around
182         the = is removed. everything after the = to the end of
183         the line is the value
184         """
185         
186         vars_file= file(self.VARS_FILE,'r')
187         for line in vars_file:
188             # if its a comment or a whitespace line, ignore
189             if line[:1] == "#" or string.strip(line) == "":
190                 continue
191
192             parts= string.split(line,"=")
193             if len(parts) != 2:
194                 self.LOG.LogEntry( "Invalid line in vars file: %s" % line )
195                 return 0
196
197             name= string.strip(parts[0])
198             value= string.strip(parts[1])
199
200             self.VARS[name]= value
201
202         return 1
203     
204     def Run(self):
205         """
206         core boot manager logic.
207
208         the way errors are handled is as such: if any particular step
209         cannot continue or unexpectibly fails, an exception is thrown.
210         in this case, the boot manager cannot continue running.
211
212         these step functions can also return a 0/1 depending on whether
213         or not it succeeded. In the case of steps like ConfirmInstallWithUser,
214         a 0 is returned and no exception is thrown if the user chose not
215         to confirm the install. The same goes with the CheckHardwareRequirements.
216         If requriements not met, but tests were succesfull, return 0.
217
218         for steps that run within the installer, they are expected to either
219         complete succesfully and return 1, or throw an execption.
220
221         For exact return values and expected operations, see the comments
222         at the top of each of the invididual step functions.
223         """
224
225         def _nodeNotInstalled():
226             # called by the _xxxState() functions below upon failure
227             self.VARS['BOOT_STATE']= 'dbg'
228             self.VARS['STATE_CHANGE_NOTIFY']= 1
229             self.VARS['STATE_CHANGE_NOTIFY_MESSAGE']= \
230                       notify_messages.MSG_NODE_NOT_INSTALLED
231             UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
232
233         def _rinsRun():
234             # implements the reinstall logic, which will check whether
235             # the min. hardware requirements are met, install the
236             # software, and upon correct installation will switch too
237             # 'boot' state and chainboot into the production system
238             if not CheckHardwareRequirements.Run( self.VARS, self.LOG ):
239                 self.VARS['BOOT_STATE']= 'dbg'
240                 UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
241                 raise BootManagerException, "Hardware requirements not met."
242
243             self.RunInstaller()
244
245             if ValidateNodeInstall.Run( self.VARS, self.LOG ):
246                 SendHardwareConfigToPLC.Run( self.VARS, self.LOG )
247                 ChainBootNode.Run( self.VARS, self.LOG )
248             else:
249                 _nodeNotInstalled()
250
251         def _newRun():
252             # implements the new install logic, which will first check
253             # with the user whether it is ok to install on this
254             # machine, switch to 'rins' state and then invoke the rins
255             # logic.  See rinsState logic comments for further
256             # details.
257             if not ConfirmInstallWithUser.Run( self.VARS, self.LOG ):
258                 return 0
259             self.VARS['BOOT_STATE']= 'rins'
260             UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
261             _rinsRun()
262
263         def _bootRun():
264             # implements the boot logic, which consists of first
265             # double checking that the node was properly installed,
266             # checking whether someone added or changed disks, and
267             # then finally chain boots.
268
269             if ValidateNodeInstall.Run( self.VARS, self.LOG ):
270                 UpdateNodeConfiguration.Run( self.VARS, self.LOG )
271                 CheckForNewDisks.Run( self.VARS, self.LOG )
272                 SendHardwareConfigToPLC.Run( self.VARS, self.LOG )
273                 ChainBootNode.Run( self.VARS, self.LOG )
274             else:
275                 _nodeNotInstalled()
276
277
278         def _debugRun():
279             # implements debug logic, which just starts the sshd
280             # and just waits around
281             StartDebug.Run( self.VARS, self.LOG )
282
283         def _badRun():
284             # should never happen; log event
285             self.LOG.write( "\nInvalid BOOT_STATE = %s\n" % self.VARS['BOOT_STATE'])
286             self.VARS['BOOT_STATE']= 'dbg'
287             UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
288             StartDebug.Run( self.VARS, self.LOG )            
289
290         global NodeRunStates
291         # setup state -> function hash table
292         NodeRunStates['new']  = _newRun
293         NodeRunStates['inst'] = _newRun
294         NodeRunStates['rins'] = _rinsRun
295         NodeRunStates['boot'] = _bootRun
296         NodeRunStates['dbg']  = _debugRun
297
298         try:
299             InitializeBootManager.Run( self.VARS, self.LOG )
300             ReadNodeConfiguration.Run( self.VARS, self.LOG )
301             AuthenticateWithPLC.Run( self.VARS, self.LOG )
302             GetAndUpdateNodeDetails.Run( self.VARS, self.LOG )
303
304             # override machine's current state from the command line
305             if self.forceState is not None:
306                 self.VARS['BOOT_STATE']= self.forceState
307                 UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
308
309             stateRun = NodeRunStates.get(self.VARS['BOOT_STATE'],_badRun)
310             stateRun()
311
312         except KeyError, e:
313             self.LOG.write( "\n\nKeyError while running: %s\n" % str(e) )
314         except BootManagerException, e:
315             self.LOG.write( "\n\nException while running: %s\n" % str(e) )
316         
317         return 1
318             
319
320             
321     def RunInstaller(self):
322         """
323         since the installer can be invoked at more than one place
324         in the boot manager logic, seperate the steps necessary
325         to do it here
326         """
327         
328         InstallInit.Run( self.VARS, self.LOG )                    
329         InstallPartitionDisks.Run( self.VARS, self.LOG )            
330         InstallBootstrapRPM.Run( self.VARS, self.LOG )            
331         InstallWriteConfig.Run( self.VARS, self.LOG )
332         InstallBuildVServer.Run( self.VARS, self.LOG )
333         InstallNodeInit.Run( self.VARS, self.LOG )
334         InstallUninitHardware.Run( self.VARS, self.LOG )
335         
336         self.VARS['BOOT_STATE']= 'boot'
337         self.VARS['STATE_CHANGE_NOTIFY']= 1
338         self.VARS['STATE_CHANGE_NOTIFY_MESSAGE']= \
339                                        notify_messages.MSG_INSTALL_FINISHED
340         UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
341
342         SendHardwareConfigToPLC.Run( self.VARS, self.LOG )
343
344     
345 def main(argv):
346     global NodeRunStates
347     NodeRunStates = {'new':None,
348                      'inst':None,
349                      'rins':None,
350                      'boot':None,
351                      'dbg':None}
352
353     # set to 1 if error occurred
354     error= 0
355     
356     # all output goes through this class so we can save it and post
357     # the data back to PlanetLab central
358     LOG= log( LOG_FILE )
359
360     LOG.LogEntry( "BootManager started at: %s" % \
361                   strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) )
362
363     try:
364         forceState = None
365         if len(argv) == 2:
366             fState = argv[1]
367             if NodeRunStates.has_key(fState):
368                 forceState = fState
369             else:
370                 LOG.LogEntry("FATAL: cannot force node run state to=%s" % fState)
371                 error = 1
372     except:
373         traceback.print_exc(file=LOG.OutputFile)
374         traceback.print_exc()
375         
376     if error:
377         LOG.LogEntry( "BootManager finished at: %s" % \
378                       strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) )
379         LOG.Upload()
380         return error
381
382     try:
383         bm= BootManager(LOG,forceState)
384         if bm.CAN_RUN == 0:
385             LOG.LogEntry( "Unable to initialize BootManager." )
386         else:
387             LOG.LogEntry( "Running version %s of BootManager." %
388                           bm.VARS['VERSION'] )
389             success= bm.Run()
390             if success:
391                 LOG.LogEntry( "\nDone!" );
392             else:
393                 LOG.LogEntry( "\nError occurred!" );
394                 error = 1
395     except:
396         traceback.print_exc(file=LOG.OutputFile)
397         traceback.print_exc()
398
399     LOG.LogEntry( "BootManager finished at: %s" % \
400                   strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) )
401     LOG.Upload()
402
403     return error
404
405     
406 if __name__ == "__main__":
407     error = main(sys.argv)
408     sys.exit(error)