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