1 /** 2 * Copyright © 2019 IBM Corporation 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 #include "config.h" 17 18 #include "pel.hpp" 19 20 #include "bcd_time.hpp" 21 #include "extended_user_data.hpp" 22 #include "extended_user_header.hpp" 23 #include "failing_mtms.hpp" 24 #include "fru_identity.hpp" 25 #include "json_utils.hpp" 26 #include "log_id.hpp" 27 #include "pel_rules.hpp" 28 #include "pel_values.hpp" 29 #include "section_factory.hpp" 30 #include "src.hpp" 31 #include "stream.hpp" 32 #include "user_data_formats.hpp" 33 34 #ifdef PEL_ENABLE_PHAL 35 #include "phal_service_actions.hpp" 36 #include "sbe_ffdc_handler.hpp" 37 #endif 38 39 #include <fmt/format.h> 40 #include <sys/stat.h> 41 #include <unistd.h> 42 43 #include <iostream> 44 #include <phosphor-logging/log.hpp> 45 46 namespace openpower 47 { 48 namespace pels 49 { 50 namespace message = openpower::pels::message; 51 namespace pv = openpower::pels::pel_values; 52 using namespace phosphor::logging; 53 54 constexpr auto unknownValue = "Unknown"; 55 56 PEL::PEL(const message::Entry& regEntry, uint32_t obmcLogID, uint64_t timestamp, 57 phosphor::logging::Entry::Level severity, 58 const AdditionalData& additionalData, const PelFFDC& ffdcFilesIn, 59 const DataInterfaceBase& dataIface) 60 { 61 // No changes in input, for non SBE error related requests 62 PelFFDC ffdcFiles = ffdcFilesIn; 63 64 #ifdef PEL_ENABLE_PHAL 65 // Add sbe ffdc processed data into ffdcfiles. 66 namespace sbe = openpower::pels::sbe; 67 auto processReq = 68 std::any_of(ffdcFiles.begin(), ffdcFiles.end(), [](const auto& file) { 69 return file.format == UserDataFormat::custom && 70 file.subType == sbe::sbeFFDCSubType; 71 }); 72 // sbeFFDC can't be destroyed until the end of the PEL constructor 73 // because it needs to keep around the FFDC Files to be used below. 74 std::unique_ptr<sbe::SbeFFDC> sbeFFDCPtr; 75 if (processReq) 76 { 77 sbeFFDCPtr = 78 std::make_unique<sbe::SbeFFDC>(additionalData, ffdcFilesIn); 79 const auto& sbeFFDCFiles = sbeFFDCPtr->getSbeFFDC(); 80 ffdcFiles.insert(ffdcFiles.end(), sbeFFDCFiles.begin(), 81 sbeFFDCFiles.end()); 82 } 83 #endif 84 85 std::map<std::string, std::vector<std::string>> debugData; 86 nlohmann::json callouts; 87 88 _ph = std::make_unique<PrivateHeader>(regEntry.componentID, obmcLogID, 89 timestamp); 90 _uh = std::make_unique<UserHeader>(regEntry, severity, additionalData, 91 dataIface); 92 93 // Extract any callouts embedded in an FFDC file. 94 if (!ffdcFiles.empty()) 95 { 96 try 97 { 98 callouts = getCalloutJSON(ffdcFiles); 99 } 100 catch (const std::exception& e) 101 { 102 debugData.emplace("FFDC file JSON callouts error", 103 std::vector<std::string>{e.what()}); 104 } 105 } 106 107 auto src = 108 std::make_unique<SRC>(regEntry, additionalData, callouts, dataIface); 109 110 if (!src->getDebugData().empty()) 111 { 112 // Something didn't go as planned 113 debugData.emplace("SRC", src->getDebugData()); 114 } 115 116 auto euh = std::make_unique<ExtendedUserHeader>(dataIface, regEntry, *src); 117 118 _optionalSections.push_back(std::move(src)); 119 _optionalSections.push_back(std::move(euh)); 120 121 auto mtms = std::make_unique<FailingMTMS>(dataIface); 122 _optionalSections.push_back(std::move(mtms)); 123 124 auto ud = util::makeSysInfoUserDataSection(additionalData, dataIface); 125 addUserDataSection(std::move(ud)); 126 127 // Check for pel severity of type - 0x51 = critical error, system 128 // termination and update terminate bit in SRC for pels 129 updateTerminateBitInSRCSection(); 130 131 // Create a UserData section from AdditionalData. 132 if (!additionalData.empty()) 133 { 134 ud = util::makeADUserDataSection(additionalData); 135 addUserDataSection(std::move(ud)); 136 } 137 138 // Add any FFDC files into UserData sections 139 for (const auto& file : ffdcFiles) 140 { 141 ud = util::makeFFDCuserDataSection(regEntry.componentID, file); 142 if (!ud) 143 { 144 // Add this error into the debug data UserData section 145 std::ostringstream msg; 146 msg << "Could not make PEL FFDC UserData section from file" 147 << std::hex << regEntry.componentID << " " << file.subType 148 << " " << file.version; 149 if (debugData.count("FFDC File")) 150 { 151 debugData.at("FFDC File").push_back(msg.str()); 152 } 153 else 154 { 155 debugData.emplace("FFDC File", 156 std::vector<std::string>{msg.str()}); 157 } 158 159 continue; 160 } 161 162 addUserDataSection(std::move(ud)); 163 } 164 165 #ifdef PEL_ENABLE_PHAL 166 auto path = std::string(OBJ_ENTRY) + '/' + std::to_string(obmcLogID); 167 openpower::pels::phal::createServiceActions(callouts, path, dataIface, 168 plid()); 169 #endif 170 171 // Store in the PEL any important debug data created while 172 // building the PEL sections. 173 if (!debugData.empty()) 174 { 175 nlohmann::json data; 176 data["PEL Internal Debug Data"] = debugData; 177 ud = util::makeJSONUserDataSection(data); 178 179 addUserDataSection(std::move(ud)); 180 181 // Also put in the journal for debug 182 for (const auto& [name, data] : debugData) 183 { 184 for (const auto& message : data) 185 { 186 std::string entry = name + ": " + message; 187 log<level::INFO>(entry.c_str()); 188 } 189 } 190 } 191 192 _ph->setSectionCount(2 + _optionalSections.size()); 193 194 checkRulesAndFix(); 195 } 196 197 PEL::PEL(std::vector<uint8_t>& data) : PEL(data, 0) 198 { 199 } 200 201 PEL::PEL(std::vector<uint8_t>& data, uint32_t obmcLogID) 202 { 203 populateFromRawData(data, obmcLogID); 204 } 205 206 void PEL::populateFromRawData(std::vector<uint8_t>& data, uint32_t obmcLogID) 207 { 208 Stream pelData{data}; 209 _ph = std::make_unique<PrivateHeader>(pelData); 210 if (obmcLogID != 0) 211 { 212 _ph->setOBMCLogID(obmcLogID); 213 } 214 215 _uh = std::make_unique<UserHeader>(pelData); 216 217 // Use the section factory to create the rest of the objects 218 for (size_t i = 2; i < _ph->sectionCount(); i++) 219 { 220 auto section = section_factory::create(pelData); 221 _optionalSections.push_back(std::move(section)); 222 } 223 } 224 225 bool PEL::valid() const 226 { 227 bool valid = _ph->valid(); 228 229 if (valid) 230 { 231 valid = _uh->valid(); 232 } 233 234 if (valid) 235 { 236 if (!std::all_of(_optionalSections.begin(), _optionalSections.end(), 237 [](const auto& section) { return section->valid(); })) 238 { 239 valid = false; 240 } 241 } 242 243 return valid; 244 } 245 246 void PEL::setCommitTime() 247 { 248 auto now = std::chrono::system_clock::now(); 249 _ph->setCommitTimestamp(getBCDTime(now)); 250 } 251 252 void PEL::assignID() 253 { 254 _ph->setID(generatePELID()); 255 } 256 257 void PEL::flatten(std::vector<uint8_t>& pelBuffer) const 258 { 259 Stream pelData{pelBuffer}; 260 261 if (!valid()) 262 { 263 log<level::WARNING>("Unflattening an invalid PEL"); 264 } 265 266 _ph->flatten(pelData); 267 _uh->flatten(pelData); 268 269 for (auto& section : _optionalSections) 270 { 271 section->flatten(pelData); 272 } 273 } 274 275 std::vector<uint8_t> PEL::data() const 276 { 277 std::vector<uint8_t> pelData; 278 flatten(pelData); 279 return pelData; 280 } 281 282 size_t PEL::size() const 283 { 284 size_t size = 0; 285 286 if (_ph) 287 { 288 size += _ph->header().size; 289 } 290 291 if (_uh) 292 { 293 size += _uh->header().size; 294 } 295 296 for (const auto& section : _optionalSections) 297 { 298 size += section->header().size; 299 } 300 301 return size; 302 } 303 304 std::optional<SRC*> PEL::primarySRC() const 305 { 306 auto src = std::find_if( 307 _optionalSections.begin(), _optionalSections.end(), [](auto& section) { 308 return section->header().id == 309 static_cast<uint16_t>(SectionID::primarySRC); 310 }); 311 if (src != _optionalSections.end()) 312 { 313 return static_cast<SRC*>(src->get()); 314 } 315 316 return std::nullopt; 317 } 318 319 void PEL::checkRulesAndFix() 320 { 321 // Only fix if the action flags are at their default value which 322 // means they weren't specified in the registry. Otherwise 323 // assume the user knows what they are doing. 324 if (_uh->actionFlags() == actionFlagsDefault) 325 { 326 auto [actionFlags, eventType] = 327 pel_rules::check(0, _uh->eventType(), _uh->severity()); 328 329 _uh->setActionFlags(actionFlags); 330 _uh->setEventType(eventType); 331 } 332 } 333 334 void PEL::printSectionInJSON(const Section& section, std::string& buf, 335 std::map<uint16_t, size_t>& pluralSections, 336 message::Registry& registry, 337 const std::vector<std::string>& plugins, 338 uint8_t creatorID) const 339 { 340 char tmpB[5]; 341 uint8_t id[] = {static_cast<uint8_t>(section.header().id >> 8), 342 static_cast<uint8_t>(section.header().id)}; 343 sprintf(tmpB, "%c%c", id[0], id[1]); 344 std::string sectionID(tmpB); 345 std::string sectionName = pv::sectionTitles.count(sectionID) 346 ? pv::sectionTitles.at(sectionID) 347 : "Unknown Section"; 348 349 // Add a count if there are multiple of this type of section 350 auto count = pluralSections.find(section.header().id); 351 if (count != pluralSections.end()) 352 { 353 sectionName += " " + std::to_string(count->second); 354 count->second++; 355 } 356 357 if (section.valid()) 358 { 359 std::optional<std::string> json; 360 if (sectionID == "PS" || sectionID == "SS") 361 { 362 json = section.getJSON(registry, plugins, creatorID); 363 } 364 else if ((sectionID == "UD") || (sectionID == "ED")) 365 { 366 json = section.getJSON(creatorID, plugins); 367 } 368 else 369 { 370 json = section.getJSON(); 371 } 372 373 buf += "\"" + sectionName + "\": {\n"; 374 375 if (json) 376 { 377 buf += *json + "\n},\n"; 378 } 379 else 380 { 381 jsonInsert(buf, pv::sectionVer, 382 getNumberString("%d", section.header().version), 1); 383 jsonInsert(buf, pv::subSection, 384 getNumberString("%d", section.header().subType), 1); 385 jsonInsert(buf, pv::createdBy, 386 getNumberString("0x%X", section.header().componentID), 387 1); 388 389 std::vector<uint8_t> data; 390 Stream s{data}; 391 section.flatten(s); 392 std::string dstr = 393 dumpHex(std::data(data) + SectionHeader::flattenedSize(), 394 data.size() - SectionHeader::flattenedSize(), 2); 395 std::string jsonIndent(indentLevel, 0x20); 396 buf += jsonIndent + "\"Data\": [\n"; 397 buf += dstr; 398 buf += jsonIndent + "]\n"; 399 buf += "},\n"; 400 } 401 } 402 else 403 { 404 buf += "\n\"Invalid Section\": [\n \"invalid\"\n],\n"; 405 } 406 } 407 408 std::map<uint16_t, size_t> PEL::getPluralSections() const 409 { 410 std::map<uint16_t, size_t> sectionCounts; 411 412 for (const auto& section : optionalSections()) 413 { 414 if (sectionCounts.find(section->header().id) == sectionCounts.end()) 415 { 416 sectionCounts[section->header().id] = 1; 417 } 418 else 419 { 420 sectionCounts[section->header().id]++; 421 } 422 } 423 424 std::map<uint16_t, size_t> sections; 425 for (const auto& [id, count] : sectionCounts) 426 { 427 if (count > 1) 428 { 429 // Start with 0 here and printSectionInJSON() 430 // will increment it as it goes. 431 sections.emplace(id, 0); 432 } 433 } 434 435 return sections; 436 } 437 438 void PEL::toJSON(message::Registry& registry, 439 const std::vector<std::string>& plugins) const 440 { 441 auto sections = getPluralSections(); 442 443 std::string buf = "{\n"; 444 printSectionInJSON(*(_ph.get()), buf, sections, registry, plugins); 445 printSectionInJSON(*(_uh.get()), buf, sections, registry, plugins); 446 for (auto& section : this->optionalSections()) 447 { 448 printSectionInJSON(*(section.get()), buf, sections, registry, plugins, 449 _ph->creatorID()); 450 } 451 buf += "}"; 452 std::size_t found = buf.rfind(","); 453 if (found != std::string::npos) 454 buf.replace(found, 1, ""); 455 std::cout << buf << std::endl; 456 } 457 458 bool PEL::addUserDataSection(std::unique_ptr<UserData> userData) 459 { 460 if (size() + userData->header().size > _maxPELSize) 461 { 462 if (userData->shrink(_maxPELSize - size())) 463 { 464 _optionalSections.push_back(std::move(userData)); 465 } 466 else 467 { 468 log<level::WARNING>( 469 "Could not shrink UserData section. Dropping", 470 entry("SECTION_SIZE=%d\n", userData->header().size), 471 entry("COMPONENT_ID=0x%02X", userData->header().componentID), 472 entry("SUBTYPE=0x%X", userData->header().subType), 473 entry("VERSION=0x%X", userData->header().version)); 474 return false; 475 } 476 } 477 else 478 { 479 _optionalSections.push_back(std::move(userData)); 480 } 481 return true; 482 } 483 484 nlohmann::json PEL::getCalloutJSON(const PelFFDC& ffdcFiles) 485 { 486 nlohmann::json callouts; 487 488 for (const auto& file : ffdcFiles) 489 { 490 if ((file.format == UserDataFormat::json) && 491 (file.subType == jsonCalloutSubtype)) 492 { 493 auto data = util::readFD(file.fd); 494 if (data.empty()) 495 { 496 throw std::runtime_error{ 497 "Could not get data from JSON callout file descriptor"}; 498 } 499 500 std::string jsonString{data.begin(), data.begin() + data.size()}; 501 502 callouts = nlohmann::json::parse(jsonString); 503 break; 504 } 505 } 506 507 return callouts; 508 } 509 510 bool PEL::isHwCalloutPresent() const 511 { 512 auto pSRC = primarySRC(); 513 if (!pSRC) 514 { 515 return false; 516 } 517 518 bool calloutPresent = false; 519 if ((*pSRC)->callouts()) 520 { 521 for (auto& i : (*pSRC)->callouts()->callouts()) 522 { 523 if (((*i).fruIdentity())) 524 { 525 auto& fruId = (*i).fruIdentity(); 526 if ((*fruId).failingComponentType() == 527 src::FRUIdentity::hardwareFRU) 528 { 529 calloutPresent = true; 530 break; 531 } 532 } 533 } 534 } 535 536 return calloutPresent; 537 } 538 539 void PEL::updateSysInfoInExtendedUserDataSection( 540 const DataInterfaceBase& dataIface) 541 { 542 const AdditionalData additionalData; 543 544 // Check for PEL from Hostboot 545 if (_ph->creatorID() == static_cast<uint8_t>(CreatorID::hostboot)) 546 { 547 // Get the ED section from PEL 548 auto op = std::find_if(_optionalSections.begin(), 549 _optionalSections.end(), [](auto& section) { 550 return section->header().id == 551 static_cast<uint16_t>( 552 SectionID::extUserData); 553 }); 554 555 // Check for ED section found and its not the last section of PEL 556 if (op != _optionalSections.end()) 557 { 558 // Get the extended user data class mapped to found section 559 auto extUserData = static_cast<ExtendedUserData*>(op->get()); 560 561 // Check for the creator ID is for OpenBMC 562 if (extUserData->creatorID() == 563 static_cast<uint8_t>(CreatorID::openBMC)) 564 { 565 // Update subtype and component id 566 auto subType = static_cast<uint8_t>(UserDataFormat::json); 567 auto componentId = 568 static_cast<uint16_t>(ComponentID::phosphorLogging); 569 570 // Update system data to ED section 571 auto ud = 572 util::makeSysInfoUserDataSection(additionalData, dataIface); 573 extUserData->updateDataSection(subType, componentId, 574 ud->data()); 575 } 576 } 577 } 578 } 579 580 void PEL::updateTerminateBitInSRCSection() 581 { 582 // Check for pel severity of type - 0x51 = critical error, system 583 // termination 584 if (_uh->severity() == 0x51) 585 { 586 // Get the primary SRC section 587 auto pSRC = primarySRC(); 588 if (pSRC) 589 { 590 (*pSRC)->setTerminateBit(); 591 } 592 } 593 } 594 595 namespace util 596 { 597 598 std::unique_ptr<UserData> makeJSONUserDataSection(const nlohmann::json& json) 599 { 600 auto jsonString = json.dump(); 601 std::vector<uint8_t> jsonData(jsonString.begin(), jsonString.end()); 602 603 // Pad to a 4 byte boundary 604 while ((jsonData.size() % 4) != 0) 605 { 606 jsonData.push_back(0); 607 } 608 609 return std::make_unique<UserData>( 610 static_cast<uint16_t>(ComponentID::phosphorLogging), 611 static_cast<uint8_t>(UserDataFormat::json), 612 static_cast<uint8_t>(UserDataFormatVersion::json), jsonData); 613 } 614 615 std::unique_ptr<UserData> makeADUserDataSection(const AdditionalData& ad) 616 { 617 assert(!ad.empty()); 618 nlohmann::json json; 619 620 // Remove the 'ESEL' entry, as it contains a full PEL in the value. 621 if (ad.getValue("ESEL")) 622 { 623 auto newAD = ad; 624 newAD.remove("ESEL"); 625 json = newAD.toJSON(); 626 } 627 else 628 { 629 json = ad.toJSON(); 630 } 631 632 return makeJSONUserDataSection(json); 633 } 634 635 void addProcessNameToJSON(nlohmann::json& json, 636 const std::optional<std::string>& pid, 637 const DataInterfaceBase& dataIface) 638 { 639 std::string name{unknownValue}; 640 641 try 642 { 643 if (pid) 644 { 645 auto n = dataIface.getProcessName(*pid); 646 if (n) 647 { 648 name = *n; 649 } 650 } 651 } 652 catch (const std::exception& e) 653 { 654 } 655 656 if (pid) 657 { 658 json["Process Name"] = std::move(name); 659 } 660 } 661 662 void addBMCFWVersionIDToJSON(nlohmann::json& json, 663 const DataInterfaceBase& dataIface) 664 { 665 auto id = dataIface.getBMCFWVersionID(); 666 if (id.empty()) 667 { 668 id = unknownValue; 669 } 670 671 json["FW Version ID"] = std::move(id); 672 } 673 674 std::string lastSegment(char separator, std::string data) 675 { 676 auto pos = data.find_last_of(separator); 677 if (pos != std::string::npos) 678 { 679 data = data.substr(pos + 1); 680 } 681 682 return data; 683 } 684 685 void addIMKeyword(nlohmann::json& json, const DataInterfaceBase& dataIface) 686 { 687 auto keyword = dataIface.getSystemIMKeyword(); 688 689 std::string value{}; 690 691 std::for_each(keyword.begin(), keyword.end(), [&](const auto& byte) { 692 value += fmt::format("{:02X}", byte); 693 }); 694 695 json["System IM"] = value; 696 } 697 698 void addStatesToJSON(nlohmann::json& json, const DataInterfaceBase& dataIface) 699 { 700 json["BMCState"] = lastSegment('.', dataIface.getBMCState()); 701 json["ChassisState"] = lastSegment('.', dataIface.getChassisState()); 702 json["HostState"] = lastSegment('.', dataIface.getHostState()); 703 json["BootState"] = lastSegment('.', dataIface.getBootState()); 704 } 705 706 std::unique_ptr<UserData> 707 makeSysInfoUserDataSection(const AdditionalData& ad, 708 const DataInterfaceBase& dataIface) 709 { 710 nlohmann::json json; 711 712 addProcessNameToJSON(json, ad.getValue("_PID"), dataIface); 713 addBMCFWVersionIDToJSON(json, dataIface); 714 addIMKeyword(json, dataIface); 715 addStatesToJSON(json, dataIface); 716 717 return makeJSONUserDataSection(json); 718 } 719 720 std::vector<uint8_t> readFD(int fd) 721 { 722 std::vector<uint8_t> data; 723 724 // Get the size 725 struct stat s; 726 int r = fstat(fd, &s); 727 if (r != 0) 728 { 729 auto e = errno; 730 log<level::ERR>("Could not get FFDC file size from FD", 731 entry("ERRNO=%d", e)); 732 return data; 733 } 734 735 if (0 == s.st_size) 736 { 737 log<level::ERR>("FFDC file is empty"); 738 return data; 739 } 740 741 data.resize(s.st_size); 742 743 // Make sure its at the beginning, as maybe another 744 // extension already used it. 745 r = lseek(fd, 0, SEEK_SET); 746 if (r == -1) 747 { 748 auto e = errno; 749 log<level::ERR>("Could not seek to beginning of FFDC file", 750 entry("ERRNO=%d", e)); 751 return data; 752 } 753 754 r = read(fd, data.data(), s.st_size); 755 if (r == -1) 756 { 757 auto e = errno; 758 log<level::ERR>("Could not read FFDC file", entry("ERRNO=%d", e)); 759 } 760 else if (r != s.st_size) 761 { 762 log<level::WARNING>("Could not read full FFDC file", 763 entry("FILE_SIZE=%d", s.st_size), 764 entry("SIZE_READ=%d", r)); 765 } 766 767 return data; 768 } 769 770 std::unique_ptr<UserData> makeFFDCuserDataSection(uint16_t componentID, 771 const PelFFDCfile& file) 772 { 773 auto data = readFD(file.fd); 774 775 if (data.empty()) 776 { 777 return std::unique_ptr<UserData>(); 778 } 779 780 // The data needs 4 Byte alignment, and save amount padded for the 781 // CBOR case. 782 uint32_t pad = 0; 783 while (data.size() % 4) 784 { 785 data.push_back(0); 786 pad++; 787 } 788 789 // For JSON, CBOR, and Text use our component ID, subType, and version, 790 // otherwise use the supplied ones. 791 uint16_t compID = static_cast<uint16_t>(ComponentID::phosphorLogging); 792 uint8_t subType{}; 793 uint8_t version{}; 794 795 switch (file.format) 796 { 797 case UserDataFormat::json: 798 subType = static_cast<uint8_t>(UserDataFormat::json); 799 version = static_cast<uint8_t>(UserDataFormatVersion::json); 800 break; 801 case UserDataFormat::cbor: 802 subType = static_cast<uint8_t>(UserDataFormat::cbor); 803 version = static_cast<uint8_t>(UserDataFormatVersion::cbor); 804 805 // The CBOR parser will fail on the extra pad bytes since they 806 // aren't CBOR. Add the amount we padded to the end and other 807 // code will remove it all before parsing. 808 { 809 data.resize(data.size() + 4); 810 Stream stream{data}; 811 stream.offset(data.size() - 4); 812 stream << pad; 813 } 814 815 break; 816 case UserDataFormat::text: 817 subType = static_cast<uint8_t>(UserDataFormat::text); 818 version = static_cast<uint8_t>(UserDataFormatVersion::text); 819 break; 820 case UserDataFormat::custom: 821 default: 822 // Use the passed in values 823 compID = componentID; 824 subType = file.subType; 825 version = file.version; 826 break; 827 } 828 829 return std::make_unique<UserData>(compID, subType, version, data); 830 } 831 832 } // namespace util 833 834 } // namespace pels 835 } // namespace openpower 836