Don't need the ESSID or IWCONFIG in BootManager.
[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,"=")
379             if len(parts) != 2:
380                 log.write( "Invalid line %d in configuration file:\n" % line_num )
381                 log.write( line + "\n" )
382                 return 0
383
384             name= string.strip(parts[0])
385             value= string.strip(parts[1])
386
387             # make sure value starts and ends with
388             # single or double quotes
389             quotes= value[0] + value[len(value)-1]
390             if quotes != "''" and quotes != '""':
391                 log.write( "Invalid line %d in configuration file:\n" % line_num )
392                 log.write( line + "\n" )
393                 return 0
394
395             # get rid of the quotes around the value
396             value= string.strip(value[1:len(value)-1])
397
398             if name == "NODE_ID":
399                 try:
400                     vars['NODE_ID']= int(value)
401                     vars['WAS_NODE_ID_IN_CONF']= 1
402                 except ValueError, e:
403                     log.write( "Non-numeric node_id in configuration file.\n" )
404                     return 0
405
406             if name == "NODE_KEY":
407                 vars['NODE_KEY']= value
408                 vars['WAS_NODE_KEY_IN_CONF']= 1
409
410             if name == "IP_METHOD":
411                 value= string.lower(value)
412                 if value != "static" and value != "dhcp":
413                     log.write( "Invalid IP_METHOD in configuration file:\n" )
414                     log.write( line + "\n" )
415                     return 0
416                 NETWORK_SETTINGS['method']= value.strip()
417
418             if name == "IP_ADDRESS":
419                 NETWORK_SETTINGS['ip']= value.strip()
420
421             if name == "IP_GATEWAY":
422                 NETWORK_SETTINGS['gateway']= value.strip()
423
424             if name == "IP_NETMASK":
425                 NETWORK_SETTINGS['netmask']= value.strip()
426
427             if name == "IP_NETADDR":
428                 NETWORK_SETTINGS['network']= value.strip()
429
430             if name == "IP_BROADCASTADDR":
431                 NETWORK_SETTINGS['broadcast']= value.strip()
432
433             if name == "IP_DNS1":
434                 NETWORK_SETTINGS['dns1']= value.strip()
435
436             if name == "IP_DNS2":
437                 NETWORK_SETTINGS['dns2']= value.strip()
438
439             if name == "HOST_NAME":
440                 NETWORK_SETTINGS['hostname']= string.lower(value)
441
442             if name == "DOMAIN_NAME":
443                 NETWORK_SETTINGS['domainname']= string.lower(value)
444
445             if name == "NET_DEVICE":
446                 NETWORK_SETTINGS['mac']= string.upper(value)
447
448             if name == "DISCONNECTED_OPERATION":
449                 vars['DISCONNECTED_OPERATION']= value.strip()
450
451     except IndexError, e:
452         log.write( "Unable to parse configuration file\n" )
453         return 0
454
455     # now if we are set to dhcp, clear out any fields
456     # that don't make sense
457     if NETWORK_SETTINGS["method"] == "dhcp":
458         NETWORK_SETTINGS["ip"]= ""
459         NETWORK_SETTINGS["gateway"]= ""     
460         NETWORK_SETTINGS["netmask"]= ""
461         NETWORK_SETTINGS["network"]= ""
462         NETWORK_SETTINGS["broadcast"]= ""
463         NETWORK_SETTINGS["dns1"]= ""
464         NETWORK_SETTINGS["dns2"]= ""
465
466     log.write("Successfully read and parsed node configuration file.\n" )
467
468     # if the mac wasn't specified, read it in from the system.
469     if NETWORK_SETTINGS["mac"] == "":
470         device= "eth0"
471         mac_addr= utils.get_mac_from_interface(device)
472
473         if mac_addr is None:
474             log.write( "Could not get mac address for device eth0.\n" )
475             return 0
476
477         NETWORK_SETTINGS["mac"]= string.upper(mac_addr)
478
479         log.write( "Got mac address %s for device %s\n" %
480                    (NETWORK_SETTINGS["mac"],device) )
481         
482
483     # now, if the conf file didn't contain a node id, post the mac address
484     # to plc to get the node_id value
485     if vars['NODE_ID'] is None or vars['NODE_ID'] == 0:
486         log.write( "Configuration file does not contain the node_id value.\n" )
487         log.write( "Querying PLC for node_id.\n" )
488
489         bs_request= BootServerRequest.BootServerRequest()
490         
491         postVars= {"mac_addr" : NETWORK_SETTINGS["mac"]}
492         result= bs_request.DownloadFile( "%s/getnodeid.php" %
493                                          SUPPORT_FILE_DIR,
494                                          None, postVars, 1, 1,
495                                          "/tmp/node_id")
496         if result == 0:
497             log.write( "Unable to make request to get node_id.\n" )
498             return 0
499
500         try:
501             node_id_file= file("/tmp/node_id","r")
502             node_id= string.strip(node_id_file.read())
503             node_id_file.close()
504         except IOError:
505             log.write( "Unable to read node_id from /tmp/node_id\n" )
506             return 0
507
508         try:
509             node_id= int(string.strip(node_id))
510         except ValueError:
511             log.write( "Got node_id from PLC, but not numeric: %s" % str(node_id) )
512             return 0
513
514         if node_id == -1:
515             log.write( "Got node_id, but it returned -1\n\n" )
516
517             log.write( "------------------------------------------------------\n" )
518             log.write( "This indicates that this node could not be identified\n" )
519             log.write( "by PLC. You will need to add the node to your site,\n" )
520             log.write( "and regenerate the network configuration file.\n" )
521             log.write( "See the Technical Contact guide for node setup\n" )
522             log.write( "procedures.\n\n" )
523             log.write( "Boot process canceled until this is completed.\n" )
524             log.write( "------------------------------------------------------\n" )
525             
526             cancel_boot_flag= "/tmp/CANCEL_BOOT"
527             # this will make the initial script stop requesting scripts from PLC
528             utils.sysexec( "touch %s" % cancel_boot_flag, log )
529
530             return 0
531
532         log.write( "Got node_id from PLC: %s\n" % str(node_id) )
533         vars['NODE_ID']= node_id
534
535
536
537     if vars['NODE_KEY'] is None or vars['NODE_KEY'] == "":
538         log.write( "Configuration file does not contain a node_key value.\n" )
539         log.write( "Using boot nonce instead.\n" )
540
541         # 3.x cds stored the file in /tmp/nonce in ascii form, so they
542         # can be read and used directly. 2.x cds stored in the same place
543         # but in binary form, so we need to convert it to ascii the same
544         # way the old boot scripts did so it matches whats in the db
545         # (php uses bin2hex, 
546         if BOOT_CD_VERSION[0] == 2:
547             read_mode= "rb"
548         else:
549             read_mode= "r"
550             
551         try:
552             nonce_file= file("/tmp/nonce",read_mode)
553             nonce= nonce_file.read()
554             nonce_file.close()
555         except IOError:
556             log.write( "Unable to read nonce from /tmp/nonce\n" )
557             return 0
558
559         if BOOT_CD_VERSION[0] == 2:
560             nonce= nonce.encode('hex')
561
562             # there is this nice bug in the php that currently accepts the
563             # nonce for the old scripts, in that if the nonce contains
564             # null chars (2.x cds sent as binary), then
565             # the nonce is truncated. so, do the same here, truncate the nonce
566             # at the first null ('00'). This could leave us with an empty string.
567             nonce_len= len(nonce)
568             for byte_index in range(0,nonce_len,2):
569                 if nonce[byte_index:byte_index+2] == '00':
570                     nonce= nonce[:byte_index]
571                     break
572         else:
573             nonce= string.strip(nonce)
574
575         log.write( "Read nonce, using as key.\n" )
576         vars['NODE_KEY']= nonce
577         
578         
579     # at this point, we've read the network configuration file.
580     # if we were setup using dhcp, get this system's current ip
581     # address and update the vars key ip, because it
582     # is needed for future api calls.
583
584     # at the same time, we can check to make sure that the hostname
585     # in the configuration file matches the ip address. if it fails
586     # notify the owners
587
588     hostname= NETWORK_SETTINGS['hostname'] + "." + \
589               NETWORK_SETTINGS['domainname']
590
591     # set to 0 if any part of the hostname resolution check fails
592     hostname_resolve_ok= 1
593
594     # set to 0 if the above fails, and, we are using dhcp in which
595     # case we don't know the ip of this machine (without having to
596     # parse ifconfig or something). In that case, we won't be able
597     # to make api calls, so printing a message to the screen will
598     # have to suffice.
599     can_make_api_call= 1
600
601     log.write( "Checking that hostname %s resolves\n" % hostname )
602
603     # try a regular dns lookup first
604     try:
605         resolved_node_ip= socket.gethostbyname(hostname)
606     except socket.gaierror, e:
607         hostname_resolve_ok= 0
608         
609
610     if NETWORK_SETTINGS['method'] == "dhcp":
611         if hostname_resolve_ok:
612             NETWORK_SETTINGS['ip']= resolved_node_ip
613             node_ip= resolved_node_ip
614         else:
615             can_make_api_call= 0
616     else:
617         node_ip= NETWORK_SETTINGS['ip']
618
619     # make sure the dns lookup matches what the configuration file says
620     if hostname_resolve_ok:
621         if node_ip != resolved_node_ip:
622             log.write( "Hostname %s does not resolve to %s, but %s:\n" % \
623                        (hostname,node_ip,resolved_node_ip) )
624             hostname_resolve_ok= 0
625         else:
626             log.write( "Hostname %s correctly resolves to %s:\n" %
627                        (hostname,node_ip) )
628
629         
630     vars["NETWORK_SETTINGS"]= NETWORK_SETTINGS
631
632     if not hostname_resolve_ok and not vars['DISCONNECTED_OPERATION']:
633         log.write( "Hostname does not resolve correctly, will not continue.\n" )
634
635         if can_make_api_call:
636             log.write( "Notifying contacts of problem.\n" )
637
638             vars['BOOT_STATE']= 'dbg'
639             vars['STATE_CHANGE_NOTIFY']= 1
640             vars['STATE_CHANGE_NOTIFY_MESSAGE']= \
641                                      notify_messages.MSG_HOSTNAME_NOT_RESOLVE
642             
643             UpdateBootStateWithPLC.Run( vars, log )
644                     
645         log.write( "\n\n" )
646         log.write( "The hostname and/or ip in the network configuration\n" )
647         log.write( "file do not resolve and match.\n" )
648         log.write( "Please make sure the hostname set in the network\n" )
649         log.write( "configuration file resolves to the ip also specified\n" )
650         log.write( "there.\n\n" )
651         log.write( "Debug mode is being started on this cd. When the above\n" )
652         log.write( "is corrected, reboot the machine to try again.\n" )
653         
654         raise BootManagerException, \
655               "Configured node hostname does not resolve."
656     
657     return 1