def test_mem_is_range(self): '''Verify mem returns true for range''' is_range_criteria = AIdb.isRangeCriteria(self.files.database, "mem") self.assertTrue(is_range_criteria)
def set_criteria(criteria, iname, dbn, table, append=False): """ Set a manifest's record in the criteria database with the criteria provided. If append is True -- append ones that aren't already set for the manifest, and replace ones that are. if append is False -- completely remove all criteria already set for the manifest, and use only the criteria specified. """ # Build a list of criteria nvpairs to update nvpairs = list() # we need to fill in the criteria or NULLs for each criteria the database # supports (so iterate over each criteria) for crit in AIdb.getCriteria(dbn.getQueue(), table=table, onlyUsed=False, strip=True): # Determine if this crit is a range criteria or not. is_range_crit = AIdb.isRangeCriteria(dbn.getQueue(), crit) # Get the value from the manifest values = criteria[crit] # the criteria manifest didn't specify this criteria if values is None: # If we not appending criteria, then we must write in NULLs # for this criteria since we're removing all criteria not # specified. if not append: # if the criteria we're processing is a range criteria, fill in # NULL for two columns, MINcrit and MAXcrit if is_range_crit: nvpairs.append("MIN" + crit + "=NULL") nvpairs.append("MAX" + crit + "=NULL") # this is a single value else: nvpairs.append(crit + "=NULL") # Else if this is a value criteria (not a range), insert the # value as a space-separated list of values in case a list of # values have been given. elif not is_range_crit: nvstr = crit + "='" + AIdb.sanitizeSQL(" ".join(values)) + "'" nvpairs.append(nvstr) # Else the values are a list this is a range criteria else: # Set the MIN column for this range criteria nvpairs.append("MIN" + crit + "=" + AIdb.format_value(crit, values[0])) # Set the MAX column for this range criteria nvpairs.append("MAX" + crit + "=" + AIdb.format_value(crit, values[1])) query = "UPDATE " + table + " SET " + ",".join(nvpairs) + \ " WHERE name='" + iname + "'" # update the DB query = AIdb.DBrequest(query, commit=True) dbn.getQueue().put(query) query.waitAns() # in case there's an error call the response function (which # will print the error) query.getResponse()
def test_arch_not_range(self): '''Verify arch returns false for range''' is_range_criteria = AIdb.isRangeCriteria(self.files.database, "arch") self.assertFalse(is_range_criteria)
def test_ipv4_is_range(self): '''Verify ipv4 returns true for range''' is_range_criteria = AIdb.isRangeCriteria(self.files.database, "ipv4") self.assertTrue(is_range_criteria)
def sql_values_from_criteria(criteria, queue, table, gbl=False): ''' Given a criteria dictionary, for the indicated DB table and queue, return a tuple composed of lists whose elements can be used to construct SQLite clauses. If gbl is true, build a clause that will affect all database records if criteria is missing - a global effect. Args: criteria - criteria dictionary queue - database queue table - database table gbl - if True, global Returns: a tuple for SQLite clauses respectively: WHERE, INTO, VALUES ''' where = list() # for WHERE clause intol = list() # for INTO clause vals = list() # for VALUES clause for crit in AIdb.getCriteria(queue, table, onlyUsed=False, strip=True): # Determine if this crit is a range criteria or not. is_range_crit = AIdb.isRangeCriteria(queue, crit, table) # Get the value from the manifest values = criteria[crit] # the critera manifest didn't specify this criteria if values is None: # if the criteria we're processing is a range criteria, fill in # NULL for two columns, MINcrit and MAXcrit vals += ["NULL"] if is_range_crit: where += ["MIN" + crit + " IS NULL"] where += ["MAX" + crit + " IS NULL"] intol += ["MIN" + crit] intol += ["MAX" + crit] vals += ["NULL"] # this is a single value else: where += [crit + " IS NULL"] intol += [crit] # This is a value criteria (not a range). 'values' is a list # with one or more items. elif not is_range_crit: intol += [crit] val = AIdb.format_value(crit, " ".join(values)) where += [crit + "=" + val] vals += [val] # Else this is a range criteria. 'values' is a two-item list else: # Set the MIN column for this range criteria if values[0] == 'unbounded': if not gbl: where += ["MIN" + crit + " IS NULL"] intol += ["MIN" + crit] vals += ['NULL'] else: intol += ["MIN" + crit] if crit == 'mac': val = AIdb.format_value(crit, verifyXML.checkMAC(values[0])).upper() where += ["HEX(MIN" + crit + ")<=HEX(" + val + ")"] else: val = AIdb.format_value(crit, values[0]).lower() where += ["MIN" + crit + "<=" + val] vals += [val] # Set the MAX column for this range criteria if values[1] == 'unbounded': if not gbl: where += ["MAX" + crit + " IS NULL"] intol += ["MAX" + crit] vals += ['NULL'] else: intol += ["MAX" + crit] if crit == 'mac': val = AIdb.format_value(crit, verifyXML.checkMAC(values[1])).upper() where += ["HEX(MAX" + crit + ")>=HEX(" + val + ")"] else: val = AIdb.format_value(crit, values[1]).lower() where += ["MAX" + crit + ">=" + val] vals += [val] return where, intol, vals
def find_colliding_criteria(criteria, db, exclude_manifests=None): """ Returns: A dictionary of colliding criteria with keys being manifest name and instance tuples and values being the DB column names which collided Args: criteria - Criteria object holding the criteria that is to be added/set for a manifest. db - AI_database object for the install service. exclude_manifests -A list of manifest names from DB to ignore. This arg is passed in when we're calling this function to find criteria collisions for an already published manifest. Raises: SystemExit if: criteria is not found in database value is not valid for type (integer and hexadecimal checks) range is improper """ class Fields(object): """ Define convenience indexes """ # manifest name is row index 0 MANNAME = 0 # manifest instance is row index 1 MANINST = 1 # criteria is row index 2 (when a single valued criteria) CRIT = 2 # minimum criteria is row index 2 (when a range valued criteria) MINCRIT = 2 # maximum criteria is row index 3 (when a range valued criteria) MAXCRIT = 3 # collisions is a dictionary to hold keys of the form (manifest name, # instance) which will point to a comma-separated string of colliding # criteria collisions = dict() # verify each range criteria in the manifest is well formed and collect # collisions with database entries for crit in criteria: # gather this criteria's values from the manifest man_criterion = criteria[crit] # Determine if this crit is a range criteria or not. is_range_crit = AIdb.isRangeCriteria(db.getQueue(), crit, AIdb.MANIFESTS_TABLE) # Process "value" criteria here (check if the criteria exists in # DB, and then find collisions) if not is_range_crit: # only check criteria in use in the DB if crit not in AIdb.getCriteria(db.getQueue(), onlyUsed=False, strip=False): raise SystemExit( _("Error:\tCriteria %s is not a " + "valid criteria!") % crit) # get all values in the database for this criteria (and # manifest/instance pairs for each value) db_criteria = AIdb.getSpecificCriteria( db.getQueue(), crit, provideManNameAndInstance=True, excludeManifests=exclude_manifests) # will iterate over a list of the form [manName, manInst, crit, # None] for row in db_criteria: # check if a value in the list of values to be added is equal # to a value in the list of values for this criteria for this # row for value in man_criterion: if AIdb.is_in_list(crit, value, str(row[Fields.CRIT]), None): # record manifest name, instance and criteria name try: collisions[row[Fields.MANNAME], row[Fields.MANINST]] += crit + "," except KeyError: collisions[row[Fields.MANNAME], row[Fields.MANINST]] = crit + "," # This is a range criteria. (Check that ranges are valid, that # "unbounded" gets set to 0/+inf, ensure the criteria exists # in the DB, then look for collisions.) else: # Clean-up NULL's and change "unbounded"s to 0 and # really large numbers in case this Python does # not support IEEE754. Note "unbounded"s are already # converted to lower case during manifest processing. if man_criterion[0] == "unbounded": man_criterion[0] = "0" if man_criterion[1] == "unbounded": man_criterion[1] = INFINITY if crit == "mac": # convert hex mac address (w/o colons) to a number try: man_criterion[0] = long(str(man_criterion[0]).upper(), 16) man_criterion[1] = long(str(man_criterion[1]).upper(), 16) except ValueError: raise SystemExit( _("Error:\tCriteria %s " "is not a valid hexadecimal value") % crit) else: # this is a decimal value try: man_criterion = [ long(str(man_criterion[0]).upper()), long(str(man_criterion[1]).upper()) ] except ValueError: raise SystemExit( _("Error:\tCriteria %s " "is not a valid integer value") % crit) # Check for a properly ordered range (with unbounded being 0 or # Inf.) but ensure both are not unbounded. # Check for: # a range of zero to inf -- not a valid range # and # min > max -- range order reversed # if (man_criterion[0] == 0 and man_criterion[1] == long(INFINITY)): raise SystemExit( _("Error:\tCriteria %s is not a valid range, " "MIN and MAX unbounded.") % crit) if ((man_criterion[0] != 0 and man_criterion[1] != long(INFINITY)) and (long(man_criterion[0]) > long(man_criterion[1]))): raise SystemExit( _("Error:\tCriteria %s is not a valid range, " "MIN > MAX.") % crit) # check to see that this criteria exists in the database columns man_crit = AIdb.getCriteria(db.getQueue(), onlyUsed=False, strip=False) if 'MIN' + crit not in man_crit and 'MAX' + crit not in man_crit: raise SystemExit( _("Error:\tCriteria %s is not a " "valid criteria!") % crit) db_criteria = AIdb.getSpecificCriteria( db.getQueue(), 'MIN' + crit, 'MAX' + crit, provideManNameAndInstance=True, excludeManifests=exclude_manifests) # will iterate over a list of the form [manName, manInst, mincrit, # maxcrit] for row in db_criteria: # arbitrarily large number in case this Python does # not support IEEE754 db_criterion = ["0", INFINITY] # now populate in valid database values (i.e. non-NULL values) if row[Fields.MINCRIT]: db_criterion[0] = row[Fields.MINCRIT] if row[Fields.MAXCRIT]: db_criterion[1] = row[Fields.MAXCRIT] if crit == "mac": # use a hexadecimal conversion db_criterion = [ long(str(db_criterion[0]), 16), long(str(db_criterion[1]), 16) ] else: # these are decimal numbers db_criterion = [ long(str(db_criterion[0])), long(str(db_criterion[1])) ] # these three criteria can determine if there's a range overlap if ((man_criterion[1] >= db_criterion[0] and db_criterion[1] >= man_criterion[0]) or man_criterion[0] == db_criterion[1]): # range overlap so record the collision try: collisions[row[Fields.MANNAME], row[Fields.MANINST]] += "MIN" + crit + "," collisions[row[Fields.MANNAME], row[Fields.MANINST]] += "MAX" + crit + "," except KeyError: collisions[row[Fields.MANNAME], row[Fields.MANINST]] = "MIN" + crit + "," collisions[row[Fields.MANNAME], row[Fields.MANINST]] += "MAX" + crit + "," return collisions
def sql_values_from_criteria(criteria, queue, table, gbl=False): ''' Given a criteria dictionary, for the indicated DB table and queue, return a tuple composed of lists whose elements can be used to construct SQLite clauses. If gbl is true, build a clause that will affect all database records if criteria is missing - a global effect. Args: criteria - criteria dictionary queue - database queue table - database table gbl - if True, global Returns: a tuple for SQLite clauses respectively: WHERE, INTO, VALUES ''' where = list() # for WHERE clause intol = list() # for INTO clause vals = list() # for VALUES clause for crit in AIdb.getCriteria(queue, table, onlyUsed=False, strip=True): # Determine if this crit is a range criteria or not. is_range_crit = AIdb.isRangeCriteria(queue, crit, table) # Get the value from the manifest values = criteria[crit] # the critera manifest didn't specify this criteria if values is None: # if the criteria we're processing is a range criteria, fill in # NULL for two columns, MINcrit and MAXcrit vals += ["NULL"] if is_range_crit: where += ["MIN" + crit + " IS NULL"] where += ["MAX" + crit + " IS NULL"] intol += ["MIN" + crit] intol += ["MAX" + crit] vals += ["NULL"] # this is a single value else: where += [crit + " IS NULL"] intol += [crit] # This is a value criteria (not a range). 'values' is a list # with one or more items. elif not is_range_crit: intol += [crit] val = AIdb.format_value(crit, " ".join(values)) where += [crit + "=" + val] vals += [val] # Else this is a range criteria. 'values' is a two-item list else: # Set the MIN column for this range criteria if values[0] == 'unbounded': if not gbl: where += ["MIN" + crit + " IS NULL"] intol += ["MIN" + crit] vals += ['NULL'] else: intol += ["MIN" + crit] if crit == 'mac': val = AIdb.format_value(crit, verifyXML.checkMAC( values[0])).upper() where += ["HEX(MIN" + crit + ")<=HEX(" + val + ")"] else: val = AIdb.format_value(crit, values[0]).lower() where += ["MIN" + crit + "<=" + val] vals += [val] # Set the MAX column for this range criteria if values[1] == 'unbounded': if not gbl: where += ["MAX" + crit + " IS NULL"] intol += ["MAX" + crit] vals += ['NULL'] else: intol += ["MAX" + crit] if crit == 'mac': val = AIdb.format_value(crit, verifyXML.checkMAC( values[1])).upper() where += ["HEX(MAX" + crit + ")>=HEX(" + val + ")"] else: val = AIdb.format_value(crit, values[1]).lower() where += ["MAX" + crit + ">=" + val] vals += [val] return where, intol, vals
def verifyCriteriaDict(schema, criteria_dict, db, table=AIdb.MANIFESTS_TABLE): """ Used for verifying and loading criteria from a dictionary of criteria. Args: schema - path to schema file for criteria manifest. criteria_dict - dictionary of criteria to verify, in the form of { criteria: value, criteria: value, ... } db - database object for install service table - database table, distinguishing manifests from profiles Raises IOError: * if the schema does not open ValueError: * if the criteria_dict dictionary is empty * if the XML is invalid according to the schema AssertionError: * if a value in the dictionary is empty Returns: A valid XML DOM of the criteria and all MAC and IPV4 values are formatted according to verifyXML.prepValuesAndRanges(). """ schema = open(schema, 'r') if not criteria_dict: raise ValueError("Error:\tCriteria dictionary empty: %s\n" % criteria_dict) root = lxml.etree.Element("ai_criteria_manifest") for name, value_or_range in criteria_dict.iteritems(): if value_or_range is None: raise AssertionError( _("Error: Missing value for criteria " "'%s'") % name) crit = lxml.etree.SubElement(root, "ai_criteria") crit.set("name", name) # If criteria is a range, split on "-" and add to # XML DOM as a range element. if AIdb.isRangeCriteria(db.getQueue(), name, table): # Split on "-" range_value = value_or_range.split('-', 1) # If there was only a single value, means user specified # this range criteria as a single value, add it as a single # value if len(range_value) == 1: value_elem = lxml.etree.SubElement(crit, "value") value_elem.text = value_or_range else: range_elem = lxml.etree.SubElement(crit, "range") range_elem.text = " ".join(range_value) else: value_elem = lxml.etree.SubElement(crit, "value") value_elem.text = value_or_range # Verify the generated criteria DOM root, errors = verifyXML.verifyRelaxNGManifest( schema, StringIO.StringIO(lxml.etree.tostring(root))) if errors: raise ValueError( _("Error: Criteria failed validation:\n\t%s") % errors.message) try: verifyXML.prepValuesAndRanges(root, db, table) except ValueError, err: raise ValueError(_("Error:\tCriteria error: %s") % err)
def find_colliding_criteria(criteria, db, exclude_manifests=None): """ Returns: A dictionary of colliding criteria with keys being manifest name and instance tuples and values being the DB column names which collided Args: criteria - Criteria object holding the criteria that is to be added/set for a manifest. db - AI_database object for the install service. exclude_manifests -A list of manifest names from DB to ignore. This arg is passed in when we're calling this function to find criteria collisions for an already published manifest. Raises: SystemExit if: criteria is not found in database value is not valid for type (integer and hexadecimal checks) range is improper """ class Fields(object): """ Define convenience indexes """ # manifest name is row index 0 MANNAME = 0 # manifest instance is row index 1 MANINST = 1 # criteria is row index 2 (when a single valued criteria) CRIT = 2 # minimum criteria is row index 2 (when a range valued criteria) MINCRIT = 2 # maximum criteria is row index 3 (when a range valued criteria) MAXCRIT = 3 # collisions is a dictionary to hold keys of the form (manifest name, # instance) which will point to a comma-separated string of colliding # criteria collisions = dict() # verify each range criteria in the manifest is well formed and collect # collisions with database entries for crit in criteria: # gather this criteria's values from the manifest man_criterion = criteria[crit] # Determine if this crit is a range criteria or not. is_range_crit = AIdb.isRangeCriteria(db.getQueue(), crit, AIdb.MANIFESTS_TABLE) # Process "value" criteria here (check if the criteria exists in # DB, and then find collisions) if not is_range_crit: # only check criteria in use in the DB if crit not in AIdb.getCriteria(db.getQueue(), onlyUsed=False, strip=False): raise SystemExit(_("Error:\tCriteria %s is not a " + "valid criteria!") % crit) # get all values in the database for this criteria (and # manifest/instance pairs for each value) db_criteria = AIdb.getSpecificCriteria( db.getQueue(), crit, provideManNameAndInstance=True, excludeManifests=exclude_manifests) # will iterate over a list of the form [manName, manInst, crit, # None] for row in db_criteria: # check if a value in the list of values to be added is equal # to a value in the list of values for this criteria for this # row for value in man_criterion: if AIdb.is_in_list(crit, value, str(row[Fields.CRIT]), None): # record manifest name, instance and criteria name try: collisions[row[Fields.MANNAME], row[Fields.MANINST]] += crit + "," except KeyError: collisions[row[Fields.MANNAME], row[Fields.MANINST]] = crit + "," # This is a range criteria. (Check that ranges are valid, that # "unbounded" gets set to 0/+inf, ensure the criteria exists # in the DB, then look for collisions.) else: # Clean-up NULL's and change "unbounded"s to 0 and # really large numbers in case this Python does # not support IEEE754. Note "unbounded"s are already # converted to lower case during manifest processing. if man_criterion[0] == "unbounded": man_criterion[0] = "0" if man_criterion[1] == "unbounded": man_criterion[1] = INFINITY if crit == "mac": # convert hex mac address (w/o colons) to a number try: man_criterion[0] = long(str(man_criterion[0]).upper(), 16) man_criterion[1] = long(str(man_criterion[1]).upper(), 16) except ValueError: raise SystemExit(_("Error:\tCriteria %s " "is not a valid hexadecimal value") % crit) else: # this is a decimal value try: man_criterion = [long(str(man_criterion[0]).upper()), long(str(man_criterion[1]).upper())] except ValueError: raise SystemExit(_("Error:\tCriteria %s " "is not a valid integer value") % crit) # Check for a properly ordered range (with unbounded being 0 or # Inf.) but ensure both are not unbounded. # Check for: # a range of zero to inf -- not a valid range # and # min > max -- range order reversed # if (man_criterion[0] == 0 and man_criterion[1] == long(INFINITY)): raise SystemExit(_("Error:\tCriteria %s is not a valid range, " "MIN and MAX unbounded.") % crit) if ((man_criterion[0] != 0 and man_criterion[1] != long(INFINITY)) and (long(man_criterion[0]) > long(man_criterion[1]))): raise SystemExit(_("Error:\tCriteria %s is not a valid range, " "MIN > MAX.") % crit) # check to see that this criteria exists in the database columns man_crit = AIdb.getCriteria(db.getQueue(), onlyUsed=False, strip=False) if 'MIN' + crit not in man_crit and 'MAX' + crit not in man_crit: raise SystemExit(_("Error:\tCriteria %s is not a " "valid criteria!") % crit) db_criteria = AIdb.getSpecificCriteria( db.getQueue(), 'MIN' + crit, 'MAX' + crit, provideManNameAndInstance=True, excludeManifests=exclude_manifests) # will iterate over a list of the form [manName, manInst, mincrit, # maxcrit] for row in db_criteria: # arbitrarily large number in case this Python does # not support IEEE754 db_criterion = ["0", INFINITY] # now populate in valid database values (i.e. non-NULL values) if row[Fields.MINCRIT]: db_criterion[0] = row[Fields.MINCRIT] if row[Fields.MAXCRIT]: db_criterion[1] = row[Fields.MAXCRIT] if crit == "mac": # use a hexadecimal conversion db_criterion = [long(str(db_criterion[0]), 16), long(str(db_criterion[1]), 16)] else: # these are decimal numbers db_criterion = [long(str(db_criterion[0])), long(str(db_criterion[1]))] # these three criteria can determine if there's a range overlap if((man_criterion[1] >= db_criterion[0] and db_criterion[1] >= man_criterion[0]) or man_criterion[0] == db_criterion[1]): # range overlap so record the collision try: collisions[row[Fields.MANNAME], row[Fields.MANINST]] += "MIN" + crit + "," collisions[row[Fields.MANNAME], row[Fields.MANINST]] += "MAX" + crit + "," except KeyError: collisions[row[Fields.MANNAME], row[Fields.MANINST]] = "MIN" + crit + "," collisions[row[Fields.MANNAME], row[Fields.MANINST]] += "MAX" + crit + "," return collisions
def send_manifest(form_data, port=0, servicename=None, protocolversion=COMPATIBILITY_VERSION, no_default=False): '''Replies to the client with matching service for a service. Args form_data - the postData passed in from the client request port - the port of the old client servicename - the name of the service being used protocolversion - the version of the AI service RE: handshake no_default - boolean flag to signify whether or not we should hand back the default manifest and profiles if one cannot be matched based on the client criteria. Returns None Raises None ''' # figure out the appropriate path for the AI database, # and get service name if necessary. # currently service information is stored in a port directory. # When the cherrypy webserver new service directories should be # separated via service-name only. Old services will still use # port numbers as the separation mechanism. path = None found_servicename = None service = None port = str(port) if servicename: service = AIService(servicename) path = service.database_path else: for name in config.get_all_service_names(): if config.get_service_port(name) == port: found_servicename = name service = AIService(name) path = service.database_path break # Check to insure that a valid path was found if not path or not os.path.exists(path): print 'Content-Type: text/html' # HTML is following print # blank line, end of headers if servicename: print '<pre><b>Error</b>:unable to find<i>', servicename + '</i>.' else: print '<pre><b>Error</b>:unable to find<i>', port + '</i>.' print 'Available services are:<p><ol><i>' hostname = socket.gethostname() for name in config.get_all_service_names(): port = config.get_service_port(name) sys.stdout.write('<a href="http://%s:%d/cgi-bin/' 'cgi_get_manifest.py?version=%s&service=%s">%s</a><br>\n' % (hostname, port, VERSION, name, name)) print '</i></ol>Please select a service from the above list.' return if found_servicename: servicename = found_servicename # load to the AI database aisql = AIdb.DB(path) aisql.verifyDBStructure() # convert the form data into a criteria dictionary criteria = dict() orig_data = form_data while form_data: try: [key_value, form_data] = form_data.split(';', 1) except (ValueError, NameError, TypeError, KeyError): key_value = form_data form_data = '' try: [key, value] = key_value.split('=') criteria[key] = value except (ValueError, NameError, TypeError, KeyError): criteria = dict() # Generate templating dictionary from criteria template_dict = dict() for crit in criteria: template_dict["AI_" + crit.upper()] = \ AIdb.formatValue(crit, criteria[crit], units=False) # find the appropriate manifest try: manifest = AIdb.findManifest(criteria, aisql) except StandardError as err: print 'Content-Type: text/html' # HTML is following print # blank line, end of headers print '<pre><b>Error</b>:findManifest criteria<br>' print err, '<br>' print '<ol>servicename =', servicename print 'port =', port print 'path =', path print 'form_data =', orig_data print 'criteria =', criteria print 'servicename found by port =', found_servicename, '</ol>' print '</pre>' return # check if findManifest() returned a number equal to 0 # (means we got no manifests back -- thus we serve the default if desired) if manifest is None and not no_default: manifest = service.get_default_manifest() # if we have a manifest to return, prepare its return if manifest is not None: try: # construct the fully qualified filename filename = os.path.abspath(os.path.join(service.manifest_dir, manifest)) # open and read the manifest with open(filename, 'rb') as mfp: manifest_str = mfp.read() # maintain compability with older AI client if servicename is None or \ float(protocolversion) < float(PROFILES_VERSION): content_type = mimetypes.types_map.get('.xml', 'text/plain') print 'Content-Length:', len(manifest_str) # Length of the file print 'Content-Type:', content_type # XML is following print # blank line, end of headers print manifest_str logging.info('Manifest sent from %s.' % filename) return except OSError as err: print 'Content-Type: text/html' # HTML is following print # blank line, end of headers print '<pre>' # report the internal error to error_log and requesting client sys.stderr.write(_('error:manifest (%s) %s\n') % \ (str(manifest), err)) sys.stdout.write(_('error:manifest (%s) %s\n') % \ (str(manifest), err)) print '</pre>' return # get AI service image path service = AIService(servicename) image_dir = service.image.path # construct object to contain MIME multipart message outermime = MIMEMultipart() client_msg = list() # accumulate message output for AI client # If we have a manifest, attach it to the return message if manifest is not None: # add manifest as attachment msg = MIMEText(manifest_str, 'xml') # indicate manifest using special name msg.add_header('Content-Disposition', 'attachment', filename=sc.AI_MANIFEST_ATTACHMENT_NAME) outermime.attach(msg) # add manifest as an attachment # search for any profiles matching client criteria # formulate database query to profiles table q_str = "SELECT DISTINCT name, file FROM " + \ AIdb.PROFILES_TABLE + " WHERE " nvpairs = list() # accumulate criteria values from post-data # for all AI client criteria for crit in AIdb.getCriteria(aisql.getQueue(), table=AIdb.PROFILES_TABLE, onlyUsed=False): if crit not in criteria: msgtxt = _("Warning: client criteria \"%s\" not provided in " "request. Setting value to NULL for profile lookup.") \ % crit client_msg += [msgtxt] logging.warn(msgtxt) # fetch only global profiles destined for all clients if AIdb.isRangeCriteria(aisql.getQueue(), crit, AIdb.PROFILES_TABLE): nvpairs += ["MIN" + crit + " IS NULL"] nvpairs += ["MAX" + crit + " IS NULL"] else: nvpairs += [crit + " IS NULL"] continue # prepare criteria value to add to query envval = AIdb.sanitizeSQL(criteria[crit]) if AIdb.isRangeCriteria(aisql.getQueue(), crit, AIdb.PROFILES_TABLE): # If no default profiles are requested, then we mustn't allow # this criteria to be NULL. It must match the client's given # value for this criteria. if no_default: if crit == "mac": nvpairs += ["(HEX(MIN" + crit + ")<=HEX(X'" + envval + \ "'))"] nvpairs += ["(HEX(MAX" + crit + ")>=HEX(X'" + envval + \ "'))"] else: nvpairs += ["(MIN" + crit + "<='" + envval + "')"] nvpairs += ["(MAX" + crit + ">='" + envval + "')"] else: if crit == "mac": nvpairs += ["(MIN" + crit + " IS NULL OR " "HEX(MIN" + crit + ")<=HEX(X'" + envval + "'))"] nvpairs += ["(MAX" + crit + " IS NULL OR HEX(MAX" + crit + ")>=HEX(X'" + envval + "'))"] else: nvpairs += ["(MIN" + crit + " IS NULL OR MIN" + crit + "<='" + envval + "')"] nvpairs += ["(MAX" + crit + " IS NULL OR MAX" + crit + ">='" + envval + "')"] else: # If no default profiles are requested, then we mustn't allow # this criteria to be NULL. It must match the client's given # value for this criteria. # # Also, since this is a non-range criteria, the value stored # in the DB may be a whitespace separated list of single # values. We use a special user-defined function in the # determine if the given criteria is in that textual list. if no_default: nvpairs += ["(is_in_list('" + crit + "', '" + envval + \ "', " + crit + ", 'None') == 1)"] else: nvpairs += ["(" + crit + " IS NULL OR is_in_list('" + crit + \ "', '" + envval + "', " + crit + ", 'None') == 1)"] if len(nvpairs) > 0: q_str += " AND ".join(nvpairs) # issue database query logging.info("Profile query: " + q_str) query = AIdb.DBrequest(q_str) aisql.getQueue().put(query) query.waitAns() if query.getResponse() is None or len(query.getResponse()) == 0: msgtxt = _("No profiles found.") client_msg += [msgtxt] logging.info(msgtxt) else: for row in query.getResponse(): profpath = row['file'] profname = row['name'] if profname is None: # should not happen profname = 'unnamed' try: if profpath is None: msgtxt = "Database record error - profile path is " \ "empty." client_msg += [msgtxt] logging.error(msgtxt) continue msgtxt = _('Processing profile %s') % profname client_msg += [msgtxt] logging.info(msgtxt) with open(profpath, 'r') as pfp: raw_profile = pfp.read() # do any template variable replacement {{AI_xxx}} tmpl_profile = sc.perform_templating(raw_profile, template_dict) # precautionary validation of profile, logging only sc.validate_profile_string(tmpl_profile, image_dir, dtd_validation=True, warn_if_dtd_missing=True) except IOError as err: msgtxt = _("Error: I/O error: ") + str(err) client_msg += [msgtxt] logging.error(msgtxt) continue except OSError: msgtxt = _("Error: OS error on profile ") + profpath client_msg += [msgtxt] logging.error(msgtxt) continue except KeyError: msgtxt = _('Error: could not find criteria to substitute ' 'in template: ') + profpath client_msg += [msgtxt] logging.error(msgtxt) logging.error('Profile with template substitution error:' + raw_profile) continue except lxml.etree.XMLSyntaxError as err: # log validation error and proceed msgtxt = _( 'Warning: syntax error found in profile: ') \ + profpath client_msg += [msgtxt] logging.error(msgtxt) for error in err.error_log: msgtxt = _('Error: ') + error.message client_msg += [msgtxt] logging.error(msgtxt) logging.info([_('Profile failing validation: ') + lxml.etree.tostring(root)]) # build MIME message and attach to outer MIME message msg = MIMEText(tmpl_profile, 'xml') # indicate in header that this is an attachment msg.add_header('Content-Disposition', 'attachment', filename=profname) # attach this profile to the manifest and any other profiles outermime.attach(msg) msgtxt = _('Parsed and loaded profile: ') + profname client_msg += [msgtxt] logging.info(msgtxt) # any profiles and AI manifest have been attached to MIME message # specially format list of messages for display on AI client console if client_msg: outtxt = '' for msgtxt in client_msg: msgtxt = _('SC profile locator:') + msgtxt outtxt += str(msgtxt) + '\n' # add AI client console messages as single plain text attachment msg = MIMEText(outtxt, 'plain') # create MIME message outermime.attach(msg) # attach MIME message to response print outermime.as_string() # send MIME-formatted message
def set_criteria(criteria, manifest_name, db, append=False): """ Set a manifest's record in the criteria database with the criteria provided. If append is True -- append ones that aren't already set for the manifest, and replace ones that are. if append is False -- completely remove all criteria already set for the manifest, and use only the criteria specified. """ # Build a list of criteria nvpairs to update nvpairs = list() # we need to fill in the criteria or NULLs for each criteria the database # supports (so iterate over each criteria) for crit in AIdb.getCriteria(db.getQueue(), onlyUsed=False, strip=True): # Get the value from the manifest values = criteria[crit] # the critera manifest didn't specify this criteria if values is None: # If we not appending criteria, then we must write in NULLs # for this criteria since we're removing all criteria not # specified. if not append: # if the criteria we're processing is a range criteria, fill in # NULL for two columns, MINcrit and MAXcrit if AIdb.isRangeCriteria(db.getQueue(), crit): nvpairs.append("MIN" + crit + "=NULL") nvpairs.append("MAX" + crit + "=NULL") # this is a single value else: nvpairs.append(crit + "=NULL") # this is a single criteria (not a range) elif isinstance(values, basestring): # translate "unbounded" to a database NULL if values == "unbounded": nvstr = crit + "=NULL" else: # use lower case for text strings nvstr = crit + "='" + AIdb.sanitizeSQL(str(values).lower()) \ + "'" nvpairs.append(nvstr) # Else the values are a list this is a range criteria else: # Set the MIN column for this range criteria nvpairs.append("MIN" + crit + "=" + format_value(crit, values[0])) # Set the MAX column for this range criteria nvpairs.append("MAX" + crit + "=" + format_value(crit, values[1])) query = "UPDATE manifests SET " + ",".join(nvpairs) + \ " WHERE name='" + manifest_name + "'" # update the DB query = AIdb.DBrequest(query, commit=True) db.getQueue().put(query) query.waitAns() # in case there's an error call the response function (which # will print the error) query.getResponse()
def verifyCriteriaDict(schema, criteria_dict, db, table=AIdb.MANIFESTS_TABLE): """ Used for verifying and loading criteria from a dictionary of criteria. Args: schema - path to schema file for criteria manifest. criteria_dict - dictionary of criteria to verify, in the form of { criteria: value, criteria: value, ... } db - database object for install service table - database table, distinguishing manifests from profiles Raises IOError: * if the schema does not open ValueError: * if the criteria_dict dictionary is empty * if the XML is invalid according to the schema AssertionError: * if a value in the dictionary is empty Returns: A valid XML DOM of the criteria and all MAC and IPV4 values are formatted according to verifyXML.prepValuesAndRanges(). """ schema = open(schema, 'r') if not criteria_dict: raise ValueError("Error:\tCriteria dictionary empty: %s\n" % criteria_dict) root = lxml.etree.Element("ai_criteria_manifest") for name, value_or_range in criteria_dict.iteritems(): if value_or_range is None: raise AssertionError(_("Error: Missing value for criteria " "'%s'") % name) crit = lxml.etree.SubElement(root, "ai_criteria") crit.set("name", name) # If criteria is a range, split on "-" and add to # XML DOM as a range element. if AIdb.isRangeCriteria(db.getQueue(), name, table): # Split on "-" range_value = value_or_range.split('-', 1) # If there was only a single value, means user specified # this range criteria as a single value, add it as a single # value if len(range_value) == 1: value_elem = lxml.etree.SubElement(crit, "value") value_elem.text = value_or_range else: range_elem = lxml.etree.SubElement(crit, "range") range_elem.text = " ".join(range_value) else: value_elem = lxml.etree.SubElement(crit, "value") value_elem.text = value_or_range # Verify the generated criteria DOM root, errors = verifyXML.verifyRelaxNGManifest(schema, StringIO.StringIO(lxml.etree.tostring(root))) if errors: raise ValueError(_("Error: Criteria failed validation:\n\t%s") % errors.message) try: verifyXML.prepValuesAndRanges(root, db, table) except ValueError, err: raise ValueError(_("Error:\tCriteria error: %s") % err)
def validate_criteria_from_user(criteria, dbo, table): ''' Validate profile criteria from dictionary containing command line input Args: criteria - Criteria object holding the criteria that is to be added/set for a manifest. dbo - AI_database object for the install service. table - name of database table Raises: SystemExit if: - criteria is not found in database - value is not valid for type (integer and hexadecimal checks) - range is improper Returns: nothing ''' # find all possible profile criteria expressed as DB table columns critlist = [] # take criteria from generator for crit in AIdb.getCriteria(dbo.getQueue(), table, onlyUsed=False, strip=False): critlist.append(crit) # verify each range criteria is well formed for crit in criteria: # gather this criteria's values man_criterion = criteria[crit] # check "value" criteria here (check the criteria exists in DB if not AIdb.isRangeCriteria(dbo.getQueue(), crit, table): # only check criteria in use in the DB if crit not in critlist: raise SystemExit( _("Error:\tCriteria %s is not a valid criteria!") % crit) # This is a range criteria. (Check that ranges are valid, that # "unbounded" gets set to 0/+inf, ensure the criteria exists # in the DB else: # check for a properly ordered range (with unbounded being 0 or # Inf.) if man_criterion[0] != "unbounded" and \ man_criterion[1] != "unbounded" and \ man_criterion[0] > man_criterion[1]: # Check min > max raise SystemExit( _("Error:\tCriteria %s is not a valid range (MIN > MAX) ") % crit) # Clean-up NULL's and changed "unbounded"s to 0 and # the maximum integer value # Note "unbounded"s are already converted to lower case during # input processing if man_criterion[0] == "unbounded": man_criterion[0] = "0" if man_criterion[1] == "unbounded": man_criterion[1] = str(sys.maxint) if crit == "mac": # convert hex mac address (w/o colons) to a number try: man_criterion[0] = long(str(man_criterion[0]).upper(), 16) man_criterion[1] = long(str(man_criterion[1]).upper(), 16) except ValueError: raise SystemExit( _("Error:\tCriteria %s is not a valid hexadecimal value" ) % crit) else: # this is a decimal value try: man_criterion = [ long(str(man_criterion[0]).upper()), long(str(man_criterion[1]).upper()) ] except ValueError: raise SystemExit( _("Error:\tCriteria %s is not a valid integer value") % crit) # check to see that this criteria exists in the database columns if 'MIN' + crit not in critlist and 'MAX' + crit not in critlist: raise SystemExit( _("Error:\tCriteria %s is not a valid criteria!") % crit)
def send_manifest(form_data, port=0, servicename=None, protocolversion=COMPATIBILITY_VERSION, no_default=False): '''Replies to the client with matching service for a service. Args form_data - the postData passed in from the client request port - the port of the old client servicename - the name of the service being used protocolversion - the version of the AI service RE: handshake no_default - boolean flag to signify whether or not we should hand back the default manifest and profiles if one cannot be matched based on the client criteria. Returns None Raises None ''' # figure out the appropriate path for the AI database, # and get service name if necessary. # currently service information is stored in a port directory. # When the cherrypy webserver new service directories should be # separated via service-name only. Old services will still use # port numbers as the separation mechanism. path = None found_servicename = None service = None port = str(port) if servicename: service = AIService(servicename) path = service.database_path else: for name in config.get_all_service_names(): if config.get_service_port(name) == port: found_servicename = name service = AIService(name) path = service.database_path break # Check to insure that a valid path was found if not path or not os.path.exists(path): print 'Content-Type: text/html' # HTML is following print # blank line, end of headers if servicename: print '<pre><b>Error</b>:unable to find<i>', servicename + '</i>.' else: print '<pre><b>Error</b>:unable to find<i>', port + '</i>.' print 'Available services are:<p><ol><i>' hostname = socket.gethostname() for name in config.get_all_service_names(): port = config.get_service_port(name) sys.stdout.write( '<a href="http://%s:%d/cgi-bin/' 'cgi_get_manifest.py?version=%s&service=%s">%s</a><br>\n' % (hostname, port, VERSION, name, name)) print '</i></ol>Please select a service from the above list.' return if found_servicename: servicename = found_servicename # load to the AI database aisql = AIdb.DB(path) aisql.verifyDBStructure() # convert the form data into a criteria dictionary criteria = dict() orig_data = form_data while form_data: try: [key_value, form_data] = form_data.split(';', 1) except (ValueError, NameError, TypeError, KeyError): key_value = form_data form_data = '' try: [key, value] = key_value.split('=') criteria[key] = value except (ValueError, NameError, TypeError, KeyError): criteria = dict() # Generate templating dictionary from criteria template_dict = dict() for crit in criteria: template_dict["AI_" + crit.upper()] = \ AIdb.formatValue(crit, criteria[crit], units=False) # find the appropriate manifest try: manifest = AIdb.findManifest(criteria, aisql) except StandardError as err: print 'Content-Type: text/html' # HTML is following print # blank line, end of headers print '<pre><b>Error</b>:findManifest criteria<br>' print err, '<br>' print '<ol>servicename =', servicename print 'port =', port print 'path =', path print 'form_data =', orig_data print 'criteria =', criteria print 'servicename found by port =', found_servicename, '</ol>' print '</pre>' return # check if findManifest() returned a number equal to 0 # (means we got no manifests back -- thus we serve the default if desired) if manifest is None and not no_default: manifest = service.get_default_manifest() # if we have a manifest to return, prepare its return if manifest is not None: try: # construct the fully qualified filename filename = os.path.abspath( os.path.join(service.manifest_dir, manifest)) # open and read the manifest with open(filename, 'rb') as mfp: manifest_str = mfp.read() # maintain compability with older AI client if servicename is None or \ float(protocolversion) < float(PROFILES_VERSION): content_type = mimetypes.types_map.get('.xml', 'text/plain') print 'Content-Length:', len( manifest_str) # Length of the file print 'Content-Type:', content_type # XML is following print # blank line, end of headers print manifest_str logging.info('Manifest sent from %s.' % filename) return except OSError as err: print 'Content-Type: text/html' # HTML is following print # blank line, end of headers print '<pre>' # report the internal error to error_log and requesting client sys.stderr.write(_('error:manifest (%s) %s\n') % \ (str(manifest), err)) sys.stdout.write(_('error:manifest (%s) %s\n') % \ (str(manifest), err)) print '</pre>' return # get AI service image path service = AIService(servicename) image_dir = service.image.path # construct object to contain MIME multipart message outermime = MIMEMultipart() client_msg = list() # accumulate message output for AI client # If we have a manifest, attach it to the return message if manifest is not None: # add manifest as attachment msg = MIMEText(manifest_str, 'xml') # indicate manifest using special name msg.add_header('Content-Disposition', 'attachment', filename=sc.AI_MANIFEST_ATTACHMENT_NAME) outermime.attach(msg) # add manifest as an attachment # search for any profiles matching client criteria # formulate database query to profiles table q_str = "SELECT DISTINCT name, file FROM " + \ AIdb.PROFILES_TABLE + " WHERE " nvpairs = list() # accumulate criteria values from post-data # for all AI client criteria for crit in AIdb.getCriteria(aisql.getQueue(), table=AIdb.PROFILES_TABLE, onlyUsed=False): if crit not in criteria: msgtxt = _("Warning: client criteria \"%s\" not provided in " "request. Setting value to NULL for profile lookup.") \ % crit client_msg += [msgtxt] logging.warn(msgtxt) # fetch only global profiles destined for all clients if AIdb.isRangeCriteria(aisql.getQueue(), crit, AIdb.PROFILES_TABLE): nvpairs += ["MIN" + crit + " IS NULL"] nvpairs += ["MAX" + crit + " IS NULL"] else: nvpairs += [crit + " IS NULL"] continue # prepare criteria value to add to query envval = AIdb.sanitizeSQL(criteria[crit]) if AIdb.isRangeCriteria(aisql.getQueue(), crit, AIdb.PROFILES_TABLE): # If no default profiles are requested, then we mustn't allow # this criteria to be NULL. It must match the client's given # value for this criteria. if no_default: if crit == "mac": nvpairs += ["(HEX(MIN" + crit + ")<=HEX(X'" + envval + \ "'))"] nvpairs += ["(HEX(MAX" + crit + ")>=HEX(X'" + envval + \ "'))"] else: nvpairs += ["(MIN" + crit + "<='" + envval + "')"] nvpairs += ["(MAX" + crit + ">='" + envval + "')"] else: if crit == "mac": nvpairs += [ "(MIN" + crit + " IS NULL OR " "HEX(MIN" + crit + ")<=HEX(X'" + envval + "'))" ] nvpairs += [ "(MAX" + crit + " IS NULL OR HEX(MAX" + crit + ")>=HEX(X'" + envval + "'))" ] else: nvpairs += [ "(MIN" + crit + " IS NULL OR MIN" + crit + "<='" + envval + "')" ] nvpairs += [ "(MAX" + crit + " IS NULL OR MAX" + crit + ">='" + envval + "')" ] else: # If no default profiles are requested, then we mustn't allow # this criteria to be NULL. It must match the client's given # value for this criteria. # # Also, since this is a non-range criteria, the value stored # in the DB may be a whitespace separated list of single # values. We use a special user-defined function in the # determine if the given criteria is in that textual list. if no_default: nvpairs += ["(is_in_list('" + crit + "', '" + envval + \ "', " + crit + ", 'None') == 1)"] else: nvpairs += ["(" + crit + " IS NULL OR is_in_list('" + crit + \ "', '" + envval + "', " + crit + ", 'None') == 1)"] if len(nvpairs) > 0: q_str += " AND ".join(nvpairs) # issue database query logging.info("Profile query: " + q_str) query = AIdb.DBrequest(q_str) aisql.getQueue().put(query) query.waitAns() if query.getResponse() is None or len(query.getResponse()) == 0: msgtxt = _("No profiles found.") client_msg += [msgtxt] logging.info(msgtxt) else: for row in query.getResponse(): profpath = row['file'] profname = row['name'] if profname is None: # should not happen profname = 'unnamed' try: if profpath is None: msgtxt = "Database record error - profile path is " \ "empty." client_msg += [msgtxt] logging.error(msgtxt) continue msgtxt = _('Processing profile %s') % profname client_msg += [msgtxt] logging.info(msgtxt) with open(profpath, 'r') as pfp: raw_profile = pfp.read() # do any template variable replacement {{AI_xxx}} tmpl_profile = sc.perform_templating( raw_profile, template_dict) # precautionary validation of profile, logging only sc.validate_profile_string(tmpl_profile, image_dir, dtd_validation=True, warn_if_dtd_missing=True) except IOError as err: msgtxt = _("Error: I/O error: ") + str(err) client_msg += [msgtxt] logging.error(msgtxt) continue except OSError: msgtxt = _("Error: OS error on profile ") + profpath client_msg += [msgtxt] logging.error(msgtxt) continue except KeyError: msgtxt = _('Error: could not find criteria to substitute ' 'in template: ') + profpath client_msg += [msgtxt] logging.error(msgtxt) logging.error('Profile with template substitution error:' + raw_profile) continue except lxml.etree.XMLSyntaxError as err: # log validation error and proceed msgtxt = _( 'Warning: syntax error found in profile: ') \ + profpath client_msg += [msgtxt] logging.error(msgtxt) for error in err.error_log: msgtxt = _('Error: ') + error.message client_msg += [msgtxt] logging.error(msgtxt) logging.info([ _('Profile failing validation: ') + lxml.etree.tostring(root) ]) # build MIME message and attach to outer MIME message msg = MIMEText(tmpl_profile, 'xml') # indicate in header that this is an attachment msg.add_header('Content-Disposition', 'attachment', filename=profname) # attach this profile to the manifest and any other profiles outermime.attach(msg) msgtxt = _('Parsed and loaded profile: ') + profname client_msg += [msgtxt] logging.info(msgtxt) # any profiles and AI manifest have been attached to MIME message # specially format list of messages for display on AI client console if client_msg: outtxt = '' for msgtxt in client_msg: msgtxt = _('SC profile locator:') + msgtxt outtxt += str(msgtxt) + '\n' # add AI client console messages as single plain text attachment msg = MIMEText(outtxt, 'plain') # create MIME message outermime.attach(msg) # attach MIME message to response print outermime.as_string() # send MIME-formatted message
def validate_criteria_from_user(criteria, dbo, table): ''' Validate profile criteria from dictionary containing command line input Args: criteria - Criteria object holding the criteria that is to be added/set for a manifest. dbo - AI_database object for the install service. table - name of database table Raises: SystemExit if: - criteria is not found in database - value is not valid for type (integer and hexadecimal checks) - range is improper Returns: nothing ''' # find all possible profile criteria expressed as DB table columns critlist = [] # take criteria from generator for crit in AIdb.getCriteria(dbo.getQueue(), table, onlyUsed=False, strip=False): critlist.append(crit) # verify each range criteria is well formed for crit in criteria: # gather this criteria's values man_criterion = criteria[crit] # check "value" criteria here (check the criteria exists in DB if not AIdb.isRangeCriteria(dbo.getQueue(), crit, table): # only check criteria in use in the DB if crit not in critlist: raise SystemExit(_( "Error:\tCriteria %s is not a valid criteria!") % crit) # This is a range criteria. (Check that ranges are valid, that # "unbounded" gets set to 0/+inf, ensure the criteria exists # in the DB else: # check for a properly ordered range (with unbounded being 0 or # Inf.) if man_criterion[0] != "unbounded" and \ man_criterion[1] != "unbounded" and \ man_criterion[0] > man_criterion[1]: # Check min > max raise SystemExit(_( "Error:\tCriteria %s is not a valid range (MIN > MAX) ") % crit) # Clean-up NULL's and changed "unbounded"s to 0 and # the maximum integer value # Note "unbounded"s are already converted to lower case during # input processing if man_criterion[0] == "unbounded": man_criterion[0] = "0" if man_criterion[1] == "unbounded": man_criterion[1] = str(sys.maxint) if crit == "mac": # convert hex mac address (w/o colons) to a number try: man_criterion[0] = long(str(man_criterion[0]).upper(), 16) man_criterion[1] = long(str(man_criterion[1]).upper(), 16) except ValueError: raise SystemExit(_( "Error:\tCriteria %s is not a valid hexadecimal value") % crit) else: # this is a decimal value try: man_criterion = [long(str(man_criterion[0]).upper()), long(str(man_criterion[1]).upper())] except ValueError: raise SystemExit(_( "Error:\tCriteria %s is not a valid integer value") % crit) # check to see that this criteria exists in the database columns if 'MIN' + crit not in critlist and 'MAX' + crit not in critlist: raise SystemExit(_( "Error:\tCriteria %s is not a valid criteria!") % crit)