root/build-tools/stx/patch/metadata.py
Dostoievski Batista a1a167bb71 Change scripts names in the patch's metadata
When adding a pre-install or post-install script to the patch, we rename
it to "pre-install.sh" or "post-install.sh" to facilitate the use of the
patch afterwards. This change makes the metadata inside the patch
reflect this. Together with this change we move PATCH_SCRIPTS constant
to separate file as the same will be call in different files.

Test plan:
    PASS: Create patch without any scripts
    PASS: Create patch with only pre-install script
    PASS: Create patch with only post-install script
    PASS: Create patch with all scripts
    PASS: Install patch with all scripts in a running AIO-SX,
    check if scripts are present in /opt/software/software-scripts/
    PASS: Delete patch with all scripts in a running AIO-SX,
    check if scripts are not present in /opt/software/software-scripts/

Story: 2010676
Task: 50926

Change-Id: I639ff15e306ec69ed1bbbfea8c99aa96affa1dec
Signed-off-by: Dostoievski Batista <dostoievski.albinobatista@windriver.com>
2024-08-28 09:56:23 -03:00

229 lines
8.0 KiB
Python

#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
'''
Class that holds the patch metadata information
'''
import json
import logging
import os
import sys
sys.path.append('..')
import utils
import xml.etree.ElementTree as ET
from lxml import etree
from xml.dom import minidom
import constants
logger = logging.getLogger('metadata_parser')
utils.set_logger(logger)
PATCH_BUILDER_PATH = os.environ.get('PATCH_BUILDER_PATH')
INPUT_XML_SCHEMA = f'{PATCH_BUILDER_PATH}/config/patch-recipe-schema.xsd'
# Metadata components
PATCH_ROOT_TAG = 'patch'
PATCH_ID = 'id'
SW_VERSION = 'sw_version'
COMPONENT = 'component'
STATUS = 'status'
SUMMARY = 'summary'
DESCRIPTION = 'description'
INSTALL_INSTRUCTIONS = 'install_instructions'
WARNINGS = 'warnings'
REBOOT_REQUIRED = 'reboot_required'
PRE_INSTALL = 'pre_install'
POST_INSTALL = 'post_install'
UNREMOVABLE = 'unremovable'
REQUIRES = 'requires'
REQUIRES_PATCH_ID = 'req_patch_id'
PACKAGES = 'packages'
STX_PACKAGES = 'stx_packages'
BINARY_PACKAGES = 'binary_packages'
SEMANTICS = 'semantics'
class PatchMetadata(object):
def __init__(self, patch_recipe_file):
self.patch_recipe_file = patch_recipe_file
self.stx_packages = []
self.binary_packages = []
self.requires = []
# Verify if the path to the patch builder folder is set
if not PATCH_BUILDER_PATH:
raise Exception("Environment variable PATCH_BUILDER_PATH is not set.")
def __str__(self):
return json.dumps(self.__dict__)
def __repr__(self):
return self.__str__()
def __add_text_tag_to_xml(self, parent, name, text):
"""
Utility function for adding a text tag to an XML object
:param parent: Parent element
:param name: Element name
:param text: Text value
:return:The created element
"""
tag = ET.SubElement(parent, name)
tag.text = text
return tag
def __xml_to_dict(self, element):
"""
Converts xml into a dict
:param xml element
"""
if len(element) == 0:
return element.text.strip() if element.text else ""
result = {}
for child in element:
child_data = self.__xml_to_dict(child)
# Verify if child.tag is comment
if child.tag == etree.Comment:
continue
if child.tag in result:
if isinstance(result[child.tag], list):
result[child.tag].append(child_data)
else:
result[child.tag] = [result[child.tag], child_data]
else:
result[child.tag] = child_data
return result
def generate_patch_metadata(self, file_path):
# Generate patch metadata.xml
top_tag = ET.Element(PATCH_ROOT_TAG)
self.__add_text_tag_to_xml(top_tag, PATCH_ID, self.patch_id)
self.__add_text_tag_to_xml(top_tag, SW_VERSION, self.sw_version)
self.__add_text_tag_to_xml(top_tag, COMPONENT, self.component)
self.__add_text_tag_to_xml(top_tag, SUMMARY, self.summary)
self.__add_text_tag_to_xml(top_tag, DESCRIPTION, self.description)
self.__add_text_tag_to_xml(top_tag, INSTALL_INSTRUCTIONS, self.install_instructions)
self.__add_text_tag_to_xml(top_tag, WARNINGS, self.warnings)
self.__add_text_tag_to_xml(top_tag, STATUS, self.status)
if self.unremovable.upper() in ["Y","N"]:
self.__add_text_tag_to_xml(top_tag, UNREMOVABLE, self.unremovable.upper())
else:
raise Exception('Supported values for "Unremovable" are Y or N, for "Yes" or "No" respectively')
if self.reboot_required.upper() in ["Y","N"]:
self.__add_text_tag_to_xml(top_tag, REBOOT_REQUIRED, self.reboot_required.upper())
else:
raise Exception('Supported values for "Reboot Required" are Y or N, for "Yes" or "No" respectively')
self.__add_text_tag_to_xml(top_tag, SEMANTICS, self.semantics)
requires_atg = ET.SubElement(top_tag, REQUIRES)
for req_patch in sorted(self.requires):
self.__add_text_tag_to_xml(requires_atg, REQUIRES_PATCH_ID, req_patch)
if self.pre_install:
self.__add_text_tag_to_xml(top_tag, PRE_INSTALL,
constants.PATCH_SCRIPTS['PRE_INSTALL'])
else:
self.__add_text_tag_to_xml(top_tag, PRE_INSTALL, "")
if self.post_install:
self.__add_text_tag_to_xml(top_tag, POST_INSTALL,
constants.PATCH_SCRIPTS['POST_INSTALL'])
else:
self.__add_text_tag_to_xml(top_tag, POST_INSTALL, "")
packages_tag = ET.SubElement(top_tag, PACKAGES)
for package in sorted(self.debs):
self.__add_text_tag_to_xml(packages_tag, "deb", package)
# Save xml
outfile = open(file_path, "w")
tree = ET.tostring(top_tag)
outfile.write(minidom.parseString(tree).toprettyxml(indent=" "))
def __tag_to_list(self, tag_content):
if type(tag_content) != list:
return [tag_content]
return tag_content
def parse_metadata(self, patch_recipe):
self.patch_id = f"{patch_recipe[COMPONENT]}-{patch_recipe[SW_VERSION]}"
self.sw_version = patch_recipe[SW_VERSION]
self.component = patch_recipe[COMPONENT]
self.summary = patch_recipe[SUMMARY]
self.description = patch_recipe[DESCRIPTION]
if 'package' in patch_recipe[STX_PACKAGES]:
self.stx_packages = self.__tag_to_list(patch_recipe[STX_PACKAGES]['package'])
if 'package' in patch_recipe[BINARY_PACKAGES]:
self.binary_packages = self.__tag_to_list(patch_recipe[BINARY_PACKAGES]['package'])
self.install_instructions = patch_recipe[INSTALL_INSTRUCTIONS]
self.warnings = patch_recipe[WARNINGS]
self.reboot_required = patch_recipe[REBOOT_REQUIRED]
self.pre_install = self.check_script_path(patch_recipe[PRE_INSTALL])
self.post_install = self.check_script_path(patch_recipe[POST_INSTALL])
self.unremovable = patch_recipe[UNREMOVABLE]
self.status = patch_recipe[STATUS]
if 'id' in patch_recipe[REQUIRES]:
self.requires = self.__tag_to_list(patch_recipe[REQUIRES]['id'])
self.semantics = patch_recipe[SEMANTICS]
self.debs = []
if self.status != 'DEV' and self.status != 'REL':
raise Exception('Supported status are DEV and REL, selected')
logger.debug("Metadata parsed: %s", self)
def parse_input_xml_data(self):
# Parse and validate the XML
try:
xml_tree = etree.parse(self.patch_recipe_file)
except Exception as e:
logger.error(f"Error while parsing the input xml {e}")
sys.exit(1)
root = xml_tree.getroot()
xml_schema = etree.XMLSchema(etree.parse(INPUT_XML_SCHEMA))
# Validate the XML against the schema
is_valid = xml_schema.validate(root)
xml_dict = {}
if is_valid:
logger.info("XML is valid against the schema.")
xml_dict = self.__xml_to_dict(root)
else:
logger.error("XML is not valid against the schema. Validation errors:")
for error in xml_schema.error_log:
logger.error(f"Line {error.line}: {error.message}")
sys.exit(1)
print(xml_dict)
self.parse_metadata(xml_dict)
def check_script_path(self, script_path):
if not script_path:
# No scripts provided
return None
if not os.path.isabs(script_path):
script_path = os.path.join(os.getcwd(), script_path)
if not os.path.isfile(script_path):
erro_msg = f"Install script {script_path} not found"
logger.error(erro_msg)
raise FileNotFoundError(erro_msg)
return script_path
if __name__ == "__main__":
patch_recipe_file = f"${PATCH_BUILDER_PATH}/EXAMPLES/patch-recipe-sample.xml"
patch_metadata = PatchMetadata(patch_recipe_file)
patch_metadata.parse_input_xml_data()