# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # kiwi is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with kiwi. If not, see # from xml.dom import minidom from lxml import ( etree, isoschematron ) from tempfile import NamedTemporaryFile import os from six import BytesIO # In python2 bytes is string which is different from # the bytes type in python3. The bytes type from the # builtins generalizes this type to be bytes always from builtins import bytes # project from .defaults import Defaults from . import xml_parse from .command import Command from .logger import log from .exceptions import ( KiwiSchemaImportError, KiwiValidationError, KiwiDescriptionInvalid, KiwiDataStructureError, KiwiDescriptionConflict, KiwiExtensionError, KiwiCommandNotFound ) class XMLDescription(object): """ Implements data management for the XML description * XSLT Style Sheet processing to apply on this version of kiwi * Schema Validation based on RelaxNG schema * Loading XML data into internal data structures Attributes * :attr:`description_xslt_processed` XML data after xsltproc processing * :attr:`description` path to XML description file * :attr:`derived_from` path to base XML description file * :attr:`xml_content` XML description data as content string """ def __init__(self, description=None, derived_from=None, xml_content=None): if description and xml_content: raise KiwiDescriptionConflict( 'description and xml_content are mutually exclusive' ) self.description_xslt_processed = NamedTemporaryFile() self.derived_from = derived_from self.description = description self.xml_content = xml_content self.extension_data = {} def load(self): """ Read XML description, pass it along to the XSLT processor, validate it against the schema and finally pass it to the autogenerated(generateDS) parser. :return: instance of XML toplevel domain (image) :rtype: object """ try: self._xsltproc() schema_doc = etree.parse(Defaults.get_schema_file()) relaxng = etree.RelaxNG(schema_doc) schematron = isoschematron.Schematron( schema_doc, store_report=True ) except Exception as e: raise KiwiSchemaImportError( '%s: %s' % (type(e).__name__, format(e)) ) try: description = etree.parse(self.description_xslt_processed.name) validation_rng = relaxng.validate(description) validation_sch = schematron.validate(description) except Exception as e: raise KiwiValidationError( '%s: %s' % (type(e).__name__, format(e)) ) if not validation_rng: self._get_relaxng_validation_details( Defaults.get_schema_file(), self.description_xslt_processed.name ) if not validation_sch: self._get_schematron_validation_details( schematron.validation_report ) if not validation_rng or not validation_sch: if self.description: message = 'Schema validation for {description} failed'.format( description=self.description ) else: message = 'Schema validation for XML content failed' raise KiwiDescriptionInvalid(message) parse_result = self._parse() if parse_result.get_extension(): extension_namespace_map = \ description.getroot().xpath('extension')[0].nsmap for namespace_name in extension_namespace_map: extensions_for_namespace = description.getroot().xpath( 'extension/{namespace}:*'.format(namespace=namespace_name), namespaces=extension_namespace_map ) if extensions_for_namespace: # one toplevel entry point per extension via xmlns if len(extensions_for_namespace) > 1: raise KiwiExtensionError( 'Multiple toplevel sections for "{0}" found'.format( namespace_name ) ) # store extension xml data parse tree for this namespace self.extension_data[namespace_name] = \ etree.ElementTree(extensions_for_namespace[0]) # validate extension xml data try: xml_catalog = Command.run( [ 'xmlcatalog', '/etc/xml/catalog', extension_namespace_map[namespace_name] ] ) extension_schema = xml_catalog.output.rstrip().replace( 'file://', '' ) extension_relaxng = etree.RelaxNG( etree.parse(extension_schema) ) except Exception as e: raise KiwiExtensionError( 'Extension schema error: {0}: {1}'.format( type(e).__name__, e ) ) validation_result = extension_relaxng.validate( self.extension_data[namespace_name] ) if not validation_result: xml_data_unformatted = etree.tostring( self.extension_data[namespace_name], encoding='utf-8' ) xml_data_domtree = minidom.parseString( xml_data_unformatted ) extension_file = NamedTemporaryFile() with open(extension_file.name, 'w') as xml_data: xml_data.write(xml_data_domtree.toprettyxml()) self._get_relaxng_validation_details( extension_schema, extension_file.name ) raise KiwiExtensionError( 'Schema validation for extension XML data failed' ) return parse_result def get_extension_xml_data(self, namespace_name): """ Return the xml etree parse result for the specified extension namespace :param string namespace_name: name of the extension namespace :return: result of etree.parse :rtype: object """ if namespace_name in self.extension_data: return self.extension_data[namespace_name] def _get_relaxng_validation_details(self, schema_file, description_file): """ Run jing process to validate description against the schema Jing command provides detailed feedback in case of schema validation failure """ try: cmd = Command.run( ['jing', schema_file, description_file], raise_on_error=False ) except KiwiCommandNotFound as e: log.info( 'For detailed schema validation report, please install: jing' ) log.info( '%s: %s: %s', 'jing', type(e).__name__, format(e) ) return log.info('RelaxNG validation failed. See jing report:') log.info('--> %s', cmd.output) def _get_schematron_validation_details(self, validation_report): """ Extract error message form the schematron validation report :param etree validation_report: the schematron validation report """ nspaces = validation_report.getroot().nsmap log.info('Schematron validation failed:') for msg in validation_report.xpath( '//svrl:failed-assert/svrl:text', namespaces=nspaces ): log.info('--> %s', msg.text) def _parse(self): try: parse = xml_parse.parse( self.description_xslt_processed.name, True ) parse.description_dir = self.description and os.path.dirname( self.description ) parse.derived_description_dir = self.derived_from return parse except Exception as e: raise KiwiDataStructureError( '%s: %s' % (type(e).__name__, format(e)) ) def _xsltproc(self): """ Apply XSLT style sheet rules to the XML data The result of the XSLT processing is stored in a named temporary file and used for further schema validation and parsing into the data structure classes """ xslt_transform = etree.XSLT( etree.parse(Defaults.get_xsl_stylesheet_file()) ) xml_source = self.description if self.description else \ BytesIO(bytes(self.xml_content)) with open(self.description_xslt_processed.name, "wb") as xsltout: xsltout.write( etree.tostring(xslt_transform(etree.parse(xml_source))) )