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