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