generalized versions of plc configuration utilities
authorMarc Fiuczynski <mef@cs.princeton.edu>
Wed, 29 Apr 2009 21:23:54 +0000 (21:23 +0000)
committerMarc Fiuczynski <mef@cs.princeton.edu>
Wed, 29 Apr 2009 21:23:54 +0000 (21:23 +0000)
plc-config-tty
plc_config.py

index 67ebe61..26c5bb7 100755 (executable)
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/bin/env python
 
 # Interactively prompts for variable values
 # expected arguments are
@@ -15,50 +15,78 @@ import sys
 import os
 import re
 import readline
-import getopt
+import traceback
+from optparse import OptionParser
 
 from plc_config import PLCConfiguration
+from plc_config import ConfigurationException
 
 ####################
 release_id = "$Id$"
 release_rev = "$Revision$"
 
+
+flavours={}
+flavours["plc-devel"]={'service':"plc-devel",
+                       'usual_variables':["PLC_DEVEL_FEDORA_URL","PLC_DEVEL_CVSROOT"],
+                       'config_dir':"/plc/devel/data/etc/planetlab"}
+
+def noop_validator(v):
+    pass
+
+def plc_validator(validated_variables):
+    maint_user = validated_variables["PLC_API_MAINTENANCE_USER"]
+    root_user = validated_variables["PLC_ROOT_USER"]
+    if maint_user == root_user:
+        raise ConfigurationException("The Maintenance Account email address cannot be the same as the Root User email address")
+
+flavours["plc"]={'service':"plc",
+                 'usual_variables':["PLC_NAME",
+                                    "PLC_SHORTNAME",
+                                    "PLC_SLICE_PREFIX",
+                                    "PLC_ROOT_USER",
+                                    "PLC_ROOT_PASSWORD",
+                                    "PLC_MAIL_ENABLED",
+                                    "PLC_MAIL_SUPPORT_ADDRESS",
+                                    "PLC_DB_HOST",
+                                    "PLC_API_HOST",
+                                    "PLC_WWW_HOST",
+                                    "PLC_BOOT_HOST",
+                                    "PLC_NET_DNS1",
+                                    "PLC_NET_DNS2",
+                                    ],
+                 'config_dir':"/etc/planetlab",
+                 'validate_variables':{"PLC_API":"MAINTENANCE_USER","PLC":"ROOT_USER"},
+                 'validator':plc_validator,
+                 }
+
+defined_flavour = "plc"
+
+
 def init_flavour (flavour):
-    global service
-    global usual_variables
-    if (flavour == "devel"):
-        service="plc-devel"
-        usual_variables=("PLC_DEVEL_FEDORA_URL",
-                          "PLC_DEVEL_CVSROOT")
-        config_dir = "/plc/devel/data/etc/planetlab"
+    global service, usual_variables
+    
+    global defined_flavour
+    if flavours.has_key(flavour):
+        defined_flavour = flavour
     else:
-        service="plc"
-        usual_variables=("PLC_NAME",
-                         "PLC_SLICE_PREFIX",
-                         "PLC_ROOT_USER",
-                         "PLC_ROOT_PASSWORD",
-                         "PLC_MAIL_ENABLED",
-                         "PLC_MAIL_SUPPORT_ADDRESS",
-                         "PLC_DB_HOST",
-                         "PLC_API_HOST",
-                         "PLC_WWW_HOST",
-                         "PLC_BOOT_HOST",
-                         "PLC_NET_DNS1",
-                         "PLC_NET_DNS2",
-                         )
-        config_dir = "/etc/planetlab"
-    global def_default_config
+        defined_flavour = "plc"
+
+    flav=flavours.get(flavour,flavours["plc"])
+    service=flav["service"]
+    usual_variables=flav["usual_variables"]
+    config_dir=flav["config_dir"]
+
+    global def_default_config, def_site_config, def_consolidated_config
     def_default_config= "%s/default_config.xml" % config_dir
-    global def_site_config
     def_site_config = "%s/configs/site.xml" % config_dir
-    global def_consolidated_config
     def_consolidated_config = "%s/plc_config.xml" % config_dir
 
     global mainloop_usage
     mainloop_usage= """Available commands:
  Uppercase versions give variables comments, when available
  u/U\t\t\tEdit usual variables
- w\t\t\tWrite & consolidate
+ w/W\t\t\tWrite / Write & reload
  r\t\t\tRestart %s service
  q\t\t\tQuit (without saving)
  h/?\t\t\tThis help
@@ -74,23 +102,21 @@ Typical usage involves: u, [l,] w, r, q
 """ % service
 
 def usage ():
-    command_usage="Usage: %s [-d] [-v] [default-xml [site-xml [consolidated-xml]]]"% sys.argv[0]
-    init_flavour ("boot")
+    command_usage="%prog [options] [default-xml [site-xml [consolidated-xml]]]"
+    init_flavour ("plc")
     command_usage +="""
-  -v shows version and exits
 \t default-xml defaults to %s
 \t site-xml defaults to %s
 \t consolidated-xml defaults to %s""" % (def_default_config,def_site_config, def_consolidated_config)
     command_usage += """
   Unless you specify the -d option, meaning you want to configure
   myplc-devel instead of regular myplc, in which case""" 
-    init_flavour ("devel")
+    init_flavour ("plc-devel")
     command_usage +="""
 \t default-xml defaults to %s
 \t site-xml defaults to %s
 \t consolidated-xml defaults to %s""" % (def_default_config,def_site_config, def_consolidated_config)
-    print(command_usage)
-    sys.exit(1)
+    return command_usage
 
 ####################
 variable_usage= """Edit Commands :
@@ -173,6 +199,7 @@ def print_category (config, cid, show_comments=True):
 
 ####################
 def consolidate (default_config, site_config, consolidated_config):
+    global service
     try:
         conso = PLCConfiguration (default_config)
         conso.load (site_config)
@@ -182,10 +209,14 @@ def consolidate (default_config, site_config, consolidated_config):
         return
     print ("Merged\n\t%s\nand\t%s\ninto\t%s"%(default_config,site_config,
                                               consolidated_config))
-    os.system("set -x ; service plc reload")
+
+def reload_service ():
+    global service
+    os.system("set -x ; service %s reload" % service)
         
 ####################
-def restart_plc ():
+def restart_service ():
+    global service
     print ("==================== Stopping %s" % service)
     os.system("service %s stop" % service)
     print ("==================== Starting %s" % service)
@@ -213,6 +244,9 @@ def prompt_variable (cdef, cread, cwrite, category, variable,
             answer = raw_input(prompt).strip()
         except EOFError :
             raise Exception ('BailOut')
+        except KeyboardInterrupt:
+            print "\n"
+            raise Exception ('BailOut')
 
         # no change
         if (answer == "") or (answer == current_value):
@@ -305,17 +339,22 @@ def show_variables_category (cdef, cread, cwrite, cid, show_value,show_comments)
                        show_value,show_comments)
 
 ####################
-re_mainloop_0arg="^(?P<command>[uUwrqlLsSeEcvVhH\?])[ \t]*$"
+re_mainloop_0arg="^(?P<command>[uUwrRqlLsSeEcvVhH\?])[ \t]*$"
 re_mainloop_1arg="^(?P<command>[sSeEvV])[ \t]+(?P<arg>\w+)$"
 matcher_mainloop_0arg=re.compile(re_mainloop_0arg)
 matcher_mainloop_1arg=re.compile(re_mainloop_1arg)
 
 def mainloop (cdef, cread, cwrite, default_config, site_config, consolidated_config):
+    global service
     while True:
         try:
             answer = raw_input("Enter command (u for usual changes, w to save, ? for help) ").strip()
         except EOFError:
             answer =""
+        except KeyboardInterrupt:
+            print "\nBye"
+            sys.exit()
+
         if (answer == "") or (answer in "?hH"):
             print mainloop_usage
             continue
@@ -358,15 +397,26 @@ def mainloop (cdef, cread, cwrite, default_config, site_config, consolidated_con
         if (command in "qQ"):
             # todo check confirmation
             return
-        elif (command in "wW"):
+        elif (command == "w"):
+            global defined_flavour
             try:
+                # Confirm that various constraints are met before saving file.
+                validate_variables = flavours[defined_flavour].get('validate_variables',{})
+                validated_variables = cwrite.verify(cdef, cread, validate_variables)
+                validator = flavours[defined_flavour].get('validator',noop_validator)
+                validator(validated_variables)
                 cwrite.save(site_config)
+            except ConfigurationException, e:
+                print "Save failed due to a configuration exception: %s" % e
+                break;
             except:
+                print traceback.print_exc()
                 print ("Could not save -- fix write access on %s" % site_config)
                 break
             print ("Wrote %s" % site_config)
             consolidate(default_config, site_config, consolidated_config)
-            print ("You might want to type 'r' (restart plc) or 'q' (quit)")
+            print ("You might want to type 'r' (restart %s), 'R' (reload %s) or 'q' (quit)" % \
+                   (service,service))
         elif (command == "u"):
             try:
                 for varname in usual_variables:
@@ -376,7 +426,9 @@ def mainloop (cdef, cread, cwrite, default_config, site_config, consolidated_con
                 if (str(inst) != 'BailOut'):
                     raise
         elif (command == "r"):
-            restart_plc()
+            restart_service()
+        elif (command == "R"):
+            reload_service()
         elif (command == "c"):
             print_categories(cread)
         elif (command in "eE"):
@@ -410,7 +462,12 @@ def mainloop (cdef, cread, cwrite, default_config, site_config, consolidated_con
 def check_dir (config_file):
     dirname = os.path.dirname (config_file)
     if (not os.path.exists (dirname)):
-        os.makedirs(dirname,0755)
+        try:
+            os.makedirs(dirname,0755)
+        except OSError, e:
+            print "Cannot create dir %s due to %s - exiting" % (dirname,e)
+            sys.exit(1)
+            
         if (not os.path.exists (dirname)):
             print "Cannot create dir %s - exiting" % dirname
             sys.exit(1)
@@ -422,31 +479,62 @@ def main ():
 
     command=sys.argv[0]
     argv = sys.argv[1:]
-
     save = True
-    # default is myplc (non -devel) unless -d is specified
-    init_flavour("boot")
-    optlist,list = getopt.getopt(argv,":dhv")
-    for opt in optlist:
-        if opt[0] == "-h":
-            usage()
-        if opt[0] == "-v":
-            print ("This is %s - %s" %(command,release_rev))
-            sys.exit(1)
-        if opt[0] == "-d":
-            init_flavour("devel")
-            argv=argv[1:]
-
-    if len(argv) == 0:
-        (default_config,site_config,consolidated_config) = (def_default_config, def_site_config, def_consolidated_config)
-    elif len(argv) == 1:
-        (default_config,site_config,consolidated_config) = (argv[0], def_site_config, def_consolidated_config)
-    elif len(argv) == 2:
-        (default_config, site_config,consolidated_config)  = (argv[0], argv[1], def_consolidated_config)
-    elif len(argv) == 3:
-        (default_config, site_config,consolidated_config)  = argv
+    parser = OptionParser(usage=usage(), version="%prog 1.0")
+    parser.set_defaults(flavour="plc",
+                        devel=False,
+                        config="flavour.config",
+                        config_dir=None,
+                        service=None,
+                        usual_variables=[])
+    parser.add_option("-d","",dest="devel",action="store_true",help="Sets the configuration flavour")
+    parser.add_option("","--configdir",dest="config_dir",help="specify configuration directory")
+    parser.add_option("","--service",dest="service",help="specify /etc/init.d style service name")
+    parser.add_option("","--usual_variable",dest="usual_variables",action="append", help="add a usual variable")
+    parser.add_option("","--flavour",dest="flavour", help="Sets the configuration flavour")
+
+    (config,args) = parser.parse_args()
+    if len(args)>3:
+        parser.error("too many arguments")
+
+    # if -d then set flavour to "plc-devel"
+    if config.devel:
+        config.flavour="plc-devel"
+        
+    if config.flavour not in flavours:
+        if config.service==None:
+            parser.error("unknown flavour '%s'" % config.flavour)
+        else:
+            flavours[config.flavour]={}
+            flavour=flavours[config.flavour]
+            flavour['service']=config.service
+            flavour['usual_variables']=config.usual_variables
+            if config.config_dir==None:
+                flavour['config_dir']="/etc/%s"%config.service
+            else:
+                flavour['config_dir']=config.config_dir
     else:
-        usage()
+        flavour=flavours[config.flavour]
+
+        # in case the config dir should be something other than /etc/planetlab
+        if config.config_dir <> None:
+            flavour['config_dir']=config.config_dir
+
+        # add in new usual_variables defined on the command line
+        for usual_variable in config.usual_variables:
+            if usual_variable not in flavour['usual_variables']:
+                flavour['usual_variables'].append(usual_variable)
+
+    # intialize flavour
+    init_flavour(config.flavour)
+
+    (default_config,site_config,consolidated_config) = (def_default_config, def_site_config, def_consolidated_config)
+    if len(args) >= 1:
+        default_config=args[0]
+    if len(args) >= 2:
+        site_config=args[1]
+    if len(args) == 3:
+        consolidated_config=args[2]
 
     for c in (default_config,site_config,consolidated_config):
         check_dir (c)
@@ -458,10 +546,15 @@ def main ():
         # in effect : default settings + local settings - read only
         cread = PLCConfiguration(default_config)
 
+    except ConfigurationException, e:
+        print ("Error %s in default config file %s" %(e,default_config))
+        return 1
     except:
-        print ("default config files not found, is myplc installed ?")
+        print traceback.print_exc()
+        print ("default config files %s not found, is myplc installed ?" % default_config)
         return 1
 
+
     # local settings only, will be modified & saved
     cwrite=PLCConfiguration()
     
@@ -471,7 +564,7 @@ def main ():
     except:
         cwrite = PLCConfiguration()
 
-    mainloop (cdef, cread, cwrite,default_config, site_config, consolidated_config)
+    mainloop (cdef, cread, cwrite, default_config, site_config, consolidated_config)
     return 0
 
 if __name__ == '__main__':
index c9382d2..45805b9 100644 (file)
@@ -11,6 +11,7 @@
 #
 
 import xml.dom.minidom
+from xml.parsers.expat import ExpatError
 from StringIO import StringIO
 import time
 import re
@@ -20,6 +21,8 @@ import os
 import types
 
 
+class ConfigurationException(Exception): pass
+
 class PLCConfiguration:
     """
     Configuration file store. Optionally instantiate with a file path
@@ -187,7 +190,11 @@ class PLCConfiguration:
         Merge file into configuration store.
         """
 
-        dom = xml.dom.minidom.parse(file)
+        try:
+            dom = xml.dom.minidom.parse(file)
+        except ExpatError, e:
+            raise ConfigurationException, e
+
         if type(file) in types.StringTypes:
             self._files.append(os.path.abspath(file))
 
@@ -235,6 +242,44 @@ class PLCConfiguration:
 
         fileobj.close()
 
+    def verify(self, default, read, verify_variables={}):
+        """ Confirm that the existing configuration is consistent
+            according to the checks below.
+
+            It looks for filled-in values in the order of, local object (self),
+            followed by cread (read values), and finally default values.
+
+        Arguments: 
+
+            default configuration
+            site configuration
+            list of category/variable tuples to validate in these configurations
+
+        Returns:
+
+            dict of values for the category/variables passed in
+            If an exception is found, ConfigurationException is raised.
+
+        """
+
+        validated_variables = {}
+        for category_id, variable_id in verify_variables.iteritems():
+            category_id = category_id.lower()
+            variable_id = variable_id.lower()
+            variable_value = None
+            sources = (self, read, default)
+            for source in sources:
+                (category_value, variable_value) = source.get(category_id,variable_id)
+                if variable_value <> None:
+                    entry = validated_variables.get(category_id,[])
+                    entry.append(variable_value['value'])
+                    validated_variables["%s_%s"%(category_id.upper(),variable_id.upper())]=entry
+                    break
+            if variable_value == None:
+                raise ConfigurationException("Cannot find %s_%s)" % \
+                                             (category_id.upper(),
+                                              variable_id.upper()))
+        return validated_variables
 
     def get(self, category_id, variable_id):
         """