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