kiwi.xml_description

  1# Copyright (c) 2015 SUSE Linux GmbH.  All rights reserved.
  2#
  3# This file is part of kiwi.
  4#
  5# kiwi is free software: you can redistribute it and/or modify
  6# it under the terms of the GNU General Public License as published by
  7# the Free Software Foundation, either version 3 of the License, or
  8# (at your option) any later version.
  9#
 10# kiwi is distributed in the hope that it will be useful,
 11# but WITHOUT ANY WARRANTY; without even the implied warranty of
 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13# GNU General Public License for more details.
 14#
 15# You should have received a copy of the GNU General Public License
 16# along with kiwi.  If not, see <http://www.gnu.org/licenses/>
 17#
 18import importlib
 19from typing import (
 20    Dict, Any
 21)
 22import os
 23import logging
 24from xml.dom import minidom
 25from lxml import etree
 26
 27# project
 28from kiwi.utils.temporary import Temporary
 29from kiwi.markup import Markup
 30from kiwi.defaults import Defaults
 31from kiwi import xml_parse
 32from kiwi.command import Command
 33
 34from kiwi.exceptions import (
 35    KiwiCommandError,
 36    KiwiSchemaImportError,
 37    KiwiValidationError,
 38    KiwiDescriptionInvalid,
 39    KiwiDataStructureError,
 40    KiwiExtensionError,
 41    KiwiCommandNotFound
 42)
 43
 44log = logging.getLogger('kiwi')
 45
 46
 47class XMLDescription:
 48    """
 49    **Implements data management for the image description**
 50
 51    Supported description markup languages are XML, YAML, JSON and INI.
 52    The provided input file is converted into XML, transformed to the
 53    current RelaxNG schema via XSLT and validated against this result.
 54
 55    * XSLT Style Sheet processing to apply on this version of kiwi
 56    * Schema Validation based on RelaxNG schema
 57    * Loading XML data into internal data structures
 58
 59    Attributes
 60
 61    :param str description: path to description file
 62    :param str derived_from: path to base description file
 63    """
 64    def __init__(
 65        self, description: str = '', derived_from: str = None
 66    ):
 67        log.info(f'Loading XML description: {description}')
 68        self.markup = Markup.new(description)
 69        self.description = self.markup.get_xml_description()
 70        self.derived_from = derived_from
 71        self.description_origin = description
 72        self.extension_data: Dict = {}
 73
 74    def load(self) -> Any:
 75        """
 76        Read XML description, validate it against the schema
 77        and the schematron rules and pass it to the
 78        autogenerated(generateDS) parser.
 79
 80        :return: instance of XML toplevel domain (image)
 81
 82        :rtype: object
 83        """
 84        isoschematron = None
 85        schematron = None
 86        try:
 87            isoschematron = importlib.import_module(
 88                Defaults.get_schematron_module_name()
 89            )
 90        except Exception as error:
 91            log.warning(f"schematron validation skipped: {error}")
 92        try:
 93            schema_doc = etree.parse(Defaults.get_schema_file())
 94            relaxng = etree.RelaxNG(schema_doc)
 95            if isoschematron:
 96                schematron = isoschematron.Schematron(
 97                    schema_doc, store_report=True
 98                )
 99        except Exception as issue:
100            raise KiwiSchemaImportError(issue)
101        try:
102            description = etree.parse(self.description)
103            validation_rng = relaxng.validate(description)
104            if schematron:
105                validation_schematron = schematron.validate(description)
106        except Exception as issue:
107            raise KiwiValidationError(issue)
108        if not validation_rng:
109            XMLDescription._get_relaxng_validation_details(
110                Defaults.get_schema_file(),
111                self.description,
112                relaxng.error_log
113            )
114        if schematron and not validation_schematron:
115            XMLDescription._get_schematron_validation_details(
116                schematron.validation_report
117            )
118        if not validation_rng or (schematron and not validation_schematron):
119            log.debug(open(self.description).read())
120            raise KiwiDescriptionInvalid(
121                'Failed to validate schema and/or schematron rules. '
122                'Use --debug for more details'
123            )
124
125        parse_result = self._parse()
126
127        if parse_result.get_extension():
128            extension_namespace_map = \
129                description.getroot().xpath('extension')[0].nsmap
130
131            for namespace_name in extension_namespace_map:
132                extensions_for_namespace = description.getroot().xpath(
133                    'extension/{namespace}:*'.format(namespace=namespace_name),
134                    namespaces=extension_namespace_map
135                )
136                if extensions_for_namespace:
137                    # one toplevel entry point per extension via xmlns
138                    if len(extensions_for_namespace) > 1:
139                        raise KiwiExtensionError(
140                            'Multiple toplevel sections for "{0}" found'.format(
141                                namespace_name
142                            )
143                        )
144
145                    # store extension xml data parse tree for this namespace
146                    self.extension_data[namespace_name] = \
147                        etree.ElementTree(extensions_for_namespace[0])
148
149                    # validate extension xml data
150                    try:
151                        xml_catalog = Command.run(
152                            [
153                                'xmlcatalog', '/etc/xml/catalog',
154                                extension_namespace_map[namespace_name]
155                            ]
156                        )
157                        extension_schema = xml_catalog.output.rstrip().replace(
158                            'file://', ''
159                        )
160                        extension_relaxng = etree.RelaxNG(
161                            etree.parse(extension_schema)
162                        )
163                    except Exception as issue:
164                        raise KiwiExtensionError(
165                            'Extension schema error: {0}'.format(issue)
166                        )
167                    validation_result = extension_relaxng.validate(
168                        self.extension_data[namespace_name]
169                    )
170                    if not validation_result:
171                        xml_data_unformatted = etree.tostring(
172                            self.extension_data[namespace_name],
173                            encoding='utf-8'
174                        )
175                        xml_data_domtree = minidom.parseString(
176                            xml_data_unformatted
177                        )
178                        extension_file = Temporary().new_file()
179                        with open(extension_file.name, 'w') as xml_data:
180                            xml_data.write(xml_data_domtree.toprettyxml())
181                        XMLDescription._get_relaxng_validation_details(
182                            extension_schema,
183                            extension_file.name,
184                            extension_relaxng.error_log
185                        )
186                        raise KiwiExtensionError(
187                            'Schema validation for extension XML data failed'
188                        )
189
190        return parse_result
191
192    def get_extension_xml_data(self, namespace_name: str) -> Any:
193        """
194        Return the xml etree parse result for the specified extension namespace
195
196        :param str namespace_name: name of the extension namespace
197
198        :return: result of etree.parse
199
200        :rtype: object
201        """
202        return self.extension_data.get(namespace_name)
203
204    @staticmethod
205    def _get_relaxng_validation_details(
206        schema_file, description_file, error_log
207    ):
208        """
209        Run jing program to validate description against the schema
210
211        Jing provides detailed error information in case of a schema
212        validation failure. If jing is not present the standard
213        error_log as provided from the raw XML libraries is used
214        """
215        try:
216            Command.run(
217                ['jing', schema_file, description_file]
218            )
219        except KiwiCommandError as issue:
220            log.info('RelaxNG validation failed. See jing report:')
221            log.info('--> {0}'.format(issue))
222        except KiwiCommandNotFound as issue:
223            log.warning(issue)
224            log.warning(
225                'For detailed schema validation report, install: jing'
226            )
227            log.info('Showing only raw library error log:')
228            log.info('--> {0}'.format(error_log))
229
230    @staticmethod
231    def _get_schematron_validation_details(validation_report):
232        """
233        Extract error message form the schematron validation report
234
235        :param etree validation_report: the schematron validation report
236        """
237        nspaces = validation_report.getroot().nsmap
238        log.info('Schematron validation failed:')
239        for msg in validation_report.xpath(
240            '//svrl:failed-assert/svrl:text', namespaces=nspaces
241        ):
242            log.info('--> %s', msg.text)
243
244    def _parse(self):
245        try:
246            parse = xml_parse.parse(
247                self.description, True
248            )
249            parse.description_dir = self.description_origin and os.path.dirname(
250                self.description_origin
251            )
252            parse.derived_description_dir = self.derived_from
253            return parse
254        except Exception as issue:
255            raise KiwiDataStructureError(issue)
log = <Logger kiwi (DEBUG)>
class XMLDescription:
 48class XMLDescription:
 49    """
 50    **Implements data management for the image description**
 51
 52    Supported description markup languages are XML, YAML, JSON and INI.
 53    The provided input file is converted into XML, transformed to the
 54    current RelaxNG schema via XSLT and validated against this result.
 55
 56    * XSLT Style Sheet processing to apply on this version of kiwi
 57    * Schema Validation based on RelaxNG schema
 58    * Loading XML data into internal data structures
 59
 60    Attributes
 61
 62    :param str description: path to description file
 63    :param str derived_from: path to base description file
 64    """
 65    def __init__(
 66        self, description: str = '', derived_from: str = None
 67    ):
 68        log.info(f'Loading XML description: {description}')
 69        self.markup = Markup.new(description)
 70        self.description = self.markup.get_xml_description()
 71        self.derived_from = derived_from
 72        self.description_origin = description
 73        self.extension_data: Dict = {}
 74
 75    def load(self) -> Any:
 76        """
 77        Read XML description, validate it against the schema
 78        and the schematron rules and pass it to the
 79        autogenerated(generateDS) parser.
 80
 81        :return: instance of XML toplevel domain (image)
 82
 83        :rtype: object
 84        """
 85        isoschematron = None
 86        schematron = None
 87        try:
 88            isoschematron = importlib.import_module(
 89                Defaults.get_schematron_module_name()
 90            )
 91        except Exception as error:
 92            log.warning(f"schematron validation skipped: {error}")
 93        try:
 94            schema_doc = etree.parse(Defaults.get_schema_file())
 95            relaxng = etree.RelaxNG(schema_doc)
 96            if isoschematron:
 97                schematron = isoschematron.Schematron(
 98                    schema_doc, store_report=True
 99                )
100        except Exception as issue:
101            raise KiwiSchemaImportError(issue)
102        try:
103            description = etree.parse(self.description)
104            validation_rng = relaxng.validate(description)
105            if schematron:
106                validation_schematron = schematron.validate(description)
107        except Exception as issue:
108            raise KiwiValidationError(issue)
109        if not validation_rng:
110            XMLDescription._get_relaxng_validation_details(
111                Defaults.get_schema_file(),
112                self.description,
113                relaxng.error_log
114            )
115        if schematron and not validation_schematron:
116            XMLDescription._get_schematron_validation_details(
117                schematron.validation_report
118            )
119        if not validation_rng or (schematron and not validation_schematron):
120            log.debug(open(self.description).read())
121            raise KiwiDescriptionInvalid(
122                'Failed to validate schema and/or schematron rules. '
123                'Use --debug for more details'
124            )
125
126        parse_result = self._parse()
127
128        if parse_result.get_extension():
129            extension_namespace_map = \
130                description.getroot().xpath('extension')[0].nsmap
131
132            for namespace_name in extension_namespace_map:
133                extensions_for_namespace = description.getroot().xpath(
134                    'extension/{namespace}:*'.format(namespace=namespace_name),
135                    namespaces=extension_namespace_map
136                )
137                if extensions_for_namespace:
138                    # one toplevel entry point per extension via xmlns
139                    if len(extensions_for_namespace) > 1:
140                        raise KiwiExtensionError(
141                            'Multiple toplevel sections for "{0}" found'.format(
142                                namespace_name
143                            )
144                        )
145
146                    # store extension xml data parse tree for this namespace
147                    self.extension_data[namespace_name] = \
148                        etree.ElementTree(extensions_for_namespace[0])
149
150                    # validate extension xml data
151                    try:
152                        xml_catalog = Command.run(
153                            [
154                                'xmlcatalog', '/etc/xml/catalog',
155                                extension_namespace_map[namespace_name]
156                            ]
157                        )
158                        extension_schema = xml_catalog.output.rstrip().replace(
159                            'file://', ''
160                        )
161                        extension_relaxng = etree.RelaxNG(
162                            etree.parse(extension_schema)
163                        )
164                    except Exception as issue:
165                        raise KiwiExtensionError(
166                            'Extension schema error: {0}'.format(issue)
167                        )
168                    validation_result = extension_relaxng.validate(
169                        self.extension_data[namespace_name]
170                    )
171                    if not validation_result:
172                        xml_data_unformatted = etree.tostring(
173                            self.extension_data[namespace_name],
174                            encoding='utf-8'
175                        )
176                        xml_data_domtree = minidom.parseString(
177                            xml_data_unformatted
178                        )
179                        extension_file = Temporary().new_file()
180                        with open(extension_file.name, 'w') as xml_data:
181                            xml_data.write(xml_data_domtree.toprettyxml())
182                        XMLDescription._get_relaxng_validation_details(
183                            extension_schema,
184                            extension_file.name,
185                            extension_relaxng.error_log
186                        )
187                        raise KiwiExtensionError(
188                            'Schema validation for extension XML data failed'
189                        )
190
191        return parse_result
192
193    def get_extension_xml_data(self, namespace_name: str) -> Any:
194        """
195        Return the xml etree parse result for the specified extension namespace
196
197        :param str namespace_name: name of the extension namespace
198
199        :return: result of etree.parse
200
201        :rtype: object
202        """
203        return self.extension_data.get(namespace_name)
204
205    @staticmethod
206    def _get_relaxng_validation_details(
207        schema_file, description_file, error_log
208    ):
209        """
210        Run jing program to validate description against the schema
211
212        Jing provides detailed error information in case of a schema
213        validation failure. If jing is not present the standard
214        error_log as provided from the raw XML libraries is used
215        """
216        try:
217            Command.run(
218                ['jing', schema_file, description_file]
219            )
220        except KiwiCommandError as issue:
221            log.info('RelaxNG validation failed. See jing report:')
222            log.info('--> {0}'.format(issue))
223        except KiwiCommandNotFound as issue:
224            log.warning(issue)
225            log.warning(
226                'For detailed schema validation report, install: jing'
227            )
228            log.info('Showing only raw library error log:')
229            log.info('--> {0}'.format(error_log))
230
231    @staticmethod
232    def _get_schematron_validation_details(validation_report):
233        """
234        Extract error message form the schematron validation report
235
236        :param etree validation_report: the schematron validation report
237        """
238        nspaces = validation_report.getroot().nsmap
239        log.info('Schematron validation failed:')
240        for msg in validation_report.xpath(
241            '//svrl:failed-assert/svrl:text', namespaces=nspaces
242        ):
243            log.info('--> %s', msg.text)
244
245    def _parse(self):
246        try:
247            parse = xml_parse.parse(
248                self.description, True
249            )
250            parse.description_dir = self.description_origin and os.path.dirname(
251                self.description_origin
252            )
253            parse.derived_description_dir = self.derived_from
254            return parse
255        except Exception as issue:
256            raise KiwiDataStructureError(issue)

Implements data management for the image description

Supported description markup languages are XML, YAML, JSON and INI. The provided input file is converted into XML, transformed to the current RelaxNG schema via XSLT and validated against this result.

  • 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

Parameters
  • str description: path to description file
  • str derived_from: path to base description file
XMLDescription(description: str = '', derived_from: str = None)
65    def __init__(
66        self, description: str = '', derived_from: str = None
67    ):
68        log.info(f'Loading XML description: {description}')
69        self.markup = Markup.new(description)
70        self.description = self.markup.get_xml_description()
71        self.derived_from = derived_from
72        self.description_origin = description
73        self.extension_data: Dict = {}
markup
description
derived_from
description_origin
extension_data: Dict
def load(self) -> Any:
 75    def load(self) -> Any:
 76        """
 77        Read XML description, validate it against the schema
 78        and the schematron rules and pass it to the
 79        autogenerated(generateDS) parser.
 80
 81        :return: instance of XML toplevel domain (image)
 82
 83        :rtype: object
 84        """
 85        isoschematron = None
 86        schematron = None
 87        try:
 88            isoschematron = importlib.import_module(
 89                Defaults.get_schematron_module_name()
 90            )
 91        except Exception as error:
 92            log.warning(f"schematron validation skipped: {error}")
 93        try:
 94            schema_doc = etree.parse(Defaults.get_schema_file())
 95            relaxng = etree.RelaxNG(schema_doc)
 96            if isoschematron:
 97                schematron = isoschematron.Schematron(
 98                    schema_doc, store_report=True
 99                )
100        except Exception as issue:
101            raise KiwiSchemaImportError(issue)
102        try:
103            description = etree.parse(self.description)
104            validation_rng = relaxng.validate(description)
105            if schematron:
106                validation_schematron = schematron.validate(description)
107        except Exception as issue:
108            raise KiwiValidationError(issue)
109        if not validation_rng:
110            XMLDescription._get_relaxng_validation_details(
111                Defaults.get_schema_file(),
112                self.description,
113                relaxng.error_log
114            )
115        if schematron and not validation_schematron:
116            XMLDescription._get_schematron_validation_details(
117                schematron.validation_report
118            )
119        if not validation_rng or (schematron and not validation_schematron):
120            log.debug(open(self.description).read())
121            raise KiwiDescriptionInvalid(
122                'Failed to validate schema and/or schematron rules. '
123                'Use --debug for more details'
124            )
125
126        parse_result = self._parse()
127
128        if parse_result.get_extension():
129            extension_namespace_map = \
130                description.getroot().xpath('extension')[0].nsmap
131
132            for namespace_name in extension_namespace_map:
133                extensions_for_namespace = description.getroot().xpath(
134                    'extension/{namespace}:*'.format(namespace=namespace_name),
135                    namespaces=extension_namespace_map
136                )
137                if extensions_for_namespace:
138                    # one toplevel entry point per extension via xmlns
139                    if len(extensions_for_namespace) > 1:
140                        raise KiwiExtensionError(
141                            'Multiple toplevel sections for "{0}" found'.format(
142                                namespace_name
143                            )
144                        )
145
146                    # store extension xml data parse tree for this namespace
147                    self.extension_data[namespace_name] = \
148                        etree.ElementTree(extensions_for_namespace[0])
149
150                    # validate extension xml data
151                    try:
152                        xml_catalog = Command.run(
153                            [
154                                'xmlcatalog', '/etc/xml/catalog',
155                                extension_namespace_map[namespace_name]
156                            ]
157                        )
158                        extension_schema = xml_catalog.output.rstrip().replace(
159                            'file://', ''
160                        )
161                        extension_relaxng = etree.RelaxNG(
162                            etree.parse(extension_schema)
163                        )
164                    except Exception as issue:
165                        raise KiwiExtensionError(
166                            'Extension schema error: {0}'.format(issue)
167                        )
168                    validation_result = extension_relaxng.validate(
169                        self.extension_data[namespace_name]
170                    )
171                    if not validation_result:
172                        xml_data_unformatted = etree.tostring(
173                            self.extension_data[namespace_name],
174                            encoding='utf-8'
175                        )
176                        xml_data_domtree = minidom.parseString(
177                            xml_data_unformatted
178                        )
179                        extension_file = Temporary().new_file()
180                        with open(extension_file.name, 'w') as xml_data:
181                            xml_data.write(xml_data_domtree.toprettyxml())
182                        XMLDescription._get_relaxng_validation_details(
183                            extension_schema,
184                            extension_file.name,
185                            extension_relaxng.error_log
186                        )
187                        raise KiwiExtensionError(
188                            'Schema validation for extension XML data failed'
189                        )
190
191        return parse_result

Read XML description, validate it against the schema and the schematron rules and pass it to the autogenerated(generateDS) parser.

Returns

instance of XML toplevel domain (image)

def get_extension_xml_data(self, namespace_name: str) -> Any:
193    def get_extension_xml_data(self, namespace_name: str) -> Any:
194        """
195        Return the xml etree parse result for the specified extension namespace
196
197        :param str namespace_name: name of the extension namespace
198
199        :return: result of etree.parse
200
201        :rtype: object
202        """
203        return self.extension_data.get(namespace_name)

Return the xml etree parse result for the specified extension namespace

Parameters
  • str namespace_name: name of the extension namespace
Returns

result of etree.parse