1 /* 2 // Copyright (c) 2017-2019 Intel 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 17 #include "storagecommands.hpp" 18 19 #include "commandutils.hpp" 20 #include "ipmi_to_redfish_hooks.hpp" 21 #include "sdrutils.hpp" 22 #include "types.hpp" 23 24 #include <boost/algorithm/string.hpp> 25 #include <boost/container/flat_map.hpp> 26 #include <ipmid/api.hpp> 27 #include <ipmid/message.hpp> 28 #include <phosphor-logging/log.hpp> 29 #include <sdbusplus/message/types.hpp> 30 #include <sdbusplus/timer.hpp> 31 32 #include <filesystem> 33 #include <fstream> 34 #include <iostream> 35 #include <stdexcept> 36 #include <unordered_set> 37 38 static constexpr bool DEBUG = false; 39 40 namespace intel_oem::ipmi::sel 41 { 42 static const std::filesystem::path selLogDir = "/var/log"; 43 static const std::string selLogFilename = "ipmi_sel"; 44 45 static int getFileTimestamp(const std::filesystem::path& file) 46 { 47 struct stat st; 48 49 if (stat(file.c_str(), &st) >= 0) 50 { 51 return st.st_mtime; 52 } 53 return ::ipmi::sel::invalidTimeStamp; 54 } 55 56 namespace erase_time 57 { 58 static constexpr const char* selEraseTimestamp = "/var/lib/ipmi/sel_erase_time"; 59 60 void save() 61 { 62 // open the file, creating it if necessary 63 int fd = open(selEraseTimestamp, O_WRONLY | O_CREAT | O_CLOEXEC, 0644); 64 if (fd < 0) 65 { 66 std::cerr << "Failed to open file\n"; 67 return; 68 } 69 70 // update the file timestamp to the current time 71 if (futimens(fd, NULL) < 0) 72 { 73 std::cerr << "Failed to update timestamp: " 74 << std::string(strerror(errno)); 75 } 76 close(fd); 77 } 78 79 int get() 80 { 81 return getFileTimestamp(selEraseTimestamp); 82 } 83 } // namespace erase_time 84 } // namespace intel_oem::ipmi::sel 85 86 namespace ipmi 87 { 88 89 namespace storage 90 { 91 92 constexpr static const size_t maxMessageSize = 64; 93 constexpr static const size_t maxFruSdrNameSize = 16; 94 using ObjectType = boost::container::flat_map< 95 std::string, boost::container::flat_map<std::string, DbusVariant>>; 96 using ManagedObjectType = 97 boost::container::flat_map<sdbusplus::message::object_path, ObjectType>; 98 using ManagedEntry = std::pair<sdbusplus::message::object_path, ObjectType>; 99 using GetObjectType = 100 std::vector<std::pair<std::string, std::vector<std::string>>>; 101 102 constexpr static const char* fruDeviceServiceName = 103 "xyz.openbmc_project.FruDevice"; 104 constexpr static const size_t writeTimeoutSeconds = 10; 105 constexpr static const char* chassisTypeRackMount = "23"; 106 107 // event direction is bit[7] of eventType where 1b = Deassertion event 108 constexpr static const uint8_t deassertionEvent = 0x80; 109 110 static std::vector<uint8_t> fruCache; 111 static uint16_t cacheBus = 0xFFFF; 112 static uint8_t cacheAddr = 0XFF; 113 static uint8_t lastDevId = 0xFF; 114 115 static uint16_t writeBus = 0xFFFF; 116 static uint8_t writeAddr = 0XFF; 117 118 std::unique_ptr<phosphor::Timer> writeTimer = nullptr; 119 static std::vector<sdbusplus::bus::match_t> fruMatches; 120 121 ManagedObjectType frus; 122 123 // we unfortunately have to build a map of hashes in case there is a 124 // collision to verify our dev-id 125 boost::container::flat_map<uint8_t, std::pair<uint16_t, uint8_t>> deviceHashes; 126 // Map devId to Object Path 127 boost::container::flat_map<uint8_t, std::string> devicePath; 128 129 void registerStorageFunctions() __attribute__((constructor)); 130 131 bool writeFru() 132 { 133 if (writeBus == 0xFFFF && writeAddr == 0xFF) 134 { 135 return true; 136 } 137 std::shared_ptr<sdbusplus::asio::connection> dbus = getSdBus(); 138 sdbusplus::message_t writeFru = dbus->new_method_call( 139 fruDeviceServiceName, "/xyz/openbmc_project/FruDevice", 140 "xyz.openbmc_project.FruDeviceManager", "WriteFru"); 141 writeFru.append(writeBus, writeAddr, fruCache); 142 try 143 { 144 sdbusplus::message_t writeFruResp = dbus->call(writeFru); 145 } 146 catch (const sdbusplus::exception_t&) 147 { 148 // todo: log sel? 149 phosphor::logging::log<phosphor::logging::level::ERR>( 150 "error writing fru"); 151 return false; 152 } 153 writeBus = 0xFFFF; 154 writeAddr = 0xFF; 155 return true; 156 } 157 158 void createTimers() 159 { 160 writeTimer = std::make_unique<phosphor::Timer>(writeFru); 161 } 162 163 void recalculateHashes() 164 { 165 deviceHashes.clear(); 166 devicePath.clear(); 167 // hash the object paths to create unique device id's. increment on 168 // collision 169 std::hash<std::string> hasher; 170 for (const auto& fru : frus) 171 { 172 auto fruIface = fru.second.find("xyz.openbmc_project.FruDevice"); 173 if (fruIface == fru.second.end()) 174 { 175 continue; 176 } 177 178 auto busFind = fruIface->second.find("BUS"); 179 auto addrFind = fruIface->second.find("ADDRESS"); 180 if (busFind == fruIface->second.end() || 181 addrFind == fruIface->second.end()) 182 { 183 phosphor::logging::log<phosphor::logging::level::INFO>( 184 "fru device missing Bus or Address", 185 phosphor::logging::entry("FRU=%s", fru.first.str.c_str())); 186 continue; 187 } 188 189 uint16_t fruBus = std::get<uint32_t>(busFind->second); 190 uint8_t fruAddr = std::get<uint32_t>(addrFind->second); 191 auto chassisFind = fruIface->second.find("CHASSIS_TYPE"); 192 std::string chassisType; 193 if (chassisFind != fruIface->second.end()) 194 { 195 chassisType = std::get<std::string>(chassisFind->second); 196 } 197 198 uint8_t fruHash = 0; 199 if (chassisType.compare(chassisTypeRackMount) != 0) 200 { 201 fruHash = hasher(fru.first.str); 202 // can't be 0xFF based on spec, and 0 is reserved for baseboard 203 if (fruHash == 0 || fruHash == 0xFF) 204 { 205 fruHash = 1; 206 } 207 } 208 std::pair<uint16_t, uint8_t> newDev(fruBus, fruAddr); 209 210 bool emplacePassed = false; 211 while (!emplacePassed) 212 { 213 auto resp = deviceHashes.emplace(fruHash, newDev); 214 215 devicePath.emplace(fruHash, fru.first); 216 217 emplacePassed = resp.second; 218 if (!emplacePassed) 219 { 220 fruHash++; 221 // can't be 0xFF based on spec, and 0 is reserved for 222 // baseboard 223 if (fruHash == 0XFF) 224 { 225 fruHash = 0x1; 226 } 227 } 228 } 229 } 230 } 231 232 void replaceCacheFru(const std::shared_ptr<sdbusplus::asio::connection>& bus, 233 boost::asio::yield_context& yield) 234 { 235 boost::system::error_code ec; 236 // ObjectPaths and Services which implements "xyz.openbmc_project.FruDevice" 237 // interface 238 GetSubTreeType fruServices = bus->yield_method_call<GetSubTreeType>( 239 yield, ec, "xyz.openbmc_project.ObjectMapper", 240 "/xyz/openbmc_project/object_mapper", 241 "xyz.openbmc_project.ObjectMapper", "GetSubTree", "/", 0, 242 std::array<const char*, 1>{"xyz.openbmc_project.FruDevice"}); 243 244 if (ec) 245 { 246 phosphor::logging::log<phosphor::logging::level::ERR>( 247 "GetSubTree failed for FruDevice Interface ", 248 phosphor::logging::entry("ERROR=%s", ec.message().c_str())); 249 250 return; 251 } 252 // Get List of services which have implemented FruDevice interface 253 std::unordered_set<std::string> services; 254 for (const auto& [path, serviceMap] : fruServices) 255 { 256 for (const auto& [service, interfaces] : serviceMap) 257 { 258 services.insert(service); 259 } 260 } 261 262 // GetAll the objects under services which implement FruDevice interface 263 for (const std::string& service : services) 264 { 265 ec = boost::system::errc::make_error_code(boost::system::errc::success); 266 ManagedObjectType obj = bus->yield_method_call<ManagedObjectType>( 267 yield, ec, service, "/", "org.freedesktop.DBus.ObjectManager", 268 "GetManagedObjects"); 269 if (ec) 270 { 271 phosphor::logging::log<phosphor::logging::level::ERR>( 272 "GetMangagedObjects failed", 273 phosphor::logging::entry("ERROR=%s", ec.message().c_str())); 274 continue; 275 } 276 // Save the object path which has FruDevice interface 277 for (const auto& [path, serviceMap] : fruServices) 278 { 279 for (const auto& serv : serviceMap) 280 { 281 if (serv.first == service) 282 { 283 auto fru = obj.find(path); 284 if (fru == obj.end()) 285 { 286 continue; 287 } 288 frus.emplace(fru->first, fru->second); 289 } 290 } 291 } 292 } 293 294 recalculateHashes(); 295 } 296 297 ipmi::Cc getFru(ipmi::Context::ptr& ctx, uint8_t devId) 298 { 299 if (lastDevId == devId && devId != 0xFF) 300 { 301 return ipmi::ccSuccess; 302 } 303 304 auto deviceFind = deviceHashes.find(devId); 305 auto devPath = devicePath.find(devId); 306 if (deviceFind == deviceHashes.end() || devPath == devicePath.end()) 307 { 308 return IPMI_CC_SENSOR_INVALID; 309 } 310 311 if (writeTimer->isRunning()) 312 { 313 phosphor::logging::log<phosphor::logging::level::ERR>( 314 "Couldn't get raw fru as fru is updating"); 315 return ipmi::ccBusy; 316 } 317 fruCache.clear(); 318 319 cacheBus = deviceFind->second.first; 320 cacheAddr = deviceFind->second.second; 321 322 boost::system::error_code ec; 323 GetObjectType fruService = ctx->bus->yield_method_call<GetObjectType>( 324 ctx->yield, ec, "xyz.openbmc_project.ObjectMapper", 325 "/xyz/openbmc_project/object_mapper", 326 "xyz.openbmc_project.ObjectMapper", "GetObject", devPath->second, 327 std::array<const char*, 1>{"xyz.openbmc_project.FruDevice"}); 328 329 if (ec) 330 { 331 phosphor::logging::log<phosphor::logging::level::ERR>( 332 "Couldn't get raw fru because of service", 333 phosphor::logging::entry("ERROR=%s", ec.message().c_str())); 334 return ipmi::ccResponseError; 335 } 336 337 bool foundFru = false; 338 for (auto& service : fruService) 339 { 340 fruCache = ctx->bus->yield_method_call<std::vector<uint8_t>>( 341 ctx->yield, ec, service.first, "/xyz/openbmc_project/FruDevice", 342 "xyz.openbmc_project.FruDeviceManager", "GetRawFru", cacheBus, 343 cacheAddr); 344 345 if (!ec) 346 { 347 foundFru = true; 348 break; 349 } 350 } 351 352 if (!foundFru) 353 { 354 phosphor::logging::log<phosphor::logging::level::ERR>( 355 "Couldn't get raw fru", 356 phosphor::logging::entry("ERROR=%s", ec.message().c_str())); 357 cacheBus = 0xFFFF; 358 cacheAddr = 0xFF; 359 return ipmi::ccResponseError; 360 } 361 362 lastDevId = devId; 363 return ipmi::ccSuccess; 364 } 365 366 void writeFruIfRunning() 367 { 368 if (!writeTimer->isRunning()) 369 { 370 return; 371 } 372 writeTimer->stop(); 373 writeFru(); 374 } 375 376 void startMatch(void) 377 { 378 if (fruMatches.size()) 379 { 380 return; 381 } 382 383 fruMatches.reserve(2); 384 385 auto bus = getSdBus(); 386 fruMatches.emplace_back(*bus, 387 "type='signal',arg0path='/xyz/openbmc_project/" 388 "FruDevice/',member='InterfacesAdded'", 389 [](sdbusplus::message_t& message) { 390 sdbusplus::message::object_path path; 391 ObjectType object; 392 try 393 { 394 message.read(path, object); 395 } 396 catch (const sdbusplus::exception_t&) 397 { 398 return; 399 } 400 auto findType = object.find("xyz.openbmc_project.FruDevice"); 401 if (findType == object.end()) 402 { 403 return; 404 } 405 writeFruIfRunning(); 406 frus[path] = object; 407 recalculateHashes(); 408 lastDevId = 0xFF; 409 }); 410 411 fruMatches.emplace_back(*bus, 412 "type='signal',arg0path='/xyz/openbmc_project/" 413 "FruDevice/',member='InterfacesRemoved'", 414 [](sdbusplus::message_t& message) { 415 sdbusplus::message::object_path path; 416 std::set<std::string> interfaces; 417 try 418 { 419 message.read(path, interfaces); 420 } 421 catch (const sdbusplus::exception_t&) 422 { 423 return; 424 } 425 auto findType = interfaces.find("xyz.openbmc_project.FruDevice"); 426 if (findType == interfaces.end()) 427 { 428 return; 429 } 430 writeFruIfRunning(); 431 frus.erase(path); 432 recalculateHashes(); 433 lastDevId = 0xFF; 434 }); 435 436 // call once to populate 437 boost::asio::spawn(*getIoContext(), [](boost::asio::yield_context yield) { 438 replaceCacheFru(getSdBus(), yield); 439 }); 440 } 441 442 /** @brief implements the read FRU data command 443 * @param fruDeviceId - FRU Device ID 444 * @param fruInventoryOffset - FRU Inventory Offset to write 445 * @param countToRead - Count to read 446 * 447 * @returns ipmi completion code plus response data 448 * - countWritten - Count written 449 */ 450 ipmi::RspType<uint8_t, // Count 451 std::vector<uint8_t> // Requested data 452 > 453 ipmiStorageReadFruData(ipmi::Context::ptr& ctx, uint8_t fruDeviceId, 454 uint16_t fruInventoryOffset, uint8_t countToRead) 455 { 456 if (fruDeviceId == 0xFF) 457 { 458 return ipmi::responseInvalidFieldRequest(); 459 } 460 461 ipmi::Cc status = getFru(ctx, fruDeviceId); 462 463 if (status != ipmi::ccSuccess) 464 { 465 return ipmi::response(status); 466 } 467 468 size_t fromFruByteLen = 0; 469 if (countToRead + fruInventoryOffset < fruCache.size()) 470 { 471 fromFruByteLen = countToRead; 472 } 473 else if (fruCache.size() > fruInventoryOffset) 474 { 475 fromFruByteLen = fruCache.size() - fruInventoryOffset; 476 } 477 else 478 { 479 return ipmi::responseReqDataLenExceeded(); 480 } 481 482 std::vector<uint8_t> requestedData; 483 484 requestedData.insert( 485 requestedData.begin(), fruCache.begin() + fruInventoryOffset, 486 fruCache.begin() + fruInventoryOffset + fromFruByteLen); 487 488 return ipmi::responseSuccess(static_cast<uint8_t>(requestedData.size()), 489 requestedData); 490 } 491 492 /** @brief implements the write FRU data command 493 * @param fruDeviceId - FRU Device ID 494 * @param fruInventoryOffset - FRU Inventory Offset to write 495 * @param dataToWrite - Data to write 496 * 497 * @returns ipmi completion code plus response data 498 * - countWritten - Count written 499 */ 500 ipmi::RspType<uint8_t> 501 ipmiStorageWriteFruData(ipmi::Context::ptr& ctx, uint8_t fruDeviceId, 502 uint16_t fruInventoryOffset, 503 std::vector<uint8_t>& dataToWrite) 504 { 505 if (fruDeviceId == 0xFF) 506 { 507 return ipmi::responseInvalidFieldRequest(); 508 } 509 510 size_t writeLen = dataToWrite.size(); 511 512 ipmi::Cc status = getFru(ctx, fruDeviceId); 513 if (status != ipmi::ccSuccess) 514 { 515 return ipmi::response(status); 516 } 517 size_t lastWriteAddr = fruInventoryOffset + writeLen; 518 if (fruCache.size() < lastWriteAddr) 519 { 520 fruCache.resize(fruInventoryOffset + writeLen); 521 } 522 523 std::copy(dataToWrite.begin(), dataToWrite.begin() + writeLen, 524 fruCache.begin() + fruInventoryOffset); 525 526 bool atEnd = false; 527 528 if (fruCache.size() >= sizeof(FRUHeader)) 529 { 530 FRUHeader* header = reinterpret_cast<FRUHeader*>(fruCache.data()); 531 532 int areaLength = 0; 533 size_t lastRecordStart = std::max( 534 {header->internalOffset, header->chassisOffset, header->boardOffset, 535 header->productOffset, header->multiRecordOffset}); 536 lastRecordStart *= 8; // header starts in are multiples of 8 bytes 537 538 if (header->multiRecordOffset) 539 { 540 // This FRU has a MultiRecord Area 541 uint8_t endOfList = 0; 542 // Walk the MultiRecord headers until the last record 543 while (!endOfList) 544 { 545 // The MSB in the second byte of the MultiRecord header signals 546 // "End of list" 547 endOfList = fruCache[lastRecordStart + 1] & 0x80; 548 // Third byte in the MultiRecord header is the length 549 areaLength = fruCache[lastRecordStart + 2]; 550 // This length is in bytes (not 8 bytes like other headers) 551 areaLength += 5; // The length omits the 5 byte header 552 if (!endOfList) 553 { 554 // Next MultiRecord header 555 lastRecordStart += areaLength; 556 } 557 } 558 } 559 else 560 { 561 // This FRU does not have a MultiRecord Area 562 // Get the length of the area in multiples of 8 bytes 563 if (lastWriteAddr > (lastRecordStart + 1)) 564 { 565 // second byte in record area is the length 566 areaLength = fruCache[lastRecordStart + 1]; 567 areaLength *= 8; // it is in multiples of 8 bytes 568 } 569 } 570 if (lastWriteAddr >= (areaLength + lastRecordStart)) 571 { 572 atEnd = true; 573 } 574 } 575 uint8_t countWritten = 0; 576 577 writeBus = cacheBus; 578 writeAddr = cacheAddr; 579 if (atEnd) 580 { 581 // cancel timer, we're at the end so might as well send it 582 writeTimer->stop(); 583 if (!writeFru()) 584 { 585 return ipmi::responseInvalidFieldRequest(); 586 } 587 countWritten = std::min(fruCache.size(), static_cast<size_t>(0xFF)); 588 } 589 else 590 { 591 // start a timer, if no further data is sent to check to see if it is 592 // valid 593 writeTimer->start(std::chrono::duration_cast<std::chrono::microseconds>( 594 std::chrono::seconds(writeTimeoutSeconds))); 595 countWritten = 0; 596 } 597 598 return ipmi::responseSuccess(countWritten); 599 } 600 601 /** @brief implements the get FRU inventory area info command 602 * @param fruDeviceId - FRU Device ID 603 * 604 * @returns IPMI completion code plus response data 605 * - inventorySize - Number of possible allocation units 606 * - accessType - Allocation unit size in bytes. 607 */ 608 ipmi::RspType<uint16_t, // inventorySize 609 uint8_t> // accessType 610 ipmiStorageGetFruInvAreaInfo(ipmi::Context::ptr& ctx, uint8_t fruDeviceId) 611 { 612 if (fruDeviceId == 0xFF) 613 { 614 return ipmi::responseInvalidFieldRequest(); 615 } 616 617 ipmi::Cc ret = getFru(ctx, fruDeviceId); 618 if (ret != ipmi::ccSuccess) 619 { 620 return ipmi::response(ret); 621 } 622 623 constexpr uint8_t accessType = 624 static_cast<uint8_t>(GetFRUAreaAccessType::byte); 625 626 return ipmi::responseSuccess(fruCache.size(), accessType); 627 } 628 629 ipmi::Cc getFruSdrCount(ipmi::Context::ptr&, size_t& count) 630 { 631 count = deviceHashes.size(); 632 return ipmi::ccSuccess; 633 } 634 635 ipmi::Cc getFruSdrs(ipmi::Context::ptr& ctx, size_t index, 636 get_sdr::SensorDataFruRecord& resp) 637 { 638 if (deviceHashes.size() < index) 639 { 640 return ipmi::ccInvalidFieldRequest; 641 } 642 auto device = deviceHashes.begin() + index; 643 uint16_t& bus = device->second.first; 644 uint8_t& address = device->second.second; 645 646 boost::container::flat_map<std::string, DbusVariant>* fruData = nullptr; 647 auto fru = std::find_if(frus.begin(), frus.end(), 648 [bus, address, &fruData](ManagedEntry& entry) { 649 auto findFruDevice = entry.second.find("xyz.openbmc_project.FruDevice"); 650 if (findFruDevice == entry.second.end()) 651 { 652 return false; 653 } 654 fruData = &(findFruDevice->second); 655 auto findBus = findFruDevice->second.find("BUS"); 656 auto findAddress = findFruDevice->second.find("ADDRESS"); 657 if (findBus == findFruDevice->second.end() || 658 findAddress == findFruDevice->second.end()) 659 { 660 return false; 661 } 662 if (std::get<uint32_t>(findBus->second) != bus) 663 { 664 return false; 665 } 666 if (std::get<uint32_t>(findAddress->second) != address) 667 { 668 return false; 669 } 670 return true; 671 }); 672 if (fru == frus.end()) 673 { 674 return ipmi::ccResponseError; 675 } 676 677 #ifdef USING_ENTITY_MANAGER_DECORATORS 678 679 boost::container::flat_map<std::string, DbusVariant>* entityData = nullptr; 680 681 // todo: this should really use caching, this is a very inefficient lookup 682 boost::system::error_code ec; 683 ManagedObjectType entities = ctx->bus->yield_method_call<ManagedObjectType>( 684 ctx->yield, ec, "xyz.openbmc_project.EntityManager", 685 "/xyz/openbmc_project/inventory", "org.freedesktop.DBus.ObjectManager", 686 "GetManagedObjects"); 687 688 if (ec) 689 { 690 phosphor::logging::log<phosphor::logging::level::ERR>( 691 "GetMangagedObjects for getSensorMap failed", 692 phosphor::logging::entry("ERROR=%s", ec.message().c_str())); 693 694 return ipmi::ccResponseError; 695 } 696 697 auto entity = 698 std::find_if(entities.begin(), entities.end(), 699 [bus, address, &entityData](ManagedEntry& entry) { 700 auto findFruDevice = entry.second.find( 701 "xyz.openbmc_project.Inventory.Decorator.FruDevice"); 702 if (findFruDevice == entry.second.end()) 703 { 704 return false; 705 } 706 707 // Integer fields added via Entity-Manager json are uint64_ts by 708 // default. 709 auto findBus = findFruDevice->second.find("Bus"); 710 auto findAddress = findFruDevice->second.find("Address"); 711 712 if (findBus == findFruDevice->second.end() || 713 findAddress == findFruDevice->second.end()) 714 { 715 return false; 716 } 717 if ((std::get<uint64_t>(findBus->second) != bus) || 718 (std::get<uint64_t>(findAddress->second) != address)) 719 { 720 return false; 721 } 722 723 // At this point we found the device entry and should return 724 // true. 725 auto findIpmiDevice = 726 entry.second.find("xyz.openbmc_project.Inventory.Decorator.Ipmi"); 727 if (findIpmiDevice != entry.second.end()) 728 { 729 entityData = &(findIpmiDevice->second); 730 } 731 732 return true; 733 }); 734 735 if (entity == entities.end()) 736 { 737 if constexpr (DEBUG) 738 { 739 std::fprintf(stderr, "Ipmi or FruDevice Decorator interface " 740 "not found for Fru\n"); 741 } 742 } 743 744 #endif 745 746 std::string name; 747 auto findProductName = fruData->find("BOARD_PRODUCT_NAME"); 748 auto findBoardName = fruData->find("PRODUCT_PRODUCT_NAME"); 749 if (findProductName != fruData->end()) 750 { 751 name = std::get<std::string>(findProductName->second); 752 } 753 else if (findBoardName != fruData->end()) 754 { 755 name = std::get<std::string>(findBoardName->second); 756 } 757 else 758 { 759 name = "UNKNOWN"; 760 } 761 if (name.size() > maxFruSdrNameSize) 762 { 763 name = name.substr(0, maxFruSdrNameSize); 764 } 765 size_t sizeDiff = maxFruSdrNameSize - name.size(); 766 767 resp.header.record_id_lsb = 0x0; // calling code is to implement these 768 resp.header.record_id_msb = 0x0; 769 resp.header.sdr_version = ipmiSdrVersion; 770 resp.header.record_type = get_sdr::SENSOR_DATA_FRU_RECORD; 771 resp.header.record_length = sizeof(resp.body) + sizeof(resp.key) - sizeDiff; 772 resp.key.deviceAddress = 0x20; 773 resp.key.fruID = device->first; 774 resp.key.accessLun = 0x80; // logical / physical fru device 775 resp.key.channelNumber = 0x0; 776 resp.body.reserved = 0x0; 777 resp.body.deviceType = 0x10; 778 resp.body.deviceTypeModifier = 0x0; 779 780 uint8_t entityID = 0; 781 uint8_t entityInstance = 0x1; 782 783 #ifdef USING_ENTITY_MANAGER_DECORATORS 784 if (entityData) 785 { 786 auto entityIdProperty = entityData->find("EntityId"); 787 auto entityInstanceProperty = entityData->find("EntityInstance"); 788 789 if (entityIdProperty != entityData->end()) 790 { 791 entityID = static_cast<uint8_t>( 792 std::get<uint64_t>(entityIdProperty->second)); 793 } 794 if (entityInstanceProperty != entityData->end()) 795 { 796 entityInstance = static_cast<uint8_t>( 797 std::get<uint64_t>(entityInstanceProperty->second)); 798 } 799 } 800 #endif 801 802 resp.body.entityID = entityID; 803 resp.body.entityInstance = entityInstance; 804 805 resp.body.oem = 0x0; 806 resp.body.deviceIDLen = name.size(); 807 name.copy(resp.body.deviceID, name.size()); 808 809 return ipmi::ccSuccess; 810 } 811 812 static bool getSELLogFiles(std::vector<std::filesystem::path>& selLogFiles) 813 { 814 // Loop through the directory looking for ipmi_sel log files 815 for (const std::filesystem::directory_entry& dirEnt : 816 std::filesystem::directory_iterator(intel_oem::ipmi::sel::selLogDir)) 817 { 818 std::string filename = dirEnt.path().filename(); 819 if (boost::starts_with(filename, intel_oem::ipmi::sel::selLogFilename)) 820 { 821 // If we find an ipmi_sel log file, save the path 822 selLogFiles.emplace_back(intel_oem::ipmi::sel::selLogDir / 823 filename); 824 } 825 } 826 // As the log files rotate, they are appended with a ".#" that is higher for 827 // the older logs. Since we don't expect more than 10 log files, we 828 // can just sort the list to get them in order from newest to oldest 829 std::sort(selLogFiles.begin(), selLogFiles.end()); 830 831 return !selLogFiles.empty(); 832 } 833 834 static int countSELEntries() 835 { 836 // Get the list of ipmi_sel log files 837 std::vector<std::filesystem::path> selLogFiles; 838 if (!getSELLogFiles(selLogFiles)) 839 { 840 return 0; 841 } 842 int numSELEntries = 0; 843 // Loop through each log file and count the number of logs 844 for (const std::filesystem::path& file : selLogFiles) 845 { 846 std::ifstream logStream(file); 847 if (!logStream.is_open()) 848 { 849 continue; 850 } 851 852 std::string line; 853 while (std::getline(logStream, line)) 854 { 855 numSELEntries++; 856 } 857 } 858 return numSELEntries; 859 } 860 861 static bool findSELEntry(const int recordID, 862 const std::vector<std::filesystem::path>& selLogFiles, 863 std::string& entry) 864 { 865 // Record ID is the first entry field following the timestamp. It is 866 // preceded by a space and followed by a comma 867 std::string search = " " + std::to_string(recordID) + ","; 868 869 // Loop through the ipmi_sel log entries 870 for (const std::filesystem::path& file : selLogFiles) 871 { 872 std::ifstream logStream(file); 873 if (!logStream.is_open()) 874 { 875 continue; 876 } 877 878 while (std::getline(logStream, entry)) 879 { 880 // Check if the record ID matches 881 if (entry.find(search) != std::string::npos) 882 { 883 return true; 884 } 885 } 886 } 887 return false; 888 } 889 890 static uint16_t 891 getNextRecordID(const uint16_t recordID, 892 const std::vector<std::filesystem::path>& selLogFiles) 893 { 894 uint16_t nextRecordID = recordID + 1; 895 std::string entry; 896 if (findSELEntry(nextRecordID, selLogFiles, entry)) 897 { 898 return nextRecordID; 899 } 900 else 901 { 902 return ipmi::sel::lastEntry; 903 } 904 } 905 906 static int fromHexStr(const std::string& hexStr, std::vector<uint8_t>& data) 907 { 908 for (unsigned int i = 0; i < hexStr.size(); i += 2) 909 { 910 try 911 { 912 data.push_back(static_cast<uint8_t>( 913 std::stoul(hexStr.substr(i, 2), nullptr, 16))); 914 } 915 catch (const std::invalid_argument& e) 916 { 917 phosphor::logging::log<phosphor::logging::level::ERR>(e.what()); 918 return -1; 919 } 920 catch (const std::out_of_range& e) 921 { 922 phosphor::logging::log<phosphor::logging::level::ERR>(e.what()); 923 return -1; 924 } 925 } 926 return 0; 927 } 928 929 ipmi::RspType<uint8_t, // SEL version 930 uint16_t, // SEL entry count 931 uint16_t, // free space 932 uint32_t, // last add timestamp 933 uint32_t, // last erase timestamp 934 uint8_t> // operation support 935 ipmiStorageGetSELInfo() 936 { 937 constexpr uint8_t selVersion = ipmi::sel::selVersion; 938 uint16_t entries = countSELEntries(); 939 uint32_t addTimeStamp = intel_oem::ipmi::sel::getFileTimestamp( 940 intel_oem::ipmi::sel::selLogDir / intel_oem::ipmi::sel::selLogFilename); 941 uint32_t eraseTimeStamp = intel_oem::ipmi::sel::erase_time::get(); 942 constexpr uint8_t operationSupport = 943 intel_oem::ipmi::sel::selOperationSupport; 944 constexpr uint16_t freeSpace = 945 0xffff; // Spec indicates that more than 64kB is free 946 947 return ipmi::responseSuccess(selVersion, entries, freeSpace, addTimeStamp, 948 eraseTimeStamp, operationSupport); 949 } 950 951 using systemEventType = std::tuple< 952 uint32_t, // Timestamp 953 uint16_t, // Generator ID 954 uint8_t, // EvM Rev 955 uint8_t, // Sensor Type 956 uint8_t, // Sensor Number 957 uint7_t, // Event Type 958 bool, // Event Direction 959 std::array<uint8_t, intel_oem::ipmi::sel::systemEventSize>>; // Event Data 960 using oemTsEventType = std::tuple< 961 uint32_t, // Timestamp 962 std::array<uint8_t, intel_oem::ipmi::sel::oemTsEventSize>>; // Event Data 963 using oemEventType = 964 std::array<uint8_t, intel_oem::ipmi::sel::oemEventSize>; // Event Data 965 966 ipmi::RspType<uint16_t, // Next Record ID 967 uint16_t, // Record ID 968 uint8_t, // Record Type 969 std::variant<systemEventType, oemTsEventType, 970 oemEventType>> // Record Content 971 ipmiStorageGetSELEntry(uint16_t reservationID, uint16_t targetID, 972 uint8_t offset, uint8_t size) 973 { 974 // Only support getting the entire SEL record. If a partial size or non-zero 975 // offset is requested, return an error 976 if (offset != 0 || size != ipmi::sel::entireRecord) 977 { 978 return ipmi::responseRetBytesUnavailable(); 979 } 980 981 // Check the reservation ID if one is provided or required (only if the 982 // offset is non-zero) 983 if (reservationID != 0 || offset != 0) 984 { 985 if (!checkSELReservation(reservationID)) 986 { 987 return ipmi::responseInvalidReservationId(); 988 } 989 } 990 991 // Get the ipmi_sel log files 992 std::vector<std::filesystem::path> selLogFiles; 993 if (!getSELLogFiles(selLogFiles)) 994 { 995 return ipmi::responseSensorInvalid(); 996 } 997 998 std::string targetEntry; 999 1000 if (targetID == ipmi::sel::firstEntry) 1001 { 1002 // The first entry will be at the top of the oldest log file 1003 std::ifstream logStream(selLogFiles.back()); 1004 if (!logStream.is_open()) 1005 { 1006 return ipmi::responseUnspecifiedError(); 1007 } 1008 1009 if (!std::getline(logStream, targetEntry)) 1010 { 1011 return ipmi::responseUnspecifiedError(); 1012 } 1013 } 1014 else if (targetID == ipmi::sel::lastEntry) 1015 { 1016 // The last entry will be at the bottom of the newest log file 1017 std::ifstream logStream(selLogFiles.front()); 1018 if (!logStream.is_open()) 1019 { 1020 return ipmi::responseUnspecifiedError(); 1021 } 1022 1023 std::string line; 1024 while (std::getline(logStream, line)) 1025 { 1026 targetEntry = line; 1027 } 1028 } 1029 else 1030 { 1031 if (!findSELEntry(targetID, selLogFiles, targetEntry)) 1032 { 1033 return ipmi::responseSensorInvalid(); 1034 } 1035 } 1036 1037 // The format of the ipmi_sel message is "<Timestamp> 1038 // <ID>,<Type>,<EventData>,[<Generator ID>,<Path>,<Direction>]". 1039 // First get the Timestamp 1040 size_t space = targetEntry.find_first_of(" "); 1041 if (space == std::string::npos) 1042 { 1043 return ipmi::responseUnspecifiedError(); 1044 } 1045 std::string entryTimestamp = targetEntry.substr(0, space); 1046 // Then get the log contents 1047 size_t entryStart = targetEntry.find_first_not_of(" ", space); 1048 if (entryStart == std::string::npos) 1049 { 1050 return ipmi::responseUnspecifiedError(); 1051 } 1052 std::string_view entry(targetEntry); 1053 entry.remove_prefix(entryStart); 1054 // Use split to separate the entry into its fields 1055 std::vector<std::string> targetEntryFields; 1056 boost::split(targetEntryFields, entry, boost::is_any_of(","), 1057 boost::token_compress_on); 1058 if (targetEntryFields.size() < 3) 1059 { 1060 return ipmi::responseUnspecifiedError(); 1061 } 1062 std::string& recordIDStr = targetEntryFields[0]; 1063 std::string& recordTypeStr = targetEntryFields[1]; 1064 std::string& eventDataStr = targetEntryFields[2]; 1065 1066 uint16_t recordID; 1067 uint8_t recordType; 1068 try 1069 { 1070 recordID = std::stoul(recordIDStr); 1071 recordType = std::stoul(recordTypeStr, nullptr, 16); 1072 } 1073 catch (const std::invalid_argument&) 1074 { 1075 return ipmi::responseUnspecifiedError(); 1076 } 1077 uint16_t nextRecordID = getNextRecordID(recordID, selLogFiles); 1078 std::vector<uint8_t> eventDataBytes; 1079 if (fromHexStr(eventDataStr, eventDataBytes) < 0) 1080 { 1081 return ipmi::responseUnspecifiedError(); 1082 } 1083 1084 if (recordType == intel_oem::ipmi::sel::systemEvent) 1085 { 1086 // Get the timestamp 1087 std::tm timeStruct = {}; 1088 std::istringstream entryStream(entryTimestamp); 1089 1090 uint32_t timestamp = ipmi::sel::invalidTimeStamp; 1091 if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S")) 1092 { 1093 timestamp = std::mktime(&timeStruct); 1094 } 1095 1096 // Set the event message revision 1097 uint8_t evmRev = intel_oem::ipmi::sel::eventMsgRev; 1098 1099 uint16_t generatorID = 0; 1100 uint8_t sensorType = 0; 1101 uint16_t sensorAndLun = 0; 1102 uint8_t sensorNum = 0xFF; 1103 uint7_t eventType = 0; 1104 bool eventDir = 0; 1105 // System type events should have six fields 1106 if (targetEntryFields.size() >= 6) 1107 { 1108 std::string& generatorIDStr = targetEntryFields[3]; 1109 std::string& sensorPath = targetEntryFields[4]; 1110 std::string& eventDirStr = targetEntryFields[5]; 1111 1112 // Get the generator ID 1113 try 1114 { 1115 generatorID = std::stoul(generatorIDStr, nullptr, 16); 1116 } 1117 catch (const std::invalid_argument&) 1118 { 1119 std::cerr << "Invalid Generator ID\n"; 1120 } 1121 1122 // Get the sensor type, sensor number, and event type for the sensor 1123 sensorType = getSensorTypeFromPath(sensorPath); 1124 sensorAndLun = getSensorNumberFromPath(sensorPath); 1125 sensorNum = static_cast<uint8_t>(sensorAndLun); 1126 generatorID |= sensorAndLun >> 8; 1127 eventType = getSensorEventTypeFromPath(sensorPath); 1128 1129 // Get the event direction 1130 try 1131 { 1132 eventDir = std::stoul(eventDirStr) ? 0 : 1; 1133 } 1134 catch (const std::invalid_argument&) 1135 { 1136 std::cerr << "Invalid Event Direction\n"; 1137 } 1138 } 1139 1140 // Only keep the eventData bytes that fit in the record 1141 std::array<uint8_t, intel_oem::ipmi::sel::systemEventSize> eventData{}; 1142 std::copy_n(eventDataBytes.begin(), 1143 std::min(eventDataBytes.size(), eventData.size()), 1144 eventData.begin()); 1145 1146 return ipmi::responseSuccess( 1147 nextRecordID, recordID, recordType, 1148 systemEventType{timestamp, generatorID, evmRev, sensorType, 1149 sensorNum, eventType, eventDir, eventData}); 1150 } 1151 else if (recordType >= intel_oem::ipmi::sel::oemTsEventFirst && 1152 recordType <= intel_oem::ipmi::sel::oemTsEventLast) 1153 { 1154 // Get the timestamp 1155 std::tm timeStruct = {}; 1156 std::istringstream entryStream(entryTimestamp); 1157 1158 uint32_t timestamp = ipmi::sel::invalidTimeStamp; 1159 if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S")) 1160 { 1161 timestamp = std::mktime(&timeStruct); 1162 } 1163 1164 // Only keep the bytes that fit in the record 1165 std::array<uint8_t, intel_oem::ipmi::sel::oemTsEventSize> eventData{}; 1166 std::copy_n(eventDataBytes.begin(), 1167 std::min(eventDataBytes.size(), eventData.size()), 1168 eventData.begin()); 1169 1170 return ipmi::responseSuccess(nextRecordID, recordID, recordType, 1171 oemTsEventType{timestamp, eventData}); 1172 } 1173 else if (recordType >= intel_oem::ipmi::sel::oemEventFirst) 1174 { 1175 // Only keep the bytes that fit in the record 1176 std::array<uint8_t, intel_oem::ipmi::sel::oemEventSize> eventData{}; 1177 std::copy_n(eventDataBytes.begin(), 1178 std::min(eventDataBytes.size(), eventData.size()), 1179 eventData.begin()); 1180 1181 return ipmi::responseSuccess(nextRecordID, recordID, recordType, 1182 eventData); 1183 } 1184 1185 return ipmi::responseUnspecifiedError(); 1186 } 1187 1188 ipmi::RspType<uint16_t> ipmiStorageAddSELEntry( 1189 uint16_t recordID, uint8_t recordType, uint32_t timestamp, 1190 uint16_t generatorID, uint8_t evmRev, uint8_t sensorType, uint8_t sensorNum, 1191 uint8_t eventType, uint8_t eventData1, uint8_t eventData2, 1192 uint8_t eventData3) 1193 { 1194 // Per the IPMI spec, need to cancel any reservation when a SEL entry is 1195 // added 1196 cancelSELReservation(); 1197 1198 // Send this request to the Redfish hooks to log it as a Redfish message 1199 // instead. There is no need to add it to the SEL, so just return success. 1200 intel_oem::ipmi::sel::checkRedfishHooks( 1201 recordID, recordType, timestamp, generatorID, evmRev, sensorType, 1202 sensorNum, eventType, eventData1, eventData2, eventData3); 1203 1204 uint16_t responseID = 0xFFFF; 1205 return ipmi::responseSuccess(responseID); 1206 } 1207 1208 ipmi::RspType<uint8_t> ipmiStorageClearSEL(ipmi::Context::ptr&, 1209 uint16_t reservationID, 1210 const std::array<uint8_t, 3>& clr, 1211 uint8_t eraseOperation) 1212 { 1213 if (!checkSELReservation(reservationID)) 1214 { 1215 return ipmi::responseInvalidReservationId(); 1216 } 1217 1218 static constexpr std::array<uint8_t, 3> clrExpected = {'C', 'L', 'R'}; 1219 if (clr != clrExpected) 1220 { 1221 return ipmi::responseInvalidFieldRequest(); 1222 } 1223 1224 // Erasure status cannot be fetched, so always return erasure status as 1225 // `erase completed`. 1226 if (eraseOperation == ipmi::sel::getEraseStatus) 1227 { 1228 return ipmi::responseSuccess(ipmi::sel::eraseComplete); 1229 } 1230 1231 // Check that initiate erase is correct 1232 if (eraseOperation != ipmi::sel::initiateErase) 1233 { 1234 return ipmi::responseInvalidFieldRequest(); 1235 } 1236 1237 // Per the IPMI spec, need to cancel any reservation when the SEL is 1238 // cleared 1239 cancelSELReservation(); 1240 1241 // Save the erase time 1242 intel_oem::ipmi::sel::erase_time::save(); 1243 1244 // Clear the SEL by deleting the log files 1245 std::vector<std::filesystem::path> selLogFiles; 1246 if (getSELLogFiles(selLogFiles)) 1247 { 1248 for (const std::filesystem::path& file : selLogFiles) 1249 { 1250 std::error_code ec; 1251 std::filesystem::remove(file, ec); 1252 } 1253 } 1254 1255 // Reload rsyslog so it knows to start new log files 1256 std::shared_ptr<sdbusplus::asio::connection> dbus = getSdBus(); 1257 sdbusplus::message_t rsyslogReload = dbus->new_method_call( 1258 "org.freedesktop.systemd1", "/org/freedesktop/systemd1", 1259 "org.freedesktop.systemd1.Manager", "ReloadUnit"); 1260 rsyslogReload.append("rsyslog.service", "replace"); 1261 try 1262 { 1263 sdbusplus::message_t reloadResponse = dbus->call(rsyslogReload); 1264 } 1265 catch (const sdbusplus::exception_t& e) 1266 { 1267 phosphor::logging::log<phosphor::logging::level::ERR>(e.what()); 1268 } 1269 1270 return ipmi::responseSuccess(ipmi::sel::eraseComplete); 1271 } 1272 1273 ipmi::RspType<uint32_t> ipmiStorageGetSELTime() 1274 { 1275 struct timespec selTime = {}; 1276 1277 if (clock_gettime(CLOCK_REALTIME, &selTime) < 0) 1278 { 1279 return ipmi::responseUnspecifiedError(); 1280 } 1281 1282 return ipmi::responseSuccess(selTime.tv_sec); 1283 } 1284 1285 ipmi::RspType<> ipmiStorageSetSELTime([[maybe_unused]] uint32_t selTime) 1286 { 1287 // Set SEL Time is not supported 1288 return ipmi::responseInvalidCommand(); 1289 } 1290 1291 std::vector<uint8_t> getType12SDRs(uint16_t index, uint16_t recordId) 1292 { 1293 std::vector<uint8_t> resp; 1294 if (index == 0) 1295 { 1296 std::string bmcName = "Basbrd Mgmt Ctlr"; 1297 Type12Record bmc(recordId, 0x20, 0, 0, 0xbf, 0x2e, 1, 0, bmcName); 1298 uint8_t* bmcPtr = reinterpret_cast<uint8_t*>(&bmc); 1299 resp.insert(resp.end(), bmcPtr, bmcPtr + sizeof(Type12Record)); 1300 } 1301 else if (index == 1) 1302 { 1303 std::string meName = "Mgmt Engine"; 1304 Type12Record me(recordId, 0x2c, 6, 0x24, 0x21, 0x2e, 2, 0, meName); 1305 uint8_t* mePtr = reinterpret_cast<uint8_t*>(&me); 1306 resp.insert(resp.end(), mePtr, mePtr + sizeof(Type12Record)); 1307 } 1308 else 1309 { 1310 throw std::runtime_error("getType12SDRs:: Illegal index " + 1311 std::to_string(index)); 1312 } 1313 1314 return resp; 1315 } 1316 1317 std::vector<uint8_t> getNMDiscoverySDR(uint16_t index, uint16_t recordId) 1318 { 1319 std::vector<uint8_t> resp; 1320 if (index == 0) 1321 { 1322 NMDiscoveryRecord nm = {}; 1323 nm.header.record_id_lsb = recordId; 1324 nm.header.record_id_msb = recordId >> 8; 1325 nm.header.sdr_version = ipmiSdrVersion; 1326 nm.header.record_type = 0xC0; 1327 nm.header.record_length = 0xB; 1328 nm.oemID0 = 0x57; 1329 nm.oemID1 = 0x1; 1330 nm.oemID2 = 0x0; 1331 nm.subType = 0x0D; 1332 nm.version = 0x1; 1333 nm.targetAddress = 0x2C; 1334 nm.channelNumber = 0x60; 1335 nm.healthEventSensor = 0x19; 1336 nm.exceptionEventSensor = 0x18; 1337 nm.operationalCapSensor = 0x1A; 1338 nm.thresholdExceededSensor = 0x1B; 1339 1340 uint8_t* nmPtr = reinterpret_cast<uint8_t*>(&nm); 1341 resp.insert(resp.end(), nmPtr, nmPtr + sizeof(NMDiscoveryRecord)); 1342 } 1343 else 1344 { 1345 throw std::runtime_error("getNMDiscoverySDR:: Illegal index " + 1346 std::to_string(index)); 1347 } 1348 1349 return resp; 1350 } 1351 1352 void registerStorageFunctions() 1353 { 1354 createTimers(); 1355 startMatch(); 1356 1357 // <Get FRU Inventory Area Info> 1358 ipmi::registerHandler(ipmi::prioOemBase, ipmi::netFnStorage, 1359 ipmi::storage::cmdGetFruInventoryAreaInfo, 1360 ipmi::Privilege::User, ipmiStorageGetFruInvAreaInfo); 1361 // <READ FRU Data> 1362 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage, 1363 ipmi::storage::cmdReadFruData, ipmi::Privilege::User, 1364 ipmiStorageReadFruData); 1365 1366 // <WRITE FRU Data> 1367 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage, 1368 ipmi::storage::cmdWriteFruData, 1369 ipmi::Privilege::Operator, ipmiStorageWriteFruData); 1370 1371 // <Get SEL Info> 1372 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage, 1373 ipmi::storage::cmdGetSelInfo, ipmi::Privilege::User, 1374 ipmiStorageGetSELInfo); 1375 1376 // <Get SEL Entry> 1377 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage, 1378 ipmi::storage::cmdGetSelEntry, ipmi::Privilege::User, 1379 ipmiStorageGetSELEntry); 1380 1381 // <Add SEL Entry> 1382 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage, 1383 ipmi::storage::cmdAddSelEntry, 1384 ipmi::Privilege::Operator, ipmiStorageAddSELEntry); 1385 1386 // <Clear SEL> 1387 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage, 1388 ipmi::storage::cmdClearSel, ipmi::Privilege::Operator, 1389 ipmiStorageClearSEL); 1390 1391 // <Get SEL Time> 1392 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage, 1393 ipmi::storage::cmdGetSelTime, ipmi::Privilege::User, 1394 ipmiStorageGetSELTime); 1395 1396 // <Set SEL Time> 1397 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage, 1398 ipmi::storage::cmdSetSelTime, 1399 ipmi::Privilege::Operator, ipmiStorageSetSELTime); 1400 } 1401 } // namespace storage 1402 } // namespace ipmi 1403