55684bada1d40f10a5bf6fc965dd917f6ec50242
[bootmanager.git] / source / steps / ReadNodeConfiguration.py
1 #!/usr/bin/python
2 #
3 # Copyright (c) 2003 Intel Corporation
4 # All rights reserved.
5 #
6 # Copyright (c) 2004-2006 The Trustees of Princeton University
7 # All rights reserved.
8 # expected /proc/partitions format
9
10 import sys, os, traceback
11 import string
12 import socket
13 import re
14
15 import utils
16 from Exceptions import *
17 import BootServerRequest
18 import BootAPI
19 import notify_messages
20 import UpdateRunLevelWithPLC
21
22
23 # two possible names of the configuration files
24 NEW_CONF_FILE_NAME= "plnode.txt"
25 OLD_CONF_FILE_NAME= "planet.cnf"
26
27
28 def Run( vars, log ):   
29     """
30     read the machines node configuration file, which contains
31     the node key and the node_id for this machine.
32     
33     these files can exist in several different locations with
34     several different names. Below is the search order:
35
36     filename      floppy   flash    ramdisk    cd
37     plnode.txt      1        2      4 (/)      5 (/usr/boot), 6 (/usr)
38     planet.cnf      3
39
40     The locations will be searched in the above order, plnode.txt
41     will be checked first, then planet.cnf. Flash devices will only
42     be searched on 3.0 cds.
43
44     Because some of the earlier
45     boot cds don't validate the configuration file (which results
46     in a file named /tmp/planet-clean.cnf), and some do, lets
47     bypass this, and mount and attempt to read in the conf
48     file ourselves. If it doesn't exist, we cannot continue, and a
49     BootManagerException will be raised. If the configuration file is found
50     and read, return 1.
51
52     Expect the following variables from the store:
53     
54     Sets the following variables from the configuration file:
55     WAS_NODE_ID_IN_CONF         Set to 1 if the node id was in the conf file
56     WAS_NODE_KEY_IN_CONF         Set to 1 if the node key was in the conf file
57     NONE_ID                     The db node_id for this machine
58     NODE_KEY                    The key for this node
59     INTERFACE_SETTINGS            A dictionary of the values from the network
60                                 configuration file. keys set:
61                                    method               IP_METHOD
62                                    ip                   IP_ADDRESS
63                                    mac                  NET_DEVICE       
64                                    gateway              IP_GATEWAY
65                                    network              IP_NETADDR
66                                    broadcast            IP_BROADCASTADDR
67                                    netmask              IP_NETMASK
68                                    dns1                 IP_DNS1
69                                    dns2                 IP_DNS2
70                                    hostname             HOST_NAME
71                                    domainname           DOMAIN_NAME
72                                 -- wlan oriented --
73                                    ssid                 WLAN_SSID
74                                    iwconfig             WLAN_IWCONFIG
75
76     the mac address is read from the machine unless it exists in the
77     configuration file.
78     """
79
80     log.write( "\n\nStep: Reading node configuration file.\n" )
81
82
83     # make sure we have the variables we need
84
85     INTERFACE_SETTINGS= {}
86     INTERFACE_SETTINGS['method']= "dhcp"
87     INTERFACE_SETTINGS['ip']= ""
88     INTERFACE_SETTINGS['mac']= ""
89     INTERFACE_SETTINGS['gateway']= ""
90     INTERFACE_SETTINGS['network']= ""
91     INTERFACE_SETTINGS['broadcast']= ""
92     INTERFACE_SETTINGS['netmask']= ""
93     INTERFACE_SETTINGS['dns1']= ""
94     INTERFACE_SETTINGS['dns2']= ""
95     INTERFACE_SETTINGS['hostname']= "localhost"
96     INTERFACE_SETTINGS['domainname']= "localdomain"
97     vars['INTERFACE_SETTINGS']= INTERFACE_SETTINGS
98
99     vars['NODE_ID']= 0
100     vars['NODE_KEY']= ""
101
102     vars['WAS_NODE_ID_IN_CONF']= 0
103     vars['WAS_NODE_KEY_IN_CONF']= 0
104
105     vars['DISCONNECTED_OPERATION']= ''
106
107     # for any devices that need to be mounted to get the configuration
108     # file, mount them here.
109     mount_point= "/tmp/conffilemount"
110     utils.makedirs( mount_point )
111
112     old_conf_file_contents= None
113     conf_file_contents= None
114     
115     
116     # 1. check the regular floppy device
117     log.write( "Checking standard floppy disk for plnode.txt file.\n" )
118
119     log.write( "Mounting /dev/fd0 on %s\n" % mount_point )
120     utils.sysexec_noerr( "mount -o ro -t ext2,msdos /dev/fd0 %s " \
121                          % mount_point, log )
122
123     conf_file_path= "%s/%s" % (mount_point,NEW_CONF_FILE_NAME)
124     
125     log.write( "Checking for existence of %s\n" % conf_file_path )
126     if os.access( conf_file_path, os.R_OK ):
127         try:
128             conf_file= file(conf_file_path,"r")
129             conf_file_contents= conf_file.read()
130             conf_file.close()
131             log.write( "Read in contents of file %s\n" % conf_file_path )
132         except IOError, e:
133             log.write( "Unable to read file %s\n" % conf_file_path )
134             pass
135
136         utils.sysexec_noerr( "umount %s" % mount_point, log )
137         if __parse_configuration_file( vars, log, conf_file_contents):
138             return 1
139         else:
140             raise BootManagerException( "Found configuration file plnode.txt " \
141                                         "on floppy, but was unable to parse it." )
142
143
144     # try the old file name, same device. its actually number 3 on the search
145     # order, but do it now to save mounting/unmounting the disk twice.
146     # try to parse it later...
147     conf_file_path= "%s/%s" % (mount_point,OLD_CONF_FILE_NAME)
148
149     log.write( "Checking for existence of %s (used later)\n" % conf_file_path )
150     if os.access( conf_file_path, os.R_OK ):
151         try:
152             old_conf_file= file(conf_file_path,"r")
153             old_conf_file_contents= old_conf_file.read()
154             old_conf_file.close()
155             log.write( "Read in contents of file %s\n" % conf_file_path )
156         except IOError, e:
157             log.write( "Unable to read file %s\n" % conf_file_path )
158             pass
159         
160     utils.sysexec_noerr( "umount %s" % mount_point, log )
161
162     # 2. check flash devices on 3.0 based cds
163     log.write( "Checking flash devices for plnode.txt file.\n" )
164
165     # this is done the same way the 3.0 cds do it, by attempting
166     # to mount and sd*1 devices that are removable
167     devices= os.listdir("/sys/block/")
168
169     for device in devices:
170         if device[:2] != "sd":
171             log.write( "Skipping non-scsi device %s\n" % device )
172             continue
173
174         # test removable
175         removable_file_path= "/sys/block/%s/removable" % device
176         try:
177             removable= int(file(removable_file_path,"r").read().strip())
178         except ValueError, e:
179             continue
180         except IOError, e:
181             continue
182
183         if not removable:
184             log.write( "Skipping non-removable device %s\n" % device )
185             continue
186
187         log.write( "Checking removable device %s\n" % device )
188
189         partitions= file("/proc/partitions", "r")
190         for line in partitions:
191             found_file= 0
192             parsed_file= 0
193             
194             if not re.search("%s[0-9]*$" % device, line):
195                 continue
196
197             try:
198                 # major minor  #blocks  name
199                 parts= string.split(line)
200
201                 # ok, try to mount it and see if we have a conf file.
202                 full_device= "/dev/%s" % parts[3]
203             except IndexError, e:
204                 log.write( "Incorrect /proc/partitions line:\n%s\n" % line )
205                 continue
206
207             log.write( "Mounting %s on %s\n" % (full_device,mount_point) )
208             try:
209                 utils.sysexec( "mount -o ro -t ext2,msdos %s %s" \
210                                % (full_device,mount_point), log )
211             except BootManagerException, e:
212                 log.write( "Unable to mount, trying next partition\n" )
213                 continue
214
215             conf_file_path= "%s/%s" % (mount_point,NEW_CONF_FILE_NAME)
216
217             log.write( "Checking for existence of %s\n" % conf_file_path )
218             if os.access( conf_file_path, os.R_OK ):
219                 try:
220                     conf_file= file(conf_file_path,"r")
221                     conf_file_contents= conf_file.read()
222                     conf_file.close()
223                     found_file= 1
224                     log.write( "Read in contents of file %s\n" % \
225                                conf_file_path )
226
227                     if __parse_configuration_file( vars, log, \
228                                                    conf_file_contents):
229                         parsed_file= 1
230                 except IOError, e:
231                     log.write( "Unable to read file %s\n" % conf_file_path )
232
233             utils.sysexec_noerr( "umount %s" % mount_point, log )
234             if found_file:
235                 if parsed_file:
236                     return 1
237                 else:
238                     raise BootManagerException( \
239                         "Found configuration file plnode.txt " \
240                         "on floppy, but was unable to parse it.")
241
242
243             
244     # 3. check standard floppy disk for old file name planet.cnf
245     log.write( "Checking standard floppy disk for planet.cnf file " \
246                "(for legacy nodes).\n" )
247
248     if old_conf_file_contents:
249         if __parse_configuration_file( vars, log, old_conf_file_contents):
250             return 1
251         else:
252             raise BootManagerException( "Found configuration file planet.cnf " \
253                                         "on floppy, but was unable to parse it." )
254
255
256     # 4. check for plnode.txt in / (ramdisk)
257     log.write( "Checking / (ramdisk) for plnode.txt file.\n" )
258     
259     conf_file_path= "/%s" % NEW_CONF_FILE_NAME
260
261     log.write( "Checking for existence of %s\n" % conf_file_path )
262     if os.access(conf_file_path,os.R_OK):
263         try:
264             conf_file= file(conf_file_path,"r")
265             conf_file_contents= conf_file.read()
266             conf_file.close()
267             log.write( "Read in contents of file %s\n" % conf_file_path )
268         except IOError, e:
269             log.write( "Unable to read file %s\n" % conf_file_path )
270             pass
271     
272         if __parse_configuration_file( vars, log, conf_file_contents):            
273             return 1
274         else:
275             raise BootManagerException( "Found configuration file plnode.txt " \
276                                         "in /, but was unable to parse it.")
277
278     
279     # 5. check for plnode.txt in /usr/boot (mounted already)
280     log.write( "Checking /usr/boot (cd) for plnode.txt file.\n" )
281     
282     conf_file_path= "/usr/boot/%s" % NEW_CONF_FILE_NAME
283
284     log.write( "Checking for existence of %s\n" % conf_file_path )
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             log.write( "Read in contents of file %s\n" % conf_file_path )
291         except IOError, e:
292             log.write( "Unable to read file %s\n" % conf_file_path )
293             pass
294     
295         if __parse_configuration_file( vars, log, conf_file_contents):            
296             return 1
297         else:
298             raise BootManagerException( "Found configuration file plnode.txt " \
299                                         "in /usr/boot, but was unable to parse it.")
300
301
302
303     # 6. check for plnode.txt in /usr (mounted already)
304     log.write( "Checking /usr (cd) for plnode.txt file.\n" )
305     
306     conf_file_path= "/usr/%s" % NEW_CONF_FILE_NAME
307
308     log.write( "Checking for existence of %s\n" % conf_file_path )
309     if os.access(conf_file_path,os.R_OK):
310         try:
311             conf_file= file(conf_file_path,"r")
312             conf_file_contents= conf_file.read()
313             conf_file.close()
314             log.write( "Read in contents of file %s\n" % conf_file_path )
315         except IOError, e:
316             log.write( "Unable to read file %s\n" % conf_file_path )
317             pass    
318     
319         if __parse_configuration_file( vars, log, conf_file_contents):            
320             return 1
321         else:
322             raise BootManagerException( "Found configuration file plnode.txt " \
323                                         "in /usr, but was unable to parse it.")
324
325
326     raise BootManagerException, "Unable to find and read a node configuration file."
327     
328
329
330
331 def __parse_configuration_file( vars, log, file_contents ):
332     """
333     parse a configuration file, set keys in var INTERFACE_SETTINGS
334     in vars (see comment for function ReadNodeConfiguration). this
335     also reads the mac address from the machine if successful parsing
336     of the configuration file is completed.
337     """
338
339     INTERFACE_SETTINGS= vars["INTERFACE_SETTINGS"]
340     
341     if file_contents is None:
342         log.write( "__parse_configuration_file called with no file contents\n" )
343         return 0
344     
345     try:
346         line_num= 0
347         for line in file_contents.split("\n"):
348
349             line_num = line_num + 1
350             
351             # if its a comment or a whitespace line, ignore
352             if line[:1] == "#" or string.strip(line) == "":
353                 continue
354
355             # file is setup as name="value" pairs
356             parts= string.split(line, "=", 1)
357
358             name= string.strip(parts[0])
359             value= string.strip(parts[1])
360
361             # make sure value starts and ends with
362             # single or double quotes
363             quotes= value[0] + value[len(value)-1]
364             if quotes != "''" and quotes != '""':
365                 log.write( "Invalid line %d in configuration file:\n" % line_num )
366                 log.write( line + "\n" )
367                 return 0
368
369             # get rid of the quotes around the value
370             value= string.strip(value[1:len(value)-1])
371
372             if name == "NODE_ID":
373                 try:
374                     vars['NODE_ID']= int(value)
375                     vars['WAS_NODE_ID_IN_CONF']= 1
376                 except ValueError, e:
377                     log.write( "Non-numeric node_id in configuration file.\n" )
378                     return 0
379
380             if name == "NODE_KEY":
381                 vars['NODE_KEY']= value
382                 vars['WAS_NODE_KEY_IN_CONF']= 1
383
384             if name == "IP_METHOD":
385                 value= string.lower(value)
386                 if value != "static" and value != "dhcp":
387                     log.write( "Invalid IP_METHOD in configuration file:\n" )
388                     log.write( line + "\n" )
389                     return 0
390                 INTERFACE_SETTINGS['method']= value.strip()
391
392             if name == "IP_ADDRESS":
393                 INTERFACE_SETTINGS['ip']= value.strip()
394
395             if name == "IP_GATEWAY":
396                 INTERFACE_SETTINGS['gateway']= value.strip()
397
398             if name == "IP_NETMASK":
399                 INTERFACE_SETTINGS['netmask']= value.strip()
400
401             if name == "IP_NETADDR":
402                 INTERFACE_SETTINGS['network']= value.strip()
403
404             if name == "IP_BROADCASTADDR":
405                 INTERFACE_SETTINGS['broadcast']= value.strip()
406
407             if name == "IP_DNS1":
408                 INTERFACE_SETTINGS['dns1']= value.strip()
409
410             if name == "IP_DNS2":
411                 INTERFACE_SETTINGS['dns2']= value.strip()
412
413             if name == "HOST_NAME":
414                 INTERFACE_SETTINGS['hostname']= string.lower(value)
415
416             if name == "DOMAIN_NAME":
417                 INTERFACE_SETTINGS['domainname']= string.lower(value)
418
419             if name == "NET_DEVICE":
420                 INTERFACE_SETTINGS['mac']= string.upper(value)
421
422             if name == "DISCONNECTED_OPERATION":
423                 vars['DISCONNECTED_OPERATION']= value.strip()
424
425     except IndexError, e:
426         log.write( "Unable to parse configuration file\n" )
427         return 0
428
429     # now if we are set to dhcp, clear out any fields
430     # that don't make sense
431     if INTERFACE_SETTINGS["method"] == "dhcp":
432         INTERFACE_SETTINGS["ip"]= ""
433         INTERFACE_SETTINGS["gateway"]= ""     
434         INTERFACE_SETTINGS["netmask"]= ""
435         INTERFACE_SETTINGS["network"]= ""
436         INTERFACE_SETTINGS["broadcast"]= ""
437         INTERFACE_SETTINGS["dns1"]= ""
438         INTERFACE_SETTINGS["dns2"]= ""
439
440     log.write("Successfully read and parsed node configuration file.\n" )
441
442     # if the mac wasn't specified, read it in from the system.
443     if INTERFACE_SETTINGS["mac"] == "":
444         device= "eth0"
445         mac_addr= utils.get_mac_from_interface(device)
446
447         if mac_addr is None:
448             log.write( "Could not get mac address for device eth0.\n" )
449             return 0
450
451         INTERFACE_SETTINGS["mac"]= string.upper(mac_addr)
452
453         log.write( "Got mac address %s for device %s\n" %
454                    (INTERFACE_SETTINGS["mac"],device) )
455         
456
457     # now, if the conf file didn't contain a node id, post the mac address
458     # to plc to get the node_id value
459     if vars['NODE_ID'] is None or vars['NODE_ID'] == 0:
460         log.write( "Configuration file does not contain the node_id value.\n" )
461         log.write( "Querying PLC for node_id.\n" )
462
463         bs_request= BootServerRequest.BootServerRequest(vars)
464         
465         postVars= {"mac_addr" : INTERFACE_SETTINGS["mac"]}
466         result= bs_request.DownloadFile( "/boot/getnodeid.php",
467                                          None, postVars, 1, 1,
468                                          "/tmp/node_id")
469         if result == 0:
470             log.write( "Unable to make request to get node_id.\n" )
471             return 0
472
473         try:
474             node_id_file= file("/tmp/node_id","r")
475             node_id= string.strip(node_id_file.read())
476             node_id_file.close()
477         except IOError:
478             log.write( "Unable to read node_id from /tmp/node_id\n" )
479             return 0
480
481         try:
482             node_id= int(string.strip(node_id))
483         except ValueError:
484             log.write( "Got node_id from PLC, but not numeric: %s" % str(node_id) )
485             return 0
486
487         if node_id == -1:
488             log.write( "Got node_id, but it returned -1\n\n" )
489
490             log.write( "------------------------------------------------------\n" )
491             log.write( "This indicates that this node could not be identified\n" )
492             log.write( "by PLC. You will need to add the node to your site,\n" )
493             log.write( "and regenerate the network configuration file.\n" )
494             log.write( "See the Technical Contact guide for node setup\n" )
495             log.write( "procedures.\n\n" )
496             log.write( "Boot process canceled until this is completed.\n" )
497             log.write( "------------------------------------------------------\n" )
498             
499             cancel_boot_flag= "/tmp/CANCEL_BOOT"
500             # this will make the initial script stop requesting scripts from PLC
501             utils.sysexec( "touch %s" % cancel_boot_flag, log )
502
503             return 0
504
505         log.write( "Got node_id from PLC: %s\n" % str(node_id) )
506         vars['NODE_ID']= node_id
507
508
509
510     if vars['NODE_KEY'] is None or vars['NODE_KEY'] == "":
511         log.write( "Configuration file does not contain a node_key value.\n" )
512         log.write( "Using boot nonce instead.\n" )
513
514         # 3.x cds stored the file in /tmp/nonce in ascii form, so they
515         # can be read and used directly. 2.x cds stored in the same place
516         # but in binary form, so we need to convert it to ascii the same
517         # way the old boot scripts did so it matches whats in the db
518         # (php uses bin2hex, 
519         read_mode= "r"
520             
521         try:
522             nonce_file= file("/tmp/nonce",read_mode)
523             nonce= nonce_file.read()
524             nonce_file.close()
525         except IOError:
526             log.write( "Unable to read nonce from /tmp/nonce\n" )
527             return 0
528
529         nonce= string.strip(nonce)
530
531         log.write( "Read nonce, using as key.\n" )
532         vars['NODE_KEY']= nonce
533         
534         
535     # at this point, we've read the network configuration file.
536     # if we were setup using dhcp, get this system's current ip
537     # address and update the vars key ip, because it
538     # is needed for future api calls.
539
540     # at the same time, we can check to make sure that the hostname
541     # in the configuration file matches the ip address. if it fails
542     # notify the owners
543
544     hostname= INTERFACE_SETTINGS['hostname'] + "." + \
545               INTERFACE_SETTINGS['domainname']
546
547     # set to 0 if any part of the hostname resolution check fails
548     hostname_resolve_ok= 1
549
550     # set to 0 if the above fails, and, we are using dhcp in which
551     # case we don't know the ip of this machine (without having to
552     # parse ifconfig or something). In that case, we won't be able
553     # to make api calls, so printing a message to the screen will
554     # have to suffice.
555     can_make_api_call= 1
556
557     log.write( "Checking that hostname %s resolves\n" % hostname )
558
559     # try a regular dns lookup first
560     try:
561         resolved_node_ip_list = socket.gethostbyname_ex(hostname)[2]
562     except socket.gaierror, e:
563         hostname_resolve_ok = 0
564         
565
566     if INTERFACE_SETTINGS['method'] == "dhcp":
567         if hostname_resolve_ok:
568             INTERFACE_SETTINGS['ip']= resolved_node_ip_list[0]
569             node_ip = resolved_node_ip_list[0]
570         else:
571             can_make_api_call= 0
572     else:
573         node_ip = INTERFACE_SETTINGS['ip']
574
575     # make sure the dns lookup matches what the configuration file says
576     if hostname_resolve_ok:
577         if node_ip not in resolved_node_ip_list:
578             log.write( "Hostname %s does not resolve to %s, but %s:\n" % \
579                        (hostname,node_ip,resolved_node_ip_list) )
580             hostname_resolve_ok= 0
581         else:
582             log.write( "Hostname %s correctly resolves to %s:\n" %
583                        (hostname,node_ip) )
584
585         
586     vars["INTERFACE_SETTINGS"]= INTERFACE_SETTINGS
587
588     if (not hostname_resolve_ok and not vars['DISCONNECTED_OPERATION'] and
589         'NAT_MODE' not in vars):
590         log.write( "Hostname does not resolve correctly, will not continue.\n" )
591
592         if can_make_api_call:
593             log.write( "Notifying contacts of problem.\n" )
594
595             vars['RUN_LEVEL']= 'failboot'
596             vars['STATE_CHANGE_NOTIFY']= 1
597             vars['STATE_CHANGE_NOTIFY_MESSAGE']= \
598                                      notify_messages.MSG_HOSTNAME_NOT_RESOLVE
599             
600             UpdateRunLevelWithPLC.Run( vars, log )
601                     
602         log.write( "\n\n" )
603         log.write( "The hostname and/or ip in the network configuration\n" )
604         log.write( "file do not resolve and match.\n" )
605         log.write( "Please make sure the hostname set in the network\n" )
606         log.write( "configuration file resolves to the ip also specified\n" )
607         log.write( "there.\n\n" )
608         log.write( "Debug mode is being started on this cd. When the above\n" )
609         log.write( "is corrected, reboot the machine to try again.\n" )
610         
611         raise BootManagerException, \
612               "Configured node hostname does not resolve."
613     
614     return 1