Initial version of pldm_fwup_pkg_creator.py

pldm_fwup_pkg_creator.py is a python script that can generate a PLDM
firmware update package out of one or more image files, given a
corresponding metadata JSON file.

Signed-off-by: Deepak Kodihalli <deepak.kodihalli.83@gmail.com>
Change-Id: I7655ab2be72731332afc686b1c55b1312455ba1a
Signed-off-by: Tom Joseph <rushtotom@gmail.com>
diff --git a/tools/fw-update/pldm_fwup_pkg_creator.py b/tools/fw-update/pldm_fwup_pkg_creator.py
new file mode 100755
index 0000000..3eb3ecc
--- /dev/null
+++ b/tools/fw-update/pldm_fwup_pkg_creator.py
@@ -0,0 +1,497 @@
+#!/usr/bin/env python3
+
+"""Script to create PLDM FW update package"""
+
+import argparse
+import binascii
+from datetime import datetime
+import json
+import os
+import struct
+import sys
+
+import math
+from bitarray import bitarray
+from bitarray.util import ba2int
+
+string_types = dict([
+    ("Unknown", 0),
+    ("ASCII", 1),
+    ("UTF8", 2),
+    ("UTF16", 3),
+    ("UTF16LE", 4),
+    ("UTF16BE", 5)])
+
+descriptor_type_name_length = {
+    0x0000: ["PCI Vendor ID", 2],
+    0x0001: ["IANA Enterprise ID", 4],
+    0x0002: ["UUID", 16],
+    0x0003: ["PnP Vendor ID", 3],
+    0x0004: ["ACPI Vendor ID", 4]}
+
+
+def check_string_length(string):
+    """Check if the length of the string is not greater than 255."""
+    if len(string) > 255:
+        sys.exit("ERROR: Max permitted string length is 255")
+
+
+def write_pkg_release_date_time(pldm_fw_up_pkg, release_date_time):
+    '''
+    Write the timestamp into the package header. The timestamp is formatted as
+    series of 13 bytes defined in DSP0240 specification.
+
+        Parameters:
+            pldm_fw_up_pkg: PLDM FW update package
+            release_date_time: Package Release Date Time
+    '''
+    time = release_date_time.time()
+    date = release_date_time.date()
+    us_bytes = time.microsecond.to_bytes(3, byteorder='little')
+    pldm_fw_up_pkg.write(
+        struct.pack(
+            '<hBBBBBBBBHB',
+            0,
+            us_bytes[0],
+            us_bytes[1],
+            us_bytes[2],
+            time.second,
+            time.minute,
+            time.hour,
+            date.day,
+            date.month,
+            date.year,
+            0))
+
+
+def write_package_version_string(pldm_fw_up_pkg, metadata):
+    '''
+    Write PackageVersionStringType, PackageVersionStringLength and
+    PackageVersionString to the package header.
+
+        Parameters:
+            pldm_fw_up_pkg: PLDM FW update package
+            metadata: metadata about PLDM FW update package
+    '''
+    # Hardcoded string type to ASCII
+    string_type = string_types["ASCII"]
+    package_version_string = \
+        metadata["PackageHeaderInformation"]["PackageVersionString"]
+    check_string_length(package_version_string)
+    format_string = '<BB' + str(len(package_version_string)) + 's'
+    pldm_fw_up_pkg.write(
+        struct.pack(
+            format_string,
+            string_type,
+            len(package_version_string),
+            package_version_string.encode('ascii')))
+
+
+def write_component_bitmap_bit_length(pldm_fw_up_pkg, metadata):
+    '''
+    ComponentBitmapBitLength in the package header indicates the number of bits
+    that will be used represent the bitmap in the ApplicableComponents field
+    for a matching device. The value shall be a multiple of 8 and be large
+    enough to contain a bit for each component in the package. The number of
+    components in the JSON file is used to populate the bitmap length.
+
+        Parameters:
+            pldm_fw_up_pkg: PLDM FW update package
+            metadata: metadata about PLDM FW update package
+
+        Returns:
+            ComponentBitmapBitLength: number of bits that will be used
+            represent the bitmap in the ApplicableComponents field for a
+            matching device
+    '''
+    # The script supports upto 32 components now
+    max_components = 32
+    bitmap_multiple = 8
+
+    num_components = len(metadata["ComponentImageInformationArea"])
+    if num_components > max_components:
+        sys.exit("ERROR: only upto 32 components supported now")
+    component_bitmap_bit_length = bitmap_multiple * \
+        math.ceil(num_components/bitmap_multiple)
+    pldm_fw_up_pkg.write(struct.pack('<H', int(component_bitmap_bit_length)))
+    return component_bitmap_bit_length
+
+
+def write_pkg_header_info(pldm_fw_up_pkg, metadata):
+    '''
+    ComponentBitmapBitLength in the package header indicates the number of bits
+    that will be used represent the bitmap in the ApplicableComponents field
+    for a matching device. The value shall be a multiple of 8 and be large
+    enough to contain a bit for each component in the package. The number of
+    components in the JSON file is used to populate the bitmap length.
+
+        Parameters:
+            pldm_fw_up_pkg: PLDM FW update package
+            metadata: metadata about PLDM FW update package
+
+        Returns:
+            ComponentBitmapBitLength: number of bits that will be used
+            represent the bitmap in the ApplicableComponents field for a
+            matching device
+    '''
+    uuid = metadata["PackageHeaderInformation"]["PackageHeaderIdentifier"]
+    package_header_identifier = bytearray.fromhex(uuid)
+    pldm_fw_up_pkg.write(package_header_identifier)
+
+    package_header_format_revision = \
+        metadata["PackageHeaderInformation"]["PackageHeaderFormatVersion"]
+    # Size will be computed and updated subsequently
+    package_header_size = 0
+    pldm_fw_up_pkg.write(
+        struct.pack(
+            '<BH',
+            package_header_format_revision,
+            package_header_size))
+
+    try:
+        release_date_time = datetime.strptime(
+            metadata["PackageHeaderInformation"]["PackageReleaseDateTime"],
+            "%d/%m/%Y %H:%M:%S")
+        write_pkg_release_date_time(pldm_fw_up_pkg, release_date_time)
+    except KeyError:
+        write_pkg_release_date_time(pldm_fw_up_pkg, datetime.now())
+
+    component_bitmap_bit_length = write_component_bitmap_bit_length(
+        pldm_fw_up_pkg, metadata)
+    write_package_version_string(pldm_fw_up_pkg, metadata)
+    return component_bitmap_bit_length
+
+
+def get_applicable_components(device, components, component_bitmap_bit_length):
+    '''
+    This function figures out the components applicable for the device and sets
+    the ApplicableComponents bitfield accordingly.
+
+        Parameters:
+            device: device information
+            components: list of components in the package
+            component_bitmap_bit_length: length of the ComponentBitmapBitLength
+
+        Returns:
+            The ApplicableComponents bitfield
+    '''
+    applicable_components_list = device["ApplicableComponents"]
+    applicable_components = bitarray(component_bitmap_bit_length,
+                                     endian='little')
+    applicable_components.setall(0)
+    for component in components:
+        if component["ComponentIdentifier"] in applicable_components_list:
+            applicable_components[components.index(component)] = 1
+    return applicable_components
+
+
+def write_fw_device_identification_area(pldm_fw_up_pkg, metadata,
+                                        component_bitmap_bit_length):
+    '''
+    Write firmware device ID records into the PLDM package header
+
+    This function writes the DeviceIDRecordCount and the
+    FirmwareDeviceIDRecords into the firmware update package by processing the
+    metadata JSON. Currently there is no support for optional
+    FirmwareDevicePackageData and for Additional descriptors.
+
+        Parameters:
+            pldm_fw_up_pkg: PLDM FW update package
+            metadata: metadata about PLDM FW update package
+            component_bitmap_bit_length: length of the ComponentBitmapBitLength
+    '''
+    # The spec limits the number of firmware device ID records to 255
+    max_device_id_record_count = 255
+    devices = metadata["FirmwareDeviceIdentificationArea"]
+    device_id_record_count = len(devices)
+    if device_id_record_count > max_device_id_record_count:
+        sys.exit(
+            "ERROR: there can be only upto 255 entries in the \
+                FirmwareDeviceIdentificationArea section")
+
+    # DeviceIDRecordCount
+    pldm_fw_up_pkg.write(struct.pack('<B', device_id_record_count))
+
+    for device in devices:
+        # RecordLength size
+        record_length = 2
+
+        # Only initial descriptor type supported now
+        descriptor_count = 1
+        record_length += 1
+
+        # DeviceUpdateOptionFlags
+        device_update_option_flags = bitarray(32, endian='little')
+        device_update_option_flags.setall(0)
+        # Continue component updates after failure
+        supported_device_update_option_flags = [0]
+        for option in device["DeviceUpdateOptionFlags"]:
+            if option not in supported_device_update_option_flags:
+                sys.exit("ERROR: unsupported DeviceUpdateOptionFlag entry")
+            device_update_option_flags[option] = 1
+        record_length += 4
+
+        # ComponentImageSetVersionStringType supports only ASCII for now
+        component_image_set_version_string_type = string_types["ASCII"]
+        record_length += 1
+
+        # ComponentImageSetVersionStringLength
+        component_image_set_version_string = \
+            device["ComponentImageSetVersionString"]
+        check_string_length(component_image_set_version_string)
+        record_length += len(component_image_set_version_string)
+        record_length += 1
+
+        # Optional FirmwareDevicePackageData not supported now,
+        # FirmwareDevicePackageDataLength is set to 0x0000
+        fw_device_pkg_data_length = 0
+        record_length += 2
+
+        # ApplicableComponents
+        components = metadata["ComponentImageInformationArea"]
+        applicable_components = \
+            get_applicable_components(device,
+                                      components,
+                                      component_bitmap_bit_length)
+        applicable_components_bitfield_length = \
+            round(len(applicable_components)/8)
+        record_length += applicable_components_bitfield_length
+
+        initial_descriptor = device["InitialDescriptor"]
+        initial_descriptor_type = initial_descriptor["InitialDescriptorType"]
+        initial_descriptor_data = initial_descriptor["InitialDescriptorData"]
+
+        # InitialDescriptorType
+        if descriptor_type_name_length.get(initial_descriptor_type) is None:
+            sys.exit("ERROR: Initial descriptor type not supported")
+        record_length += 2
+
+        # InitialDescriptorLength
+        initial_descriptor_length = \
+            len(bytearray.fromhex(initial_descriptor_data))
+        if initial_descriptor_length != \
+                descriptor_type_name_length.get(initial_descriptor_type)[1]:
+            err_string = "ERROR: Initial descriptor type - " + \
+                descriptor_type_name_length.get(initial_descriptor_type)[0] + \
+                " length is incorrect"
+            sys.exit(err_string)
+        record_length += 2
+
+        # InitialDescriptorData, the byte order in the JSON is retained.
+        record_length += initial_descriptor_length
+
+        format_string = '<HBIBBH' + \
+            str(applicable_components_bitfield_length) + 's' + \
+            str(len(component_image_set_version_string)) + 'sHH'
+        pldm_fw_up_pkg.write(
+            struct.pack(
+                format_string,
+                record_length,
+                descriptor_count,
+                ba2int(device_update_option_flags),
+                component_image_set_version_string_type,
+                len(component_image_set_version_string),
+                fw_device_pkg_data_length,
+                applicable_components.tobytes(),
+                component_image_set_version_string.encode('ascii'),
+                initial_descriptor_type,
+                initial_descriptor_length))
+        pldm_fw_up_pkg.write(bytearray.fromhex(initial_descriptor_data))
+
+
+def write_component_image_info_area(pldm_fw_up_pkg, metadata, image_files):
+    '''
+    Write component image information area into the PLDM package header
+
+    This function writes the ComponentImageCount and the
+    ComponentImageInformation into the firmware update package by processing
+    the metadata JSON. Currently there is no support for
+    ComponentComparisonStamp field and the component option use component
+    comparison stamp.
+
+    Parameters:
+        pldm_fw_up_pkg: PLDM FW update package
+        metadata: metadata about PLDM FW update package
+        image_files: component images
+    '''
+    components = metadata["ComponentImageInformationArea"]
+    # ComponentImageCount
+    pldm_fw_up_pkg.write(struct.pack('<H', len(components)))
+    component_location_offsets = []
+    # ComponentLocationOffset position in individual component image
+    # information
+    component_location_offset_pos = 12
+
+    for component in components:
+        # Record the location of the ComponentLocationOffset to be updated
+        # after appending images to the firmware update package
+        component_location_offsets.append(pldm_fw_up_pkg.tell() +
+                                          component_location_offset_pos)
+
+        # ComponentClassification
+        component_classification = component["ComponentClassification"]
+        if component_classification < 0 or component_classification > 0xFFFF:
+            sys.exit(
+                "ERROR: ComponentClassification should be [0x0000 - 0xFFFF]")
+
+        # ComponentIdentifier
+        component_identifier = component["ComponentIdentifier"]
+        if component_identifier < 0 or component_identifier > 0xFFFF:
+            sys.exit(
+                "ERROR: ComponentIdentifier should be [0x0000 - 0xFFFF]")
+
+        # ComponentComparisonStamp not supported
+        component_comparison_stamp = 0xFFFFFFFF
+
+        # ComponentOptions
+        component_options = bitarray(16, endian='little')
+        component_options.setall(0)
+        supported_component_options = [0]
+        for option in component["ComponentOptions"]:
+            if option not in supported_component_options:
+                sys.exit(
+                    "ERROR: unsupported ComponentOption in\
+                    ComponentImageInformationArea section")
+            component_options[option] = 1
+
+        # RequestedComponentActivationMethod
+        requested_component_activation_method = bitarray(16, endian='little')
+        requested_component_activation_method.setall(0)
+        supported_requested_component_activation_method = [0, 1, 2, 3, 4, 5]
+        for option in component["RequestedComponentActivationMethod"]:
+            if option not in supported_requested_component_activation_method:
+                sys.exit(
+                    "ERROR: unsupported RequestedComponent\
+                        ActivationMethod entry")
+            requested_component_activation_method[option] = 1
+
+        # ComponentLocationOffset
+        component_location_offset = 0
+        # ComponentSize
+        component_size = 0
+        # ComponentVersionStringType
+        component_version_string_type = string_types["ASCII"]
+        # ComponentVersionStringlength
+        # ComponentVersionString
+        component_version_string = component["ComponentVersionString"]
+        check_string_length(component_version_string)
+
+        format_string = '<HHIHHIIBB' + str(len(component_version_string)) + 's'
+        pldm_fw_up_pkg.write(
+            struct.pack(
+                format_string,
+                component_classification,
+                component_identifier,
+                component_comparison_stamp,
+                ba2int(component_options),
+                ba2int(requested_component_activation_method),
+                component_location_offset,
+                component_size,
+                component_version_string_type,
+                len(component_version_string),
+                component_version_string.encode('ascii')))
+
+    index = 0
+    pkg_header_checksum_size = 4
+    start_offset = pldm_fw_up_pkg.tell() + pkg_header_checksum_size
+    # Update ComponentLocationOffset and ComponentSize for all the components
+    for offset in component_location_offsets:
+        file_size = os.stat(image_files[index]).st_size
+        pldm_fw_up_pkg.seek(offset)
+        pldm_fw_up_pkg.write(
+            struct.pack(
+                '<II', start_offset, file_size))
+        start_offset += file_size
+        index += 1
+    pldm_fw_up_pkg.seek(0, os.SEEK_END)
+
+
+def write_pkg_header_checksum(pldm_fw_up_pkg):
+    '''
+    Write PackageHeaderChecksum into the PLDM package header.
+
+        Parameters:
+            pldm_fw_up_pkg: PLDM FW update package
+    '''
+    pldm_fw_up_pkg.seek(0)
+    package_header_checksum = binascii.crc32(pldm_fw_up_pkg.read())
+    pldm_fw_up_pkg.seek(0, os.SEEK_END)
+    pldm_fw_up_pkg.write(struct.pack('<I', package_header_checksum))
+
+
+def update_pkg_header_size(pldm_fw_up_pkg):
+    '''
+    Update PackageHeader in the PLDM package header. The package header size
+    which is the count of all bytes in the PLDM package header structure is
+    calculated once the package header contents is complete.
+
+        Parameters:
+            pldm_fw_up_pkg: PLDM FW update package
+    '''
+    file_size = pldm_fw_up_pkg.tell()
+    pkg_header_size_offset = 17
+    # Seek past PackageHeaderIdentifier and PackageHeaderFormatRevision
+    pldm_fw_up_pkg.seek(pkg_header_size_offset)
+    pldm_fw_up_pkg.write(struct.pack('<H', file_size))
+    pldm_fw_up_pkg.seek(0, os.SEEK_END)
+
+
+def append_component_images(pldm_fw_up_pkg, image_files):
+    '''
+    Append the component images to the firmware update package.
+
+        Parameters:
+            pldm_fw_up_pkg: PLDM FW update package
+            image_files: component images
+    '''
+    for image in image_files:
+        with open(image, 'rb') as file:
+            for line in file:
+                pldm_fw_up_pkg.write(line)
+
+
+def main():
+    """Create PLDM FW update (DSP0267) package based on a JSON metadata file"""
+    parser = argparse.ArgumentParser()
+    parser.add_argument("pldmfwuppkgname",
+                        help="Name of the PLDM FW update package")
+    parser.add_argument("metadatafile", help="Path of metadata JSON file")
+    parser.add_argument(
+        "images", nargs='+',
+        help="One or more firmware image paths, in the same order as\
+            ComponentImageInformationArea entries")
+
+    args = parser.parse_args()
+    image_files = args.images
+    with open(args.metadatafile) as file:
+        try:
+            metadata = json.load(file)
+        except ValueError:
+            sys.exit("ERROR: Invalid metadata JSON file")
+
+    # Validate the number of component images
+    if len(image_files) != len(metadata["ComponentImageInformationArea"]):
+        sys.exit("ERROR: number of images passed != number of entries \
+            in ComponentImageInformationArea")
+
+    try:
+        with open(args.pldmfwuppkgname, 'w+b') as pldm_fw_up_pkg:
+            component_bitmap_bit_length = write_pkg_header_info(pldm_fw_up_pkg,
+                                                                metadata)
+            write_fw_device_identification_area(pldm_fw_up_pkg,
+                                                metadata,
+                                                component_bitmap_bit_length)
+            write_component_image_info_area(pldm_fw_up_pkg, metadata,
+                                            image_files)
+            write_pkg_header_checksum(pldm_fw_up_pkg)
+            update_pkg_header_size(pldm_fw_up_pkg)
+            append_component_images(pldm_fw_up_pkg, image_files)
+            pldm_fw_up_pkg.close()
+    except BaseException:
+        pldm_fw_up_pkg.close()
+        os.remove(args.pldmfwuppkgname)
+        raise
+
+
+if __name__ == "__main__":
+    main()