config_tools: check XML file structures on load

This patch validates the structure of the XML files given before they are
further used by the rest of the configurator. With this validation process,
the configurator confirms the XML files are well-structured and can thus
access certain nodes and contents without checking their existence or data
types.

Upon validation failure, an alert will pop up informing the user that the
given XML file is ill-formed. No further details are given as of now
because we assume users should not care about the internal structure of
those files.

Tracked-On: #6691
Signed-off-by: Junjie Mao <junjie.mao@intel.com>
This commit is contained in:
Junjie Mao 2022-05-18 13:44:13 +08:00 committed by acrnsi-robot
parent 54ff64acc1
commit 22a47fe795
10 changed files with 201 additions and 23 deletions

View File

@ -11,31 +11,43 @@ enum HistoryType {
export type HistoryTypeString = keyof typeof HistoryType;
class PythonObject {
api(scriptName, ...params) {
api(scriptName, output_format, ...params) {
// @ts-ignore
let pythonFunction = window.pyodide.pyimport(`configurator.pyodide.${scriptName}`);
let result = pythonFunction.main(...params);
return JSON.parse(result);
if (output_format === 'json') {
return JSON.parse(result);
} else {
return result;
}
}
loadBoard(boardXMLText) {
return this.api('loadBoard', boardXMLText)
return this.api('loadBoard', 'json', boardXMLText)
}
loadScenario(scenarioXMLText) {
return this.api('loadScenario', scenarioXMLText)
return this.api('loadScenario', 'json', scenarioXMLText)
}
validateBoardStructure(boardXMLText) {
return this.api('validateBoardStructure', 'plaintext', boardXMLText)
}
validateScenarioStructure(scenarioXMLText) {
return this.api('validateScenarioStructure', 'plaintext', scenarioXMLText)
}
validateScenario(boardXMLText, scenarioXMLText) {
return this.api('validateScenario', boardXMLText, scenarioXMLText)
return this.api('validateScenario', 'json', boardXMLText, scenarioXMLText)
}
generateLaunchScript(boardXMLText, scenarioXMLText) {
return this.api('generateLaunchScript', boardXMLText, scenarioXMLText)
return this.api('generateLaunchScript', 'json', boardXMLText, scenarioXMLText)
}
populateDefaultValues(scenarioXMLText) {
return this.api('populateDefaultValues', scenarioXMLText)
return this.api('populateDefaultValues', 'json', scenarioXMLText)
}
}
@ -104,12 +116,20 @@ class Configurator {
loadBoard(path: String) {
return this.readFile(path)
.then((fileContent) => {
return this.pythonObject.loadBoard(fileContent)
let syntactical_errors = this.pythonObject.validateBoardStructure(fileContent);
if (syntactical_errors !== "") {
throw Error("The file has broken structure.");
}
return this.pythonObject.loadBoard(fileContent);
})
}
loadScenario(path: String): Object {
return this.readFile(path).then((fileContent) => {
let syntactical_errors = this.pythonObject.validateScenarioStructure(fileContent);
if (syntactical_errors !== "") {
throw Error("The file has broken structure.");
}
return this.pythonObject.loadScenario(fileContent)
})
}
@ -155,4 +175,4 @@ class Configurator {
}
let configurator = new Configurator()
export default configurator
export default configurator

View File

@ -146,7 +146,7 @@ export default {
.then(() => this.getBoardHistory())
})
.catch((err)=> {
alert(`Failed to load the file ${filepath}, it may not exist`)
alert(`Loading ${filepath} failed: ${err}`)
console.log(err)
})
}

View File

@ -98,7 +98,7 @@ export default {
}
}).catch((err) => {
console.log(err)
alert(`Failed to open ${this.currentSelectedScenario}, file may not exist`)
alert(`Loading ${this.currentSelectedScenario} failed: ${err}`)
})
}
},

View File

@ -22,6 +22,7 @@ def file_text(path):
config_tools_dir = Path(__file__).absolute().parent.parent.parent
configurator_dir = config_tools_dir / 'configurator' / 'packages' / 'configurator'
schema_dir = config_tools_dir / 'schema'
board_xml_schema_path = schema_dir / 'board.xsd'
scenario_xml_schema_path = schema_dir / 'sliced.xsd'
datachecks_xml_schema_path = schema_dir / 'allchecks.xsd'

View File

@ -4,6 +4,8 @@ __package__ = 'configurator.pyodide'
from .loadBoard import test as load_board_test
from .loadScenario import test as load_scenario_test
from .generateLaunchScript import test as generate_launch_script_test
from .validateBoardStructure import test as validate_board_structure_test
from .validateScenarioStructure import test as validate_scenario_structure_test
from .validateScenario import test as validate_scenario_test
from .populateDefaultValues import test as populate_default_values
@ -12,6 +14,8 @@ def main():
load_board_test()
load_scenario_test()
generate_launch_script_test()
validate_board_structure_test()
validate_scenario_structure_test()
validate_scenario_test()
populate_default_values()

View File

@ -0,0 +1,51 @@
#!/usr/bin/env python3
__package__ = 'configurator.pyodide'
from pathlib import Path
from tempfile import TemporaryDirectory
from scenario_config.default_populator import DefaultValuePopulatingStage
from scenario_config.pipeline import PipelineObject, PipelineEngine
from scenario_config.validator import ValidatorConstructionByFileStage, SyntacticValidationStage
from scenario_config.xml_loader import XMLLoadStage
from .pyodide import (
convert_result, write_temp_file,
nuc11_board, board_xml_schema_path
)
def main(board):
pipeline = PipelineEngine(["board_path", "schema_path", "datachecks_path"])
pipeline.add_stages([
ValidatorConstructionByFileStage(),
XMLLoadStage("board"),
SyntacticValidationStage(etree_tag = "board"),
])
try:
with TemporaryDirectory() as tmpdir:
write_temp_file(tmpdir, {
'board.xml': board
})
board_file_path = Path(tmpdir) / 'board.xml'
obj = PipelineObject(
board_path=board_file_path,
schema_path=board_xml_schema_path,
datachecks_path=None
)
pipeline.run(obj)
validate_result = obj.get("syntactic_errors")
return "\n\n".join(map(lambda x: x["message"], validate_result))
except Exception as e:
return str(e)
def test():
print(main(nuc11_board))
if __name__ == '__main__':
test()

View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
__package__ = 'configurator.pyodide'
from pathlib import Path
from tempfile import TemporaryDirectory
from scenario_config.default_populator import DefaultValuePopulatingStage
from scenario_config.pipeline import PipelineObject, PipelineEngine
from scenario_config.validator import ValidatorConstructionByFileStage, SyntacticValidationStage
from scenario_config.xml_loader import XMLLoadStage
from .pyodide import (
convert_result, write_temp_file,
nuc11_scenario, scenario_xml_schema_path, datachecks_xml_schema_path
)
def main(scenario):
pipeline = PipelineEngine(["scenario_path", "schema_path", "datachecks_path"])
pipeline.add_stages([
ValidatorConstructionByFileStage(),
XMLLoadStage("schema"),
XMLLoadStage("scenario"),
DefaultValuePopulatingStage(),
SyntacticValidationStage(),
])
try:
with TemporaryDirectory() as tmpdir:
write_temp_file(tmpdir, {
'scenario.xml': scenario
})
scenario_file_path = Path(tmpdir) / 'scenario.xml'
obj = PipelineObject(
scenario_path=scenario_file_path,
schema_path=scenario_xml_schema_path,
datachecks_path=None
)
pipeline.run(obj)
validate_result = obj.get("syntactic_errors")
return "\n\n".join(map(lambda x: x["message"], validate_result))
except Exception as e:
return str(e)
def test():
print(main(nuc11_scenario))
if __name__ == '__main__':
test()

View File

@ -50,7 +50,7 @@ class PipelineEngine:
all_uses = consumes.union(uses)
if not all_uses.issubset(self.available_data):
raise Exception(f"Data {uses - self.available_data} need by stage {stage.__class__.__name__} but not provided by the pipeline")
raise Exception(f"Data {all_uses - self.available_data} need by stage {stage.__class__.__name__} but not provided by the pipeline")
self.stages.append(stage)
self.available_data = self.available_data.difference(consumes).union(provides)

View File

@ -71,7 +71,7 @@ class ScenarioValidator:
def __init__(self, schema_etree, datachecks_etree):
"""Initialize the validator with preprocessed schemas in ElementTree."""
self.schema = xmlschema.XMLSchema11(schema_etree)
self.datachecks = xmlschema.XMLSchema11(datachecks_etree)
self.datachecks = xmlschema.XMLSchema11(datachecks_etree) if datachecks_etree else None
def check_syntax(self, scenario_etree):
errors = []
@ -88,14 +88,15 @@ class ScenarioValidator:
def check_semantics(self, board_etree, scenario_etree):
errors = []
unified_node = copy(scenario_etree.getroot())
parent_map = {c : p for p in unified_node.iter() for c in p}
unified_node.extend(board_etree.getroot())
it = self.datachecks.iter_errors(unified_node)
for error in it:
e = self.format_error(unified_node, parent_map, error)
e.log()
errors.append(e)
if self.datachecks:
unified_node = copy(scenario_etree.getroot())
parent_map = {c : p for p in unified_node.iter() for c in p}
unified_node.extend(board_etree.getroot())
it = self.datachecks.iter_errors(unified_node)
for error in it:
e = self.format_error(unified_node, parent_map, error)
e.log()
errors.append(e)
return errors
@ -190,11 +191,14 @@ class ValidatorConstructionByFileStage(PipelineStage):
obj.set("validator", validator)
class SyntacticValidationStage(PipelineStage):
uses = {"validator", "scenario_etree"}
provides = {"syntactic_errors"}
def __init__(self, etree_tag = "scenario"):
self.etree_tag = f"{etree_tag}_etree"
self.uses = {"validator", self.etree_tag}
def run(self, obj):
errors = obj.get("validator").check_syntax(obj.get("scenario_etree"))
errors = obj.get("validator").check_syntax(obj.get(self.etree_tag))
obj.set("syntactic_errors", errors)
class SemanticValidationStage(PipelineStage):

View File

@ -0,0 +1,45 @@
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="arbitrary" mixed="true">
<xs:all>
<xs:any maxOccurs="unbounded" processContents="skip" />
</xs:all>
</xs:complexType>
<xs:element name="acrn-config">
<xs:complexType>
<xs:all>
<xs:element name="BIOS_INFO" type="xs:string" />
<xs:element name="BASE_BOARD_INFO" type="xs:string" />
<xs:element name="PCI_DEVICE" type="xs:string" />
<xs:element name="PCI_VID_PID" type="xs:string" />
<xs:element name="WAKE_VECTOR_INFO" type="xs:string" />
<xs:element name="RESET_REGISTER_INFO" type="xs:string" />
<xs:element name="PM_INFO" type="xs:string" />
<xs:element name="S3_INFO" type="xs:string" />
<xs:element name="S5_INFO" type="xs:string" />
<xs:element name="DRHD_INFO" type="xs:string" />
<xs:element name="CPU_BRAND" type="xs:string" />
<xs:element name="CX_INFO" type="xs:string" />
<xs:element name="PX_INFO" type="xs:string" />
<xs:element name="MMCFG_BASE_INFO" type="xs:string" />
<xs:element name="TPM_INFO" type="xs:string" />
<xs:element name="CLOS_INFO" type="xs:string" />
<xs:element name="IOMEM_INFO" type="xs:string" />
<xs:element name="BLOCK_DEVICE_INFO" type="xs:string" />
<xs:element name="TTYS_INFO" type="xs:string" />
<xs:element name="AVAILABLE_IRQ_INFO" type="xs:string" />
<xs:element name="TOTAL_MEM_INFO" type="xs:string" />
<xs:element name="CPU_PROCESSOR_INFO" type="xs:string" />
<xs:element name="MAX_MSIX_TABLE_NUM" type="xs:string" />
<xs:element name="processors" type="arbitrary" />
<xs:element name="memory" type="arbitrary" />
<xs:element name="caches" type="arbitrary" />
<xs:element name="ioapics" type="arbitrary" />
<xs:element name="devices" type="arbitrary" />
<xs:element name="device-classes" type="arbitrary" minOccurs="0" />
</xs:all>
<xs:attribute name="board" type="xs:string" />
</xs:complexType>
</xs:element>
</xs:schema>