c511f58446cc5eae1ac6d7cce85b6c3b6e185487
[bootmanager.git] / source / steps / ReadNodeConfiguration.py
1 # Copyright (c) 2003 Intel Corporation
2 # All rights reserved.
3
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7
8 #     * Redistributions of source code must retain the above copyright
9 #       notice, this list of conditions and the following disclaimer.
10
11 #     * Redistributions in binary form must reproduce the above
12 #       copyright notice, this list of conditions and the following
13 #       disclaimer in the documentation and/or other materials provided
14 #       with the distribution.
15
16 #     * Neither the name of the Intel Corporation nor the names of its
17 #       contributors may be used to endorse or promote products derived
18 #       from this software without specific prior written permission.
19
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE INTEL OR
24 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32 # EXPORT LAWS: THIS LICENSE ADDS NO RESTRICTIONS TO THE EXPORT LAWS OF
33 # YOUR JURISDICTION. It is licensee's responsibility to comply with any
34 # export regulations applicable in licensee's jurisdiction. Under
35 # CURRENT (May 2000) U.S. export regulations this software is eligible
36 # for export from the U.S. and can be downloaded by or otherwise
37 # exported or reexported worldwide EXCEPT to U.S. embargoed destinations
38 # which include Cuba, Iraq, Libya, North Korea, Iran, Syria, Sudan,
39 # Afghanistan and any other country to which the U.S. has embargoed
40 # goods and services.
41
42
43 import sys, os, traceback
44 import string
45 import socket
46
47 import utils
48 from Exceptions import *
49 import BootServerRequest
50 import BootAPI
51 import StartDebug
52 import notify_messages
53 import UpdateBootStateWithPLC
54
55
56 # two possible names of the configuration files
57 NEW_CONF_FILE_NAME= "plnode.txt"
58 OLD_CONF_FILE_NAME= "planet.cnf"
59
60
61 def Run( vars, log ):   
62     """
63     read the machines node configuration file, which contains
64     the node key and the node_id for this machine.
65     
66     these files can exist in several different locations with
67     several different names. Below is the search order:
68
69     filename      floppy   flash    cd
70     plnode.txt      1        2      4 (/usr/boot), 5 (/usr)
71     planet.cnf      3
72
73     The locations will be searched in the above order, plnode.txt
74     will be checked first, then planet.cnf. Flash devices will only
75     be searched on 3.0 cds.
76
77     Because some of the earlier
78     boot cds don't validate the configuration file (which results
79     in a file named /tmp/planet-clean.cnf), and some do, lets
80     bypass this, and mount and attempt to read in the conf
81     file ourselves. If it doesn't exist, we cannot continue, and a
82     BootManagerException will be raised. If the configuration file is found
83     and read, return 1.
84
85     Expect the following variables from the store:
86     BOOT_CD_VERSION          A tuple of the current bootcd version
87     SUPPORT_FILE_DIR         directory on the boot servers containing
88                              scripts and support files
89     
90     Sets the following variables from the configuration file:
91     WAS_NODE_ID_IN_CONF         Set to 1 if the node id was in the conf file
92     WAS_NODE_KEY_IN_CONF         Set to 1 if the node key was in the conf file
93     NONE_ID                     The db node_id for this machine
94     NODE_KEY                    The key for this node
95     NETWORK_SETTINGS            A dictionary of the values from the network
96                                 configuration file. keys set:
97                                    method
98                                    ip        
99                                    mac       
100                                    gateway   
101                                    network   
102                                    broadcast 
103                                    netmask   
104                                    dns1      
105                                    dns2      
106                                    hostname  
107                                    domainname
108     """
109
110     log.write( "\n\nStep: Reading node configuration file.\n" )
111
112
113     # make sure we have the variables we need
114     try:
115         BOOT_CD_VERSION= vars["BOOT_CD_VERSION"]
116         if BOOT_CD_VERSION == "":
117             raise ValueError, "BOOT_CD_VERSION"
118
119         SUPPORT_FILE_DIR= vars["SUPPORT_FILE_DIR"]
120         if SUPPORT_FILE_DIR == None:
121             raise ValueError, "SUPPORT_FILE_DIR"
122
123     except KeyError, var:
124         raise BootManagerException, "Missing variable in vars: %s\n" % var
125     except ValueError, var:
126         raise BootManagerException, "Variable in vars, shouldn't be: %s\n" % var
127
128
129     NETWORK_SETTINGS= {}
130     NETWORK_SETTINGS['method']= "dhcp"
131     NETWORK_SETTINGS['ip']= ""
132     NETWORK_SETTINGS['mac']= ""
133     NETWORK_SETTINGS['gateway']= ""
134     NETWORK_SETTINGS['network']= ""
135     NETWORK_SETTINGS['broadcast']= ""
136     NETWORK_SETTINGS['netmask']= ""
137     NETWORK_SETTINGS['dns1']= ""
138     NETWORK_SETTINGS['dns2']= ""
139     NETWORK_SETTINGS['hostname']= "localhost"
140     NETWORK_SETTINGS['domainname']= "localdomain"
141     vars['NETWORK_SETTINGS']= NETWORK_SETTINGS
142
143     vars['NODE_ID']= 0
144     vars['NODE_KEY']= ""
145
146     vars['WAS_NODE_ID_IN_CONF']= 0
147     vars['WAS_NODE_KEY_IN_CONF']= 0
148
149     # for any devices that need to be mounted to get the configuration
150     # file, mount them here.
151     mount_point= "/tmp/conffilemount"
152     utils.makedirs( mount_point )
153
154     old_conf_file_contents= None
155     conf_file_contents= None
156     
157     
158     # 1. check the regular floppy device
159     log.write( "Checking standard floppy disk for plnode.txt file.\n" )
160     
161     utils.sysexec_noerr( "mount -o ro -t ext2,msdos /dev/fd0 %s " \
162                          % mount_point, log )
163
164     conf_file_path= "%s/%s" % (mount_point,NEW_CONF_FILE_NAME)
165     if os.access( conf_file_path, os.R_OK ):
166         try:
167             conf_file= file(conf_file_path,"r")
168             conf_file_contents= conf_file.read()
169             conf_file.close()
170         except IOError, e:
171             pass
172
173         utils.sysexec_noerr( "umount /dev/fd0", log )
174         if __parse_configuration_file( vars, log, conf_file_contents):
175             return 1
176         else:
177             raise BootManagerException( "Found configuration file plnode.txt " \
178                                         "on floppy, but was unable to parse it." )
179
180
181     # try the old file name, same device. its actually number 3 on the search
182     # order, but do it now to save mounting/unmounting the disk twice.
183     # try to parse it later...
184     conf_file_path= "%s/%s" % (mount_point,OLD_CONF_FILE_NAME)
185     if os.access( conf_file_path, os.R_OK ):
186         try:
187             old_conf_file= file(conf_file_path,"r")
188             old_conf_file_contents= old_conf_file.read()
189             old_conf_file.close()
190         except IOError, e:
191             pass
192         
193     utils.sysexec_noerr( "umount /dev/fd0", log )
194
195
196
197     if BOOT_CD_VERSION[0] == 3:
198         # 2. check flash devices on 3.0 based cds
199         log.write( "Checking flash devices for plnode.txt file.\n" )
200
201         # this is done the same way the 3.0 cds do it, by attempting
202         # to mount and sd*1 devices that are removable
203         devices= os.listdir("/sys/block/")
204
205         for device in devices:
206             if device[:2] != "sd":
207                 continue
208
209             # test removable
210             removable_file_path= "/sys/block/%s/removable" % device
211             try:
212                 removable= int(file(removable_file_path,"r").read().strip())
213             except ValueError, e:
214                 continue
215             except IOError, e:
216                 continue
217
218             if not removable:
219                 continue
220
221             log.write( "Checking removable device %s\n" % device )
222         
223             # ok, try to mount it and see if we have a conf file.
224             full_device= "/dev/%s1" % device
225
226             try:
227                 utils.sysexec( "mount -o ro -t ext2,msdos %s %s" \
228                                % (full_device,mount_point), log )
229             except BootManagerException, e:
230                 continue
231             
232             conf_file_path= "%s/%s" % (mount_point,NEW_CONF_FILE_NAME)
233             if os.access( conf_file_path, os.R_OK ):
234                 try:
235                     conf_file= file(conf_file_path,"r")
236                     conf_file_contents= conf_file.read()
237                     conf_file.close()
238                 except IOError, e:
239                     pass
240
241             utils.sysexec_noerr( "umount %s" % full_device, log )
242             if __parse_configuration_file( vars, log, conf_file_contents):
243                 return 1
244             else:
245                 raise BootManagerException("Found configuration file plnode.txt " \
246                                            "on floppy, but was unable to parse it.")
247             
248
249             
250     # 3. check standard floppy disk for old file name planet.cnf
251     log.write( "Checking standard floppy disk for planet.cnf file.\n" )
252
253     if old_conf_file_contents:
254         if __parse_configuration_file( vars, log, old_conf_file_contents):
255             return 1
256         else:
257             raise BootManagerException( "Found configuration file planet.cnf " \
258                                         "on floppy, but was unable to parse it." )
259
260
261     # 4. check for plnode.txt in /usr/boot (mounted already)
262     log.write( "Checking /usr/boot (cd) for plnode.txt file.\n" )
263     
264     conf_file_path= "/usr/boot/%s" % NEW_CONF_FILE_NAME
265     if os.access(conf_file_path,os.R_OK):
266         try:
267             conf_file= file(conf_file_path,"r")
268             conf_file_contents= conf_file.read()
269             conf_file.close()
270         except IOError, e:
271             pass    
272     
273         if __parse_configuration_file( vars, log, conf_file_contents):            
274             return 1
275         else:
276             raise BootManagerException( "Found configuration file plnode.txt " \
277                                         "in /usr/boot, but was unable to parse it.")
278
279
280
281     # 5. check for plnode.txt in /usr (mounted already)
282     log.write( "Checking /usr (cd) for plnode.txt file.\n" )
283     
284     conf_file_path= "/usr/%s" % NEW_CONF_FILE_NAME
285     if os.access(conf_file_path,os.R_OK):
286         try:
287             conf_file= file(conf_file_path,"r")
288             conf_file_contents= conf_file.read()
289             conf_file.close()
290         except IOError, e:
291             pass    
292     
293         if __parse_configuration_file( vars, log, conf_file_contents):            
294             return 1
295         else:
296             raise BootManagerException( "Found configuration file plnode.txt " \
297                                         "in /usr, but was unable to parse it.")
298
299
300     raise BootManagerException, "Unable to find and read a node configuration file."
301     
302
303
304
305 def __parse_configuration_file( vars, log, file_contents ):
306     """
307     parse a configuration file, set keys in var NETWORK_SETTINGS
308     in vars (see comment for function ReadNodeConfiguration)
309     """
310
311     BOOT_CD_VERSION= vars["BOOT_CD_VERSION"]
312     SUPPORT_FILE_DIR= vars["SUPPORT_FILE_DIR"]
313     NETWORK_SETTINGS= vars["NETWORK_SETTINGS"]
314     
315     if file_contents is None:
316         return 0
317     
318     try:
319         line_num= 0
320         for line in file_contents.split("\n"):
321
322             line_num = line_num + 1
323             
324             # if its a comment or a whitespace line, ignore
325             if line[:1] == "#" or string.strip(line) == "":
326                 continue
327
328             # file is setup as name="value" pairs
329             parts= string.split(line,"=")
330             if len(parts) != 2:
331                 log.write( "Invalid line %d in configuration file:\n" % line_num )
332                 log.write( line + "\n" )
333                 return 0
334
335             name= string.strip(parts[0])
336             value= string.strip(parts[1])
337
338             # make sure value starts and ends with
339             # single or double quotes
340             quotes= value[0] + value[len(value)-1]
341             if quotes != "''" and quotes != '""':
342                 log.write( "Invalid line %d in configuration file:\n" % line_num )
343                 log.write( line + "\n" )
344                 return 0
345
346             # get rid of the quotes around the value
347             value= string.strip(value[1:len(value)-1])
348
349             if name == "NODE_ID":
350                 try:
351                     vars['NODE_ID']= int(value)
352                     vars['WAS_NODE_ID_IN_CONF']= 1
353                 except ValueError, e:
354                     log.write( "Non-numeric node_id in configuration file.\n" )
355                     return 0
356
357             if name == "NODE_KEY":
358                 vars['NODE_KEY']= value
359                 vars['WAS_NODE_KEY_IN_CONF']= 1
360
361             if name == "IP_METHOD":
362                 value= string.lower(value)
363                 if value != "static" and value != "dhcp":
364                     log.write( "Invalid IP_METHOD in configuration file:\n" )
365                     log.write( line + "\n" )
366                     return 0
367                 NETWORK_SETTINGS['method']= value.strip()
368
369             if name == "IP_ADDRESS":
370                 NETWORK_SETTINGS['ip']= value.strip()
371
372             if name == "IP_GATEWAY":
373                 NETWORK_SETTINGS['gateway']= value.strip()
374
375             if name == "IP_NETMASK":
376                 NETWORK_SETTINGS['netmask']= value.strip()
377
378             if name == "IP_NETADDR":
379                 NETWORK_SETTINGS['network']= value.strip()
380
381             if name == "IP_BROADCASTADDR":
382                 NETWORK_SETTINGS['broadcast']= value.strip()
383
384             if name == "IP_DNS1":
385                 NETWORK_SETTINGS['dns1']= value.strip()
386
387             if name == "IP_DNS2":
388                 NETWORK_SETTINGS['dns2']= value.strip()
389
390             if name == "HOST_NAME":
391                 NETWORK_SETTINGS['hostname']= string.lower(value)
392
393             if name == "DOMAIN_NAME":
394                 NETWORK_SETTINGS['domainname']= string.lower(value)
395
396     except IndexError, e:
397         log.write( "Unable to parse configuration file\n" )
398         return 0
399
400     # now if we are set to dhcp, clear out any fields
401     # that don't make sense
402     if NETWORK_SETTINGS["method"] == "dhcp":
403         NETWORK_SETTINGS["ip"]= ""
404         NETWORK_SETTINGS["gateway"]= ""     
405         NETWORK_SETTINGS["netmask"]= ""
406         NETWORK_SETTINGS["network"]= ""
407         NETWORK_SETTINGS["broadcast"]= ""
408         NETWORK_SETTINGS["dns1"]= ""
409         NETWORK_SETTINGS["dns2"]= ""
410
411
412     log.write("Successfully read and parsed node configuration file.\n" )
413
414     
415     if vars['NODE_ID'] is None or vars['NODE_ID'] == 0:
416         log.write( "Configuration file does not contain the node_id value.\n" )
417         log.write( "Querying PLC for node_id.\n" )
418
419         bs_request= BootServerRequest.BootServerRequest()
420
421         try:
422             ifconfig_file= file("/tmp/ifconfig","r")
423             ifconfig= ifconfig_file.read()
424             ifconfig_file.close()
425         except IOError:
426             log.write( "Unable to read ifconfig output from /tmp/ifconfig\n" )
427             return 0
428         
429         postVars= {"ifconfig" : ifconfig}
430         result= bs_request.DownloadFile( "%s/getnodeid.php" %
431                                          SUPPORT_FILE_DIR,
432                                          None, postVars, 1, 1,
433                                          "/tmp/node_id")
434         if result == 0:
435             log.write( "Unable to make request to get node_id.\n" )
436             return 0
437
438         try:
439             node_id_file= file("/tmp/node_id","r")
440             node_id= string.strip(node_id_file.read())
441             node_id_file.close()
442         except IOError:
443             log.write( "Unable to read node_id from /tmp/node_id\n" )
444             return 0
445
446         try:
447             node_id= int(string.strip(node_id))
448         except ValueError:
449             log.write( "Got node_id from PLC, but not numeric: %s" % str(node_id) )
450             return 0
451
452         if node_id == -1:
453             log.write( "Got node_id, but it returned -1\n\n" )
454
455             log.write( "------------------------------------------------------\n" )
456             log.write( "This indicates that this node could not be identified\n" )
457             log.write( "by PLC. You will need to add the node to your site,\n" )
458             log.write( "and regenerate the network configuration file.\n" )
459             log.write( "See the Technical Contact guide for node setup\n" )
460             log.write( "procedures.\n\n" )
461             log.write( "Boot process canceled until this is completed.\n" )
462             log.write( "------------------------------------------------------\n" )
463             
464             cancel_boot_flag= "/tmp/CANCEL_BOOT"
465             # this will make the initial script stop requesting scripts from PLC
466             utils.sysexec( "touch %s" % cancel_boot_flag, log )
467
468             return 0
469
470         log.write( "Got node_id from PLC: %s\n" % str(node_id) )
471         vars['NODE_ID']= node_id
472
473
474
475     if vars['NODE_KEY'] is None or vars['NODE_KEY'] == "":
476         log.write( "Configuration file does not contain a node_key value.\n" )
477         log.write( "Using boot nonce instead.\n" )
478
479         # 3.x cds stored the file in /tmp/nonce in ascii form, so they
480         # can be read and used directly. 2.x cds stored in the same place
481         # but in binary form, so we need to convert it to ascii the same
482         # way the old boot scripts did so it matches whats in the db
483         # (php uses bin2hex, 
484         if BOOT_CD_VERSION[0] == 2:
485             read_mode= "rb"
486         else:
487             read_mode= "r"
488             
489         try:
490             nonce_file= file("/tmp/nonce",read_mode)
491             nonce= nonce_file.read()
492             nonce_file.close()
493         except IOError:
494             log.write( "Unable to read nonce from /tmp/nonce\n" )
495             return 0
496
497         if BOOT_CD_VERSION[0] == 2:
498             nonce= nonce.encode('hex')
499
500             # there is this nice bug in the php that currently accepts the
501             # nonce for the old scripts, in that if the nonce contains
502             # null chars (2.x cds sent as binary), then
503             # the nonce is truncated. so, do the same here, truncate the nonce
504             # at the first null ('00'). This could leave us with an empty string.
505             nonce_len= len(nonce)
506             for byte_index in range(0,nonce_len,2):
507                 if nonce[byte_index:byte_index+2] == '00':
508                     nonce= nonce[:byte_index]
509                     break
510         else:
511             nonce= string.strip(nonce)
512
513         log.write( "Read nonce, using as key.\n" )
514         vars['NODE_KEY']= nonce
515         
516         
517     # at this point, we've read the network configuration file.
518     # if we were setup using dhcp, get this system's current ip
519     # address and update the vars key ip, because it
520     # is needed for future api calls.
521
522     # at the same time, we can check to make sure that the hostname
523     # in the configuration file matches the ip address. if it fails
524     # notify the owners
525
526     hostname= NETWORK_SETTINGS['hostname'] + "." + \
527               NETWORK_SETTINGS['domainname']
528
529     # set to 0 if any part of the hostname resolution check fails
530     hostname_resolve_ok= 1
531
532     # set to 0 if the above fails, and, we are using dhcp in which
533     # case we don't know the ip of this machine (without having to
534     # parse ifconfig or something). In that case, we won't be able
535     # to make api calls, so printing a message to the screen will
536     # have to suffice.
537     can_make_api_call= 1
538
539     log.write( "Checking that hostname %s resolves\n" % hostname )
540
541     # try a regular dns lookup first
542     try:
543         resolved_node_ip= socket.gethostbyname(hostname)
544     except socket.gaierror, e:
545         hostname_resolve_ok= 0
546         
547
548     if NETWORK_SETTINGS['method'] == "dhcp":
549         if hostname_resolve_ok:
550             NETWORK_SETTINGS['ip']= resolved_node_ip
551             node_ip= resolved_node_ip
552         else:
553             can_make_api_call= 0
554     else:
555         node_ip= NETWORK_SETTINGS['ip']
556
557     # make sure the dns lookup matches what the configuration file says
558     if hostname_resolve_ok:
559         if node_ip != resolved_node_ip:
560             log.write( "Hostname %s does not resolve to %s, but %s:\n" % \
561                        (hostname,node_ip,resolved_node_ip) )
562             hostname_resolve_ok= 0
563         else:
564             log.write( "Hostname %s correctly resolves to %s:\n" %
565                        (hostname,node_ip) )
566
567
568     # 3.x cds, with a node_key on the floppy, can update their mac address
569     # at plc, so get it here
570     if BOOT_CD_VERSION[0] == 3 and vars['WAS_NODE_ID_IN_CONF'] == 1:
571         eth_device= "eth0"
572         try:
573             hw_addr_file= file("/sys/class/net/%s/address" % eth_device, "r")
574             hw_addr= hw_addr_file.read().strip().upper()
575             hw_addr_file.close()
576         except IOError, e:
577             raise BootmanagerException, \
578                   "could not get hw address for device %s" % eth_device
579
580         NETWORK_SETTINGS['mac']= hw_addr
581
582         
583     vars["NETWORK_SETTINGS"]= NETWORK_SETTINGS
584
585     if not hostname_resolve_ok:
586         log.write( "Hostname does not resolve correctly, will not continue.\n" )
587
588         StartDebug.Run( vars, log )
589
590         if can_make_api_call:
591             log.write( "Notifying contacts of problem.\n" )
592
593             vars['BOOT_STATE']= 'dbg'
594             vars['STATE_CHANGE_NOTIFY']= 1
595             vars['STATE_CHANGE_NOTIFY_MESSAGE']= \
596                                      notify_messages.MSG_HOSTNAME_NOT_RESOLVE
597             
598             UpdateBootStateWithPLC.Run( vars, log )
599                     
600         log.write( "\n\n" )
601         log.write( "The hostname and/or ip in the network configuration\n" )
602         log.write( "file do not resolve and match.\n" )
603         log.write( "Please make sure the hostname set in the network\n" )
604         log.write( "configuration file resolves to the ip also specified\n" )
605         log.write( "there.\n\n" )
606         log.write( "Debug mode is being started on this cd. When the above\n" )
607         log.write( "is corrected, reboot the machine to try again.\n" )
608         
609         raise BootManagerException, \
610               "Configured node hostname does not resolve."
611     
612     return 1