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