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