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