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