From 51e0cf67dc5961134ef4bd3e7a1b1659ab95dd63 Mon Sep 17 00:00:00 2001
From: Tobias Poganiuch <tobias.poganiuch@seco.com>
Date: Tue, 14 Nov 2023 17:08:14 +0100
Subject: [PATCH] build:stages: Add Firmware Package JSON stage

The old Alphaplan stage is deprecated and was removed in a prior commit.
Add new stage to generate the Firmware Package JSON files that do replace
the old Alphaplan articles.
---
 .gitignore                           |   2 +
 build-pipeline-yocto.yml.jinja2      |  31 +++
 build-pipeline.yml                   |  27 +++
 scripts/firmware_package_keys.py     | 133 ++++++++++++
 scripts/generate_firmware_package.py | 291 +++++++++++++++++++++++++++
 5 files changed, 484 insertions(+)
 create mode 100755 scripts/firmware_package_keys.py
 create mode 100755 scripts/generate_firmware_package.py

diff --git a/.gitignore b/.gitignore
index b611c2ec..418ff824 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
 .idea/
 .vscode/
 scripts/__pycache__
+scripts/fng*
+scripts/yocto*
diff --git a/build-pipeline-yocto.yml.jinja2 b/build-pipeline-yocto.yml.jinja2
index 4b338659..6ce2e3f3 100644
--- a/build-pipeline-yocto.yml.jinja2
+++ b/build-pipeline-yocto.yml.jinja2
@@ -20,6 +20,7 @@ stages:
   - Deploy SoftwareStore Internal
   - Deploy FTP
   - Deploy Azure
+  - Firmware Package
   - Confluence
 
 variables:
@@ -337,6 +338,36 @@ azure-{{ machine }}:
 {% endif %}
 
 
+# --------------------------------------------------------------------------------------
+# Stage: Firmware Package
+# --------------------------------------------------------------------------------------
+{% for machine in MACHINES.split(' ') %}
+
+generate-firmware-package-{{ machine }}:
+  extends: .generate_firmware_package
+  variables:
+    MACHINE: {{ machine }}
+  needs:
+    - deploy-{{ machine }}
+    - build-version
+
+{% endfor %}
+
+{% for machine in MACHINES.split(' ') %}
+
+deploy-firmware-package-{{ machine }}:
+  extends: .deploy-software-store
+  stage: Firmware Package
+  variables:
+    ASSOCIATED_PACKAGE_JOB: generate-firmware-package-{{ machine }}
+  needs:
+    - job: generate-firmware-package-{{ machine }}
+      artifacts: false
+    - job: build-version
+
+{% endfor %}
+
+
 # --------------------------------------------------------------------------------------
 # Stage: Confluence
 # --------------------------------------------------------------------------------------
diff --git a/build-pipeline.yml b/build-pipeline.yml
index 47edd008..42e31abf 100644
--- a/build-pipeline.yml
+++ b/build-pipeline.yml
@@ -468,6 +468,33 @@ workflow:
     # Get links to uploaded files and remove query string containing the SAS token
     - jq -r '.[] | .Blob' result.json | sed "s/?.*//" | tee files.txt
 
+# --------------------------------------------------------------------------------------
+# Stage: Firmware Package
+# --------------------------------------------------------------------------------------
+.generate_firmware_package:
+  extends:
+    - .infrastructure
+  tags:
+    - misc
+  stage: Firmware Package
+  # build-version is set in the jinja2 template
+  script:
+    # RELEASE_NAME is available from build-version.env
+    - .gitlab-ci/scripts/generate_firmware_package.py
+        --machine="${MACHINE}"
+        --release-name="${RELEASE_NAME}"
+        --files="${FILES}"
+        --md5sums=**/md5sums.txt
+        --output-file="${RELEASE_NAME}/${MACHINE}/firmware-package.json"
+  artifacts:
+    paths:
+      - ${RELEASE_NAME}/${MACHINE}/firmware-package.json
+  cache:
+    - key: ${CI_PIPELINE_ID}-${CI_JOB_NAME}
+      policy: push
+      paths:
+        - ${RELEASE_NAME}/${MACHINE}/firmware-package.json
+
 # --------------------------------------------------------------------------------------
 # Stage: Confluence
 # --------------------------------------------------------------------------------------
diff --git a/scripts/firmware_package_keys.py b/scripts/firmware_package_keys.py
new file mode 100755
index 00000000..193486ff
--- /dev/null
+++ b/scripts/firmware_package_keys.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python3
+
+from enum import Flag, auto
+
+
+class FirmwarePackageKeys(Flag):
+    """Class to specify firmware package keys"""
+
+    YOCTO_PKG_PY = auto()
+    YOCTO_FNG_INSTALL = auto()
+    YOCTO_FS = auto()
+    FNGSYS_INIT = auto()
+    FNGSYS_UPDATE = auto()
+    FNGSYS_FS = auto()
+    FNGSYS_CHECKSUM = auto()
+    FNGSYS_UBOOT_UPDATE = auto()
+    FNGSYS_UBOOT_IMAGE = auto()
+    FNGSYS_UBOOT_CHECKSUM = auto()
+    FNGSYS_UBOOT_IMAGETAR = auto()
+
+
+class FirmwarePackageSubKeys(Flag):
+    """Class to specify firmware package subkeys"""
+
+    MATCH = auto()
+    MATCHCODE = auto()
+    BEZEICHNUNG = auto()
+    LANGTEXT = auto()
+    TYP = auto()
+    ATTRIBUTESET = auto()
+
+
+def get_fwr_pkg_dict(machine_name, machine_name_long, release_name):
+    """Return a customized dict with information for the firmware package"""
+
+    return {
+        FirmwarePackageKeys.YOCTO_PKG_PY: {
+            FirmwarePackageSubKeys.MATCH: "pkg.py",
+            FirmwarePackageSubKeys.MATCHCODE: "FNGUpdate",
+            FirmwarePackageSubKeys.BEZEICHNUNG: f"{machine_name_long} Flash-N-Go Update general pkg.py "
+            "update script for nonverbose fng-install.sh",
+            FirmwarePackageSubKeys.LANGTEXT: "To be used with packages the contain an "
+            "fng-install.sh.\n"
+            "* with --nonverbose mode (new output)\n"
+            "* Able to to local installation with unset TFTP variable\n"
+            "* Handle --Paramfile",
+            FirmwarePackageSubKeys.TYP: "FNGUpdate",
+            FirmwarePackageSubKeys.ATTRIBUTESET: "Firmware, Bestandteil eines SW-Paketes",
+        },
+        FirmwarePackageKeys.YOCTO_FNG_INSTALL: {
+            FirmwarePackageSubKeys.MATCH: "fng-install.sh",
+            FirmwarePackageSubKeys.MATCHCODE: "InstallScript",
+            FirmwarePackageSubKeys.BEZEICHNUNG: f"{machine_name_long} {release_name} Install Script",
+            FirmwarePackageSubKeys.LANGTEXT: "",
+            FirmwarePackageSubKeys.TYP: "US",
+            FirmwarePackageSubKeys.ATTRIBUTESET: "Firmware, Bestandteil eines SW-Paketes",
+        },
+        FirmwarePackageKeys.YOCTO_FS: {
+            FirmwarePackageSubKeys.MATCH: f"{machine_name}.tar.gz",
+            FirmwarePackageSubKeys.MATCHCODE: "OS-Filesystem",
+            FirmwarePackageSubKeys.BEZEICHNUNG: f"{machine_name_long} {release_name} Filesystem",
+            FirmwarePackageSubKeys.LANGTEXT: "",
+            FirmwarePackageSubKeys.TYP: "FS",
+            FirmwarePackageSubKeys.ATTRIBUTESET: "Firmware, Bestandteil eines SW-Paketes",
+        },
+        FirmwarePackageKeys.FNGSYS_UPDATE: {
+            FirmwarePackageSubKeys.MATCH: "fngsystem-self-update.sh",
+            FirmwarePackageSubKeys.MATCHCODE: "TFTP",
+            FirmwarePackageSubKeys.BEZEICHNUNG: f"{machine_name_long} {release_name} Self Update",
+            FirmwarePackageSubKeys.LANGTEXT: "",
+            FirmwarePackageSubKeys.TYP: "TFTP",
+            FirmwarePackageSubKeys.ATTRIBUTESET: "Firmware, Bestandteil eines SW-Paketes",
+        },
+        FirmwarePackageKeys.FNGSYS_INIT: {
+            FirmwarePackageSubKeys.MATCH: "fngsystem-self-init.sh",
+            FirmwarePackageSubKeys.MATCHCODE: "InstallScript",
+            FirmwarePackageSubKeys.BEZEICHNUNG: f"{machine_name_long} {release_name} Init Script",
+            FirmwarePackageSubKeys.LANGTEXT: "",
+            FirmwarePackageSubKeys.TYP: "Updateskript",
+            FirmwarePackageSubKeys.ATTRIBUTESET: "Firmware, Bestandteil eines SW-Paketes",
+        },
+        FirmwarePackageKeys.FNGSYS_FS: {
+            FirmwarePackageSubKeys.MATCH: f"{machine_name}.tgz",
+            FirmwarePackageSubKeys.MATCHCODE: "FS",
+            FirmwarePackageSubKeys.BEZEICHNUNG: f"{machine_name_long} {release_name} Filesystem",
+            FirmwarePackageSubKeys.LANGTEXT: "",
+            FirmwarePackageSubKeys.TYP: "FS",
+            FirmwarePackageSubKeys.ATTRIBUTESET: "Firmware, Bestandteil eines SW-Paketes",
+        },
+        FirmwarePackageKeys.FNGSYS_CHECKSUM: {
+            FirmwarePackageSubKeys.MATCH: f"{machine_name}.md5",
+            FirmwarePackageSubKeys.MATCHCODE: "TFTP",
+            FirmwarePackageSubKeys.BEZEICHNUNG: f"{machine_name_long} {release_name} Checksum",
+            FirmwarePackageSubKeys.LANGTEXT: "",
+            FirmwarePackageSubKeys.TYP: "TFTP",
+            FirmwarePackageSubKeys.ATTRIBUTESET: "Firmware, Bestandteil eines SW-Paketes",
+        },
+        FirmwarePackageKeys.FNGSYS_UBOOT_UPDATE: {
+            FirmwarePackageSubKeys.MATCH: "fng-install-uboot.sh",
+            FirmwarePackageSubKeys.MATCHCODE: "US",
+            FirmwarePackageSubKeys.BEZEICHNUNG: f"{machine_name_long} U-Boot {release_name} "
+            "Update script",
+            FirmwarePackageSubKeys.LANGTEXT: "",
+            FirmwarePackageSubKeys.TYP: "Updateskript",
+            FirmwarePackageSubKeys.ATTRIBUTESET: "Firmware, Bestandteil eines SW-Paketes",
+        },
+        FirmwarePackageKeys.FNGSYS_UBOOT_IMAGE: {
+            FirmwarePackageSubKeys.MATCH: "imx-boot",
+            FirmwarePackageSubKeys.MATCHCODE: "FS",
+            FirmwarePackageSubKeys.BEZEICHNUNG: f"{machine_name_long} U-Boot {release_name} "
+            "Bootloader Image",
+            FirmwarePackageSubKeys.LANGTEXT: "",
+            FirmwarePackageSubKeys.TYP: "FS",
+            FirmwarePackageSubKeys.ATTRIBUTESET: "Firmware, Bestandteil eines SW-Paketes",
+        },
+        FirmwarePackageKeys.FNGSYS_UBOOT_IMAGETAR: {
+            FirmwarePackageSubKeys.MATCH: "imx-boot.tar.gz",
+            FirmwarePackageSubKeys.MATCHCODE: "FS",
+            FirmwarePackageSubKeys.BEZEICHNUNG: f"{machine_name_long} U-Boot {release_name} "
+            "Bootloader Image",
+            FirmwarePackageSubKeys.LANGTEXT: "",
+            FirmwarePackageSubKeys.TYP: "FS",
+            FirmwarePackageSubKeys.ATTRIBUTESET: "Firmware, Bestandteil eines SW-Paketes",
+        },
+        FirmwarePackageKeys.FNGSYS_UBOOT_CHECKSUM: {
+            FirmwarePackageSubKeys.MATCH: "imx-boot.md5",
+            FirmwarePackageSubKeys.MATCHCODE: "TFTP",
+            FirmwarePackageSubKeys.BEZEICHNUNG: f"{machine_name_long} U-Boot {release_name} Checksum",
+            FirmwarePackageSubKeys.LANGTEXT: "",
+            FirmwarePackageSubKeys.TYP: "TFTP",
+            FirmwarePackageSubKeys.ATTRIBUTESET: "Firmware, Bestandteil eines SW-Paketes",
+        },
+    }
diff --git a/scripts/generate_firmware_package.py b/scripts/generate_firmware_package.py
new file mode 100755
index 00000000..872d09d4
--- /dev/null
+++ b/scripts/generate_firmware_package.py
@@ -0,0 +1,291 @@
+#!/usr/bin/env python3
+
+import argparse
+import fnmatch
+import glob
+import json
+import os
+import sys
+
+from firmware_package_keys import (
+    FirmwarePackageKeys,
+    FirmwarePackageSubKeys,
+    get_fwr_pkg_dict,
+)
+
+
+def generate_entry(
+    name: str,
+    _type: str,
+    description: str = None,
+    path: str = None,
+    md5sum: str = None,
+    files: dict = None,
+    packages: dict = None,
+):
+    """Create a JSON object for a new firmware package entry"""
+
+    package_entry = {}
+
+    package_entry["name"] = name
+    package_entry["type"] = _type
+
+    if description:
+        package_entry["description"] = description
+
+    if path:
+        package_entry["path"] = path
+
+    if md5sum:
+        package_entry["md5sum"] = md5sum
+
+    if files:
+        package_entry["files"] = files
+
+    if packages:
+        package_entry["packages"] = packages
+
+    return package_entry
+
+
+def generate_subpackage(
+    files: list[str],
+    pkg_key: FirmwarePackageKeys,
+    machine_name: str,
+    machine_name_long: str,
+    release_name: str,
+    md5sums: dict[str, str],
+):
+    """Create a new firmware subpackage"""
+
+    fwr_pkg = None
+
+    # Generate a dictionary for all files that should be included in the firmware package
+    fwr_pkg_dict = get_fwr_pkg_dict(machine_name, machine_name_long, release_name)
+
+    # Match files and get the paths and md5sums
+    for filepath in files:
+        filename = os.path.basename(filepath)
+
+        # Compare the filename with the match from pkg_key
+        if filename.casefold().endswith(
+            fwr_pkg_dict[pkg_key][FirmwarePackageSubKeys.MATCH].casefold()
+        ):
+            md5sum = md5sums[filename]
+
+            fwr_pkg = generate_entry(
+                name=fwr_pkg_dict[pkg_key][FirmwarePackageSubKeys.BEZEICHNUNG],
+                description=fwr_pkg_dict[pkg_key][FirmwarePackageSubKeys.LANGTEXT],
+                _type=fwr_pkg_dict[pkg_key][FirmwarePackageSubKeys.TYP],
+                path=filepath,
+                md5sum=md5sum,
+            )
+
+            break
+
+    if fwr_pkg is None:
+        sys.exit(f"ERROR: Can not find key:{pkg_key} in files")
+
+    return fwr_pkg
+
+
+def generate_firmware_package(
+    release_name: str,
+    machine: str,
+    files: list[str],
+    md5sums: dict[str, str],
+    filepath: str,
+    filename: str,
+):
+    """Main function to generate the firmware package for Flash-N-Go System and Yocto"""
+
+    # Modify name strings for firmware package
+    machine_name_long = machine.upper()
+    # Old machine name was something like "imx6guf"
+    machine_name_long = machine_name_long.replace("IMX", "i.MX")
+    machine_name_long = machine_name_long.replace("GUF", "")
+    # New machine name is something like "seco-mx6"
+    machine_name_long = machine_name_long.replace("MX", "i.MX")
+    machine_name_long = machine_name_long.replace("SECO-", "")
+    release_name = release_name.replace("Yocto-", "Yocto ")
+
+    sbom = []
+    sbom_packages = []
+
+    if "fngsystem".casefold() in release_name.casefold():
+        # Flash-N-Go System
+        file_types = [
+            FirmwarePackageKeys.FNGSYS_INIT,
+            FirmwarePackageKeys.FNGSYS_UPDATE,
+            FirmwarePackageKeys.FNGSYS_FS,
+            FirmwarePackageKeys.FNGSYS_CHECKSUM,
+        ]
+
+        sbom_files = []
+
+        for file_type in file_types:
+            sbom_files.append(
+                generate_subpackage(
+                    files, file_type, machine, machine_name_long, release_name, md5sums
+                )
+            )
+
+        sbom_packages.append(
+            generate_entry(name="", _type="FNG-SYSTEM", files=sbom_files)
+        )
+
+        # U-Boot (only on i.MX8/Genio)
+
+        sbom_uboot_files = []
+
+        if fnmatch.filter(files, "*/imx-boot.tar.gz"):
+            uboot_file_types = [
+                FirmwarePackageKeys.FNGSYS_UBOOT_UPDATE,
+                FirmwarePackageKeys.FNGSYS_UBOOT_IMAGETAR,
+            ]
+        elif fnmatch.filter(files, "*/imx-boot"):
+            uboot_file_types = [
+                FirmwarePackageKeys.FNGSYS_UBOOT_UPDATE,
+                FirmwarePackageKeys.FNGSYS_UBOOT_IMAGE,
+                FirmwarePackageKeys.FNGSYS_UBOOT_CHECKSUM,
+            ]
+
+        if uboot_file_types:
+            sbom_uboot_files = [
+                generate_subpackage(
+                    files,
+                    file_type,
+                    machine,
+                    machine_name_long,
+                    release_name,
+                    md5sums,
+                )
+                for file_type in uboot_file_types
+            ]
+
+            sbom_packages.append(
+                generate_entry(name="", _type="UBOOT", files=sbom_uboot_files)
+            )
+
+    else:
+        # Yocto OS
+        file_types = [
+            FirmwarePackageKeys.YOCTO_FNG_INSTALL,
+            FirmwarePackageKeys.YOCTO_FS,
+        ]
+
+        sbom_files = []
+
+        for file_type in file_types:
+            sbom_files.append(
+                generate_subpackage(
+                    files, file_type, machine, machine_name_long, release_name, md5sums
+                )
+            )
+
+        sbom_packages.append(generate_entry(name="", _type="YOCTO", files=sbom_files))
+
+    sbom = generate_entry(
+        name="", description="", _type="YOCTO", packages=sbom_packages
+    )
+
+    if filepath:
+        if not os.path.exists(filepath):
+            os.makedirs(filepath)
+    else:
+        filepath = "."
+
+    print(
+        f'Saving Firmware Package JSON for "{machine_name_long} {release_name}" '
+        f"to {filepath}/{filename}"
+    )
+    with open(os.path.join(filepath, filename), "w", encoding="utf-8") as jsonfile:
+        json.dump(sbom, jsonfile, indent=2)
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "--release-name",
+        help="""Name of the release""",
+        dest="release_name",
+        required=True,
+    )
+    parser.add_argument(
+        "--machine",
+        help="""Machine the release it built for""",
+        dest="machine",
+        required=True,
+    )
+    parser.add_argument(
+        "--files",
+        help="""Space-separated list of all released files""",
+        dest="files",
+        required=False,
+    )
+    parser.add_argument(
+        "--files-list",
+        help="""Text file containing a list of all released files""",
+        dest="files_list",
+        required=False,
+    )
+    parser.add_argument(
+        "--md5sums",
+        help="""Text file containing MD5 sums of released files""",
+        dest="md5sums",
+        required=True,
+    )
+    parser.add_argument(
+        "--output-file",
+        help="""Output file name and path""",
+        dest="output_file",
+        default="firmware-package.json",
+    )
+    args, _ = parser.parse_known_args()
+
+    output_filepath, output_filename = os.path.split(args.output_file)
+
+    # Sanity checking
+    if not args.machine:
+        sys.exit("ERROR: --machine requires a non-empty argument")
+    if not args.release_name:
+        sys.exit("ERROR: --release-name requires a non-empty argument")
+    if not output_filename:
+        sys.exit("ERROR: --output-file must at least contain a valid filename")
+
+    # Parse/read file list
+    files = []
+
+    if args.files:
+        files = args.files.split()
+
+    if args.files_list:
+        for files_file in glob.glob(args.files_list, recursive=True):
+            print(f"Reading files from {files_file}")
+            with open(files_file, "r", encoding="utf-8") as f:
+                files = files + f.read().splitlines()
+
+    # Read MD5 sums
+    md5sums = {}
+    for md5sums_file in glob.glob(args.md5sums, recursive=True):
+        print(f"Reading md5sums from {md5sums_file}")
+        with open(md5sums_file, "r", encoding="utf-8") as f:
+            for line in f:
+                # Assuming line format: "<md5sum>  <filename>\n"
+                name = line.split("  ")[1].rstrip()
+                md5sum = line.split("  ")[0]
+                md5sums[name] = md5sum
+
+    # Generate firmware package JSON
+    generate_firmware_package(
+        args.release_name,
+        args.machine,
+        files,
+        md5sums,
+        output_filepath,
+        output_filename,
+    )
+
+
+if __name__ == "__main__":
+    main()
-- 
GitLab