be extra clear about what happens when a node that doesn't have node_id
[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
47 import utils
48 from Exceptions import *
49 import BootServerRequest
50
51
52 # two possible names of the configuration files
53 NEW_CONF_FILE_NAME= "plnode.txt"
54 OLD_CONF_FILE_NAME= "planet.cnf"
55
56
57 def Run( vars, log ):   
58     """
59     read the machines node configuration file, which contains
60     the node key and the node_id for this machine.
61     
62     these files can exist in several different locations with
63     several different names. Below is the search order:
64
65     filename      floppy   flash    cd
66     plnode.txt      1        2      4 (/usr/boot), 5 (/usr)
67     planet.cnf      3
68
69     The locations will be searched in the above order, plnode.txt
70     will be checked first, then planet.cnf. Flash devices will only
71     be searched on 3.0 cds.
72
73     Because some of the earlier
74     boot cds don't validate the configuration file (which results
75     in a file named /tmp/planet-clean.cnf), and some do, lets
76     bypass this, and mount and attempt to read in the conf
77     file ourselves. If it doesn't exist, we cannot continue, and a
78     BootManagerException will be raised. If the configuration file is found
79     and read, return 1.
80
81     Expect the following variables from the store:
82     BOOT_CD_VERSION          A tuple of the current bootcd version
83     ALPINA_SERVER_DIR        directory on the boot servers containing alpina
84                              scripts and support files
85     
86     Sets the following variables from the configuration file:
87     WAS_NODE_ID_IN_CONF         Set to 1 if the node id was in the conf file
88     WAS_NODE_KEY_IN_CONF         Set to 1 if the node key was in the conf file
89     NONE_ID                     The db node_id for this machine
90     NODE_KEY                    The key for this node
91     NETWORK_SETTINGS            A dictionary of the values from the network
92                                 configuration file. keys set:
93                                    method
94                                    ip        
95                                    mac       
96                                    gateway   
97                                    network   
98                                    broadcast 
99                                    netmask   
100                                    dns1      
101                                    dns2      
102                                    hostname  
103                                    domainname
104     """
105
106     log.write( "\n\nStep: Reading node configuration file.\n" )
107
108
109     # make sure we have the variables we need
110     try:
111         BOOT_CD_VERSION= vars["BOOT_CD_VERSION"]
112         if BOOT_CD_VERSION == "":
113             raise ValueError, "BOOT_CD_VERSION"
114
115         ALPINA_SERVER_DIR= vars["ALPINA_SERVER_DIR"]
116         if ALPINA_SERVER_DIR == None:
117             raise ValueError, "ALPINA_SERVER_DIR"
118
119     except KeyError, var:
120         raise BootManagerException, "Missing variable in vars: %s\n" % var
121     except ValueError, var:
122         raise BootManagerException, "Variable in vars, shouldn't be: %s\n" % var
123
124
125     NETWORK_SETTINGS= {}
126     NETWORK_SETTINGS['method']= "dhcp"
127     NETWORK_SETTINGS['ip']= ""
128     NETWORK_SETTINGS['mac']= ""
129     NETWORK_SETTINGS['gateway']= ""
130     NETWORK_SETTINGS['network']= ""
131     NETWORK_SETTINGS['broadcast']= ""
132     NETWORK_SETTINGS['netmask']= ""
133     NETWORK_SETTINGS['dns1']= ""
134     NETWORK_SETTINGS['dns2']= ""
135     NETWORK_SETTINGS['hostname']= "localhost"
136     NETWORK_SETTINGS['domainname']= "localdomain"
137     vars['NETWORK_SETTINGS']= NETWORK_SETTINGS
138
139     vars['NODE_ID']= 0
140     vars['NODE_KEY']= ""
141
142     vars['WAS_NODE_ID_IN_CONF']= 0
143     vars['WAS_NODE_KEY_IN_CONF']= 0
144
145     # for any devices that need to be mounted to get the configuration
146     # file, mount them here.
147     mount_point= "/tmp/conffilemount"
148     utils.makedirs( mount_point )
149
150     old_conf_file_contents= None
151     conf_file_contents= None
152     
153     
154     # 1. check the regular floppy device
155     log.write( "Checking standard floppy disk for plnode.txt file.\n" )
156     
157     utils.sysexec_noerr( "mount -o ro -t ext2,msdos /dev/fd0 %s " \
158                          % mount_point, log )
159
160     conf_file_path= "%s/%s" % (mount_point,NEW_CONF_FILE_NAME)
161     if os.access( conf_file_path, os.R_OK ):
162         try:
163             conf_file= file(conf_file_path,"r")
164             conf_file_contents= conf_file.read()
165             conf_file.close()
166         except IOError, e:
167             pass
168
169         utils.sysexec_noerr( "umount /dev/fd0", log )
170         if __parse_configuration_file( vars, log, conf_file_contents):
171             return 1
172         else:
173             raise BootManagerException( "Found configuration file plnode.txt " \
174                                         "on floppy, but was unable to parse it." )
175
176
177     # try the old file name, same device. its actually number 3 on the search
178     # order, but do it now to save mounting/unmounting the disk twice.
179     # try to parse it later...
180     conf_file_path= "%s/%s" % (mount_point,OLD_CONF_FILE_NAME)
181     if os.access( conf_file_path, os.R_OK ):
182         try:
183             old_conf_file= file(conf_file_path,"r")
184             old_conf_file_contents= old_conf_file.read()
185             old_conf_file.close()
186         except IOError, e:
187             pass
188         
189     utils.sysexec_noerr( "umount /dev/fd0", log )
190
191
192
193     if BOOT_CD_VERSION[0] == 3:
194         # 2. check flash devices on 3.0 based cds
195         log.write( "Checking flash devices for plnode.txt file.\n" )
196
197         # this is done the same way the 3.0 cds do it, by attempting
198         # to mount and sd*1 devices that are removable
199         devices= os.listdir("/sys/block/")
200
201         for device in devices:
202             if device[:2] != "sd":
203                 continue
204
205             # test removable
206             removable_file_path= "/sys/block/%s/removable" % device
207             try:
208                 removable= int(file(removable_file_path,"r").read().strip())
209             except ValueError, e:
210                 continue
211             except IOError, e:
212                 continue
213
214             if not removable:
215                 continue
216
217             log.write( "Checking removable device %s\n" % device )
218         
219             # ok, try to mount it and see if we have a conf file.
220             full_device= "/dev/%s1" % device
221
222             try:
223                 utils.sysexec( "mount -o ro -t ext2,msdos %s %s" \
224                                % (full_device,mount_point), log )
225             except BootManagerException, e:
226                 continue
227             
228             conf_file_path= "%s/%s" % (mount_point,NEW_CONF_FILE_NAME)
229             if os.access( conf_file_path, os.R_OK ):
230                 try:
231                     conf_file= file(conf_file_path,"r")
232                     conf_file_contents= conf_file.read()
233                     conf_file.close()
234                 except IOError, e:
235                     pass
236
237             utils.sysexec_noerr( "umount %s" % full_device, log )
238             if __parse_configuration_file( vars, log, conf_file_contents):
239                 return 1
240             else:
241                 raise BootManagerException("Found configuration file plnode.txt " \
242                                            "on floppy, but was unable to parse it.")
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.\n" )
248
249     if old_conf_file_contents:
250         if __parse_configuration_file( vars, log, old_conf_file_contents):
251             return 1
252         else:
253             raise BootManagerException( "Found configuration file planet.cnf " \
254                                         "on floppy, but was unable to parse it." )
255
256
257     # 4. check for plnode.txt in /usr/boot (mounted already)
258     log.write( "Checking /usr/boot (cd) for plnode.txt file.\n" )
259     
260     conf_file_path= "/usr/boot/%s" % NEW_CONF_FILE_NAME
261     if os.access(conf_file_path,os.R_OK):
262         try:
263             conf_file= file(conf_file_path,"r")
264             conf_file_contents= conf_file.read()
265             conf_file.close()
266         except IOError, e:
267             pass    
268     
269         if __parse_configuration_file( vars, log, conf_file_contents):            
270             return 1
271         else:
272             raise BootManagerException( "Found configuration file plnode.txt " \
273                                         "in /usr/boot, but was unable to parse it.")
274
275
276
277     # 5. check for plnode.txt in /usr (mounted already)
278     log.write( "Checking /usr (cd) for plnode.txt file.\n" )
279     
280     conf_file_path= "/usr/%s" % NEW_CONF_FILE_NAME
281     if os.access(conf_file_path,os.R_OK):
282         try:
283             conf_file= file(conf_file_path,"r")
284             conf_file_contents= conf_file.read()
285             conf_file.close()
286         except IOError, e:
287             pass    
288     
289         if __parse_configuration_file( vars, log, conf_file_contents):            
290             return 1
291         else:
292             raise BootManagerException( "Found configuration file plnode.txt " \
293                                         "in /usr, but was unable to parse it.")
294
295
296     raise BootManagerException, "Unable to find and read a node configuration file."
297     
298
299
300
301 def __parse_configuration_file( vars, log, file_contents ):
302     """
303     parse a configuration file, set keys in var NETWORK_SETTINGS
304     in vars (see comment for function ReadNodeConfiguration)
305     """
306
307     BOOT_CD_VERSION= vars["BOOT_CD_VERSION"]
308     ALPINA_SERVER_DIR= vars["ALPINA_SERVER_DIR"]
309     NETWORK_SETTINGS= vars["NETWORK_SETTINGS"]
310     
311     if file_contents is None:
312         return 0
313     
314     try:
315         line_num= 0
316         for line in file_contents.split("\n"):
317
318             line_num = line_num + 1
319             
320             # if its a comment or a whitespace line, ignore
321             if line[:1] == "#" or string.strip(line) == "":
322                 continue
323
324             # file is setup as name="value" pairs
325             parts= string.split(line,"=")
326             if len(parts) != 2:
327                 log.write( "Invalid line %d in configuration file:\n" % line_num )
328                 log.write( line + "\n" )
329                 return 0
330
331             name= string.strip(parts[0])
332             value= string.strip(parts[1])
333
334             # make sure value starts and ends with
335             # single or double quotes
336             quotes= value[0] + value[len(value)-1]
337             if quotes != "''" and quotes != '""':
338                 log.write( "Invalid line %d in configuration file:\n" % line_num )
339                 log.write( line + "\n" )
340                 return 0
341
342             # get rid of the quotes around the value
343             value= string.strip(value[1:len(value)-1])
344
345             if name == "NODE_ID":
346                 try:
347                     vars['NODE_ID']= int(value)
348                     vars['WAS_NODE_ID_IN_CONF']= 1
349                 except ValueError, e:
350                     log.write( "Non-numeric node_id in configuration file.\n" )
351                     return 0
352
353             if name == "NODE_KEY":
354                 vars['NODE_KEY']= value
355                 vars['WAS_NODE_KEY_IN_CONF']= 1
356
357             if name == "IP_METHOD":
358                 value= string.lower(value)
359                 if value != "static" and value != "dhcp":
360                     log.write( "Invalid IP_METHOD in configuration file:\n" )
361                     log.write( line + "\n" )
362                     return 0
363                 NETWORK_SETTINGS['method']= value.strip()
364
365             if name == "IP_ADDRESS":
366                 NETWORK_SETTINGS['ip']= value.strip()
367
368             if name == "IP_GATEWAY":
369                 NETWORK_SETTINGS['gateway']= value.strip()
370
371             if name == "IP_NETMASK":
372                 NETWORK_SETTINGS['netmask']= value.strip()
373
374             if name == "IP_NETADDR":
375                 NETWORK_SETTINGS['network']= value.strip()
376
377             if name == "IP_BROADCASTADDR":
378                 NETWORK_SETTINGS['broadcast']= value.strip()
379
380             if name == "IP_DNS1":
381                 NETWORK_SETTINGS['dns1']= value.strip()
382
383             if name == "IP_DNS2":
384                 NETWORK_SETTINGS['dns2']= value.strip()
385
386             if name == "HOST_NAME":
387                 NETWORK_SETTINGS['hostname']= string.lower(value)
388
389             if name == "DOMAIN_NAME":
390                 NETWORK_SETTINGS['domainname']= string.lower(value)
391
392     except IndexError, e:
393         log.write( "Unable to parse configuration file\n" )
394         return 0
395
396     # now if we are set to dhcp, clear out any fields
397     # that don't make sense
398     if NETWORK_SETTINGS["method"] == "dhcp":
399         NETWORK_SETTINGS["ip"]= ""
400         NETWORK_SETTINGS["gateway"]= ""     
401         NETWORK_SETTINGS["netmask"]= ""
402         NETWORK_SETTINGS["network"]= ""
403         NETWORK_SETTINGS["broadcast"]= ""
404         NETWORK_SETTINGS["dns1"]= ""
405         NETWORK_SETTINGS["dns2"]= ""
406
407
408     log.write("Successfully read and parsed node configuration file.\n" )
409
410     
411     if vars['NODE_ID'] is None or vars['NODE_ID'] == 0:
412         log.write( "Configuration file does not contain the node_id value.\n" )
413         log.write( "Querying PLC for node_id.\n" )
414
415         bs_request= BootServerRequest.BootServerRequest()
416
417         try:
418             ifconfig_file= file("/tmp/ifconfig","r")
419             ifconfig= ifconfig_file.read()
420             ifconfig_file.close()
421         except IOError:
422             log.write( "Unable to read ifconfig output from /tmp/ifconfig\n" )
423             return 0
424         
425         postVars= {"ifconfig" : ifconfig}
426         result= bs_request.DownloadFile( "%s/getnodeid.php" %
427                                          ALPINA_SERVER_DIR,
428                                          None, postVars, 1, 1,
429                                          "/tmp/node_id")
430         if result == 0:
431             log.write( "Unable to make request to get node_id.\n" )
432             return 0
433
434         try:
435             node_id_file= file("/tmp/node_id","r")
436             node_id= string.strip(node_id_file.read())
437             node_id_file.close()
438         except IOError:
439             log.write( "Unable to read node_id from /tmp/node_id\n" )
440             return 0
441
442         try:
443             node_id= int(string.strip(node_id))
444         except ValueError:
445             log.write( "Got node_id from PLC, but not numeric: %s" % str(node_id) )
446             return 0
447
448         if node_id == -1:
449             log.write( "Got node_id, but it returned -1\n\n" )
450
451             log.write( "------------------------------------------------------\n" )
452             log.write( "This indicates that this node could not be identified\n" )
453             log.write( "by PLC. You will need to add the node to your site,\n" )
454             log.write( "and regenerate the network configuration file.\n" )
455             log.write( "See the Technical Contact guide for node setup\n" )
456             log.write( "procedures.\n\n" )
457             log.write( "Boot process canceled until this is completed.\n" )
458             log.write( "------------------------------------------------------\n" )
459             
460             cancel_boot_flag= "/tmp/CANCEL_BOOT"
461             # this will make the initial script stop requesting scripts from PLC
462             utils.sysexec( "touch %s" % cancel_boot_flag, log )
463
464             return 0
465
466         log.write( "Got node_id from PLC: %s\n" % str(node_id) )
467         vars['NODE_ID']= node_id
468
469
470
471     if vars['NODE_KEY'] is None or vars['NODE_KEY'] == "":
472         log.write( "Configuration file does not contain a node_key value.\n" )
473         log.write( "Using boot nonce instead.\n" )
474
475         # 3.x cds stored the file in /tmp/nonce in ascii form, so they
476         # can be read and used directly. 2.x cds stored in the same place
477         # but in binary form, so we need to convert it to ascii the same
478         # way the old boot scripts did so it matches whats in the db
479         # (php uses bin2hex, 
480         if BOOT_CD_VERSION[0] == 2:
481             read_mode= "rb"
482         else:
483             read_mode= "r"
484             
485         try:
486             nonce_file= file("/tmp/nonce",read_mode)
487             nonce= nonce_file.read()
488             nonce_file.close()
489         except IOError:
490             log.write( "Unable to read nonce from /tmp/nonce\n" )
491             return 0
492
493         if BOOT_CD_VERSION[0] == 2:
494             nonce= nonce.encode('hex')
495
496             # there is this nice bug in the php that currently accepts the
497             # nonce for the old scripts, in that if the nonce contains
498             # null chars (2.x cds sent as binary), then
499             # the nonce is truncated. so, do the same here, truncate the nonce
500             # at the first null ('00'). This could leave us with an empty string.
501             nonce_len= len(nonce)
502             for byte_index in range(0,nonce_len,2):
503                 if nonce[byte_index:byte_index+2] == '00':
504                     nonce= nonce[:byte_index]
505                     break
506         else:
507             nonce= string.strip(nonce)
508
509         log.write( "Read nonce, using as key.\n" )
510         vars['NODE_KEY']= nonce
511         
512         
513     # at this point, we've read the network configuration file.
514     # if we were setup using dhcp, get this system's current ip
515     # address and update the vars key ip, because it
516     # is needed for future api calls.
517
518     # at the same time, we can check to make sure that the hostname
519     # in the configuration file matches the ip address.
520
521     hostname= NETWORK_SETTINGS['hostname'] + "." + \
522               NETWORK_SETTINGS['domainname']
523
524     log.write( "Checking that hostname %s resolves\n" % hostname )
525     try:
526         resolved_node_ip= socket.gethostbyname(hostname)
527     except socket.gaierror, e:
528         raise BootManagerException, \
529               "Configured node hostname does not resolve."
530
531     if NETWORK_SETTINGS['method'] == "dhcp":
532         NETWORK_SETTINGS['ip']= resolved_node_ip
533         node_ip= resolved_node_ip
534     else:
535         node_ip= NETWORK_SETTINGS['ip']
536
537     if node_ip != resolved_node_ip:
538         log.write( "Hostname %s does not resolve to %s, but %s:\n" % \
539                    (hostname,node_ip,resolved_node_ip) )
540     else:
541         log.write( "Hostname %s resolves to %s:\n" % (hostname,node_ip) )
542
543     # 3.x cds, with a node_key on the floppy, can update their mac address
544     # at plc, so get it here
545     if BOOT_CD_VERSION[0] == 3 and vars['WAS_NODE_ID_IN_CONF'] == 1:
546         eth_device= "eth0"
547         try:
548             hw_addr_file= file("/sys/class/net/%s/address" % eth_device, "r")
549             hw_addr= hw_addr_file.read().strip().upper()
550             hw_addr_file.close()
551         except IOError, e:
552             raise BootmanagerException, \
553                   "could not get hw address for device %s" % eth_device
554
555         NETWORK_SETTINGS['mac']= hw_addr
556
557         
558     vars["NETWORK_SETTINGS"]= NETWORK_SETTINGS
559     
560     return 1