Added support to force the node run state from command line.
[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 = {'new':None,
79                  'inst':None,
80                  'rins':None,
81                  'boot':None,
82                  'dbg':None}
83
84 class log:
85
86     def __init__( self, OutputFilePath= None ):
87         if OutputFilePath:
88             try:
89                 self.OutputFilePath= OutputFilePath
90                 self.OutputFile= GzipFile( OutputFilePath, "w", 9 )
91             except:
92                 print( "Unable to open output file for log, continuing" )
93                 self.OutputFile= None
94
95     
96     def LogEntry( self, str, inc_newline= 1, display_screen= 1 ):
97         if self.OutputFile:
98             self.OutputFile.write( str )
99         if display_screen:
100             sys.stdout.write( str )
101             
102         if inc_newline:
103             if display_screen:
104                 sys.stdout.write( "\n" )
105             if self.OutputFile:
106                 self.OutputFile.write( "\n" )
107
108         if self.OutputFile:
109             self.OutputFile.flush()
110
111             
112
113     def write( self, str ):
114         """
115         make log behave like a writable file object (for traceback
116         prints)
117         """
118         self.LogEntry( str, 0, 1 )
119
120
121     
122     def Upload( self ):
123         """
124         upload the contents of the log to the server
125         """
126
127         if self.OutputFile is not None:
128             self.LogEntry( "Uploading logs to %s" % UPLOAD_LOG_PATH )
129             
130             self.OutputFile.close()
131             self.OutputFile= None
132
133             bs_request = BootServerRequest.BootServerRequest()
134             bs_request.MakeRequest(PartialPath = UPLOAD_LOG_PATH,
135                                    GetVars = None, PostVars = None,
136                                    FormData = ["log=@" + self.OutputFilePath],
137                                    DoSSL = True, DoCertCheck = True)
138         
139     
140
141         
142
143
144 class BootManager:
145
146     # file containing initial variables/constants
147     VARS_FILE = "configuration"
148
149     
150     def __init__(self, log, forceState):
151         # override machine's current state from the command line
152         self.forceState = forceState
153
154         # this contains a set of information used and updated
155         # by each step
156         self.VARS= {}
157
158         # the main logging point
159         self.LOG= log
160
161         # set to 1 if we can run after initialization
162         self.CAN_RUN = 0
163              
164         if not self.ReadBMConf():
165             self.LOG.LogEntry( "Unable to read configuration vars." )
166             return
167
168         # find out which directory we are running it, and set a variable
169         # for that. future steps may need to get files out of the bootmanager
170         # directory
171         current_dir= os.getcwd()
172         self.VARS['BM_SOURCE_DIR']= current_dir
173
174         # not sure what the current PATH is set to, replace it with what
175         # we know will work with all the boot cds
176         os.environ['PATH']= string.join(BIN_PATH,":")
177                    
178         self.CAN_RUN= 1
179
180     def ReadBMConf(self):
181         """
182         read in and store all variables in VARS_FILE into
183         self.VARS
184         
185         each line is in the format name=val (any whitespace around
186         the = is removed. everything after the = to the end of
187         the line is the value
188         """
189         
190         vars_file= file(self.VARS_FILE,'r')
191         for line in vars_file:
192             # if its a comment or a whitespace line, ignore
193             if line[:1] == "#" or string.strip(line) == "":
194                 continue
195
196             parts= string.split(line,"=")
197             if len(parts) != 2:
198                 self.LOG.LogEntry( "Invalid line in vars file: %s" % line )
199                 return 0
200
201             name= string.strip(parts[0])
202             value= string.strip(parts[1])
203
204             self.VARS[name]= value
205
206         return 1
207     
208     def Run(self):
209         """
210         core boot manager logic.
211
212         the way errors are handled is as such: if any particular step
213         cannot continue or unexpectibly fails, an exception is thrown.
214         in this case, the boot manager cannot continue running.
215
216         these step functions can also return a 0/1 depending on whether
217         or not it succeeded. In the case of steps like ConfirmInstallWithUser,
218         a 0 is returned and no exception is thrown if the user chose not
219         to confirm the install. The same goes with the CheckHardwareRequirements.
220         If requriements not met, but tests were succesfull, return 0.
221
222         for steps that run within the installer, they are expected to either
223         complete succesfully and return 1, or throw an execption.
224
225         For exact return values and expected operations, see the comments
226         at the top of each of the invididual step functions.
227         """
228
229         def _nodeNotInstalled():
230             # called by the _xxxState() functions below upon failure
231             self.VARS['BOOT_STATE']= 'dbg'
232             self.VARS['STATE_CHANGE_NOTIFY']= 1
233             self.VARS['STATE_CHANGE_NOTIFY_MESSAGE']= \
234                       notify_messages.MSG_NODE_NOT_INSTALLED
235             UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
236
237         def _rinsRun():
238             # implements the reinstall logic, which will check whether
239             # the min. hardware requirements are met, install the
240             # software, and upon correct installation will switch too
241             # 'boot' state and chainboot into the production system
242             if not CheckHardwareRequirements.Run( self.VARS, self.LOG ):
243                 self.VARS['BOOT_STATE']= 'dbg'
244                 UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
245                 raise BootManagerException, "Hardware requirements not met."
246
247             self.RunInstaller()
248
249             if ValidateNodeInstall.Run( self.VARS, self.LOG ):
250                 SendHardwareConfigToPLC.Run( self.VARS, self.LOG )
251                 ChainBootNode.Run( self.VARS, self.LOG )
252             else:
253                 _nodeNotInstalled()
254
255         def _newRun():
256             # implements the new install logic, which will first check
257             # with the user whether it is ok to install on this
258             # machine, switch to 'rins' state and then invoke the rins
259             # logic.  See rinsState logic comments for further
260             # details.
261             if not ConfirmInstallWithUser.Run( self.VARS, self.LOG ):
262                 return 0
263             self.VARS['BOOT_STATE']= 'rins'
264             UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
265             _rinsRun()
266
267         def _bootRun():
268             # implements the boot logic, which consists of first
269             # double checking that the node was properly installed,
270             # checking whether someone added or changed disks, and
271             # then finally chain boots.
272
273             if ValidateNodeInstall.Run( self.VARS, self.LOG ):
274                 UpdateNodeConfiguration.Run( self.VARS, self.LOG )
275                 CheckForNewDisks.Run( self.VARS, self.LOG )
276                 SendHardwareConfigToPLC.Run( self.VARS, self.LOG )
277                 ChainBootNode.Run( self.VARS, self.LOG )
278             else:
279                 _nodeNotInstalled()
280
281
282         def _debugRun():
283             # implements debug logic, which just starts the sshd
284             # and just waits around
285             StartDebug.Run( self.VARS, self.LOG )
286
287         def _badRun():
288             # should never happen; log event
289             self.LOG.write( "\nInvalid BOOT_STATE = %s\n" % self.VARS['BOOT_STATE'])
290             self.VARS['BOOT_STATE']= 'dbg'
291             UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
292             StartDebug.Run( self.VARS, self.LOG )            
293
294         global NodeRunStates
295         # setup state -> function hash table
296         NodeRunStates['new']  = _newRun
297         NodeRunStates['inst'] = _newRun
298         NodeRunStates['rins'] = _rinsRun
299         NodeRunStates['boot'] = _bootRun
300         NodeRunStates['dbg']  = _debugRun
301
302         try:
303             InitializeBootManager.Run( self.VARS, self.LOG )
304             ReadNodeConfiguration.Run( self.VARS, self.LOG )
305             AuthenticateWithPLC.Run( self.VARS, self.LOG )
306             GetAndUpdateNodeDetails.Run( self.VARS, self.LOG )
307
308             # override machine's current state from the command line
309             if self.forceState is not None:
310                 self.VARS['BOOT_STATE']= self.forceState
311                 UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
312
313             stateRun = NodeRunStates.get(self.VARS['BOOT_STATE'],_badRun)
314             stateRun()
315
316         except KeyError, e:
317             self.LOG.write( "\n\nKeyError while running: %s\n" % str(e) )
318         except BootManagerException, e:
319             self.LOG.write( "\n\nException while running: %s\n" % str(e) )
320         
321         return 1
322             
323
324             
325     def RunInstaller(self):
326         """
327         since the installer can be invoked at more than one place
328         in the boot manager logic, seperate the steps necessary
329         to do it here
330         """
331         
332         InstallInit.Run( self.VARS, self.LOG )                    
333         InstallPartitionDisks.Run( self.VARS, self.LOG )            
334         InstallBootstrapRPM.Run( self.VARS, self.LOG )            
335         InstallWriteConfig.Run( self.VARS, self.LOG )
336         InstallBuildVServer.Run( self.VARS, self.LOG )
337         InstallNodeInit.Run( self.VARS, self.LOG )
338         InstallUninitHardware.Run( self.VARS, self.LOG )
339         
340         self.VARS['BOOT_STATE']= 'boot'
341         self.VARS['STATE_CHANGE_NOTIFY']= 1
342         self.VARS['STATE_CHANGE_NOTIFY_MESSAGE']= \
343                                        notify_messages.MSG_INSTALL_FINISHED
344         UpdateBootStateWithPLC.Run( self.VARS, self.LOG )
345
346         SendHardwareConfigToPLC.Run( self.VARS, self.LOG )
347
348     
349 def main(argv):
350     global NodeRunStates
351     # set to 0 if no error occurred
352     error= 1
353     
354     # all output goes through this class so we can save it and post
355     # the data back to PlanetLab central
356     LOG= log( LOG_FILE )
357
358     LOG.LogEntry( "BootManager started at: %s" % \
359                   strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) )
360
361     try:
362         forceState = None
363         if len(argv) == 2:
364             fState = argv[1]
365             if NodeRunStates.has_key(fState):
366                 forceState = fState
367                 error = 0
368             else:
369                 LOG.LogEntry("FATAL: cannot force node run state to=%s" % fState)
370     except:
371         traceback.print_exc(file=LOG.OutputFile)
372         traceback.print_exc()
373         
374     if error:
375         LOG.LogEntry( "BootManager finished at: %s" % \
376                       strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) )
377         LOG.Upload()
378         return error
379     else:
380         error = 1
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                 error = 0
393             else:
394                 LOG.LogEntry( "\nError occurred!" );
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)