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