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