#!/usr/bin/python2.7 -E # # Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. # ''' This CGI takes two params: - output: contains the XML text to be verified and/or written to a file - server_action: one of - verify_xml - save_local - save_server For all server-actions except save_local, it returns a JSON object with the following fields: - errno: 0 for success, non-zero for error - err: if errno is non-zero, this will contain a text description of error - msg1: optionally may contain more details of error - msg2: optionally may contain more details of error - filename: is server_action=save_server and no error occurred, will contain name of output file on server For server-action=save_local, it returns a HTTP attachment containing the contents of 'output' ''' import cgi from io import StringIO import json import logging from lxml import etree import operator import os import re import sys import tempfile from solaris_install import AI_MANIFEST_ATTACHMENT_NAME, PERM_OWNER_RW, \ SCRIPT_LOGGER_NAME from solaris_install.ai.server import Server from solaris_install.ai.server.profile import WEBSERVD_GID, WEBSERVD_UID from solaris_install.version import DTD_VERSION_AI MAX_MANIFEST_FILES = 100 VERSIONED_DTD = 'ai.dtd.%s' % (DTD_VERSION_AI) DEFAULT_DTD_FILE = "/usr/share/install/%s" % (VERSIONED_DTD) SERVER_MANIFEST_SAVE_DIR = '/var/ai/wizard-manifest' # These are all the possible values returned in the 'errno' # field of the returned JSON data. SUCCESS = 0 # Task completed successfully ERR_NO_XML = 1 # No 'output' param supplied ERR_NO_ACTION = 2 # No 'server_action' param supplied ERR_MANIFEST_IO = 3 # IOError accessing Manifest ERR_MANIFEST_XML = 4 # XMLSyntaxError accessing Manifest ERR_DTD_PARSING = 5 # DTD could not be parsed ERR_DTD_VALIDATION = 6 # Manifest did not validate against DTD ERR_NO_SERVER_SAVE = 7 # all_services/wizard_saves_to_server set to false ERR_LOCAL_SAVE_FAILED = 8 # Unspecified failure sending attachment back ERR_SERVER_SAVE_FAILED = 9 # Unspecified failure saving locally on server ERR_INTERNAL = 10 # Catchall # # Implement an external entity resolver to specifically disallow its use. # class InvalidEntityResolution(Exception): '''Exception thrown anytime an attempt is made to resolve an external entity ''' pass class DisableEntityResolution(etree.Resolver): '''Fail on any attempt to resolve an external entity''' def resolve(self, system_url, public_id, context): raise InvalidEntityResolution("Unable to resolve external entity %s" % system_url) def get_dtd_for_service(this_service): '''Retrieve the DTD used for this service''' dtd_file = None services = Server.get_instance().get_services() for svc in services: if svc.name == this_service: # ai.dtd is a symlink'd file to the correct versioned ai.dtd svc_dtd = os.path.join(svc.image_path, 'auto_install/ai.dtd') # Ensure file exists if os.path.exists(svc_dtd): dtd_file = svc_dtd return dtd_file def parse_validate_xml(form, ret_obj): '''Parse and validate XML to be written''' # fetch the XML text string passed in from the browser input_manifest = StringIO(form['output'].value) # create an XML parser and parse the XML text parser = etree.XMLParser(remove_blank_text=True, dtd_validation=False, attribute_defaults=False) # Disable external entity resolution parser.resolvers.add(DisableEntityResolution()) try: tree = etree.parse(input_manifest, parser) except IOError, error: ret_obj['errno'] = ERR_MANIFEST_IO ret_obj['err'] = "IOError accessing manifest" ret_obj['msg1'] = "[%s]" % (input_manifest) ret_obj['msg2'] = "[%s]" % (error) return None except etree.XMLSyntaxError, error: ret_obj['errno'] = ERR_MANIFEST_XML ret_obj['err'] = "XMLSyntaxError processing manifest" ret_obj['msg1'] = "[%s]" % (input_manifest) ret_obj['msg2'] = "[%s]" % (error) return None except InvalidEntityResolution, error: ret_obj['errno'] = ERR_MANIFEST_XML ret_obj['err'] = "Error processing external entities in manifest" ret_obj['msg1'] = "[%s]" % (input_manifest) ret_obj['msg2'] = "[%s]" % (error) return None # Find which DTD to validate against: either the default one # in /usr/share/... or the AI service-specifc one dtd_file = None if 'service' in form: this_service = form['service'].value.strip('\'') dtd_file = get_dtd_for_service(this_service) if dtd_file is None: dtd_file = DEFAULT_DTD_FILE # validate against the DTD try: dtd = etree.DTD(dtd_file) except etree.DTDParseError, error: ret_obj['errno'] = ERR_DTD_PARSING ret_obj['err'] = "DTD parsing error" ret_obj['msg1'] = "[%s]" % (dtd_file) return None if not dtd.validate(tree.getroot()): ret_obj['errno'] = ERR_DTD_VALIDATION ret_obj['err'] = "DTD validation failed" ret_obj['msg1'] = "[%s]" % (dtd_file) msg = "" for error in dtd.error_log.filter_from_errors(): msg = msg + str(error) + " : " ret_obj['msg2'] = msg return None # Convert back to text and return return etree.tostring(tree, pretty_print=True) def save_server(output_text, ret_obj): ''' Check manifest file location, if >= MAX_MANIFEST_FILES files exist created by webserver user, remove oldest file(s), leaving at most MAX_MANIFEST_FILES Files owned by other users will be ignored as only webserer user should be writing files into this dir. ''' # Double check that we are allowed to save to the server before trying it if not Server.get_instance().wizard_saves_to_server: # This scenario should never happen as client should never call # server side saving if not configured to do so ret_obj['errno'] = ERR_NO_SERVER_SAVE ret_obj['err'] = "Server side saving is disabled." return # From here on we know server side save is enabled # Get webservd users's UID and GID for file ownership comparison # Get list of all files if not os.path.exists(SERVER_MANIFEST_SAVE_DIR): ret_obj['errno'] = ERR_SERVER_SAVE_FAILED ret_obj['err'] = "Server side save location does not exist." return files = os.listdir(SERVER_MANIFEST_SAVE_DIR) ftimes = {} for fl in files: fp = os.path.join(SERVER_MANIFEST_SAVE_DIR, fl) fstat = os.stat(fp) if fstat.st_uid == WEBSERVD_UID and fstat.st_gid == WEBSERVD_GID: ftimes[fp] = fstat.st_mtime if len(ftimes) >= MAX_MANIFEST_FILES: sfiles = sorted(ftimes.iteritems(), key=operator.itemgetter(1)) for cnt in range(0, len(ftimes) - (MAX_MANIFEST_FILES - 1)): os.remove(sfiles[cnt][0]) # Attempt to create new temporary manifest file (_tmp_fh, tmp_file) = tempfile.mkstemp(dir=SERVER_MANIFEST_SAVE_DIR) # Ensure manifest file created is owned by webservd os.chmod(tmp_file, PERM_OWNER_RW) os.chown(tmp_file, WEBSERVD_UID, WEBSERVD_GID) with open(tmp_file, 'w') as fh: fh.write("%s\n" % output_text) # Return new temporary file name for display to user ret_obj['filename'] = tmp_file def save_local(form, output_text): '''Save manifest locally''' # Tell the browser how to render the text print "Content-Type: text/xml" print "Content-Disposition: attachment; filename=%s\n" % \ (AI_MANIFEST_ATTACHMENT_NAME) # Check client OS is UNIX or Windows, remove \r from string if Unix if 'OSName' in form and form['OSName'].value == "Unix": regexp = re.compile(r'[\r]') print regexp.sub("", output_text) else: print output_text def print_json(ret_obj): '''Print JSON text''' # Print JSON output for browser and exit script print "Content-Type: text/json\n" print json.dumps(ret_obj) sys.exit(0) def main(): '''Main writeManifest method''' ret_obj = { 'errno': SUCCESS, 'err': None, 'msg1': None, 'msg2': None, 'filename': None } try: form = cgi.FieldStorage() if ('output' not in form): ret_obj['errno'] = ERR_NO_XML ret_obj['err'] = "No XML received" print_json(ret_obj) if ('server_action' not in form): ret_obj['errno'] = ERR_NO_ACTION ret_obj['err'] = "No server action specified" print_json(ret_obj) # Configure script logger to output to stderr so logging gets stored # in apache error_log logging.basicConfig(stream=sys.stderr, level=logging.INFO, format="%(message)s") logging.getLogger(SCRIPT_LOGGER_NAME) # Ensure default handlers created are at the level we want here. for handler in logging.getLogger().handlers: handler.setLevel(logging.INFO) # Parse the XML and validate against DTD output_text = parse_validate_xml(form, ret_obj) if output_text is None or ret_obj['err'] is not None: print_json(ret_obj) if (form['server_action'].value == "validate_xml"): # Just validating so simply return straight away print_json(ret_obj) elif (form['server_action'].value == "save_local"): try: save_local(form, output_text) except: ret_obj['errno'] = ERR_LOCAL_SAVE_FAILED ret_obj['err'] = "Failed to save manifest locally" print_json(ret_obj) elif (form['server_action'].value == "save_server"): try: save_server(output_text, ret_obj) except: ret_obj['errno'] = ERR_SERVER_SAVE_FAILED ret_obj['err'] = "Failed to save manifest to server" print_json(ret_obj) except SystemExit: pass except: # Catch all other possible exceptions to ensure valid output # is returned to the client ret_obj['errno'] = ERR_INTERNAL ret_obj['err'] = "Internal server error" print_json(ret_obj) if __name__ == "__main__": main()