3fcc414a329a3b69a6c90f579ac629eafa72f73c
[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" )
450             return 0
451
452         log.write( "Got node_id from PLC: %s\n" % str(node_id) )
453         vars['NODE_ID']= node_id
454
455
456
457     if vars['NODE_KEY'] is None or vars['NODE_KEY'] == "":
458         log.write( "Configuration file does not contain a node_key value.\n" )
459         log.write( "Using boot nonce instead.\n" )
460
461         # 3.x cds stored the file in /tmp/nonce in ascii form, so they
462         # can be read and used directly. 2.x cds stored in the same place
463         # but in binary form, so we need to convert it to ascii the same
464         # way the old boot scripts did so it matches whats in the db
465         # (php uses bin2hex, 
466         if BOOT_CD_VERSION[0] == 2:
467             read_mode= "rb"
468         else:
469             read_mode= "r"
470             
471         try:
472             nonce_file= file("/tmp/nonce",read_mode)
473             nonce= nonce_file.read()
474             nonce_file.close()
475         except IOError:
476             log.write( "Unable to read nonce from /tmp/nonce\n" )
477             return 0
478
479         if BOOT_CD_VERSION[0] == 2:
480             nonce= nonce.encode('hex')
481
482             # there is this nice bug in the php that currently accepts the
483             # nonce for the old scripts, in that if the nonce contains
484             # null chars (2.x cds sent as binary), then
485             # the nonce is truncated. so, do the same here, truncate the nonce
486             # at the first null ('00'). This could leave us with an empty string.
487             nonce_len= len(nonce)
488             for byte_index in range(0,nonce_len,2):
489                 if nonce[byte_index:byte_index+2] == '00':
490                     nonce= nonce[:byte_index]
491                     break
492         else:
493             nonce= string.strip(nonce)
494
495         log.write( "Read nonce, using as key.\n" )
496         vars['NODE_KEY']= nonce
497         
498         
499     # at this point, we've read the network configuration file.
500     # if we were setup using dhcp, get this system's current ip
501     # address and update the vars key ip, because it
502     # is needed for future api calls.
503
504     # at the same time, we can check to make sure that the hostname
505     # in the configuration file matches the ip address.
506
507     hostname= NETWORK_SETTINGS['hostname'] + "." + \
508               NETWORK_SETTINGS['domainname']
509
510     log.write( "Checking that hostname %s resolves\n" % hostname )
511     try:
512         resolved_node_ip= socket.gethostbyname(hostname)
513     except socket.gaierror, e:
514         raise BootManagerException, \
515               "Configured node hostname does not resolve."
516
517     if NETWORK_SETTINGS['method'] == "dhcp":
518         NETWORK_SETTINGS['ip']= resolved_node_ip
519         node_ip= resolved_node_ip
520     else:
521         node_ip= NETWORK_SETTINGS['ip']
522
523     if node_ip != resolved_node_ip:
524         log.write( "Hostname %s does not resolve to %s, but %s:\n" % \
525                    (hostname,node_ip,resolved_node_ip) )
526     else:
527         log.write( "Hostname %s resolves to %s:\n" % (hostname,node_ip) )
528
529     # 3.x cds, with a node_key on the floppy, can update their mac address
530     # at plc, so get it here
531     if BOOT_CD_VERSION[0] == 3 and vars['WAS_NODE_ID_IN_CONF'] == 1:
532         eth_device= "eth0"
533         try:
534             hw_addr_file= file("/sys/class/net/%s/address" % eth_device, "r")
535             hw_addr= hw_addr_file.read().strip().upper()
536             hw_addr_file.close()
537         except IOError, e:
538             raise BootmanagerException, \
539                   "could not get hw address for device %s" % eth_device
540
541         NETWORK_SETTINGS['mac']= hw_addr
542
543         
544     vars["NETWORK_SETTINGS"]= NETWORK_SETTINGS
545     
546     return 1