diff --git a/Makefile b/Makefile index 8f766c5661c008dd1b2be772b52d0b3d7f8e9e33..0b3957dc97374ff878c6c48216eb25cdf2fd82f9 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,8 @@ VALIDS = \ guid-tool-schema.yaml__guid-tool.yaml \ identify-schema.yaml__identify.yaml \ identify-schema.yaml__tests/data/test-check-sr-results/identify.yaml \ - boot_sources_result_schema.yaml__boot_sources_result.yaml + boot_sources_result_schema.yaml__boot_sources_result.yaml \ + psci-version-check-schema.yaml__psci-version-check.yaml VALID_TARGETS = $(addsuffix .valid,$(VALIDS)) diff --git a/psci-version-check.yaml b/psci-version-check.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ec72d6981abb37fcb56237b60aad7d6e8df28e1a --- /dev/null +++ b/psci-version-check.yaml @@ -0,0 +1,17 @@ +############################################################################### +# psci_version_checker.py configuration file # +############################################################################### + +--- + +criterias: + - results: + - psci_version: '>=1.2' + criteria: 'PASS' + quality: 'BEST' + recommendation: 'PSCI version meets the minimum requirement.' + - results: + - psci_version: '<1.2' + criteria: 'FAIL' + quality: 'BAD' + recommendation: 'PSCI version is below the minimum required.' diff --git a/psci_version_checker.py b/psci_version_checker.py new file mode 100755 index 0000000000000000000000000000000000000000..126fcce48c9bf6e0c2d8f6995e8ae144f105d488 --- /dev/null +++ b/psci_version_checker.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 + +import argparse +import logging +import os +import sys +import re +from typing import Any, Dict, Optional, cast + +import yaml +import jsonschema + +DbType = Dict[str, Any] + + +# Load YAML diagnostics database and validate against schema +def load_diagnostics_db(config_filename: str, schema_filename: str) -> DbType: + logging.debug(f"Loading configuration file `{config_filename}`") + logging.debug(f"Loading schema file `{schema_filename}`") + + # Check if the schema file exists + if not os.path.isfile(schema_filename): + logging.error(f'Schema file `{schema_filename}` does not exist') + sys.exit(1) + + # Load the schema from the schema file + with open(schema_filename, 'r') as schemafile: + try: + schema = cast( + Dict[str, Any], + yaml.load(schemafile, Loader=yaml.FullLoader)) + except yaml.YAMLError as err: + logging.error( + f"Error parsing schema file `{schema_filename}`: {err}" + ) + sys.exit(1) + + # Check if the configuration file exists + if not os.path.isfile(config_filename): + logging.error(f'Configuration file `{config_filename}` does not exist') + sys.exit(1) + + # Load the configuration file + with open(config_filename, 'r') as yamlfile: + try: + db = cast(DbType, yaml.load(yamlfile, Loader=yaml.FullLoader)) + except yaml.YAMLError as err: + logging.error( + f"Error parsing configuration file `{config_filename}`: {err}" + ) + sys.exit(1) + + # Validate the configuration against the schema + try: + jsonschema.validate(instance=db, schema=schema) + except jsonschema.exceptions.ValidationError as err: + logging.error(f"YAML file validation error: {err.message}") + sys.exit(1) + + logging.debug(f"YAML contents: {db}") + return db + + +# Parse the log file to extract PSCI version information +def parse_psci_log(log_path: str) -> Dict[str, str]: + psci_info: Dict[str, str] = {} + version_pattern = re.compile(r'psci: PSCIv([\d\.]+) detected in firmware.') + with open(log_path, 'r') as log_file: + for line in log_file: + match = version_pattern.search(line) + if match: + version_str = match.group(1) + psci_info['psci_version'] = version_str + logging.debug(f"Detected PSCI version: {version_str}") + break # Assuming only one PSCI version line exists + if 'psci_version' not in psci_info: + logging.error("PSCI version not found in log file.") + psci_info['psci_version'] = 'unknown' + return psci_info + + +# Apply criteria from the YAML to the parsed PSCI info +def apply_criteria(db: DbType, psci_info: Dict[str, str]) -> str: + logging.debug('Applying criteria from the database') + result = 'FAIL' # Default to FAIL + psci_version_str = psci_info.get('psci_version', 'unknown') + if psci_version_str == 'unknown': + logging.error("PSCI version is unknown. Cannot apply criteria.") + return result + + # Convert PSCI version to float for comparison + try: + psci_version = float(psci_version_str) + except ValueError: + logging.error(f"Invalid PSCI version format: {psci_version_str}") + return result + + found_match = False + + for criteria in db['criterias']: + for result_dict in criteria['results']: + version_condition = result_dict.get('psci_version', '') + if version_condition.startswith('>='): + try: + min_version = float(version_condition[2:]) + if psci_version >= min_version: + logging.debug( + f"PSCI version {psci_version} meets criteria " + f">= {min_version}" + ) + result = criteria['criteria'] + found_match = True + logging.info(criteria['recommendation']) + break + except ValueError: + logging.error( + f"Invalid minimum version in criteria: " + f"{version_condition}" + ) + continue + elif version_condition.startswith('<'): + try: + max_version = float(version_condition[1:]) + if psci_version < max_version: + logging.debug( + f"PSCI version {psci_version} meets criteria " + f"< {max_version}" + ) + result = criteria['criteria'] + found_match = True + logging.info(criteria['recommendation']) + break + except ValueError: + logging.error( + f"Invalid maximum version in criteria: " + f"{version_condition}" + ) + continue + else: + logging.error( + f"Invalid psci_version in criteria: {version_condition}" + ) + if found_match: + break + + if not found_match: + logging.error("No matching criteria found for PSCI version.") + return result + + +# Function to find the schema file +def find_schema_file(filename: str) -> Optional[str]: + me = os.path.realpath(__file__) + directories = [ + os.path.join(os.path.dirname(me), 'schemas'), + os.path.join(os.path.dirname(os.path.dirname(me)), 'schemas'), + os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(me))), + 'schema', + ), + ] + for schema_dir in directories: + schema_file = os.path.join(schema_dir, filename) + if os.path.isfile(schema_file): + return schema_file + return None + + +if __name__ == "__main__": + me = os.path.realpath(__file__) + here = os.path.dirname(me) + + parser = argparse.ArgumentParser( + description='Parse PSCI version from logs.', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + '--config', + help='Configuration filename', + default=f"{here}/psci-version-check.yaml", + ) + parser.add_argument( + '--schema', + help=( + 'Schema filename. If not provided, the script will search for ' + 'it automatically.' + ), + default=None, + ) + parser.add_argument( + '--debug', action='store_true', help='Turn on debug messages' + ) + parser.add_argument('log', help="Input log filename") + args = parser.parse_args() + + logging.basicConfig( + format='%(levelname)s %(funcName)s: %(message)s', + level=logging.DEBUG if args.debug else logging.INFO, + ) + + if args.schema is None: + args.schema = find_schema_file('psci-version-check-schema.yaml') + if args.schema is None: + logging.error( + 'Schema file psci-version-check-schema.yaml not found ' + 'in schema directories' + ) + sys.exit(1) + + db = load_diagnostics_db(args.config, args.schema) + psci_info = parse_psci_log(args.log) + + result = apply_criteria(db, psci_info) + + logging.info(f'PSCI version check result is: {result}') + if result == "PASS": + sys.exit(0) + else: + sys.exit(1) diff --git a/schemas/psci-version-check-schema.yaml b/schemas/psci-version-check-schema.yaml new file mode 100644 index 0000000000000000000000000000000000000000..82a05948491ff5bb5984d549742d14904b21529e --- /dev/null +++ b/schemas/psci-version-check-schema.yaml @@ -0,0 +1,49 @@ +--- +$id: "https://gitlab.arm.com/systemready/systemready-scripts/-/raw/master/\ + schemas/psci-version-check-schema.yaml" +$schema: https://json-schema.org/draft/2020-12/schema +title: psci-version-check-schema +description: | + Logs produced by the psci in system_ready band is checked for + the version of psci used. + + The pass/fail criteria for the tests are described in a yaml + configuration file psci-version-check.yaml . + + This schema describes requirements on the configuration file. + It can be used by the valid.py script. + + See the README for details. + +type: object +properties: + criterias: + type: array + items: + type: object + properties: + results: + type: array + items: + type: object + properties: + psci_version: + type: string + required: + - psci_version + additionalProperties: false + criteria: + type: string + quality: + type: string + recommendation: + type: string + required: + - results + - criteria + - quality + - recommendation + additionalProperties: false +required: + - criterias +additionalProperties: false diff --git a/tests/data/test-psci-version-check/psci_version_fail.log b/tests/data/test-psci-version-check/psci_version_fail.log new file mode 100644 index 0000000000000000000000000000000000000000..b670bf24a5c594f1951f1aab9df3f01a28cf1d0a --- /dev/null +++ b/tests/data/test-psci-version-check/psci_version_fail.log @@ -0,0 +1,24 @@ +[ 0.000000] psci: probing for conduit method from DT. +[ 0.000000] psci: PSCIv1.1 detected in firmware. +[ 0.000000] psci: Using standard PSCI v0.2 function IDs +[ 0.000000] psci: Trusted OS migration not required +[ 0.000000] psci: SMC Calling Convention v1.4 +[ 4.898673] psci_checker: PSCI checker is enabled by default. +[ 4.899059] psci_checker: PSCI checker started using 4 CPUs +[ 4.899325] psci_checker: Starting hotplug tests +[ 4.899699] psci_checker: Trying to turn off and on again all CPUs +[ 4.918600] psci: CPU0 killed (polled 0 ms) +[ 4.945261] psci: CPU1 killed (polled 0 ms) +[ 4.955895] psci: CPU2 killed (polled 0 ms) +[ 5.000270] psci_checker: Trying to turn off and on again group 0 (CPUs 0-3) +[ 5.005682] psci: CPU0 killed (polled 0 ms) +[ 5.012800] psci: CPU1 killed (polled 0 ms) +[ 5.020525] psci: CPU2 killed (polled 0 ms) +[ 5.048041] psci_checker: Hotplug tests passed OK +[ 5.048443] psci_checker: Starting suspend tests (10 cycles per state) +[ 5.049465] psci_checker: cpuidle not available on CPU 0, ignoring +[ 5.050344] psci_checker: cpuidle not available on CPU 1, ignoring +[ 5.050813] psci_checker: cpuidle not available on CPU 2, ignoring +[ 5.051104] psci_checker: cpuidle not available on CPU 3, ignoring +[ 5.051455] psci_checker: Could not start suspend tests on any CPU +[ 5.051751] psci_checker: PSCI checker completed diff --git a/tests/data/test-psci-version-check/psci_version_pass.log b/tests/data/test-psci-version-check/psci_version_pass.log new file mode 100644 index 0000000000000000000000000000000000000000..17e531720fe7ce78aa0313e5750cc1eb831e06b5 --- /dev/null +++ b/tests/data/test-psci-version-check/psci_version_pass.log @@ -0,0 +1,24 @@ +[ 0.000000] psci: probing for conduit method from DT. +[ 0.000000] psci: PSCIv1.3 detected in firmware. +[ 0.000000] psci: Using standard PSCI v0.2 function IDs +[ 0.000000] psci: Trusted OS migration not required +[ 0.000000] psci: SMC Calling Convention v1.4 +[ 4.898673] psci_checker: PSCI checker is enabled by default. +[ 4.899059] psci_checker: PSCI checker started using 4 CPUs +[ 4.899325] psci_checker: Starting hotplug tests +[ 4.899699] psci_checker: Trying to turn off and on again all CPUs +[ 4.918600] psci: CPU0 killed (polled 0 ms) +[ 4.945261] psci: CPU1 killed (polled 0 ms) +[ 4.955895] psci: CPU2 killed (polled 0 ms) +[ 5.000270] psci_checker: Trying to turn off and on again group 0 (CPUs 0-3) +[ 5.005682] psci: CPU0 killed (polled 0 ms) +[ 5.012800] psci: CPU1 killed (polled 0 ms) +[ 5.020525] psci: CPU2 killed (polled 0 ms) +[ 5.048041] psci_checker: Hotplug tests passed OK +[ 5.048443] psci_checker: Starting suspend tests (10 cycles per state) +[ 5.049465] psci_checker: cpuidle not available on CPU 0, ignoring +[ 5.050344] psci_checker: cpuidle not available on CPU 1, ignoring +[ 5.050813] psci_checker: cpuidle not available on CPU 2, ignoring +[ 5.051104] psci_checker: cpuidle not available on CPU 3, ignoring +[ 5.051455] psci_checker: Could not start suspend tests on any CPU +[ 5.051751] psci_checker: PSCI checker completed diff --git a/tests/test-psci-version-check b/tests/test-psci-version-check new file mode 100755 index 0000000000000000000000000000000000000000..0b870a9f796cba85402c4f756cb21f8a50de2825 --- /dev/null +++ b/tests/test-psci-version-check @@ -0,0 +1,46 @@ +#!/bin/bash +set -eu -o pipefail + +# Unit test for psci_version_checker.py. +# Usage: test-psci-version-checker.sh [keep] +# Keeps the temporary folder when 'keep' is specified. + +# Redirect all output to a log file in the current folder. +# Keep stdout on fd 3. +bn=$(basename "$0") +log="$bn.log" +exec 3>&1 >"$log" 2>&1 +set -x + +echo -n 'Testing psci_version_checker.py... ' >&3 +trap 'echo "ERROR! (see $log)" >&3' ERR + +# Ensure psci_version_checker.py is in the PATH. +me=$(realpath "$0") +here="${me%/*}" +export PATH="$here/..:$PATH" + +# Create a temporary folder. +if [ "${1:-unset}" == "keep" ]; then + tmp=$(mktemp -d "$(basename "$0").XXX") +else + tmp=$(mktemp -d) + trap 'rm -fr "$tmp"' EXIT +fi + +data="$here/data/$(basename "$0")" +out="$tmp/out" + +# Test 1: PSCI version meets criteria (PASS). +echo -n 'test 1: PSCI version meets criteria (PASS)' >&3 +psci_version_checker.py "$data/psci_version_pass.log" --debug |& tee "$out" +grep 'PSCI version check result is: PASS' "$out" + +# Test 2: PSCI version does not meet criteria (FAIL). +echo -n ', test 2: PSCI version does not meet criteria (FAIL)' >&3 +if psci_version_checker.py "$data/psci_version_fail.log" --debug |& tee "$out"; then + false +fi +grep 'PSCI version check result is: FAIL' "$out" + +echo ', ok.' >&3