1 #include "ipmi_fru_info_area.hpp" 2 3 #include <phosphor-logging/elog.hpp> 4 #include <phosphor-logging/lg2.hpp> 5 6 #include <algorithm> 7 #include <ctime> 8 #include <iomanip> 9 #include <map> 10 #include <numeric> 11 #include <sstream> 12 13 namespace ipmi 14 { 15 namespace fru 16 { 17 using namespace phosphor::logging; 18 19 // Property variables 20 static constexpr auto partNumber = "Part Number"; 21 static constexpr auto serialNumber = "Serial Number"; 22 static constexpr auto manufacturer = "Manufacturer"; 23 static constexpr auto buildDate = "Mfg Date"; 24 static constexpr auto modelNumber = "Model Number"; 25 static constexpr auto prettyName = "Name"; 26 static constexpr auto version = "Version"; 27 static constexpr auto type = "Type"; 28 29 // Board info areas 30 static constexpr auto board = "Board"; 31 static constexpr auto chassis = "Chassis"; 32 static constexpr auto product = "Product"; 33 34 static constexpr auto specVersion = 0x1; 35 static constexpr auto recordUnitOfMeasurement = 0x8; // size in bytes 36 static constexpr auto checksumSize = 0x1; // size in bytes 37 static constexpr auto recordNotPresent = 0x0; 38 static constexpr auto englishLanguageCode = 0x0; 39 static constexpr auto typeLengthByteNull = 0x0; 40 static constexpr auto endOfCustomFields = 0xC1; 41 static constexpr auto commonHeaderFormatSize = 0x8; // size in bytes 42 static constexpr auto manufacturingDateSize = 0x3; 43 static constexpr auto areaSizeOffset = 0x1; 44 static constexpr uint8_t typeASCII = 0xC0; 45 static constexpr auto maxRecordAttributeValue = 0x3F; 46 47 static constexpr auto secs_from_1970_1996 = 820454400; 48 static constexpr auto maxMfgDateValue = 0xFFFFFF; // 3 Byte length 49 static constexpr auto secs_per_min = 60; 50 static constexpr auto secsToMaxMfgdate = secs_from_1970_1996 + 51 secs_per_min * maxMfgDateValue; 52 53 // Minimum size of resulting FRU blob. 54 // This is also the theoretical maximum size according to the spec: 55 // 8 bytes header + 5 areas at 0xff*8 bytes max each 56 // 8 + 5*0xff*8 = 0x27e0 57 static constexpr auto fruMinSize = 0x27E0; 58 59 // Value to use for padding. 60 // Using 0xff to match the default (blank) value in a physical EEPROM. 61 static constexpr auto fruPadValue = 0xff; 62 63 /** 64 * @brief Format Beginning of Individual IPMI FRU Data Section 65 * 66 * @param[in] langCode Language code 67 * @param[in/out] data FRU area data 68 */ 69 void preFormatProcessing(bool langCode, FruAreaData& data) 70 { 71 // Add id for version of FRU Info Storage Spec used 72 data.emplace_back(specVersion); 73 74 // Add Data Size - 0 as a placeholder, can edit after the data is finalized 75 data.emplace_back(typeLengthByteNull); 76 77 if (langCode) 78 { 79 data.emplace_back(englishLanguageCode); 80 } 81 } 82 83 /** 84 * @brief Append checksum of the FRU area data 85 * 86 * @param[in/out] data FRU area data 87 */ 88 void appendDataChecksum(FruAreaData& data) 89 { 90 uint8_t checksumVal = std::accumulate(data.begin(), data.end(), 0); 91 // Push the Zero checksum as the last byte of this data 92 // This appears to be a simple summation of all the bytes 93 data.emplace_back(-checksumVal); 94 } 95 96 /** 97 * @brief Append padding bytes for the FRU area data 98 * 99 * @param[in/out] data FRU area data 100 */ 101 void padData(FruAreaData& data) 102 { 103 uint8_t pad = (data.size() + checksumSize) % recordUnitOfMeasurement; 104 if (pad) 105 { 106 data.resize((data.size() + recordUnitOfMeasurement - pad)); 107 } 108 } 109 110 /** 111 * @brief Format End of Individual IPMI FRU Data Section 112 * 113 * @param[in/out] fruAreaData FRU area info data 114 */ 115 void postFormatProcessing(FruAreaData& data) 116 { 117 // This area needs to be padded to a multiple of 8 bytes (after checksum) 118 padData(data); 119 120 // Set size of data info area 121 data.at(areaSizeOffset) = (data.size() + checksumSize) / 122 (recordUnitOfMeasurement); 123 124 // Finally add area checksum 125 appendDataChecksum(data); 126 } 127 128 /** 129 * @brief Read chassis type property value from inventory and append to the FRU 130 * area data. 131 * 132 * @param[in] propMap map of property values 133 * @param[in,out] data FRU area data to be appended 134 */ 135 void appendChassisType(const PropertyMap& propMap, FruAreaData& data) 136 { 137 uint8_t chassisType = 0; // Not specified 138 auto iter = propMap.find(type); 139 if (iter != propMap.end()) 140 { 141 auto value = iter->second; 142 try 143 { 144 chassisType = std::stoi(value); 145 } 146 catch (const std::exception& e) 147 { 148 lg2::error("Could not parse chassis type, value: {VALUE}, " 149 "error: {ERROR}", 150 "VALUE", value, "ERROR", e); 151 chassisType = 0; 152 } 153 } 154 data.emplace_back(chassisType); 155 } 156 157 /** 158 * @brief Read property value from inventory and append to the FRU area data 159 * 160 * @param[in] key key to search for in the property inventory data 161 * @param[in] propMap map of property values 162 * @param[in,out] data FRU area data to be appended 163 */ 164 void appendData(const Property& key, const PropertyMap& propMap, 165 FruAreaData& data) 166 { 167 auto iter = propMap.find(key); 168 if (iter != propMap.end()) 169 { 170 auto value = iter->second; 171 // If starts with 0x or 0X remove them 172 // ex: 0x123a just take 123a 173 if ((value.compare(0, 2, "0x")) == 0 || 174 (value.compare(0, 2, "0X") == 0)) 175 { 176 value.erase(0, 2); 177 } 178 179 // 6 bits for length as per FRU spec v1.0 180 // if length is greater then 63(2^6) bytes then trim the data to 63 181 // bytess. 182 auto valueLength = (value.length() > maxRecordAttributeValue) 183 ? maxRecordAttributeValue 184 : value.length(); 185 // 2 bits for type 186 // Set the type to ascii 187 uint8_t typeLength = valueLength | ipmi::fru::typeASCII; 188 189 data.emplace_back(typeLength); 190 std::copy(value.begin(), value.begin() + valueLength, 191 std::back_inserter(data)); 192 } 193 else 194 { 195 // set 0 size 196 data.emplace_back(typeLengthByteNull); 197 } 198 } 199 200 std::time_t timeStringToRaw(const std::string& input) 201 { 202 // TODO: For non-US region timestamps, pass in region information for the 203 // FRU to avoid the month/day swap. 204 // 2017-02-24 - 13:59:00, Tue Nov 20 23:08:00 2018 205 static const std::vector<std::string> patterns = {"%Y-%m-%d - %H:%M:%S", 206 "%a %b %d %H:%M:%S %Y"}; 207 208 std::tm time = {}; 209 210 for (const auto& pattern : patterns) 211 { 212 std::istringstream timeStream(input); 213 timeStream >> std::get_time(&time, pattern.c_str()); 214 if (!timeStream.fail()) 215 { 216 break; 217 } 218 } 219 220 return timegm(&time); 221 } 222 223 /** 224 * @brief Appends Build Date 225 * 226 * @param[in] propMap map of property values 227 * @param[in/out] data FRU area to add the manfufacture date 228 */ 229 void appendMfgDate(const PropertyMap& propMap, FruAreaData& data) 230 { 231 // MFG Date/Time 232 auto iter = propMap.find(buildDate); 233 if ((iter != propMap.end()) && (iter->second.size() > 0)) 234 { 235 std::time_t raw = timeStringToRaw(iter->second); 236 237 // From FRU Spec: 238 // "Mfg. Date / Time 239 // Number of minutes from 0:00 hrs 1/1/96. 240 // LSbyte first (little endian) 241 // 00_00_00h = unspecified." 242 if ((raw >= secs_from_1970_1996) && (raw <= secsToMaxMfgdate)) 243 { 244 raw -= secs_from_1970_1996; 245 raw /= secs_per_min; 246 uint8_t fru_raw[3]; 247 fru_raw[0] = raw & 0xFF; 248 fru_raw[1] = (raw >> 8) & 0xFF; 249 fru_raw[2] = (raw >> 16) & 0xFF; 250 std::copy(fru_raw, fru_raw + 3, std::back_inserter(data)); 251 return; 252 } 253 std::fprintf(stderr, "MgfDate invalid date: %u secs since UNIX epoch\n", 254 static_cast<unsigned int>(raw)); 255 } 256 // Blank date 257 data.emplace_back(0); 258 data.emplace_back(0); 259 data.emplace_back(0); 260 } 261 262 /** 263 * @brief Builds a section of the common header 264 * 265 * @param[in] infoAreaSize size of the FRU area to write 266 * @param[in] offset Current offset for data in overall record 267 * @param[in/out] data Common Header section data container 268 */ 269 void buildCommonHeaderSection(const uint32_t& infoAreaSize, uint16_t& offset, 270 FruAreaData& data) 271 { 272 // Check if data for internal use section populated 273 if (infoAreaSize == 0) 274 { 275 // Indicate record not present 276 data.emplace_back(recordNotPresent); 277 } 278 else 279 { 280 // offset should be multiple of 8. 281 auto remainder = offset % recordUnitOfMeasurement; 282 // add the padding bytes in the offset so that offset 283 // will be multiple of 8 byte. 284 offset += (remainder > 0) ? recordUnitOfMeasurement - remainder : 0; 285 // Place data to define offset to area data section 286 data.emplace_back(offset / recordUnitOfMeasurement); 287 288 offset += infoAreaSize; 289 } 290 } 291 292 /** 293 * @brief Builds the Chassis info area data section 294 * 295 * @param[in] propMap map of properties for chassis info area 296 * @return FruAreaData container with chassis info area 297 */ 298 FruAreaData buildChassisInfoArea(const PropertyMap& propMap) 299 { 300 FruAreaData fruAreaData; 301 if (!propMap.empty()) 302 { 303 // Set formatting data that goes at the beginning of the record 304 preFormatProcessing(false, fruAreaData); 305 306 // chassis type 307 appendChassisType(propMap, fruAreaData); 308 309 // Chasiss part number, in config.yaml it is configured as model 310 appendData(modelNumber, propMap, fruAreaData); 311 312 // Board serial number 313 appendData(serialNumber, propMap, fruAreaData); 314 315 // Indicate End of Custom Fields 316 fruAreaData.emplace_back(endOfCustomFields); 317 318 // Complete record data formatting 319 postFormatProcessing(fruAreaData); 320 } 321 return fruAreaData; 322 } 323 324 /** 325 * @brief Builds the Board info area data section 326 * 327 * @param[in] propMap map of properties for board info area 328 * @return FruAreaData container with board info area 329 */ 330 FruAreaData buildBoardInfoArea(const PropertyMap& propMap) 331 { 332 FruAreaData fruAreaData; 333 if (!propMap.empty()) 334 { 335 preFormatProcessing(true, fruAreaData); 336 337 // Manufacturing date 338 appendMfgDate(propMap, fruAreaData); 339 340 // manufacturer 341 appendData(manufacturer, propMap, fruAreaData); 342 343 // Product name/Pretty name 344 appendData(prettyName, propMap, fruAreaData); 345 346 // Board serial number 347 appendData(serialNumber, propMap, fruAreaData); 348 349 // Board part number 350 appendData(partNumber, propMap, fruAreaData); 351 352 // FRU File ID - Empty 353 fruAreaData.emplace_back(typeLengthByteNull); 354 355 // Empty FRU File ID bytes 356 fruAreaData.emplace_back(recordNotPresent); 357 358 // End of custom fields 359 fruAreaData.emplace_back(endOfCustomFields); 360 361 postFormatProcessing(fruAreaData); 362 } 363 return fruAreaData; 364 } 365 366 /** 367 * @brief Builds the Product info area data section 368 * 369 * @param[in] propMap map of FRU properties for Board info area 370 * @return FruAreaData container with product info area data 371 */ 372 FruAreaData buildProductInfoArea(const PropertyMap& propMap) 373 { 374 FruAreaData fruAreaData; 375 if (!propMap.empty()) 376 { 377 // Set formatting data that goes at the beginning of the record 378 preFormatProcessing(true, fruAreaData); 379 380 // manufacturer 381 appendData(manufacturer, propMap, fruAreaData); 382 383 // Product name/Pretty name 384 appendData(prettyName, propMap, fruAreaData); 385 386 // Product part/model number 387 appendData(modelNumber, propMap, fruAreaData); 388 389 // Product version 390 appendData(version, propMap, fruAreaData); 391 392 // Serial Number 393 appendData(serialNumber, propMap, fruAreaData); 394 395 // Add Asset Tag 396 fruAreaData.emplace_back(recordNotPresent); 397 398 // FRU File ID - Empty 399 fruAreaData.emplace_back(typeLengthByteNull); 400 401 // Empty FRU File ID bytes 402 fruAreaData.emplace_back(recordNotPresent); 403 404 // End of custom fields 405 fruAreaData.emplace_back(endOfCustomFields); 406 407 postFormatProcessing(fruAreaData); 408 } 409 return fruAreaData; 410 } 411 412 FruAreaData buildFruAreaData(const FruInventoryData& inventory) 413 { 414 FruAreaData combFruArea{}; 415 // Now build common header with data for this FRU Inv Record 416 // Use this variable to increment size of header as we go along to determine 417 // offset for the subsequent area offsets 418 uint16_t curDataOffset = commonHeaderFormatSize; 419 // First byte is id for version of FRU Info Storage Spec used 420 combFruArea.emplace_back(specVersion); 421 422 // 2nd byte is offset to internal use data 423 combFruArea.emplace_back(recordNotPresent); 424 425 // 3rd byte is offset to chassis data 426 FruAreaData chassisArea; 427 auto chassisIt = inventory.find(chassis); 428 if (chassisIt != inventory.end()) 429 { 430 chassisArea = buildChassisInfoArea(chassisIt->second); 431 } 432 // update the offset to chassis data. 433 buildCommonHeaderSection(chassisArea.size(), curDataOffset, combFruArea); 434 435 // 4th byte is offset to board data 436 FruAreaData boardArea; 437 auto boardIt = inventory.find(board); 438 if (boardIt != inventory.end()) 439 { 440 boardArea = buildBoardInfoArea(boardIt->second); 441 } 442 // update the offset to the board data. 443 buildCommonHeaderSection(boardArea.size(), curDataOffset, combFruArea); 444 445 // 5th byte is offset to product data 446 FruAreaData prodArea; 447 auto prodIt = inventory.find(product); 448 if (prodIt != inventory.end()) 449 { 450 prodArea = buildProductInfoArea(prodIt->second); 451 } 452 // update the offset to the product data. 453 buildCommonHeaderSection(prodArea.size(), curDataOffset, combFruArea); 454 455 // 6th byte is offset to multirecord data 456 combFruArea.emplace_back(recordNotPresent); 457 458 // 7th byte is PAD 459 combFruArea.emplace_back(recordNotPresent); 460 461 // 8th (Final byte of Header Format) is the checksum 462 appendDataChecksum(combFruArea); 463 464 // Combine everything into one full IPMI FRU specification Record 465 // add chassis use area data 466 combFruArea.insert(combFruArea.end(), chassisArea.begin(), 467 chassisArea.end()); 468 469 // add board area data 470 combFruArea.insert(combFruArea.end(), boardArea.begin(), boardArea.end()); 471 472 // add product use area data 473 combFruArea.insert(combFruArea.end(), prodArea.begin(), prodArea.end()); 474 475 // If area is smaller than the minimum size, pad it. This enables ipmitool 476 // to update the FRU blob with values longer than the original payload. 477 if (combFruArea.size() < fruMinSize) 478 { 479 combFruArea.resize(fruMinSize, fruPadValue); 480 } 481 482 return combFruArea; 483 } 484 485 } // namespace fru 486 } // namespace ipmi 487