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