usr/src/cmd/ai-webserver/publish-manifest.py
changeset 862 e9f31f2f2f2d
parent 861 ccd399d2c6f7
child 863 5a915c215754
equal deleted inserted replaced
861:ccd399d2c6f7 862:e9f31f2f2f2d
     1 #!/usr/bin/python2.6
       
     2 #
       
     3 # CDDL HEADER START
       
     4 #
       
     5 # The contents of this file are subject to the terms of the
       
     6 # Common Development and Distribution License (the "License").
       
     7 # You may not use this file except in compliance with the License.
       
     8 #
       
     9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
       
    10 # or http://www.opensolaris.org/os/licensing.
       
    11 # See the License for the specific language governing permissions
       
    12 # and limitations under the License.
       
    13 #
       
    14 # When distributing Covered Code, include this CDDL HEADER in each
       
    15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
       
    16 # If applicable, add the following below this CDDL HEADER, with the
       
    17 # fields enclosed by brackets "[]" replaced with your own identifying
       
    18 # information: Portions Copyright [yyyy] [name of copyright owner]
       
    19 #
       
    20 # CDDL HEADER END
       
    21 #
       
    22 # Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
       
    23 # Use is subject to license terms.
       
    24 """
       
    25 
       
    26 A/I Publish_Manifest
       
    27 
       
    28 """
       
    29 
       
    30 import os.path
       
    31 import sys
       
    32 import StringIO
       
    33 import gettext
       
    34 import lxml.etree
       
    35 import hashlib
       
    36 from optparse import OptionParser
       
    37 
       
    38 import osol_install.auto_install.AI_database as AIdb
       
    39 import osol_install.auto_install.verifyXML as verifyXML
       
    40 import osol_install.libaiscf as smf
       
    41 
       
    42 INFINITY = str(0xFFFFFFFFFFFFFFFF)
       
    43 IMG_AI_MANIFEST_SCHEMA = "auto_install/ai_manifest.rng"
       
    44 SYS_AI_MANIFEST_SCHEMA = "/usr/share/auto_install/ai_manifest.rng"
       
    45 
       
    46 def parse_options():
       
    47     """
       
    48     Parse and validate options
       
    49     Args: None
       
    50     Returns: the DataFiles object populated and initialized
       
    51     Raises: The DataFiles initialization of manifest(s) A/I, SC, SMF looks for
       
    52             many error conditions and, when caught, are flagged to the user
       
    53             via raising SystemExit exceptions.
       
    54     """
       
    55 
       
    56     usage = _("usage: %prog service_name criteria_manifest")
       
    57     parser = OptionParser(usage=usage)
       
    58     # since no options are specified simply retrieve the args list
       
    59     # args should be a list with indices:
       
    60     # 0 - service name
       
    61     # 1 - manifest path to work with
       
    62     args = parser.parse_args()[1]
       
    63 
       
    64     # check that we got the install service's name and
       
    65     # a criteria manifest
       
    66     if len(args) != 2:
       
    67         parser.print_help()
       
    68         sys.exit(1)
       
    69 
       
    70     # get an AIservice object for requested service
       
    71     try:
       
    72         svc = smf.AIservice(smf.AISCF(FMRI="system/install/server"), args[0])
       
    73     except KeyError:
       
    74         raise SystemExit(_("Error:\tFailed to find service %s") % args[0])
       
    75 
       
    76     # argument two is the criteria manifest
       
    77     crit_manifest = args[1]
       
    78 
       
    79     # get the service's data directory path and imagepath
       
    80     try:
       
    81         image_path = svc['image_path']
       
    82         # txt_record is of the form "aiwebserver=jumprope:46503" so split on ":"
       
    83         # and take the trailing portion for the port number
       
    84         port = svc['txt_record'].rsplit(':')[-1]
       
    85     except KeyError:
       
    86         raise SystemExit(_("SMF data for service %s is corrupt.\n") %
       
    87                          args[0])
       
    88     service_dir = os.path.abspath("/var/ai/" + port)
       
    89 
       
    90     # check that the service and imagepath directories exist,
       
    91     # and the AI.db, criteria_schema.rng and ai_manifest.rng files
       
    92     # are present otherwise the service is misconfigured
       
    93     if not (os.path.isdir(service_dir) and
       
    94             os.path.exists(os.path.join(service_dir, "AI.db"))):
       
    95         raise SystemExit("Error:\tNeed a valid A/I service directory")
       
    96 
       
    97     files = DataFiles(service_dir = service_dir, image_path = image_path,
       
    98                       database_path = os.path.join(service_dir, "AI.db"),
       
    99                       criteria_path = crit_manifest)
       
   100 
       
   101     return(files)
       
   102 
       
   103 def find_colliding_criteria(files):
       
   104     """
       
   105     Returns: A dictionary of colliding criteria with keys being manifest name
       
   106              and instance tuples and values being the DB column names which
       
   107              collided
       
   108     Args: DataFiles object with a valid _criteria_root and database object
       
   109     Raises: SystemExit if: criteria is not found in database
       
   110                            value is not valid for type (integer and hexadecimal
       
   111                              checks)
       
   112                            range is improper
       
   113     """
       
   114     # define convenience strings:
       
   115     class fields(object):
       
   116         # manifest name is row index 0
       
   117         MANNAME = 0
       
   118         # manifest instance is row index 1
       
   119         MANINST = 1
       
   120         # criteria is row index 2 (when a single valued criteria)
       
   121         CRIT = 2
       
   122         # minimum criteria is row index 2 (when a range valued criteria)
       
   123         MINCRIT = 2
       
   124         # maximum criteria is row index 3 (when a range valued criteria)
       
   125         MAXCRIT = 3
       
   126 
       
   127     # collisions is a dictionary to hold keys of the form (manifest name,
       
   128     # instance) which will point to a comma-separated string of colliding
       
   129     # criteria
       
   130     collisions = dict()
       
   131 
       
   132     # verify each range criteria in the manifest is well formed and collect
       
   133     # collisions with database entries
       
   134     for crit in files.criteria:
       
   135         # gather this criteria's values from the manifest
       
   136         man_criterion = files.criteria[crit]
       
   137 
       
   138         # check "value" criteria here (check the criteria exists in DB, and
       
   139         # then find collisions)
       
   140         if not isinstance(man_criterion, list):
       
   141             # only check criteria in use in the DB
       
   142             if crit not in AIdb.getCriteria(files.database.getQueue(),
       
   143                                             onlyUsed=False, strip=False):
       
   144                 raise SystemExit(_("Error:\tCriteria %s is not a " +
       
   145                                    "valid criteria!") % crit)
       
   146 
       
   147             # get all values in the database for this criteria (and
       
   148             # manifest/instance paris for each value)
       
   149             db_criteria = AIdb.getSpecificCriteria(
       
   150                 files.database.getQueue(), crit, None,
       
   151                 provideManNameAndInstance=True)
       
   152 
       
   153             # will iterate over a list of the form [manName, manInst, crit,
       
   154             # None]
       
   155             for row in db_criteria:
       
   156                 # check if the database and manifest values differ
       
   157                 if(str(row[fields.CRIT]).lower() == str(man_criterion).lower()):
       
   158                     # record manifest name, instance and criteria name
       
   159                     try:
       
   160                         collisions[row[fields.MANNAME],
       
   161                                    row[fields.MANINST]] += crit + ","
       
   162                     except KeyError:
       
   163                         collisions[row[fields.MANNAME],
       
   164                                    row[fields.MANINST]] = crit + ","
       
   165 
       
   166         # This is a range criteria.  (Check that ranges are valid, that
       
   167         # "unbounded" gets set to 0/+inf, ensure the criteria exists
       
   168         # in the DB, then look for collisions.)
       
   169         else:
       
   170             # check for a properly ordered range (with unbounded being 0 or
       
   171             # Inf.) but ensure both are not unbounded
       
   172             if(
       
   173                # Check for a range of -inf to inf -- not a valid range
       
   174                (man_criterion[0] == "unbounded" and
       
   175                 man_criterion[1] == "unbounded"
       
   176                ) or
       
   177                # Check min > max -- range order reversed
       
   178                (
       
   179                 (man_criterion[0] != "unbounded" and
       
   180                  man_criterion[1] != "unbounded"
       
   181                 ) and
       
   182                 (man_criterion[0] > man_criterion[1])
       
   183                )
       
   184               ):
       
   185                 raise SystemExit(_("Error:\tCriteria %s "
       
   186                                    "is not a valid range (MIN > MAX) or "
       
   187                                    "(MIN and MAX unbounded).") % crit)
       
   188 
       
   189             # Clean-up NULL's and changed "unbounded"s to 0 and
       
   190             # really large numbers in case this Python does
       
   191             # not support IEEE754.  Note "unbounded"s are already
       
   192             # converted to lower case during manifest processing.
       
   193             if man_criterion[0] == "unbounded":
       
   194                 man_criterion[0] = "0"
       
   195             if man_criterion[1] == "unbounded":
       
   196                 man_criterion[1] = INFINITY
       
   197             if crit == "mac":
       
   198                 # convert hex mac address (w/o colons) to a number
       
   199                 try:
       
   200                     man_criterion[0] = long(str(man_criterion[0]).upper(), 16)
       
   201                     man_criterion[1] = long(str(man_criterion[1]).upper(), 16)
       
   202                 except ValueError:
       
   203                     raise SystemExit(_("Error:\tCriteria %s "
       
   204                                        "is not a valid hexadecimal value") %
       
   205                                      crit)
       
   206 
       
   207             else:
       
   208                 # this is a decimal value
       
   209                 try:
       
   210                     man_criterion = [long(str(man_criterion[0]).upper()),
       
   211                                      long(str(man_criterion[1]).upper())]
       
   212                 except ValueError:
       
   213                     raise SystemExit(_("Error:\tCriteria %s "
       
   214                                        "is not a valid integer value") % crit)
       
   215 
       
   216             # check to see that this criteria exists in the database columns
       
   217             if ('MIN' + crit not in AIdb.getCriteria(
       
   218                 files.database.getQueue(), onlyUsed=False, strip=False))\
       
   219             and ('MAX' + crit not in AIdb.getCriteria(
       
   220                 files.database.getQueue(), onlyUsed=False,  strip=False)):
       
   221                 raise SystemExit(_("Error:\tCriteria %s is not a " +
       
   222                                    "valid criteria!") % crit)
       
   223             db_criteria = AIdb.getSpecificCriteria(
       
   224                 files.database.getQueue(), 'MIN' + crit, 'MAX' + crit,
       
   225                 provideManNameAndInstance=True)
       
   226 
       
   227             # will iterate over a list of the form [manName, manInst, mincrit,
       
   228             # maxcrit]
       
   229             for row in db_criteria:
       
   230                 # arbitrarily large number in case this Python does
       
   231                 # not support IEEE754
       
   232                 db_criterion = ["0", INFINITY]
       
   233 
       
   234                 # now populate in valid database values (i.e. non-NULL values)
       
   235                 if row[fields.MINCRIT]:
       
   236                     db_criterion[0] = row[fields.MINCRIT]
       
   237                 if row[fields.MAXCRIT]:
       
   238                     db_criterion[1] = row[fields.MAXCRIT]
       
   239                 if crit == "mac":
       
   240                     # use a hexadecimal conversion
       
   241                     db_criterion = [long(str(db_criterion[0]), 16),
       
   242                                     long(str(db_criterion[1]), 16)]
       
   243                 else:
       
   244                     # these are decimal numbers
       
   245                     db_criterion = [long(str(db_criterion[0])),
       
   246                                     long(str(db_criterion[1]))]
       
   247 
       
   248                 # these three criteria can determine if there's a range overlap
       
   249                 if((man_criterion[1] >= db_criterion[0] and
       
   250                    db_criterion[1] >= man_criterion[0]) or
       
   251                    man_criterion[0] == db_criterion[1]):
       
   252                     # range overlap so record the collision
       
   253                     try:
       
   254                         collisions[row[fields.MANNAME],
       
   255                                    row[fields.MANINST]] += "MIN" + crit + ","
       
   256                         collisions[row[fields.MANNAME],
       
   257                                    row[fields.MANINST]] += "MAX" + crit + ","
       
   258                     except KeyError:
       
   259                         collisions[row[fields.MANNAME],
       
   260                                    row[fields.MANINST]] = "MIN" + crit + ","
       
   261                         collisions[row[fields.MANNAME],
       
   262                                    row[fields.MANINST]] += "MAX" + crit + ","
       
   263     return collisions
       
   264 
       
   265 def find_colliding_manifests(files, collisions):
       
   266     """
       
   267     For each manifest/instance pair in collisions check that the manifest
       
   268     criteria diverge (i.e. are not exactly the same) and that the ranges do not
       
   269     collide for ranges.
       
   270     Raises if: a range collides, or if the manifest has the same criteria as a
       
   271     manifest already in the database (SystemExit raised)
       
   272     Returns: Nothing
       
   273     Args: files - DataFiles object with vaild _criteria_root and database
       
   274                   object
       
   275           collisions - a dictionary with collisions, as produced by
       
   276                        find_colliding_criteria()
       
   277     """
       
   278     # check every manifest in collisions to see if manifest collides (either
       
   279     # identical criteria, or overlaping ranges)
       
   280     for man_inst in collisions:
       
   281         # get all criteria from this manifest/instance pair
       
   282         db_criteria = AIdb.getManifestCriteria(man_inst[0],
       
   283                                                man_inst[1],
       
   284                                                files.database.getQueue(),
       
   285                                                humanOutput=True,
       
   286                                                onlyUsed=False)
       
   287 
       
   288         # iterate over every criteria in the database
       
   289         for crit in AIdb.getCriteria(files.database.getQueue(),
       
   290                                      onlyUsed=False, strip=False):
       
   291 
       
   292             # Get the criteria name (i.e. no MIN or MAX)
       
   293             crit_name = crit.replace('MIN', '', 1).replace('MAX', '', 1)
       
   294             # Set man_criterion to the key of the DB criteria or None
       
   295             man_criterion = files.criteria[crit_name]
       
   296             if man_criterion and crit.startswith('MIN'):
       
   297                 man_criterion = man_criterion[0]
       
   298             elif man_criterion and crit.startswith('MAX'):
       
   299                 man_criterion = man_criterion[1]
       
   300 
       
   301             # set the database criteria
       
   302             if db_criteria[str(crit)] == '':
       
   303                 # replace database NULL's with a Python None
       
   304                 db_criterion = None
       
   305             else:
       
   306                 db_criterion = db_criteria[str(crit)]
       
   307 
       
   308             # Replace unbounded's in the criteria (i.e. 0/+inf)
       
   309             # with a Python None.
       
   310             if isinstance(man_criterion, basestring) and \
       
   311                man_criterion == "unbounded":
       
   312                 man_criterion = None
       
   313 
       
   314             # check to determine if this is a range collision by using
       
   315             # collisions and if not are the manifests divergent
       
   316 
       
   317             if((crit.startswith('MIN') and
       
   318                 collisions[man_inst].find(crit + ",") != -1) or
       
   319                (crit.startswith('MAX') and
       
   320                 collisions[man_inst].find(crit + ",") != -1)
       
   321               ):
       
   322                 if (str(db_criterion).lower() != str(man_criterion).lower()):
       
   323                     raise SystemExit(_("Error:\tManifest has a range "
       
   324                                        "collision with manifest:%s/%i"
       
   325                                        "\n\tin criteria: %s!") %
       
   326                                      (man_inst[0], man_inst[1],
       
   327                                       crit.replace('MIN', '', 1).
       
   328                                       replace('MAX', '', 1)))
       
   329 
       
   330             # the range did not collide or this is a single value (if we
       
   331             # differ we can break out knowing we diverge for this
       
   332             # manifest/instance)
       
   333             elif(str(db_criterion).lower() != str(man_criterion).lower()):
       
   334                 # manifests diverge (they don't collide)
       
   335                 break
       
   336 
       
   337         # end of for loop and we never broke out (diverged)
       
   338         else:
       
   339             raise SystemExit(_("Error:\tManifest has same criteria as " +
       
   340                                "manifest:%s/%i!") %
       
   341                              (man_inst[0], man_inst[1]))
       
   342 
       
   343 def insert_SQL(files):
       
   344     """
       
   345     Ensures all data is properly sanitized and formatted, then inserts it into
       
   346     the database
       
   347     Args: None
       
   348     Returns: None
       
   349     """
       
   350     query = "INSERT INTO manifests VALUES("
       
   351 
       
   352     # add the manifest name to the query string
       
   353     query += "'" + AIdb.sanitizeSQL(files.manifest_name) + "',"
       
   354     # check to see if manifest name is alreay in database (affects instance
       
   355     # number)
       
   356     if AIdb.sanitizeSQL(files.manifest_name) in \
       
   357         AIdb.getManNames(files.database.getQueue()):
       
   358             # database already has this manifest name get the number of
       
   359             # instances
       
   360         instance = AIdb.numInstances(AIdb.sanitizeSQL(files.manifest_name),
       
   361                                      files.database.getQueue())
       
   362 
       
   363     # this a new manifest
       
   364     else:
       
   365         instance = 0
       
   366 
       
   367     # actually add the instance to the query string
       
   368     query += str(instance) + ","
       
   369 
       
   370     # we need to fill in the criteria or NULLs for each criteria the database
       
   371     # supports (so iterate over each criteria)
       
   372     for crit in AIdb.getCriteria(files.database.getQueue(),
       
   373                                  onlyUsed=False, strip=False):
       
   374         # for range values trigger on the MAX criteria (skip the MIN's
       
   375         # arbitrary as we handle rows in one pass)
       
   376         if crit.startswith('MIN'):
       
   377             continue
       
   378 
       
   379         # get the values from the manifest
       
   380         values = files.criteria[crit.replace('MAX', '', 1)]
       
   381 
       
   382         # if the values are a list this is a range
       
   383         if isinstance(values, list):
       
   384             for value in values:
       
   385                 # translate "unbounded" to a database NULL
       
   386                 if value == "unbounded":
       
   387                     query += "NULL,"
       
   388                 # we need to deal with mac addresses specially being
       
   389                 # hexadecimal
       
   390                 elif crit.endswith("mac"):
       
   391                     # need to insert with hex operand x'<val>'
       
   392                     # use an upper case string for hex values
       
   393                     query += "x'" + AIdb.sanitizeSQL(str(value).upper())+"',"
       
   394                 else:
       
   395                     query += AIdb.sanitizeSQL(str(value).upper()) + ","
       
   396 
       
   397         # this is a single criteria (not a range)
       
   398         elif isinstance(values, basestring):
       
   399             # translate "unbounded" to a database NULL
       
   400             if values == "unbounded":
       
   401                 query += "NULL,"
       
   402             else:
       
   403                 # use lower case for text strings
       
   404                 query += "'" + AIdb.sanitizeSQL(str(values).lower()) + "',"
       
   405 
       
   406         # the critera manifest didn't specify this criteria so fill in NULLs
       
   407         else:
       
   408             # use the criteria name to determine if this is a range
       
   409             if crit.startswith('MAX'):
       
   410                 query += "NULL,NULL,"
       
   411             # this is a single value
       
   412             else:
       
   413                 query += "NULL,"
       
   414 
       
   415     # strip trailing comma and close parentheses
       
   416     query = query[:-1] + ")"
       
   417 
       
   418     # update the database
       
   419     query = AIdb.DBrequest(query, commit=True)
       
   420     files.database.getQueue().put(query)
       
   421     query.waitAns()
       
   422     # in case there's an error call the response function (which will print the
       
   423     # error)
       
   424     query.getResponse()
       
   425 
       
   426 def do_default(files):
       
   427     """
       
   428     Removes old default.xml after ensuring proper format of new manifest
       
   429     (does not copy new manifest over -- see place_manifest)
       
   430     Args: None
       
   431     Returns: None
       
   432     Raises if: Manifest has criteria, old manifest can not be removed (exits
       
   433                with SystemExit)
       
   434     """
       
   435     # check to see if any criteria is present -- if so, it can not be a default
       
   436     # manifest (as they do not have criteria)
       
   437     if files.criteria:
       
   438         raise SystemExit(_("Error:\tCan not use AI criteria in a default " +
       
   439                            "manifest"))
       
   440     # remove old manifest
       
   441     try:
       
   442         os.remove(os.path.join(files.get_service(), 'AI_data', 'default.xml'))
       
   443     except IOError, e:
       
   444         raise SystemExit(_("Error:\tUnable to remove default.xml:\n\t%s") % e)
       
   445 
       
   446 def place_manifest(files):
       
   447     """
       
   448     Compares src and dst manifests to ensure they are the same; if manifest
       
   449     does not yet exist, copies new manifest into place and sets correct
       
   450     permissions and ownership
       
   451     Args: None
       
   452     Returns: None
       
   453     Raises if: src and dst manifests differ (in MD5 sum), unable to write dst
       
   454                manifest (raises SystemExit -- no clean up of database performed)
       
   455     """
       
   456     manifest_path = os.path.join(files.get_service(), "AI_data",
       
   457                                 files.manifest_name)
       
   458 
       
   459     # if the manifest already exists see if it is different from what was
       
   460     # passed in. If so, warn the user that we're using the existing manifest
       
   461     if os.path.exists(manifest_path):
       
   462         old_manifest = open(manifest_path, "r")
       
   463         existing_MD5 = hashlib.md5("".join(old_manifest.readlines())).digest()
       
   464         old_manifest.close()
       
   465         current_MD5 = hashlib.md5(lxml.etree.tostring(files._AI_root,
       
   466                                  pretty_print=True, encoding=unicode)).digest()
       
   467         if existing_MD5 != current_MD5:
       
   468             raise SystemExit(_("Error:\tNot copying manifest, source and " +
       
   469                                "current versions differ -- criteria in place."))
       
   470 
       
   471     # the manifest does not yet exist so write it out
       
   472     else:
       
   473         try:
       
   474             new_man = open(manifest_path, "w")
       
   475             new_man.writelines('<ai_criteria_manifest>\n')
       
   476             new_man.writelines('\t<ai_embedded_manifest>\n')
       
   477             new_man.writelines(lxml.etree.tostring(
       
   478                                    files._AI_root, pretty_print=True,
       
   479                                    encoding=unicode))
       
   480             new_man.writelines('\t</ai_embedded_manifest>\n')
       
   481             # write out each SMF SC manifest
       
   482             for key in files._smfDict:
       
   483                 new_man.writelines('\t<sc_embedded_manifest name = "%s">\n'%
       
   484                                        key)
       
   485                 new_man.writelines("\t\t<!-- ")
       
   486                 new_man.writelines("<?xml version='1.0'?>\n")
       
   487                 new_man.writelines(lxml.etree.tostring(files._smfDict[key],
       
   488                                        pretty_print=True, encoding=unicode))
       
   489                 new_man.writelines('\t -->\n')
       
   490                 new_man.writelines('\t</sc_embedded_manifest>\n')
       
   491             new_man.writelines('\t</ai_criteria_manifest>\n')
       
   492             new_man.close()
       
   493         except IOError, e:
       
   494             raise SystemExit(_("Error:\tUnable to write to dest. "
       
   495                                "manifest:\n\t%s") % e)
       
   496 
       
   497     # change read for all and write for owner
       
   498     os.chmod(manifest_path, 0644)
       
   499     # change to user/group root (uid/gid 0)
       
   500     os.chown(manifest_path, 0, 0)
       
   501 
       
   502 class DataFiles(object):
       
   503     """
       
   504     Class to contain and work with data files necessary for program
       
   505     """
       
   506     # schema for validating an AI criteria manifest
       
   507     criteriaSchema = "/usr/share/auto_install/criteria_schema.rng"
       
   508     # DTD for validating an SMF SC manifest
       
   509     smfDtd = "/usr/share/lib/xml/dtd/service_bundle.dtd.1"
       
   510 
       
   511 
       
   512     def __init__(self, service_dir = None, image_path = None,
       
   513                  database_path = None, criteria_path = None):
       
   514         """
       
   515         Initialize DataFiles instance. All parameters optional, however, proper
       
   516         setup order asurred, if all data provided upon instantiation.
       
   517         """
       
   518 
       
   519         #
       
   520         # State variables
       
   521         #################
       
   522         #
       
   523 
       
   524         # Variable to cache criteria class for criteria property
       
   525         self._criteria_cache = None
       
   526 
       
   527         #
       
   528         # File system path variables
       
   529         ############################
       
   530         #
       
   531 
       
   532         # Check AI Criteria Schema exists
       
   533         if not os.path.exists(self.criteriaSchema):
       
   534             raise SystemExit(_("Error:\tUnable to find criteria_schema: " +
       
   535                                "%s") % self.criteriaSchema)
       
   536 
       
   537         # Check SC manifest SMF DTD exists
       
   538         if not os.path.exists(self.smfDtd):
       
   539             raise SystemExit(_("Error:\tUnable to find SMF system " +
       
   540                                "configuration DTD: %s") % self.smfDtd)
       
   541 
       
   542         # A/I Manifest Schema
       
   543         self._AIschema = None
       
   544 
       
   545         # Holds path to service directory (i.e. /var/ai/46501)
       
   546         self._service = None
       
   547         if service_dir:
       
   548             self.service = service_dir
       
   549 
       
   550         # Holds path to AI image
       
   551         self._imagepath = None
       
   552         if image_path:
       
   553             self.image_path = image_path
       
   554             # set the AI schema once image_path is set
       
   555             self.set_AI_schema()
       
   556 
       
   557         # Holds database object for criteria database
       
   558         self._db = None
       
   559         if database_path:
       
   560             # Set Database Path and Open SQLite3 Object
       
   561             self.database = database_path
       
   562             # verify the database's table/column structure (or exit if errors)
       
   563             self.database.verifyDBStructure()
       
   564 
       
   565         #
       
   566         # XML DOM variables
       
   567         ###################
       
   568         #
       
   569 
       
   570         #
       
   571         # Criteria manifest setup
       
   572         #
       
   573 
       
   574         # Holds DOM for criteria manifest
       
   575         self._criteria_root = None
       
   576 
       
   577         # Holds path for criteria manifest
       
   578         self.criteria_path = criteria_path
       
   579         # find SC manifests from the criteria manifest and validate according
       
   580         # to the SMF DTD (exit if errors)
       
   581         if criteria_path:
       
   582             # sets _criteria_root DOM
       
   583             self.verifyCriteria()
       
   584 
       
   585         #
       
   586         # SC manifest setup
       
   587         #
       
   588 
       
   589         # Holds DOMs for SC manifests
       
   590         self._smfDict = dict()
       
   591 
       
   592         # if we were provided a criteria manifest, look for a SC manifests
       
   593         # specified by the criteria manifest
       
   594         if self._criteria_root:
       
   595             # sets _smfDict DOMs
       
   596             self.find_SC_from_crit_man()
       
   597 
       
   598         #
       
   599         # AI manifest setup
       
   600         #
       
   601 
       
   602         # Holds DOM for AI manifest
       
   603         self._AI_root = None
       
   604 
       
   605         # Holds path to AI manifest being published (may not be set if an
       
   606         # embedded manifest)
       
   607         self._manifest = None
       
   608 
       
   609         # if we were provided a criteria manifest, look for an A/I manifest
       
   610         # specified by the criteria manifest
       
   611         if self._criteria_root:
       
   612             # this will set _manifest to be the AI manifest path (if a file),
       
   613             # set _AI_root to the correct location in the criteria DOM (if
       
   614             # embedded), or exit (if unable to find an AI manifest)
       
   615             self.find_AI_from_criteria()
       
   616             # this will verify the _AI_root DOM and exit on error
       
   617             self.verify_AI_manifest()
       
   618 
       
   619     # overload the _criteria class to be a list with a special get_item to act
       
   620     # like a dictionary
       
   621     class _criteria(list):
       
   622         """
       
   623         Wrap list class to provide lookups in the criteria file when
       
   624         requested
       
   625         """
       
   626         def __init__(self, criteria_root):
       
   627             # store the criteria manifest DOM root
       
   628             self._criteria_root = criteria_root
       
   629             # call the _init_() for the list class with a generator provided by
       
   630             # find_criteria() to populate this _criteria() instance.
       
   631             super(DataFiles._criteria, self).__init__(self.find_criteria())
       
   632 
       
   633         def __getitem__(self, key):
       
   634             """
       
   635             Look up a requested criteria (akin to dictionary access) but for an
       
   636             uninitialized key will not raise an exception but return None)
       
   637             """
       
   638             return self.get_criterion(key)
       
   639 
       
   640         def find_criteria(self):
       
   641             """
       
   642             Find criteria from the criteria manifest
       
   643             Returns: A generator providing all criteria name attributes from
       
   644                      <ai_criteria> tags
       
   645             """
       
   646             root = self._criteria_root.findall(".//ai_criteria")
       
   647 
       
   648             # actually find criteria
       
   649             for tag in root:
       
   650                 for child in tag.getchildren():
       
   651                     if (child.tag == "range" or child.tag == "value") and \
       
   652                         child.text is not None:
       
   653                         # criteria names are lower case
       
   654                         yield tag.attrib['name'].lower()
       
   655                     # should not happen according to schema
       
   656                     else:
       
   657                         raise AssertionError(_(
       
   658                             "Criteria contains no values"))
       
   659 
       
   660         def get_criterion(self, criterion):
       
   661             """
       
   662             Return criterion out of the criteria manifest
       
   663             Returns: A list for range criterion with a min and max entry
       
   664                      A string for value criterion
       
   665             """
       
   666             source = self._criteria_root
       
   667             for tag in source.getiterator('ai_criteria'):
       
   668                 crit = tag.get('name')
       
   669                 # compare criteria name case-insensitive
       
   670                 if crit.lower() == criterion.lower():
       
   671                     for child in tag.getchildren():
       
   672                         if child.tag == "range":
       
   673                             # this is a range response (split on white space)
       
   674                             return child.text.split()
       
   675                         elif child.tag == "value":
       
   676                             # this is a value response (strip white space)
       
   677                             return child.text.strip()
       
   678                         # should not happen according to schema
       
   679                         elif child.text is None:
       
   680                             raise AssertionError(_(
       
   681                                 "Criteria contains no values"))
       
   682             return None
       
   683 
       
   684         # disable trying to update criteria
       
   685         __setitem__ = None
       
   686         __delitem__ = None
       
   687 
       
   688     @property
       
   689     def criteria(self):
       
   690         """
       
   691         Function to provide access to criteria class (and provide caching of
       
   692         class created)
       
   693         Returns: A criteria instance
       
   694         """
       
   695         # if we don't have a cached _criteria class, create one and update the
       
   696         # cache
       
   697         if not self._criteria_cache:
       
   698             self._criteria_cache = self._criteria(self._criteria_root)
       
   699         # now return cached _criteria class
       
   700         return self._criteria_cache
       
   701 
       
   702     def open_database(self, db_file):
       
   703         """
       
   704         Sets self._db (opens database object) and errors if already set or file
       
   705         does not yet exist
       
   706         Args: A file path to an SQLite3 database
       
   707         Raises: SystemExit if path does not exist,
       
   708                 AssertionError if self._db is already set
       
   709         Returns: Nothing
       
   710         """
       
   711         if not os.path.exists(db_file):
       
   712             raise SystemExit(_("Error:\tFile %s is not a valid database "
       
   713                                "file") % db_file)
       
   714         elif self._db is None:
       
   715             self._db = AIdb.DB(db_file, commit=True)
       
   716         else:
       
   717             raise AssertionError('Opening database when already open!')
       
   718 
       
   719     def get_database(self):
       
   720         """
       
   721         Returns self._db (database object) and errors if not set
       
   722         Raises: AssertionError if self._db is not yet set
       
   723         Returns: SQLite3 database object
       
   724         """
       
   725         if isinstance(self._db, AIdb.DB):
       
   726             return(self._db)
       
   727         else:
       
   728             raise AssertionError('Database not yet open!')
       
   729 
       
   730     database = property(get_database, open_database, None,
       
   731                         "Holds database object for criteria database")
       
   732 
       
   733     def get_service(self):
       
   734         """
       
   735         Returns self._service and errors if not yet set
       
   736         Raises: AssertionError if self._service is not yet set
       
   737         Returns: String object
       
   738         """
       
   739         if self._service is not None:
       
   740             return(self._service)
       
   741         else:
       
   742             raise AssertionError('Service not yet set!')
       
   743 
       
   744     def set_service(self, serv=None):
       
   745         """
       
   746         Sets self._service and errors if already set
       
   747         Args: A string path to an AI service directory
       
   748         Raises: SystemExit if path does not exist,
       
   749                 AssertionError if self._service is already set
       
   750         Returns: Nothing
       
   751         """
       
   752         if not os.path.isdir(serv):
       
   753             raise SystemExit(_("Error:\tDirectory %s is not a valid AI "
       
   754                                "directory") % db_file)
       
   755         elif self._service is None:
       
   756             self._service = os.path.abspath(serv)
       
   757         else:
       
   758             raise AssertionError('Setting service when already set!')
       
   759 
       
   760     service = property(get_service, set_service, None,
       
   761                        "Holds path to service directory (i.e. /var/ai/46501)")
       
   762 
       
   763     def find_SC_from_crit_man(self):
       
   764         """
       
   765         Find SC manifests as referenced in the criteria manifest
       
   766         Preconditions: self._criteria_root is a valid XML DOM
       
   767         Postconditions: self._smfDict will be a dictionary containing all
       
   768                         SC manifest DOMs
       
   769         Raises: SystemExit for XML processing errors
       
   770                            for two SC manifests named the same
       
   771                 AssertionError if _critteria_root not set
       
   772         Args: None
       
   773         Returns: None
       
   774         """
       
   775         if self._criteria_root is None:
       
   776             raise AssertionError(_("Error:\t _criteria_root not set!"))
       
   777         try:
       
   778             root = self._criteria_root.iterfind(".//sc_manifest_file")
       
   779         except lxml.etree.LxmlError, e:
       
   780             raise SystemExit(_("Error:\tCriteria manifest error:%s") % e)
       
   781         # for each SC manifest file: get the URI and verify it, adding it to the
       
   782         # dictionary of SMF SC manifests (this means we can support a criteria
       
   783         # manifest with multiple SC manifests embedded or referenced)
       
   784         for SC_man in root:
       
   785             if SC_man.attrib['name'] in self._smfDict:
       
   786                 raise SystemExit(_("Error:\tTwo SC manfiests with name %s") %
       
   787                                    SC_man.attrib['name'])
       
   788             # if this is an absolute path just hand it off
       
   789             if os.path.isabs(str(SC_man.attrib['URI'])):
       
   790                 self._smfDict[SC_man.attrib['name']] = \
       
   791                     self.verify_SC_manifest(SC_man.attrib['URI'])
       
   792             # this is not an absolute path - make it one
       
   793             else:
       
   794                 self._smfDict[SC_man.attrib['name']] = \
       
   795                     self.verify_SC_manifest(os.path.join(os.path.dirname(
       
   796                                           self.criteria_path),
       
   797                                           SC_man.attrib['URI']))
       
   798         try:
       
   799             root = self._criteria_root.iterfind(".//sc_embedded_manifest")
       
   800         except lxml.etree.LxmlError, e:
       
   801             raise SystemExit(_("Error:\tCriteria manifest error:%s") % e)
       
   802         # for each SC manifest embedded: verify it, adding it to the
       
   803         # dictionary of SMF SC manifests
       
   804         for SC_man in root:
       
   805             # strip the comments off the SC manifest
       
   806             xml_data = lxml.etree.tostring(SC_man.getchildren()[0])
       
   807             xml_data = xml_data.replace("<!-- ", "").replace(" -->", "")
       
   808             xml_data = StringIO.StringIO(xml_data)
       
   809             # parse and read in the SC manifest
       
   810             self._smfDict[SC_man.attrib['name']] = \
       
   811                 self.verify_SC_manifest(xml_data, name=SC_man.attrib['name'])
       
   812 
       
   813     def find_AI_from_criteria(self):
       
   814         """
       
   815         Find A/I manifest as referenced or embedded in criteria manifest
       
   816         Preconditions: self._criteria_root is a valid XML DOM
       
   817         Postconditions: self.manifest_path will be set if using a free-standing
       
   818                         AI manifest otherwise self._AI_root will eb set to a
       
   819                         valid XML DOM for the AI manifest
       
   820         Raises: SystemExit for XML processing errors
       
   821                            for no ai_manifest_file specification
       
   822                 AssertionError if _critteria_root not set
       
   823         """
       
   824         if self._criteria_root is None:
       
   825             raise AssertionError(_("Error:\t_criteria_root not set!"))
       
   826         try:
       
   827             root = self._criteria_root.find(".//ai_manifest_file")
       
   828         except lxml.etree.LxmlError, e:
       
   829             raise SystemExit(_("Error:\tCriteria manifest error:%s") % e)
       
   830         if not isinstance(root, lxml.etree._Element):
       
   831             try:
       
   832                 root = self._criteria_root.find(".//ai_embedded_manifest")
       
   833             except lxml.etree.LxmlError, e:
       
   834                 raise SystemExit(_("Error:\tCriteria manifest error:%s") % e)
       
   835             if not isinstance(root, lxml.etree._Element):
       
   836                 raise SystemExit(_("Error:\tNo <ai_manifest_file> or " +
       
   837                                    "<ai_embedded_manifest> element in "
       
   838                                    "criteria manifest."))
       
   839         try:
       
   840             root.attrib['URI']
       
   841         except KeyError:
       
   842             self._AI_root = \
       
   843                 lxml.etree.tostring(root.find(".//ai_manifest"))
       
   844             return
       
   845         if os.path.isabs(root.attrib['URI']):
       
   846             self.manifest_path = root.attrib['URI']
       
   847         else:
       
   848             # if we do not have an absolute path try using the criteria
       
   849             # manifest's location for a base
       
   850             self.manifest_path = \
       
   851                 os.path.join(os.path.dirname(self.criteria_path),
       
   852                              root.attrib['URI'])
       
   853     @property
       
   854     def AI_schema(self):
       
   855         """
       
   856         Returns self._AIschema and errors if not yet set
       
   857         Args: None
       
   858         Raises: AssertionError if self._AIschema is not yet set
       
   859         Returns: String object
       
   860         """
       
   861         if self._AIschema is not None:
       
   862             return (self._AIschema)
       
   863         else:
       
   864             raise AssertionError('AIschema not set')
       
   865 
       
   866     def set_AI_schema(self):
       
   867         """
       
   868         Sets self._AIschema and errors if imagepath not yet set.
       
   869         Args: None
       
   870         Raises: SystemExit if unable to find a valid AI schema
       
   871         Returns: None
       
   872         """
       
   873         if os.path.exists(os.path.join(self.image_path,
       
   874                                        IMG_AI_MANIFEST_SCHEMA)):
       
   875             self._AIschema = os.path.join(self.image_path,
       
   876                                           IMG_AI_MANIFEST_SCHEMA)
       
   877         else:
       
   878             if os.path.exists(SYS_AI_MANIFEST_SCHEMA):
       
   879                 self._AIschema = SYS_AI_MANIFEST_SCHEMA
       
   880                 print (_("Warning: Using A/I manifest schema <%s>\n") %
       
   881                         self._AIschema)
       
   882             else:
       
   883                 raise SystemExit(_("Error:\tUnable to find an A/I schema!"))
       
   884 
       
   885     def get_image_path(self):
       
   886         """
       
   887         Returns self._imagepath and errors if not set
       
   888         Raises: AssertionError if self._imagepath is not yet set
       
   889         Returns: String object
       
   890         """
       
   891         if self._imagepath is not None:
       
   892             return (self._imagepath)
       
   893         else:
       
   894             raise AssertionError('Imagepath not set')
       
   895 
       
   896     def set_image_path(self, imagepath):
       
   897         """
       
   898         Sets self._imagepath but exits if already set or not a directory
       
   899         Args: image path to a valid AI image
       
   900         Raises: SystemExit if image path provided is not a directory
       
   901                 AssertionError if image path is already set
       
   902         Returns: None
       
   903         """
       
   904         if not os.path.isdir(imagepath):
       
   905             raise SystemExit(_("Error:\tInvalid imagepath " +
       
   906                                "directory: %s") % imagepath)
       
   907         if self._imagepath is None:
       
   908             self._imagepath = os.path.abspath(imagepath)
       
   909         else:
       
   910             raise AssertionError('imagepath already set')
       
   911 
       
   912     image_path = property(get_image_path, set_image_path, None,
       
   913                         "Holds path to service's AI image")
       
   914 
       
   915     def get_manifest_path(self):
       
   916         """
       
   917         Returns self._manifest and errors if not set
       
   918         Raises: AssertionError if self._manifest is not yet set
       
   919         Returns: String object path to AI manifest
       
   920         """
       
   921         if self._manifest is not None:
       
   922             return(self._manifest)
       
   923         else:
       
   924             raise AssertionError('Manifest path not yet set!')
       
   925 
       
   926     def set_manifest_path(self, mani=None):
       
   927         """
       
   928         Sets self._manifest and errors if already set
       
   929         Args: path to an AI manifest
       
   930         Raises: AssertionError if manifest is already set
       
   931         Returns: None
       
   932         """
       
   933         if self._manifest is None:
       
   934             self._manifest = os.path.abspath(mani)
       
   935         else:
       
   936             raise AssertionError('Setting manifest when already set!')
       
   937 
       
   938     manifest_path = property(get_manifest_path, set_manifest_path, None,
       
   939                              "Holds path to AI manifest being published")
       
   940     @property
       
   941     def manifest_name(self):
       
   942         """
       
   943         Returns: manifest name as defined in the A/I manifest (ensuring .xml is
       
   944                  applied to the string)
       
   945         Raises: SystemExit if <ai_manifest> tag can not be found
       
   946         """
       
   947         if self._AI_root.getroot().tag == "ai_manifest":
       
   948             name = self._AI_root.getroot().attrib['name']
       
   949         else:
       
   950             raise SystemExit(_("Error:\tCan not find <ai_manifest> tag!"))
       
   951         # everywhere we expect manifest names to be file names so ensure
       
   952         # the name matches
       
   953         if not name.endswith('.xml'):
       
   954             name += ".xml"
       
   955         return name
       
   956 
       
   957     def verify_AI_manifest(self):
       
   958         """
       
   959         Used for verifying and loading AI manifest as defined by
       
   960             DataFiles._AIschema.
       
   961         Args: None.
       
   962         Postconditions: Sets DataFiles._AI_root on success to a XML DOM
       
   963         Raises: SystemExit on file open error or validation error.
       
   964         """
       
   965         try:
       
   966             schema = file(self.AI_schema, 'r')
       
   967         except IOError:
       
   968             raise SystemExit(_("Error:\tCan not open: %s ") %
       
   969                                self.AI_schema)
       
   970         try:
       
   971             xml_data = file(self.manifest_path, 'r')
       
   972         except IOError:
       
   973             raise SystemExit(_("Error:\tCan not open: %s ") %
       
   974                                self.manifest_path)
       
   975         except AssertionError:
       
   976             # manifest path will be unset if we're not using a separate file for
       
   977             # A/I manifest so we must emulate a file
       
   978             xml_data = StringIO.StringIO(self._AI_root)
       
   979         self._AI_root = verifyXML.verifyRelaxNGManifest(schema, xml_data)
       
   980         if isinstance(self._AI_root, lxml.etree._LogEntry):
       
   981             # catch if we area not using a manifest we can name with
       
   982             # manifest_path
       
   983             try:
       
   984                 raise SystemExit(_("Error:\tFile %s failed validation:\n\t%s") %
       
   985                                  (os.path.basename(self.manifest_path),
       
   986                                   self._AI_root.message))
       
   987             # manifest_path will throw an AssertionError if it does not have
       
   988             # a path use a different error message
       
   989             except AssertionError:
       
   990                 raise SystemExit(_("Error:\tA/I manifest failed validation:"
       
   991                                    "\n\t%s") % self._AI_root.message)
       
   992 
       
   993 
       
   994     def verify_SC_manifest(self, data, name=None):
       
   995         """
       
   996         Used for verifying and loading SC manifest
       
   997         Args:    data - file path, or StringIO object.
       
   998                  name - Optionally, takes a name to provide error output,
       
   999                         as a StringIO object will not have a file path to
       
  1000                         provide.
       
  1001         Returns: Provide an XML DOM for the SC manifest
       
  1002         Raises:  SystemExit on validation or file open error.
       
  1003         """
       
  1004         if not isinstance(data, StringIO.StringIO):
       
  1005             try:
       
  1006                 data = file(data, 'r')
       
  1007             except IOError:
       
  1008                 if name is None:
       
  1009                     raise SystemExit(_("Error:\tCan not open: %s") % data)
       
  1010                 else:
       
  1011                     raise SystemExit(_("Error:\tCan not open: %s") % name)
       
  1012         xml_root = verifyXML.verifyDTDManifest(data, self.smfDtd)
       
  1013         if isinstance(xml_root, list):
       
  1014             if not isinstance(data, StringIO.StringIO):
       
  1015                 print >> sys.stderr, (_("Error:\tFile %s failed validation:") %
       
  1016                                       data.name)
       
  1017             else:
       
  1018                 print >> sys.stderr, (_("Error:\tSC Manifest %s failed "
       
  1019                                         "validation:") % name)
       
  1020             for err in xml_root:
       
  1021                 print >> sys.stderr, err
       
  1022             raise SystemExit()
       
  1023         return(xml_root)
       
  1024 
       
  1025     def verifyCriteria(self):
       
  1026         """
       
  1027         Used for verifying and loading criteria XML
       
  1028         Raises SystemExit:
       
  1029         *if the schema does not open
       
  1030         *if the XML file does not open
       
  1031         *if the XML is invalid according to the schema
       
  1032         Postconditions: self._criteria_root is a valid XML DOM of the criteria
       
  1033                         manifest and all MAC and IPv4 values are formatted
       
  1034                         according to verifyXML.prepValuesAndRanges()
       
  1035         """
       
  1036         try:
       
  1037             schema = file(self.criteriaSchema, 'r')
       
  1038         except IOError:
       
  1039             raise SystemExit(_("Error:\tCan not open: %s") %
       
  1040                              self.criteriaSchema)
       
  1041         try:
       
  1042             file(self.criteria_path, 'r')
       
  1043         except IOError:
       
  1044             raise SystemExit(_("Error:\tCan not open: %s") % self.criteria_path)
       
  1045         self._criteria_root = (verifyXML.verifyRelaxNGManifest(schema,
       
  1046                               self.criteria_path))
       
  1047         if isinstance(self._criteria_root, lxml.etree._LogEntry):
       
  1048             raise SystemExit(_("Error:\tFile %s failed validation:\n\t%s") %
       
  1049                              (self.criteria_path, self._criteria_root.message))
       
  1050         try:
       
  1051             verifyXML.prepValuesAndRanges(self._criteria_root,
       
  1052                                           self.database)
       
  1053         except ValueError, e:
       
  1054             raise SystemExit(_("Error:\tCriteria manifest error: %s") % e)
       
  1055 
       
  1056 
       
  1057 if __name__ == '__main__':
       
  1058     gettext.install("ai", "/usr/lib/locale")
       
  1059 
       
  1060     # check that we are root
       
  1061     if os.geteuid() != 0:
       
  1062         raise SystemExit(_("Error:\tNeed root privileges to execute"))
       
  1063 
       
  1064     # load in all the options and file data
       
  1065     data = parse_options()
       
  1066 
       
  1067     # if we have a default manifest do default manifest handling
       
  1068     if data.manifest_name == "default.xml":
       
  1069         do_default(data)
       
  1070 
       
  1071     # if we have a non-default manifest first ensure it is a unique criteria
       
  1072     # set and then, if unique, add the manifest to the criteria database
       
  1073     else:
       
  1074         # if we have a None criteria from the criteria list then the manifest
       
  1075         # has no criteria which is illegal for a non-default manifest
       
  1076         if not data.criteria:
       
  1077             raise SystemExit(_("Error:\tNo criteria found " +
       
  1078                                "in non-default manifest -- "
       
  1079                                "at least one criterion needed!"))
       
  1080         find_colliding_manifests(data, find_colliding_criteria(data))
       
  1081         insert_SQL(data)
       
  1082 
       
  1083     # move the manifest into place
       
  1084     place_manifest(data)