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