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/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/setup.cfg
diff --git a/tools/fw-update/README.md b/tools/fw-update/README.md
new file mode 100644
index 0000000..d231a1f
--- /dev/null
+++ b/tools/fw-update/README.md
@@ -0,0 +1,127 @@
+# Overview
+pldm_fwup_pkg_creator.py is a python script that can package one or more
+firmware image blobs into a PLDM firmware update package, as per the DSP0267
+specification v1.1.0 - Section 7.
+
+# Requirements
+- Python 3.6+
+- Python bitarray module
+    - For example on Ubuntu: sudo pip3 install bitarray
+
+# Usage
+
+    pldm_fwup_pkg_creator.py [-h]
+                                pldmfwuppkgname metadatafile images
+                                [images ...]
+
+    positional arguments:
+        pldmfwuppkgname  Name of the PLDM FW update package
+        metadatafile     Path of metadata JSON file
+        images           One or more firmware image paths, in the same order as
+                         ComponentImageInformationArea entries
+
+- Provide name for the PLDM FW update package, output will be written to a file
+with this name
+- Providing the metadata JSON file is a must
+- The file path of at least one image file must be provided
+- In case there are more than one images, they should be specified in the
+*same order* as the entries in the "ComponentImageInformationArea" list in the
+metadata json
+
+# Metadata JSON file
+Some fields corresponding to the PLDM firmware update package must be
+specified in an input metadata file (see the section 'Mapping fields to
+DSP0267') that's in JSON format. Typically it is not necessary to write this
+file each time the PLDM firmware update package has to be generated - one or
+more platform specific metadata JSON files can be reused. The key names used in
+the metadata JSON *match with the proprerty names* used in DSP0267.
+There is an example metadata json file included - metadata-example.json, which
+has example entries for preparing a PLDM firmware update package that targets
+three devices, and there are a total of three component images.
+
+# Mapping fields to DSP0267
+This section describes the following:
+- which fields of the PLDM firmware update package are supported by the script
+- whether those fields need to be specified in the JSON, or are generated by the
+script
+
+## Package Header Information
+Metadata JSON must have a key called "PackageHeaderInformation", which is of
+type Object. See below for details on properties:
+- PackageHeaderIdentifier: Supported, must be specified in metadata file
+- PackageHeaderFormatRevision: Supported, must be specified in metadata file
+- PackageHeaderSize: Supported, generated by the script
+- PackageReleaseDateTime: Supported, this is an optional field in the metadata
+file and the format is dd/MM/yyyy HH:mm:ss. If not specified in the metadata
+file, current time is taken.
+- ComponentBitmapBitLength: Supported, generated by the script
+    - Only 32 components supported at the moment
+- PackageVersionStringType: Supported - only ASCII at the moment. Generated by
+the script
+- PackageVersionStringLength: Supported, generated by the script
+- PackageVersionString: Supported, must be specified in metadata file
+
+## Firmware Device Identification Area
+Metadata JSON must have a key called "FirmwareDeviceIdentificationArea", which
+is of type List. Each List entry corresponds to an firmware device ID record.
+See below for details on properties:
+- DeviceIDRecordCount: Supported, generated by the script
+- FirmwareDeviceIDRecords: which is a list of Individual Firmware Device ID
+Records. Each such record can have the following properties:
+    - RecordLength: Supported, generated by the script
+    - DescriptorCount: Supported, generated by the script
+    - DeviceUpdateOptionFlags: Supported, must be specified in metadata file
+        - add each bit that is to be set to 1 to the list
+        "DeviceUpdateOptionFlags"
+    - ComponentImageSetVersionStringType: Supported - only ASCII at the moment.
+    Generated by the script
+    - ComponentImageSetVersionStringLength: Supported, generated by the script
+    - FirmwareDevicePackageDataLength: Not supported. Set to 0 by the script
+    - ApplicableComponents: Supported, must be specified in metadata file
+        - specify all "ComponentIdentifier" values that apply in the
+        "ApplicableComponents" list
+    - ComponentImageSetVersionString: Supported, must be specified in metadata
+    file
+    - RecordDescriptors:
+        - Initial Descriptor: Supported, must be specified in metadata file.
+        Metatdata file must have key called "InitialDescriptor", which is of
+        type Object:
+            - InitialDescriptorType: Supported, must be specified in metadata
+            file. The supported types are 0x0000(PCI Vendor ID), 0x0001(IANA
+            Enterprise ID), 0x0002 (UUID), 0x0003(PnP Vendor ID), 0x0004( ACPI
+            Vendor ID)
+            - InitialDescriptorLength: Supported, generated by the script
+            - InitialDescriptorData: Supported, must be specified in metadata
+            file as a hex string
+        - Optional Additional Descriptors: Not supported at the moment
+    - FirmwareDevicePackageData: Not supported at the moment
+
+## Downstream Device Identification Area
+Not supported at the moment
+
+## Component Image Information Area
+Metadata JSON must have a key called "ComponentImageInformationArea", which
+is of type List. Each List entry corresponds to an Individual Component Image
+Information. See below for details on properties:
+- ComponentImageCount: Supported, generated by the script
+- ComponentImageInformation:  which is a list of Individual Component Image
+Information records. Each such record can have the following properties:
+    - ComponentClassification: Supported, must be specified in metadata file
+    - ComponentIdentifier: Supported, must be specified in metadata file
+    - ComponentComparisonStamp: Not supported. Set to 0xFFFFFFFF by the script
+    - ComponentOptions: Supported, must be specified in metadata file
+        - add each bit that is to be set to 1 to the list "ComponentOptions"
+        - only supported option at the moment is Force Update (0x0)
+    - RequestedComponentActivationMethod: Supported, must be specified in
+    metadata file
+        - add each bit that is to be set to 1 to the list
+        "RequestedComponentActivationMethod"
+    - ComponentLocationOffset: Supported, generated by the script
+    - ComponentSize: Supported, generated by the script
+    - ComponentVersionStringType:  Supported - only ASCII at the moment.
+    Generated by the script
+    - ComponentVersionStringLength: Supported, generated by the script
+    - ComponentVersionString: Supported, must be specified in metadata file
+
+## Package Header Checksum
+Supported, generated by the script
diff --git a/tools/fw-update/metadata-example.json b/tools/fw-update/metadata-example.json
new file mode 100644
index 0000000..70751ad
--- /dev/null
+++ b/tools/fw-update/metadata-example.json
@@ -0,0 +1,83 @@
+{
+    "PackageHeaderInformation": {
+        "PackageHeaderIdentifier": "1244D2648D7D4718A030FC8A56587D5A",
+        "PackageHeaderFormatVersion": 2,
+        "PackageReleaseDateTime": "25/12/2021 00:00:00",
+        "PackageVersionString": "VersionString1"
+    },
+    "FirmwareDeviceIdentificationArea": [
+        {
+            "DeviceUpdateOptionFlags": [
+                0
+            ],
+            "ComponentImageSetVersionString": "VersionString2",
+            "ApplicableComponents": [
+                100,
+                200
+            ],
+            "InitialDescriptor": {
+                "InitialDescriptorType": 2,
+                "InitialDescriptorData": "1244D2648D7D4718A030FC8A56587D5B"
+            }
+        },
+        {
+            "DeviceUpdateOptionFlags": [
+            ],
+            "ComponentImageSetVersionString": "VersionString3",
+            "ApplicableComponents": [
+                100,
+                200,
+                300
+            ],
+            "InitialDescriptor": {
+                "InitialDescriptorType": 2,
+                "InitialDescriptorData": "1244D2648D7D4718A030FC8A56587D5C"
+            }
+        },
+        {
+            "DeviceUpdateOptionFlags": [
+            ],
+            "ComponentImageSetVersionString": "VersionString4",
+            "ApplicableComponents": [
+                100
+            ],
+            "InitialDescriptor": {
+                "InitialDescriptorType": 2,
+                "InitialDescriptorData": "1244D2648D7D4718A030FC8A56587D5D"
+            }
+        }
+    ],
+    "ComponentImageInformationArea": [
+        {
+            "ComponentClassification": 10,
+            "ComponentIdentifier": 100,
+            "ComponentOptions": [
+            ],
+            "RequestedComponentActivationMethod": [
+            ],
+            "ComponentVersionString": "VersionString5"
+        },
+        {
+            "ComponentClassification": 10,
+            "ComponentIdentifier": 200,
+            "ComponentOptions": [
+            ],
+            "RequestedComponentActivationMethod": [
+                0
+             ],
+            "ComponentVersionString": "VersionString6"
+        },
+        {
+            "ComponentClassification": 16,
+            "ComponentIdentifier": 300,
+            "ComponentOptions": [
+                0
+            ],
+            "RequestedComponentActivationMethod": [
+                2,
+                3
+            ],
+            "ComponentVersionString": "VersionString7"
+        }
+    ]
+}
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()