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