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