usr/src/cmd/ai-webserver/cgi_get_manifest.py
changeset 1160 6f7e708c38ec
parent 1098 c70e5a658299
child 1221 31c6d2de5731
equal deleted inserted replaced
1159:fbde90ccfae9 1160:6f7e708c38ec
    64 
    64 
    65     Returns
    65     Returns
    66         protocol_version   - the request version number, 0.5 indicates that the
    66         protocol_version   - the request version number, 0.5 indicates that the
    67                              original mechanisms are being used.
    67                              original mechanisms are being used.
    68         service_name       - the service name
    68         service_name       - the service name
       
    69         no_default         - boolean flag to signify whether or not we should
       
    70                              hand back the default manifest and profiles if one
       
    71                              cannot be matched based on the client criteria.
    69         post_data          - the POST-ed client criteria
    72         post_data          - the POST-ed client criteria
    70 
    73 
    71     Raises
    74     Raises
    72         None
    75         None
    73     '''
    76     '''
    74     protocol_version = COMPATIBILITY_VERSION  # assume original client
    77     protocol_version = COMPATIBILITY_VERSION  # assume original client
    75     service_name = None
    78     service_name = None
       
    79     no_default = False
    76     post_data = None
    80     post_data = None
    77     if 'version' in form:
    81     if 'version' in form:
    78         protocol_version = form['version'].value  # new client
    82         protocol_version = form['version'].value  # new client
    79         # new clients have the service name associated with the request
    83         # new clients have the service name associated with the request
    80         if 'service' in form:
    84         if 'service' in form:
    91                 logging.getLogger().setLevel(dbglogmap[int(sol_dbg)])
    95                 logging.getLogger().setLevel(dbglogmap[int(sol_dbg)])
    92             else:
    96             else:
    93                 logging.warning(_(
    97                 logging.warning(_(
    94                         "Unrecognized logging level from POST REQUEST:  ")
    98                         "Unrecognized logging level from POST REQUEST:  ")
    95                         + str(sol_dbg))
    99                         + str(sol_dbg))
       
   100     if 'no_default' in form:
       
   101         # no_default = form['no_default'].value
       
   102         no_default = (str(True).lower() == (form['no_default'].value).lower())
    96     if 'postData' in form:
   103     if 'postData' in form:
    97         post_data = form['postData'].value
   104         post_data = form['postData'].value
    98 
   105 
    99     return (protocol_version, service_name, post_data)
   106     return (protocol_version, service_name, no_default, post_data)
   100 
   107 
   101 
   108 
   102 def get_environment_information():
   109 def get_environment_information():
   103     '''Gets the environment information for old client requests
   110     '''Gets the environment information for old client requests
   104        for the port number and request type (GET or POST).
   111        for the port number and request type (GET or POST).
   177     print                                 # blank line, end of headers
   184     print                                 # blank line, end of headers
   178     print xmlstr
   185     print xmlstr
   179 
   186 
   180 
   187 
   181 def send_manifest(form_data, port=0, servicename=None,
   188 def send_manifest(form_data, port=0, servicename=None,
   182         protocolversion=COMPATIBILITY_VERSION):
   189         protocolversion=COMPATIBILITY_VERSION, no_default=False):
   183     '''Replies to the client with matching service for a service.
   190     '''Replies to the client with matching service for a service.
   184 
   191 
   185     Args
   192     Args
   186         form_data   - the postData passed in from the client request
   193         form_data   - the postData passed in from the client request
   187         port        - the port of the old client
   194         port        - the port of the old client
   188         servicename - the name of the service being used
   195         servicename - the name of the service being used
   189         protocolversion - the version of the AI service RE: handshake
   196         protocolversion - the version of the AI service RE: handshake
       
   197         no_default  - boolean flag to signify whether or not we should hand
       
   198                       back the default manifest and profiles if one cannot
       
   199                       be matched based on the client criteria.
   190 
   200 
   191     Returns
   201     Returns
   192         None
   202         None
   193 
   203 
   194     Raises
   204     Raises
   287         print 'criteria    =', criteria
   297         print 'criteria    =', criteria
   288         print 'servicename found by port =', str(found_servicename), '</ol>'
   298         print 'servicename found by port =', str(found_servicename), '</ol>'
   289         print '</pre>'
   299         print '</pre>'
   290         return
   300         return
   291 
   301 
   292     if str(manifest).isdigit() and manifest > 0:
       
   293         web_page = \
       
   294             E.HTML(
       
   295                    E.HEAD(
       
   296                           E.TITLE(_("Error!"))
       
   297                    ),
       
   298                    E.BODY(
       
   299                           E.P(_("Criteria indeterminate -- this "
       
   300                                 "should not happen! Got %s matches.") %
       
   301                               str(manifest))
       
   302                    )
       
   303             )
       
   304         print "Content-Type: text/html"     # HTML is following
       
   305         print                               # blank line, end of headers
       
   306         print lxml.etree.tostring(web_page, pretty_print=True)
       
   307         return
       
   308 
       
   309     # check if findManifest() returned a number equal to 0
   302     # check if findManifest() returned a number equal to 0
   310     # (means we got no manifests back -- thus we serve the default)
   303     # (means we got no manifests back -- thus we serve the default if desired)
   311     elif manifest == 0:
   304     if manifest is None and not no_default:
   312         manifest = get_default(servicename)
   305         manifest = get_default(servicename)
   313 
   306 
   314     # findManifest() returned the name of the manifest to serve
   307     # if we have a manifest to return, prepare its return
   315     # (or it is now set to default.xml)
   308     if manifest is not None:
   316     try:
   309         try:
   317         # construct the fully qualified filename
   310             # construct the fully qualified filename
   318         path = os.path.join(os.path.dirname(path), AI_DATA)
   311             path = os.path.join(os.path.dirname(path), AI_DATA)
   319         filename = os.path.abspath(os.path.join(path, manifest))
   312             filename = os.path.abspath(os.path.join(path, manifest))
   320         # open and read the manifest
   313             # open and read the manifest
   321         with open(filename, 'rb') as mfp:
   314             with open(filename, 'rb') as mfp:
   322             manifest_str = mfp.read()
   315                 manifest_str = mfp.read()
   323         # maintain compability with older AI client
   316             # maintain compability with older AI client
   324         if servicename is None or \
   317             if servicename is None or \
   325                 float(protocolversion) < float(PROFILES_VERSION):
   318                     float(protocolversion) < float(PROFILES_VERSION):
   326             content_type = mimetypes.types_map.get('.xml', 'text/plain')
   319                 content_type = mimetypes.types_map.get('.xml', 'text/plain')
   327             print 'Content-Length:', len(manifest_str)  # Length of the file
   320                 print 'Content-Length:', len(manifest_str)  # Length of the file
   328             print 'Content-Type:', content_type         # XML is following
   321                 print 'Content-Type:', content_type         # XML is following
   329             print                                 # blank line, end of headers
   322                 print                               # blank line, end of headers
   330             print manifest_str
   323                 print manifest_str
   331             logging.info('Manifest sent from %s.' % filename)
   324                 logging.info('Manifest sent from %s.' % filename)
       
   325                 return
       
   326 
       
   327         except OSError, err:
       
   328             print 'Content-Type: text/html'     # HTML is following
       
   329             print                               # blank line, end of headers
       
   330             print '<pre>'
       
   331             # report the internal error to error_log and requesting client
       
   332             sys.stderr.write(_('error:manifest (%s) %s\n') % (str(manifest), err))
       
   333             sys.stdout.write(_('error:manifest (%s) %s\n') % (str(manifest), err))
       
   334             print '</pre>'
   332             return
   335             return
   333 
   336 
   334     except OSError, err:
       
   335         print 'Content-Type: text/html'     # HTML is following
       
   336         print                               # blank line, end of headers
       
   337         print '<pre>'
       
   338         # report the internal error to error_log and requesting client
       
   339         sys.stderr.write(_('error:manifest (%s) %s\n') % (str(manifest), err))
       
   340         sys.stdout.write(_('error:manifest (%s) %s\n') % (str(manifest), err))
       
   341         print '</pre>'
       
   342         return
       
   343 
   337 
   344     # get AI service image path
   338     # get AI service image path
   345     service_info = get_service_info(servicename)
   339     service_info = get_service_info(servicename)
   346     # construct object to contain MIME multipart message
   340     # construct object to contain MIME multipart message
   347     outermime = MIMEMultipart()
   341     outermime = MIMEMultipart()
   348     client_msg = list()  # accumulate message output for AI client
   342     client_msg = list()  # accumulate message output for AI client
   349     # add manifest as attachment
   343 
   350     msg = MIMEText(manifest_str, 'xml')
   344     # If we have a manifest, attach it to the return message
   351     # indicate manifest using special name
   345     if manifest is not None:
   352     msg.add_header('Content-Disposition', 'attachment',
   346         # add manifest as attachment
   353                    filename=sc.AI_MANIFEST_ATTACHMENT_NAME)
   347         msg = MIMEText(manifest_str, 'xml')
   354     outermime.attach(msg)  # add manifest as an attachment
   348         # indicate manifest using special name
       
   349         msg.add_header('Content-Disposition', 'attachment',
       
   350                       filename=sc.AI_MANIFEST_ATTACHMENT_NAME)
       
   351         outermime.attach(msg)  # add manifest as an attachment
       
   352 
   355 
   353 
   356     # search for any profiles matching client criteria
   354     # search for any profiles matching client criteria
   357     # formulate database query to profiles table
   355     # formulate database query to profiles table
   358     q_str = "SELECT DISTINCT name, file FROM " + \
   356     q_str = "SELECT DISTINCT name, file FROM " + \
   359         AIdb.PROFILES_TABLE + " WHERE "
   357         AIdb.PROFILES_TABLE + " WHERE "
   360     nvpairs = list()  # accumulate criteria values from post-data
   358     nvpairs = list()  # accumulate criteria values from post-data
   361     # for all AI client criteria
   359     # for all AI client criteria
   362     for crit in AIdb.getCriteria(aisql.getQueue(), table=AIdb.PROFILES_TABLE,
   360     for crit in AIdb.getCriteria(aisql.getQueue(), table=AIdb.PROFILES_TABLE,
   363                                  onlyUsed=False):
   361                                  onlyUsed=False):
   364         if crit not in criteria:
   362         if crit not in criteria:
   365             msgtxt = _("Warning: expected client criteria \"%s\" " \
   363             msgtxt = _("Warning: client criteria \"%s\" not provided in "
   366                        "missing from post-data. Profiles may be missing.") \
   364                        "request.  Setting value to NULL for profile lookup.") \
   367                        % crit
   365                        % crit
   368             client_msg += [msgtxt]
   366             client_msg += [msgtxt]
   369             logging.warn(msgtxt)
   367             logging.warn(msgtxt)
   370             # fetch only global profiles destined for all clients
   368             # fetch only global profiles destined for all clients
   371             if AIdb.isRangeCriteria(aisql.getQueue(), crit,
   369             if AIdb.isRangeCriteria(aisql.getQueue(), crit,
   373                 nvpairs += ["MIN" + crit + " IS NULL"]
   371                 nvpairs += ["MIN" + crit + " IS NULL"]
   374                 nvpairs += ["MAX" + crit + " IS NULL"]
   372                 nvpairs += ["MAX" + crit + " IS NULL"]
   375             else:
   373             else:
   376                 nvpairs += [crit + " IS NULL"]
   374                 nvpairs += [crit + " IS NULL"]
   377             continue
   375             continue
       
   376 
   378         # prepare criteria value to add to query
   377         # prepare criteria value to add to query
   379         envval = AIdb.sanitizeSQL(criteria[crit])
   378         envval = AIdb.sanitizeSQL(criteria[crit])
   380         if AIdb.isRangeCriteria(aisql.getQueue(), crit, AIdb.PROFILES_TABLE):
   379         if AIdb.isRangeCriteria(aisql.getQueue(), crit, AIdb.PROFILES_TABLE):
   381             if crit == "mac":
   380             # If no default profiles are requested, then we mustn't allow
   382                 nvpairs += ["(MIN" + crit + " IS NULL OR "
   381             # this criteria to be NULL.  It must match the client's given
   383                     "HEX(MIN" + crit + ")<=HEX(X'" + envval + "'))"]
   382             # value for this criteria.
   384                 nvpairs += ["(MAX" + crit + " IS NULL OR HEX(MAX" +
   383             if no_default:
       
   384                 if crit == "mac":
       
   385                     nvpairs += ["(HEX(MIN" + crit + ")<=HEX(X'" + envval + \
       
   386                         "'))"]
       
   387 
       
   388                     nvpairs += ["(HEX(MAX" + crit + ")>=HEX(X'" + envval + \
       
   389                         "'))"]
       
   390                 else:
       
   391                     nvpairs += ["(MIN" + crit + "<='" + envval + "')"]
       
   392                     nvpairs += ["(MAX" + crit + ">='" + envval + "')"]
       
   393             else:
       
   394                 if crit == "mac":
       
   395                     nvpairs += ["(MIN" + crit + " IS NULL OR "
       
   396                         "HEX(MIN" + crit + ")<=HEX(X'" + envval + "'))"]
       
   397                     nvpairs += ["(MAX" + crit + " IS NULL OR HEX(MAX" +
   385                         crit + ")>=HEX(X'" + envval + "'))"]
   398                         crit + ")>=HEX(X'" + envval + "'))"]
   386             else:
   399                 else:
   387                 nvpairs += ["(MIN" + crit + " IS NULL OR MIN" +
   400                     nvpairs += ["(MIN" + crit + " IS NULL OR MIN" +
   388                         crit + "<='" + envval + "')"]
   401                         crit + "<='" + envval + "')"]
   389                 nvpairs += ["(MAX" + crit + " IS NULL OR MAX" +
   402                     nvpairs += ["(MAX" + crit + " IS NULL OR MAX" +
   390                         crit + ">='" + envval + "')"]
   403                         crit + ">='" + envval + "')"]
   391         else:
   404         else:
   392             nvpairs += ["(" + crit + " IS NULL OR " +
   405             # If no default profiles are requested, then we mustn't allow
   393                     crit + "='" + envval + "')"]
   406             # this criteria to be NULL.  It must match the client's given
   394     q_str += " AND ".join(nvpairs)
   407             # value for this criteria.
   395 
   408             #
   396     # issue database query
   409             # Also, since this is a non-range criteria, the value stored
   397     logging.info("Profile query: " + q_str)
   410             # in the DB may be a whitespace separated list of single
   398     query = AIdb.DBrequest(q_str)
   411             # values.  We use a special user-defined function in the
   399     aisql.getQueue().put(query)
   412             # determine if the given criteria is in that textual list.
   400     query.waitAns()
   413             if no_default:
   401     if query.getResponse() is None or len(query.getResponse()) == 0:
   414                 nvpairs += ["(is_in_list('" + crit + "', '" + envval + "', " + \
   402         msgtxt = _("No profiles found.")
   415                     crit + ", 'None') == 1)"]
   403         client_msg += [msgtxt]
   416             else:
   404         logging.info(msgtxt)
   417                 nvpairs += ["(" + crit + " IS NULL OR is_in_list('" + crit + \
   405     else:
   418                     "', '" + envval + "', " + crit + ", 'None') == 1)"]
   406         for row in query.getResponse():
   419 
   407             profpath = row['file']
   420     if len(nvpairs) > 0:
   408             profname = row['name']
   421         q_str += " AND ".join(nvpairs)
   409             if profname is None:  # should not happen
   422 
   410                 profname = 'unnamed'
   423         # issue database query
   411             try:
   424         logging.info("Profile query: " + q_str)
   412                 if profpath is None:
   425         query = AIdb.DBrequest(q_str)
   413                     msgtxt = "Database record error - profile path is empty."
   426         aisql.getQueue().put(query)
       
   427         query.waitAns()
       
   428         if query.getResponse() is None or len(query.getResponse()) == 0:
       
   429             msgtxt = _("No profiles found.")
       
   430             client_msg += [msgtxt]
       
   431             logging.info(msgtxt)
       
   432         else:
       
   433             for row in query.getResponse():
       
   434                 profpath = row['file']
       
   435                 profname = row['name']
       
   436                 if profname is None:  # should not happen
       
   437                     profname = 'unnamed'
       
   438                 try:
       
   439                     if profpath is None:
       
   440                         msgtxt = "Database record error - profile path is empty."
       
   441                         client_msg += [msgtxt]
       
   442                         logging.error(msgtxt)
       
   443                         continue
       
   444                     msgtxt = _('Processing profile %s') % profname
       
   445                     client_msg += [msgtxt]
       
   446                     logging.info(msgtxt)
       
   447                     with open(profpath, 'r') as pfp:
       
   448                         raw_profile = pfp.read()
       
   449                     # do any template variable replacement {{AI_xxx}}
       
   450                     tmpl_profile = sc.perform_templating(raw_profile,
       
   451                                                          validate_only=False)
       
   452                     # precautionary validation or profile, logging only
       
   453                     sc.validate_profile_string(tmpl_profile, service_info[2],
       
   454                                                dtd_validation=True,
       
   455                                                warn_if_dtd_missing=True)
       
   456                 except IOError, err:
       
   457                     msgtxt = _("Error:  I/O error: ") + str(err)
   414                     client_msg += [msgtxt]
   458                     client_msg += [msgtxt]
   415                     logging.error(msgtxt)
   459                     logging.error(msgtxt)
   416                     continue
   460                     continue
   417                 msgtxt = _('Processing profile %s') % profname
   461                 except OSError:
       
   462                     msgtxt = _("Error:  OS error on profile ") + profpath
       
   463                     client_msg += [msgtxt]
       
   464                     logging.error(msgtxt)
       
   465                     continue
       
   466                 except KeyError:
       
   467                     msgtxt = _('Error:  could not find criteria to substitute in '
       
   468                             'template: ') + profpath
       
   469                     client_msg += [msgtxt]
       
   470                     logging.error(msgtxt)
       
   471                     logging.error('Profile with template substitution error:' +
       
   472                             raw_profile)
       
   473                     continue
       
   474                 except lxml.etree.XMLSyntaxError, err:
       
   475                     # log validation error and proceed
       
   476                     msgtxt = _(
       
   477                             'Warning:  syntax error found in profile: ') \
       
   478                             + profpath
       
   479                     client_msg += [msgtxt]
       
   480                     logging.error(msgtxt)
       
   481                     for error in err.error_log:
       
   482                         msgtxt = _('Error:  ') + error.message
       
   483                         client_msg += [msgtxt]
       
   484                         logging.error(msgtxt)
       
   485                     logging.info([_('Profile failing validation:  ') +
       
   486                                  lxml.etree.tostring(root)])
       
   487                 # build MIME message and attach to outer MIME message
       
   488                 msg = MIMEText(tmpl_profile, 'xml')
       
   489                 # indicate in header that this is an attachment
       
   490                 msg.add_header('Content-Disposition', 'attachment',
       
   491                                filename=profname)
       
   492                 # attach this profile to the manifest and any other profiles
       
   493                 outermime.attach(msg)
       
   494                 msgtxt = _('Parsed and loaded profile: ') + profname
   418                 client_msg += [msgtxt]
   495                 client_msg += [msgtxt]
   419                 logging.info(msgtxt)
   496                 logging.info(msgtxt)
   420                 with open(profpath, 'r') as pfp:
       
   421                     raw_profile = pfp.read()
       
   422                 # do any template variable replacement {{AI_xxx}}
       
   423                 tmpl_profile = sc.perform_templating(raw_profile,
       
   424                                                      validate_only=False)
       
   425                 # precautionary validation or profile, logging only
       
   426                 sc.validate_profile_string(tmpl_profile, service_info[2],
       
   427                                            dtd_validation=True,
       
   428                                            warn_if_dtd_missing=True)
       
   429             except IOError, err:
       
   430                 msgtxt = _("Error:  I/O error: ") + str(err)
       
   431                 client_msg += [msgtxt]
       
   432                 logging.error(msgtxt)
       
   433                 continue
       
   434             except OSError:
       
   435                 msgtxt = _("Error:  OS error on profile ") + profpath
       
   436                 client_msg += [msgtxt]
       
   437                 logging.error(msgtxt)
       
   438                 continue
       
   439             except KeyError:
       
   440                 msgtxt = _('Error:  could not find criteria to substitute in '
       
   441                         'template: ') + profpath
       
   442                 client_msg += [msgtxt]
       
   443                 logging.error(msgtxt)
       
   444                 logging.error('Profile with template substitution error:' +
       
   445                         raw_profile)
       
   446                 continue
       
   447             except lxml.etree.XMLSyntaxError, err:
       
   448                 # log validation error and proceed
       
   449                 msgtxt = _(
       
   450                         'Warning:  syntax error found in profile: ') \
       
   451                         + profpath
       
   452                 client_msg += [msgtxt]
       
   453                 logging.error(msgtxt)
       
   454                 for error in err.error_log:
       
   455                     msgtxt = _('Error:  ') + error.message
       
   456                     client_msg += [msgtxt]
       
   457                     logging.error(msgtxt)
       
   458                 logging.info([_('Profile failing validation:  ') +
       
   459                              lxml.etree.tostring(root)])
       
   460             # build MIME message and attach to outer MIME message
       
   461             msg = MIMEText(tmpl_profile, 'xml')
       
   462             # indicate in header that this is an attachment
       
   463             msg.add_header('Content-Disposition', 'attachment',
       
   464                            filename=profname)
       
   465             # attach this profile to the manifest and any other profiles
       
   466             outermime.attach(msg)
       
   467             msgtxt = _('Parsed and loaded profile: ') + profname
       
   468             client_msg += [msgtxt]
       
   469             logging.info(msgtxt)
       
   470 
   497 
   471     # any profiles and AI manifest have been attached to MIME message
   498     # any profiles and AI manifest have been attached to MIME message
   472     # specially format list of messages for display on AI client console
   499     # specially format list of messages for display on AI client console
   473     if client_msg:
   500     if client_msg:
   474         outtxt = ''
   501         outtxt = ''
   667     print '</body></html>'
   694     print '</body></html>'
   668 
   695 
   669 if __name__ == '__main__':
   696 if __name__ == '__main__':
   670     gettext.install("ai", "/usr/lib/locale")
   697     gettext.install("ai", "/usr/lib/locale")
   671     DEFAULT_PORT = libaimdns.getinteger_property(SRVINST, PORTPROP)
   698     DEFAULT_PORT = libaimdns.getinteger_property(SRVINST, PORTPROP)
   672     (PARAM_VERSION, SERVICE, FORM_DATA) = get_parameters(cgi.FieldStorage())
   699     (PARAM_VERSION, SERVICE, NO_DEFAULT, FORM_DATA) = \
   673     print >> sys.stderr, PARAM_VERSION, SERVICE, FORM_DATA
   700         get_parameters(cgi.FieldStorage())
       
   701     print >> sys.stderr, PARAM_VERSION, SERVICE, NO_DEFAULT, FORM_DATA
   674     if PARAM_VERSION == COMPATIBILITY_VERSION or SERVICE is None:
   702     if PARAM_VERSION == COMPATIBILITY_VERSION or SERVICE is None:
   675         # Old client
   703         # Old client
   676         (REQUEST_METHOD, REQUEST_PORT) = get_environment_information()
   704         (REQUEST_METHOD, REQUEST_PORT) = get_environment_information()
   677         if REQUEST_PORT == DEFAULT_PORT:  # only new clients use default port
   705         if REQUEST_PORT == DEFAULT_PORT:  # only new clients use default port
   678             HOST = socket.gethostname()
   706             HOST = socket.gethostname()
   695         list_manifests(SERVICE)
   723         list_manifests(SERVICE)
   696     else:
   724     else:
   697         # do manifest criteria match
   725         # do manifest criteria match
   698         try:
   726         try:
   699             send_manifest(FORM_DATA, servicename=SERVICE,
   727             send_manifest(FORM_DATA, servicename=SERVICE,
   700                           protocolversion=PARAM_VERSION)
   728                           protocolversion=PARAM_VERSION,
       
   729                           no_default=NO_DEFAULT)
   701         except StandardError:
   730         except StandardError:
   702             # send error report to client (through stdout), log
   731             # send error report to client (through stdout), log
   703             print "Content-Type: text/html"     # HTML is following
   732             print "Content-Type: text/html"     # HTML is following
   704             print                               # blank line, end of headers
   733             print                               # blank line, end of headers
   705             ERRMSG = _(
   734             ERRMSG = _(