1#!/usr/bin/env python3 2 3"""Script to create PLDM FW update package""" 4 5import argparse 6import binascii 7from datetime import datetime 8import json 9import os 10import struct 11import sys 12 13import math 14from bitarray import bitarray 15from bitarray.util import ba2int 16 17string_types = dict([ 18 ("Unknown", 0), 19 ("ASCII", 1), 20 ("UTF8", 2), 21 ("UTF16", 3), 22 ("UTF16LE", 4), 23 ("UTF16BE", 5)]) 24 25initial_descriptor_type_name_length = { 26 0x0000: ["PCI Vendor ID", 2], 27 0x0001: ["IANA Enterprise ID", 4], 28 0x0002: ["UUID", 16], 29 0x0003: ["PnP Vendor ID", 3], 30 0x0004: ["ACPI Vendor ID", 4]} 31 32descriptor_type_name_length = { 33 0x0000: ["PCI Vendor ID", 2], 34 0x0001: ["IANA Enterprise ID", 4], 35 0x0002: ["UUID", 16], 36 0x0003: ["PnP Vendor ID", 3], 37 0x0004: ["ACPI Vendor ID", 4], 38 0x0100: ["PCI Device ID", 2], 39 0x0101: ["PCI Subsystem Vendor ID", 2], 40 0x0102: ["PCI Subsystem ID", 2], 41 0x0103: ["PCI Revision ID", 1], 42 0x0104: ["PnP Product Identifier", 4], 43 0x0105: ["ACPI Product Identifier", 4]} 44 45 46def check_string_length(string): 47 """Check if the length of the string is not greater than 255.""" 48 if len(string) > 255: 49 sys.exit("ERROR: Max permitted string length is 255") 50 51 52def write_pkg_release_date_time(pldm_fw_up_pkg, release_date_time): 53 ''' 54 Write the timestamp into the package header. The timestamp is formatted as 55 series of 13 bytes defined in DSP0240 specification. 56 57 Parameters: 58 pldm_fw_up_pkg: PLDM FW update package 59 release_date_time: Package Release Date Time 60 ''' 61 time = release_date_time.time() 62 date = release_date_time.date() 63 us_bytes = time.microsecond.to_bytes(3, byteorder='little') 64 pldm_fw_up_pkg.write( 65 struct.pack( 66 '<hBBBBBBBBHB', 67 0, 68 us_bytes[0], 69 us_bytes[1], 70 us_bytes[2], 71 time.second, 72 time.minute, 73 time.hour, 74 date.day, 75 date.month, 76 date.year, 77 0)) 78 79 80def write_package_version_string(pldm_fw_up_pkg, metadata): 81 ''' 82 Write PackageVersionStringType, PackageVersionStringLength and 83 PackageVersionString to the package header. 84 85 Parameters: 86 pldm_fw_up_pkg: PLDM FW update package 87 metadata: metadata about PLDM FW update package 88 ''' 89 # Hardcoded string type to ASCII 90 string_type = string_types["ASCII"] 91 package_version_string = \ 92 metadata["PackageHeaderInformation"]["PackageVersionString"] 93 check_string_length(package_version_string) 94 format_string = '<BB' + str(len(package_version_string)) + 's' 95 pldm_fw_up_pkg.write( 96 struct.pack( 97 format_string, 98 string_type, 99 len(package_version_string), 100 package_version_string.encode('ascii'))) 101 102 103def write_component_bitmap_bit_length(pldm_fw_up_pkg, metadata): 104 ''' 105 ComponentBitmapBitLength in the package header indicates the number of bits 106 that will be used represent the bitmap in the ApplicableComponents field 107 for a matching device. The value shall be a multiple of 8 and be large 108 enough to contain a bit for each component in the package. The number of 109 components in the JSON file is used to populate the bitmap length. 110 111 Parameters: 112 pldm_fw_up_pkg: PLDM FW update package 113 metadata: metadata about PLDM FW update package 114 115 Returns: 116 ComponentBitmapBitLength: number of bits that will be used 117 represent the bitmap in the ApplicableComponents field for a 118 matching device 119 ''' 120 # The script supports upto 32 components now 121 max_components = 32 122 bitmap_multiple = 8 123 124 num_components = len(metadata["ComponentImageInformationArea"]) 125 if num_components > max_components: 126 sys.exit("ERROR: only upto 32 components supported now") 127 component_bitmap_bit_length = bitmap_multiple * \ 128 math.ceil(num_components/bitmap_multiple) 129 pldm_fw_up_pkg.write(struct.pack('<H', int(component_bitmap_bit_length))) 130 return component_bitmap_bit_length 131 132 133def write_pkg_header_info(pldm_fw_up_pkg, metadata): 134 ''' 135 ComponentBitmapBitLength in the package header indicates the number of bits 136 that will be used represent the bitmap in the ApplicableComponents field 137 for a matching device. The value shall be a multiple of 8 and be large 138 enough to contain a bit for each component in the package. The number of 139 components in the JSON file is used to populate the bitmap length. 140 141 Parameters: 142 pldm_fw_up_pkg: PLDM FW update package 143 metadata: metadata about PLDM FW update package 144 145 Returns: 146 ComponentBitmapBitLength: number of bits that will be used 147 represent the bitmap in the ApplicableComponents field for a 148 matching device 149 ''' 150 uuid = metadata["PackageHeaderInformation"]["PackageHeaderIdentifier"] 151 package_header_identifier = bytearray.fromhex(uuid) 152 pldm_fw_up_pkg.write(package_header_identifier) 153 154 package_header_format_revision = \ 155 metadata["PackageHeaderInformation"]["PackageHeaderFormatVersion"] 156 # Size will be computed and updated subsequently 157 package_header_size = 0 158 pldm_fw_up_pkg.write( 159 struct.pack( 160 '<BH', 161 package_header_format_revision, 162 package_header_size)) 163 164 try: 165 release_date_time = datetime.strptime( 166 metadata["PackageHeaderInformation"]["PackageReleaseDateTime"], 167 "%d/%m/%Y %H:%M:%S") 168 write_pkg_release_date_time(pldm_fw_up_pkg, release_date_time) 169 except KeyError: 170 write_pkg_release_date_time(pldm_fw_up_pkg, datetime.now()) 171 172 component_bitmap_bit_length = write_component_bitmap_bit_length( 173 pldm_fw_up_pkg, metadata) 174 write_package_version_string(pldm_fw_up_pkg, metadata) 175 return component_bitmap_bit_length 176 177 178def get_applicable_components(device, components, component_bitmap_bit_length): 179 ''' 180 This function figures out the components applicable for the device and sets 181 the ApplicableComponents bitfield accordingly. 182 183 Parameters: 184 device: device information 185 components: list of components in the package 186 component_bitmap_bit_length: length of the ComponentBitmapBitLength 187 188 Returns: 189 The ApplicableComponents bitfield 190 ''' 191 applicable_components_list = device["ApplicableComponents"] 192 applicable_components = bitarray(component_bitmap_bit_length, 193 endian='little') 194 applicable_components.setall(0) 195 for component in components: 196 if component["ComponentIdentifier"] in applicable_components_list: 197 applicable_components[components.index(component)] = 1 198 return applicable_components 199 200 201def prepare_record_descriptors(descriptors): 202 ''' 203 This function processes the Descriptors and prepares the RecordDescriptors 204 section of the the firmware device ID record. 205 206 Parameters: 207 descriptors: Descriptors entry 208 209 Returns: 210 RecordDescriptors, DescriptorCount 211 ''' 212 record_descriptors = bytearray() 213 vendor_defined_desc_type = 65535 214 vendor_desc_title_str_type_len = 1 215 vendor_desc_title_str_len_len = 1 216 descriptor_count = 0 217 218 for descriptor in descriptors: 219 220 descriptor_type = descriptor["DescriptorType"] 221 if descriptor_count == 0: 222 if initial_descriptor_type_name_length.get(descriptor_type) \ 223 is None: 224 sys.exit("ERROR: Initial descriptor type not supported") 225 else: 226 if descriptor_type_name_length.get(descriptor_type) is None and \ 227 descriptor_type != vendor_defined_desc_type: 228 sys.exit("ERROR: Descriptor type not supported") 229 230 if descriptor_type == vendor_defined_desc_type: 231 vendor_desc_title_str = \ 232 descriptor["VendorDefinedDescriptorTitleString"] 233 vendor_desc_data = descriptor["VendorDefinedDescriptorData"] 234 check_string_length(vendor_desc_title_str) 235 vendor_desc_title_str_type = string_types["ASCII"] 236 descriptor_length = vendor_desc_title_str_type_len + \ 237 vendor_desc_title_str_len_len + len(vendor_desc_title_str) + \ 238 len(bytearray.fromhex(vendor_desc_data)) 239 format_string = '<HHBB' + str(len(vendor_desc_title_str)) + 's' 240 record_descriptors.extend(struct.pack( 241 format_string, 242 descriptor_type, 243 descriptor_length, 244 vendor_desc_title_str_type, 245 len(vendor_desc_title_str), 246 vendor_desc_title_str.encode('ascii'))) 247 record_descriptors.extend(bytearray.fromhex(vendor_desc_data)) 248 descriptor_count += 1 249 else: 250 descriptor_type = descriptor["DescriptorType"] 251 descriptor_data = descriptor["DescriptorData"] 252 descriptor_length = len(bytearray.fromhex(descriptor_data)) 253 if descriptor_length != \ 254 descriptor_type_name_length.get(descriptor_type)[1]: 255 err_string = "ERROR: Descriptor type - " + \ 256 descriptor_type_name_length.get(descriptor_type)[0] + \ 257 " length is incorrect" 258 sys.exit(err_string) 259 format_string = '<HH' 260 record_descriptors.extend(struct.pack( 261 format_string, 262 descriptor_type, 263 descriptor_length)) 264 record_descriptors.extend(bytearray.fromhex(descriptor_data)) 265 descriptor_count += 1 266 return record_descriptors, descriptor_count 267 268 269def write_fw_device_identification_area(pldm_fw_up_pkg, metadata, 270 component_bitmap_bit_length): 271 ''' 272 Write firmware device ID records into the PLDM package header 273 274 This function writes the DeviceIDRecordCount and the 275 FirmwareDeviceIDRecords into the firmware update package by processing the 276 metadata JSON. Currently there is no support for optional 277 FirmwareDevicePackageData. 278 279 Parameters: 280 pldm_fw_up_pkg: PLDM FW update package 281 metadata: metadata about PLDM FW update package 282 component_bitmap_bit_length: length of the ComponentBitmapBitLength 283 ''' 284 # The spec limits the number of firmware device ID records to 255 285 max_device_id_record_count = 255 286 devices = metadata["FirmwareDeviceIdentificationArea"] 287 device_id_record_count = len(devices) 288 if device_id_record_count > max_device_id_record_count: 289 sys.exit( 290 "ERROR: there can be only upto 255 entries in the \ 291 FirmwareDeviceIdentificationArea section") 292 293 # DeviceIDRecordCount 294 pldm_fw_up_pkg.write(struct.pack('<B', device_id_record_count)) 295 296 for device in devices: 297 # RecordLength size 298 record_length = 2 299 300 # DescriptorCount 301 record_length += 1 302 303 # DeviceUpdateOptionFlags 304 device_update_option_flags = bitarray(32, endian='little') 305 device_update_option_flags.setall(0) 306 # Continue component updates after failure 307 supported_device_update_option_flags = [0] 308 for option in device["DeviceUpdateOptionFlags"]: 309 if option not in supported_device_update_option_flags: 310 sys.exit("ERROR: unsupported DeviceUpdateOptionFlag entry") 311 device_update_option_flags[option] = 1 312 record_length += 4 313 314 # ComponentImageSetVersionStringType supports only ASCII for now 315 component_image_set_version_string_type = string_types["ASCII"] 316 record_length += 1 317 318 # ComponentImageSetVersionStringLength 319 component_image_set_version_string = \ 320 device["ComponentImageSetVersionString"] 321 check_string_length(component_image_set_version_string) 322 record_length += len(component_image_set_version_string) 323 record_length += 1 324 325 # Optional FirmwareDevicePackageData not supported now, 326 # FirmwareDevicePackageDataLength is set to 0x0000 327 fw_device_pkg_data_length = 0 328 record_length += 2 329 330 # ApplicableComponents 331 components = metadata["ComponentImageInformationArea"] 332 applicable_components = \ 333 get_applicable_components(device, 334 components, 335 component_bitmap_bit_length) 336 applicable_components_bitfield_length = \ 337 round(len(applicable_components)/8) 338 record_length += applicable_components_bitfield_length 339 340 # RecordDescriptors 341 descriptors = device["Descriptors"] 342 record_descriptors, descriptor_count = \ 343 prepare_record_descriptors(descriptors) 344 record_length += len(record_descriptors) 345 346 format_string = '<HBIBBH' + \ 347 str(applicable_components_bitfield_length) + 's' + \ 348 str(len(component_image_set_version_string)) + 's' 349 pldm_fw_up_pkg.write( 350 struct.pack( 351 format_string, 352 record_length, 353 descriptor_count, 354 ba2int(device_update_option_flags), 355 component_image_set_version_string_type, 356 len(component_image_set_version_string), 357 fw_device_pkg_data_length, 358 applicable_components.tobytes(), 359 component_image_set_version_string.encode('ascii'))) 360 pldm_fw_up_pkg.write(record_descriptors) 361 362 363def write_component_image_info_area(pldm_fw_up_pkg, metadata, image_files): 364 ''' 365 Write component image information area into the PLDM package header 366 367 This function writes the ComponentImageCount and the 368 ComponentImageInformation into the firmware update package by processing 369 the metadata JSON. Currently there is no support for 370 ComponentComparisonStamp field and the component option use component 371 comparison stamp. 372 373 Parameters: 374 pldm_fw_up_pkg: PLDM FW update package 375 metadata: metadata about PLDM FW update package 376 image_files: component images 377 ''' 378 components = metadata["ComponentImageInformationArea"] 379 # ComponentImageCount 380 pldm_fw_up_pkg.write(struct.pack('<H', len(components))) 381 component_location_offsets = [] 382 # ComponentLocationOffset position in individual component image 383 # information 384 component_location_offset_pos = 12 385 386 for component in components: 387 # Record the location of the ComponentLocationOffset to be updated 388 # after appending images to the firmware update package 389 component_location_offsets.append(pldm_fw_up_pkg.tell() + 390 component_location_offset_pos) 391 392 # ComponentClassification 393 component_classification = component["ComponentClassification"] 394 if component_classification < 0 or component_classification > 0xFFFF: 395 sys.exit( 396 "ERROR: ComponentClassification should be [0x0000 - 0xFFFF]") 397 398 # ComponentIdentifier 399 component_identifier = component["ComponentIdentifier"] 400 if component_identifier < 0 or component_identifier > 0xFFFF: 401 sys.exit( 402 "ERROR: ComponentIdentifier should be [0x0000 - 0xFFFF]") 403 404 # ComponentComparisonStamp not supported 405 component_comparison_stamp = 0xFFFFFFFF 406 407 # ComponentOptions 408 component_options = bitarray(16, endian='little') 409 component_options.setall(0) 410 supported_component_options = [0, 1, 2] 411 for option in component["ComponentOptions"]: 412 if option not in supported_component_options: 413 sys.exit( 414 "ERROR: unsupported ComponentOption in\ 415 ComponentImageInformationArea section") 416 component_options[option] = 1 417 418 # RequestedComponentActivationMethod 419 requested_component_activation_method = bitarray(16, endian='little') 420 requested_component_activation_method.setall(0) 421 supported_requested_component_activation_method = [0, 1, 2, 3, 4, 5] 422 for option in component["RequestedComponentActivationMethod"]: 423 if option not in supported_requested_component_activation_method: 424 sys.exit( 425 "ERROR: unsupported RequestedComponent\ 426 ActivationMethod entry") 427 requested_component_activation_method[option] = 1 428 429 # ComponentLocationOffset 430 component_location_offset = 0 431 # ComponentSize 432 component_size = 0 433 # ComponentVersionStringType 434 component_version_string_type = string_types["ASCII"] 435 # ComponentVersionStringlength 436 # ComponentVersionString 437 component_version_string = component["ComponentVersionString"] 438 check_string_length(component_version_string) 439 440 format_string = '<HHIHHIIBB' + str(len(component_version_string)) + 's' 441 pldm_fw_up_pkg.write( 442 struct.pack( 443 format_string, 444 component_classification, 445 component_identifier, 446 component_comparison_stamp, 447 ba2int(component_options), 448 ba2int(requested_component_activation_method), 449 component_location_offset, 450 component_size, 451 component_version_string_type, 452 len(component_version_string), 453 component_version_string.encode('ascii'))) 454 455 index = 0 456 pkg_header_checksum_size = 4 457 start_offset = pldm_fw_up_pkg.tell() + pkg_header_checksum_size 458 # Update ComponentLocationOffset and ComponentSize for all the components 459 for offset in component_location_offsets: 460 file_size = os.stat(image_files[index]).st_size 461 pldm_fw_up_pkg.seek(offset) 462 pldm_fw_up_pkg.write( 463 struct.pack( 464 '<II', start_offset, file_size)) 465 start_offset += file_size 466 index += 1 467 pldm_fw_up_pkg.seek(0, os.SEEK_END) 468 469 470def write_pkg_header_checksum(pldm_fw_up_pkg): 471 ''' 472 Write PackageHeaderChecksum into the PLDM package header. 473 474 Parameters: 475 pldm_fw_up_pkg: PLDM FW update package 476 ''' 477 pldm_fw_up_pkg.seek(0) 478 package_header_checksum = binascii.crc32(pldm_fw_up_pkg.read()) 479 pldm_fw_up_pkg.seek(0, os.SEEK_END) 480 pldm_fw_up_pkg.write(struct.pack('<I', package_header_checksum)) 481 482 483def update_pkg_header_size(pldm_fw_up_pkg): 484 ''' 485 Update PackageHeader in the PLDM package header. The package header size 486 which is the count of all bytes in the PLDM package header structure is 487 calculated once the package header contents is complete. 488 489 Parameters: 490 pldm_fw_up_pkg: PLDM FW update package 491 ''' 492 pkg_header_checksum_size = 4 493 file_size = pldm_fw_up_pkg.tell() + pkg_header_checksum_size 494 pkg_header_size_offset = 17 495 # Seek past PackageHeaderIdentifier and PackageHeaderFormatRevision 496 pldm_fw_up_pkg.seek(pkg_header_size_offset) 497 pldm_fw_up_pkg.write(struct.pack('<H', file_size)) 498 pldm_fw_up_pkg.seek(0, os.SEEK_END) 499 500 501def append_component_images(pldm_fw_up_pkg, image_files): 502 ''' 503 Append the component images to the firmware update package. 504 505 Parameters: 506 pldm_fw_up_pkg: PLDM FW update package 507 image_files: component images 508 ''' 509 for image in image_files: 510 with open(image, 'rb') as file: 511 for line in file: 512 pldm_fw_up_pkg.write(line) 513 514 515def main(): 516 """Create PLDM FW update (DSP0267) package based on a JSON metadata file""" 517 parser = argparse.ArgumentParser() 518 parser.add_argument("pldmfwuppkgname", 519 help="Name of the PLDM FW update package") 520 parser.add_argument("metadatafile", help="Path of metadata JSON file") 521 parser.add_argument( 522 "images", nargs='+', 523 help="One or more firmware image paths, in the same order as\ 524 ComponentImageInformationArea entries") 525 526 args = parser.parse_args() 527 image_files = args.images 528 with open(args.metadatafile) as file: 529 try: 530 metadata = json.load(file) 531 except ValueError: 532 sys.exit("ERROR: Invalid metadata JSON file") 533 534 # Validate the number of component images 535 if len(image_files) != len(metadata["ComponentImageInformationArea"]): 536 sys.exit("ERROR: number of images passed != number of entries \ 537 in ComponentImageInformationArea") 538 539 try: 540 with open(args.pldmfwuppkgname, 'w+b') as pldm_fw_up_pkg: 541 component_bitmap_bit_length = write_pkg_header_info(pldm_fw_up_pkg, 542 metadata) 543 write_fw_device_identification_area(pldm_fw_up_pkg, 544 metadata, 545 component_bitmap_bit_length) 546 write_component_image_info_area(pldm_fw_up_pkg, metadata, 547 image_files) 548 update_pkg_header_size(pldm_fw_up_pkg) 549 write_pkg_header_checksum(pldm_fw_up_pkg) 550 append_component_images(pldm_fw_up_pkg, image_files) 551 pldm_fw_up_pkg.close() 552 except BaseException: 553 pldm_fw_up_pkg.close() 554 os.remove(args.pldmfwuppkgname) 555 raise 556 557 558if __name__ == "__main__": 559 main() 560