blob: 42c6104dc45c97fe5f4032a542a72b1d422297df [file] [log] [blame]
Jason M. Bills3f7c5e42018-10-03 14:00:41 -07001/*
2// Copyright (c) 2017 2018 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
Jason M. Bills1d4d54d2019-04-23 11:26:11 -070017#include <boost/algorithm/string.hpp>
Jason M. Bills3f7c5e42018-10-03 14:00:41 -070018#include <boost/container/flat_map.hpp>
Jason M. Billsc04e2e72018-11-28 15:15:56 -080019#include <boost/process.hpp>
Jason M. Bills3f7c5e42018-10-03 14:00:41 -070020#include <commandutils.hpp>
Jason M. Bills1d4d54d2019-04-23 11:26:11 -070021#include <filesystem>
Jason M. Bills3f7c5e42018-10-03 14:00:41 -070022#include <iostream>
James Feist2a265d52019-04-08 11:16:27 -070023#include <ipmid/api.hpp>
Jason M. Billsc04e2e72018-11-28 15:15:56 -080024#include <phosphor-ipmi-host/selutility.hpp>
Jason M. Bills3f7c5e42018-10-03 14:00:41 -070025#include <phosphor-logging/log.hpp>
26#include <sdbusplus/message/types.hpp>
27#include <sdbusplus/timer.hpp>
Jason M. Billsc04e2e72018-11-28 15:15:56 -080028#include <sdrutils.hpp>
29#include <stdexcept>
Jason M. Bills3f7c5e42018-10-03 14:00:41 -070030#include <storagecommands.hpp>
31
Jason M. Bills1d4d54d2019-04-23 11:26:11 -070032namespace intel_oem::ipmi::sel
33{
34static const std::filesystem::path selLogDir = "/var/log";
35static const std::string selLogFilename = "ipmi_sel";
36
37static int getFileTimestamp(const std::filesystem::path& file)
38{
39 struct stat st;
40
41 if (stat(file.c_str(), &st) >= 0)
42 {
43 return st.st_mtime;
44 }
45 return ::ipmi::sel::invalidTimeStamp;
46}
47
48namespace erase_time
Jason M. Bills7944c302019-03-20 15:24:05 -070049{
50static constexpr const char* selEraseTimestamp = "/var/lib/ipmi/sel_erase_time";
51
52void save()
53{
54 // open the file, creating it if necessary
55 int fd = open(selEraseTimestamp, O_WRONLY | O_CREAT | O_CLOEXEC, 0644);
56 if (fd < 0)
57 {
58 std::cerr << "Failed to open file\n";
59 return;
60 }
61
62 // update the file timestamp to the current time
63 if (futimens(fd, NULL) < 0)
64 {
65 std::cerr << "Failed to update timestamp: "
66 << std::string(strerror(errno));
67 }
68 close(fd);
69}
70
71int get()
72{
Jason M. Bills1d4d54d2019-04-23 11:26:11 -070073 return getFileTimestamp(selEraseTimestamp);
Jason M. Bills7944c302019-03-20 15:24:05 -070074}
Jason M. Bills1d4d54d2019-04-23 11:26:11 -070075} // namespace erase_time
76} // namespace intel_oem::ipmi::sel
Jason M. Bills7944c302019-03-20 15:24:05 -070077
Jason M. Bills3f7c5e42018-10-03 14:00:41 -070078namespace ipmi
79{
80
81namespace storage
82{
83
Jason M. Billse2d1aee2018-10-03 15:57:18 -070084constexpr static const size_t maxMessageSize = 64;
Jason M. Bills3f7c5e42018-10-03 14:00:41 -070085constexpr static const size_t maxFruSdrNameSize = 16;
86using ManagedObjectType = boost::container::flat_map<
87 sdbusplus::message::object_path,
88 boost::container::flat_map<
89 std::string, boost::container::flat_map<std::string, DbusVariant>>>;
90using ManagedEntry = std::pair<
91 sdbusplus::message::object_path,
92 boost::container::flat_map<
93 std::string, boost::container::flat_map<std::string, DbusVariant>>>;
94
James Feist3bcba452018-12-20 12:31:03 -080095constexpr static const char* fruDeviceServiceName =
96 "xyz.openbmc_project.FruDevice";
Jason M. Billse2d1aee2018-10-03 15:57:18 -070097constexpr static const size_t cacheTimeoutSeconds = 10;
Jason M. Bills3f7c5e42018-10-03 14:00:41 -070098
Jason M. Bills4ed6f2c2019-04-02 12:21:25 -070099// event direction is bit[7] of eventType where 1b = Deassertion event
100constexpr static const uint8_t deassertionEvent = 0x80;
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800101
Jason M. Bills3f7c5e42018-10-03 14:00:41 -0700102static std::vector<uint8_t> fruCache;
103static uint8_t cacheBus = 0xFF;
104static uint8_t cacheAddr = 0XFF;
105
106std::unique_ptr<phosphor::Timer> cacheTimer = nullptr;
107
108// we unfortunately have to build a map of hashes in case there is a
109// collision to verify our dev-id
110boost::container::flat_map<uint8_t, std::pair<uint8_t, uint8_t>> deviceHashes;
111
Jason M. Billse2d1aee2018-10-03 15:57:18 -0700112void registerStorageFunctions() __attribute__((constructor));
Jason M. Bills3f7c5e42018-10-03 14:00:41 -0700113static sdbusplus::bus::bus dbus(ipmid_get_sd_bus_connection());
114
115bool writeFru()
116{
117 sdbusplus::message::message writeFru = dbus.new_method_call(
118 fruDeviceServiceName, "/xyz/openbmc_project/FruDevice",
119 "xyz.openbmc_project.FruDeviceManager", "WriteFru");
120 writeFru.append(cacheBus, cacheAddr, fruCache);
121 try
122 {
123 sdbusplus::message::message writeFruResp = dbus.call(writeFru);
124 }
125 catch (sdbusplus::exception_t&)
126 {
127 // todo: log sel?
128 phosphor::logging::log<phosphor::logging::level::ERR>(
129 "error writing fru");
130 return false;
131 }
132 return true;
133}
134
Jason M. Billse2d1aee2018-10-03 15:57:18 -0700135void createTimer()
136{
137 if (cacheTimer == nullptr)
138 {
139 cacheTimer = std::make_unique<phosphor::Timer>(writeFru);
140 }
141}
142
Jason M. Bills3f7c5e42018-10-03 14:00:41 -0700143ipmi_ret_t replaceCacheFru(uint8_t devId)
144{
145 static uint8_t lastDevId = 0xFF;
146
147 bool timerRunning = (cacheTimer != nullptr) && !cacheTimer->isExpired();
148 if (lastDevId == devId && timerRunning)
149 {
150 return IPMI_CC_OK; // cache already up to date
151 }
152 // if timer is running, stop it and writeFru manually
153 else if (timerRunning)
154 {
155 cacheTimer->stop();
156 writeFru();
157 }
158
159 sdbusplus::message::message getObjects = dbus.new_method_call(
160 fruDeviceServiceName, "/", "org.freedesktop.DBus.ObjectManager",
161 "GetManagedObjects");
162 ManagedObjectType frus;
163 try
164 {
165 sdbusplus::message::message resp = dbus.call(getObjects);
166 resp.read(frus);
167 }
168 catch (sdbusplus::exception_t&)
169 {
170 phosphor::logging::log<phosphor::logging::level::ERR>(
171 "replaceCacheFru: error getting managed objects");
172 return IPMI_CC_RESPONSE_ERROR;
173 }
174
175 deviceHashes.clear();
176
177 // hash the object paths to create unique device id's. increment on
178 // collision
179 std::hash<std::string> hasher;
180 for (const auto& fru : frus)
181 {
182 auto fruIface = fru.second.find("xyz.openbmc_project.FruDevice");
183 if (fruIface == fru.second.end())
184 {
185 continue;
186 }
187
188 auto busFind = fruIface->second.find("BUS");
189 auto addrFind = fruIface->second.find("ADDRESS");
190 if (busFind == fruIface->second.end() ||
191 addrFind == fruIface->second.end())
192 {
193 phosphor::logging::log<phosphor::logging::level::INFO>(
194 "fru device missing Bus or Address",
195 phosphor::logging::entry("FRU=%s", fru.first.str.c_str()));
196 continue;
197 }
198
199 uint8_t fruBus =
200 sdbusplus::message::variant_ns::get<uint32_t>(busFind->second);
201 uint8_t fruAddr =
202 sdbusplus::message::variant_ns::get<uint32_t>(addrFind->second);
203
204 uint8_t fruHash = 0;
205 if (fruBus != 0 || fruAddr != 0)
206 {
207 fruHash = hasher(fru.first.str);
208 // can't be 0xFF based on spec, and 0 is reserved for baseboard
209 if (fruHash == 0 || fruHash == 0xFF)
210 {
211 fruHash = 1;
212 }
213 }
214 std::pair<uint8_t, uint8_t> newDev(fruBus, fruAddr);
215
216 bool emplacePassed = false;
217 while (!emplacePassed)
218 {
219 auto resp = deviceHashes.emplace(fruHash, newDev);
220 emplacePassed = resp.second;
221 if (!emplacePassed)
222 {
223 fruHash++;
224 // can't be 0xFF based on spec, and 0 is reserved for
225 // baseboard
226 if (fruHash == 0XFF)
227 {
228 fruHash = 0x1;
229 }
230 }
231 }
232 }
233 auto deviceFind = deviceHashes.find(devId);
234 if (deviceFind == deviceHashes.end())
235 {
236 return IPMI_CC_SENSOR_INVALID;
237 }
238
239 fruCache.clear();
240 sdbusplus::message::message getRawFru = dbus.new_method_call(
241 fruDeviceServiceName, "/xyz/openbmc_project/FruDevice",
242 "xyz.openbmc_project.FruDeviceManager", "GetRawFru");
243 cacheBus = deviceFind->second.first;
244 cacheAddr = deviceFind->second.second;
245 getRawFru.append(cacheBus, cacheAddr);
246 try
247 {
248 sdbusplus::message::message getRawResp = dbus.call(getRawFru);
249 getRawResp.read(fruCache);
250 }
251 catch (sdbusplus::exception_t&)
252 {
253 lastDevId = 0xFF;
254 cacheBus = 0xFF;
255 cacheAddr = 0xFF;
256 return IPMI_CC_RESPONSE_ERROR;
257 }
258
259 lastDevId = devId;
260 return IPMI_CC_OK;
261}
262
Jason M. Billse2d1aee2018-10-03 15:57:18 -0700263ipmi_ret_t ipmiStorageReadFRUData(ipmi_netfn_t netfn, ipmi_cmd_t cmd,
264 ipmi_request_t request,
265 ipmi_response_t response,
266 ipmi_data_len_t dataLen,
267 ipmi_context_t context)
268{
269 if (*dataLen != 4)
270 {
271 *dataLen = 0;
272 return IPMI_CC_REQ_DATA_LEN_INVALID;
273 }
274 *dataLen = 0; // default to 0 in case of an error
275
276 auto req = static_cast<GetFRUAreaReq*>(request);
277
278 if (req->countToRead > maxMessageSize - 1)
279 {
280 return IPMI_CC_INVALID_FIELD_REQUEST;
281 }
282 ipmi_ret_t status = replaceCacheFru(req->fruDeviceID);
283
284 if (status != IPMI_CC_OK)
285 {
286 return status;
287 }
288
289 size_t fromFRUByteLen = 0;
290 if (req->countToRead + req->fruInventoryOffset < fruCache.size())
291 {
292 fromFRUByteLen = req->countToRead;
293 }
294 else if (fruCache.size() > req->fruInventoryOffset)
295 {
296 fromFRUByteLen = fruCache.size() - req->fruInventoryOffset;
297 }
298 size_t padByteLen = req->countToRead - fromFRUByteLen;
299 uint8_t* respPtr = static_cast<uint8_t*>(response);
300 *respPtr = req->countToRead;
301 std::copy(fruCache.begin() + req->fruInventoryOffset,
302 fruCache.begin() + req->fruInventoryOffset + fromFRUByteLen,
303 ++respPtr);
304 // if longer than the fru is requested, fill with 0xFF
305 if (padByteLen)
306 {
307 respPtr += fromFRUByteLen;
308 std::fill(respPtr, respPtr + padByteLen, 0xFF);
309 }
310 *dataLen = fromFRUByteLen + 1;
311
312 return IPMI_CC_OK;
313}
314
315ipmi_ret_t ipmiStorageWriteFRUData(ipmi_netfn_t netfn, ipmi_cmd_t cmd,
316 ipmi_request_t request,
317 ipmi_response_t response,
318 ipmi_data_len_t dataLen,
319 ipmi_context_t context)
320{
321 if (*dataLen < 4 ||
322 *dataLen >=
323 0xFF + 3) // count written return is one byte, so limit to one byte
324 // of data after the three request data bytes
325 {
326 *dataLen = 0;
327 return IPMI_CC_REQ_DATA_LEN_INVALID;
328 }
329
330 auto req = static_cast<WriteFRUDataReq*>(request);
331 size_t writeLen = *dataLen - 3;
332 *dataLen = 0; // default to 0 in case of an error
333
334 ipmi_ret_t status = replaceCacheFru(req->fruDeviceID);
335 if (status != IPMI_CC_OK)
336 {
337 return status;
338 }
339 int lastWriteAddr = req->fruInventoryOffset + writeLen;
340 if (fruCache.size() < lastWriteAddr)
341 {
342 fruCache.resize(req->fruInventoryOffset + writeLen);
343 }
344
345 std::copy(req->data, req->data + writeLen,
346 fruCache.begin() + req->fruInventoryOffset);
347
348 bool atEnd = false;
349
350 if (fruCache.size() >= sizeof(FRUHeader))
351 {
352
353 FRUHeader* header = reinterpret_cast<FRUHeader*>(fruCache.data());
354
355 int lastRecordStart = std::max(
356 header->internalOffset,
357 std::max(header->chassisOffset,
358 std::max(header->boardOffset, header->productOffset)));
359 // TODO: Handle Multi-Record FRUs?
360
361 lastRecordStart *= 8; // header starts in are multiples of 8 bytes
362
363 // get the length of the area in multiples of 8 bytes
364 if (lastWriteAddr > (lastRecordStart + 1))
365 {
366 // second byte in record area is the length
367 int areaLength(fruCache[lastRecordStart + 1]);
368 areaLength *= 8; // it is in multiples of 8 bytes
369
370 if (lastWriteAddr >= (areaLength + lastRecordStart))
371 {
372 atEnd = true;
373 }
374 }
375 }
376 uint8_t* respPtr = static_cast<uint8_t*>(response);
377 if (atEnd)
378 {
379 // cancel timer, we're at the end so might as well send it
380 cacheTimer->stop();
381 if (!writeFru())
382 {
383 return IPMI_CC_INVALID_FIELD_REQUEST;
384 }
385 *respPtr = std::min(fruCache.size(), static_cast<size_t>(0xFF));
386 }
387 else
388 {
389 // start a timer, if no further data is sent in cacheTimeoutSeconds
390 // seconds, check to see if it is valid
391 createTimer();
392 cacheTimer->start(std::chrono::duration_cast<std::chrono::microseconds>(
393 std::chrono::seconds(cacheTimeoutSeconds)));
394 *respPtr = 0;
395 }
396
397 *dataLen = 1;
398
399 return IPMI_CC_OK;
400}
401
402ipmi_ret_t ipmiStorageGetFRUInvAreaInfo(ipmi_netfn_t netfn, ipmi_cmd_t cmd,
403 ipmi_request_t request,
404 ipmi_response_t response,
405 ipmi_data_len_t dataLen,
406 ipmi_context_t context)
407{
408 if (*dataLen != 1)
409 {
410 *dataLen = 0;
411 return IPMI_CC_REQ_DATA_LEN_INVALID;
412 }
413 *dataLen = 0; // default to 0 in case of an error
414
415 uint8_t reqDev = *(static_cast<uint8_t*>(request));
416 if (reqDev == 0xFF)
417 {
418 return IPMI_CC_INVALID_FIELD_REQUEST;
419 }
420 ipmi_ret_t status = replaceCacheFru(reqDev);
421
422 if (status != IPMI_CC_OK)
423 {
424 return status;
425 }
426
427 GetFRUAreaResp* respPtr = static_cast<GetFRUAreaResp*>(response);
428 respPtr->inventorySizeLSB = fruCache.size() & 0xFF;
429 respPtr->inventorySizeMSB = fruCache.size() >> 8;
430 respPtr->accessType = static_cast<uint8_t>(GetFRUAreaAccessType::byte);
431
432 *dataLen = sizeof(GetFRUAreaResp);
433 return IPMI_CC_OK;
434}
435
Jason M. Bills3f7c5e42018-10-03 14:00:41 -0700436ipmi_ret_t getFruSdrCount(size_t& count)
437{
438 ipmi_ret_t ret = replaceCacheFru(0);
439 if (ret != IPMI_CC_OK)
440 {
441 return ret;
442 }
443 count = deviceHashes.size();
444 return IPMI_CC_OK;
445}
446
447ipmi_ret_t getFruSdrs(size_t index, get_sdr::SensorDataFruRecord& resp)
448{
449 ipmi_ret_t ret = replaceCacheFru(0); // this will update the hash list
450 if (ret != IPMI_CC_OK)
451 {
452 return ret;
453 }
454 if (deviceHashes.size() < index)
455 {
456 return IPMI_CC_INVALID_FIELD_REQUEST;
457 }
458 auto device = deviceHashes.begin() + index;
459 uint8_t& bus = device->second.first;
460 uint8_t& address = device->second.second;
461
462 ManagedObjectType frus;
463
464 sdbusplus::message::message getObjects = dbus.new_method_call(
465 fruDeviceServiceName, "/", "org.freedesktop.DBus.ObjectManager",
466 "GetManagedObjects");
467 try
468 {
469 sdbusplus::message::message resp = dbus.call(getObjects);
470 resp.read(frus);
471 }
472 catch (sdbusplus::exception_t&)
473 {
474 return IPMI_CC_RESPONSE_ERROR;
475 }
476 boost::container::flat_map<std::string, DbusVariant>* fruData = nullptr;
477 auto fru =
478 std::find_if(frus.begin(), frus.end(),
479 [bus, address, &fruData](ManagedEntry& entry) {
480 auto findFruDevice =
481 entry.second.find("xyz.openbmc_project.FruDevice");
482 if (findFruDevice == entry.second.end())
483 {
484 return false;
485 }
486 fruData = &(findFruDevice->second);
487 auto findBus = findFruDevice->second.find("BUS");
488 auto findAddress =
489 findFruDevice->second.find("ADDRESS");
490 if (findBus == findFruDevice->second.end() ||
491 findAddress == findFruDevice->second.end())
492 {
493 return false;
494 }
495 if (sdbusplus::message::variant_ns::get<uint32_t>(
496 findBus->second) != bus)
497 {
498 return false;
499 }
500 if (sdbusplus::message::variant_ns::get<uint32_t>(
501 findAddress->second) != address)
502 {
503 return false;
504 }
505 return true;
506 });
507 if (fru == frus.end())
508 {
509 return IPMI_CC_RESPONSE_ERROR;
510 }
511 std::string name;
512 auto findProductName = fruData->find("BOARD_PRODUCT_NAME");
513 auto findBoardName = fruData->find("PRODUCT_PRODUCT_NAME");
514 if (findProductName != fruData->end())
515 {
516 name = sdbusplus::message::variant_ns::get<std::string>(
517 findProductName->second);
518 }
519 else if (findBoardName != fruData->end())
520 {
521 name = sdbusplus::message::variant_ns::get<std::string>(
522 findBoardName->second);
523 }
524 else
525 {
526 name = "UNKNOWN";
527 }
528 if (name.size() > maxFruSdrNameSize)
529 {
530 name = name.substr(0, maxFruSdrNameSize);
531 }
532 size_t sizeDiff = maxFruSdrNameSize - name.size();
533
534 resp.header.record_id_lsb = 0x0; // calling code is to implement these
535 resp.header.record_id_msb = 0x0;
536 resp.header.sdr_version = ipmiSdrVersion;
537 resp.header.record_type = 0x11; // FRU Device Locator
538 resp.header.record_length = sizeof(resp.body) + sizeof(resp.key) - sizeDiff;
539 resp.key.deviceAddress = 0x20;
540 resp.key.fruID = device->first;
541 resp.key.accessLun = 0x80; // logical / physical fru device
542 resp.key.channelNumber = 0x0;
543 resp.body.reserved = 0x0;
544 resp.body.deviceType = 0x10;
James Feist4f86d1f2019-04-03 10:30:26 -0700545 resp.body.deviceTypeModifier = 0x0;
Jason M. Bills3f7c5e42018-10-03 14:00:41 -0700546 resp.body.entityID = 0x0;
547 resp.body.entityInstance = 0x1;
548 resp.body.oem = 0x0;
549 resp.body.deviceIDLen = name.size();
550 name.copy(resp.body.deviceID, name.size());
551
552 return IPMI_CC_OK;
553}
Jason M. Billse2d1aee2018-10-03 15:57:18 -0700554
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700555static bool getSELLogFiles(std::vector<std::filesystem::path>& selLogFiles)
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800556{
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700557 // Loop through the directory looking for ipmi_sel log files
558 for (const std::filesystem::directory_entry& dirEnt :
559 std::filesystem::directory_iterator(intel_oem::ipmi::sel::selLogDir))
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800560 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700561 std::string filename = dirEnt.path().filename();
562 if (boost::starts_with(filename, intel_oem::ipmi::sel::selLogFilename))
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800563 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700564 // If we find an ipmi_sel log file, save the path
565 selLogFiles.emplace_back(intel_oem::ipmi::sel::selLogDir /
566 filename);
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800567 }
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800568 }
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700569 // As the log files rotate, they are appended with a ".#" that is higher for
570 // the older logs. Since we don't expect more than 10 log files, we
571 // can just sort the list to get them in order from newest to oldest
572 std::sort(selLogFiles.begin(), selLogFiles.end());
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800573
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700574 return !selLogFiles.empty();
575}
576
577static int countSELEntries()
578{
579 // Get the list of ipmi_sel log files
580 std::vector<std::filesystem::path> selLogFiles;
581 if (!getSELLogFiles(selLogFiles))
582 {
583 return 0;
584 }
585 int numSELEntries = 0;
586 // Loop through each log file and count the number of logs
587 for (const std::filesystem::path& file : selLogFiles)
588 {
589 std::ifstream logStream(file);
590 if (!logStream.is_open())
591 {
592 continue;
593 }
594
595 std::string line;
596 while (std::getline(logStream, line))
597 {
598 numSELEntries++;
599 }
600 }
601 return numSELEntries;
602}
603
604static bool findSELEntry(const int recordID,
605 const std::vector<std::filesystem::path> selLogFiles,
606 std::string& entry)
607{
608 // Record ID is the first entry field following the timestamp. It is
609 // preceded by a space and followed by a comma
610 std::string search = " " + std::to_string(recordID) + ",";
611
612 // Loop through the ipmi_sel log entries
613 for (const std::filesystem::path& file : selLogFiles)
614 {
615 std::ifstream logStream(file);
616 if (!logStream.is_open())
617 {
618 continue;
619 }
620
621 while (std::getline(logStream, entry))
622 {
623 // Check if the record ID matches
624 if (entry.find(search) != std::string::npos)
625 {
626 return true;
627 }
628 }
629 }
630 return false;
631}
632
633static uint16_t
634 getNextRecordID(const uint16_t recordID,
635 const std::vector<std::filesystem::path> selLogFiles)
636{
637 uint16_t nextRecordID = recordID + 1;
638 std::string entry;
639 if (findSELEntry(nextRecordID, selLogFiles, entry))
640 {
641 return nextRecordID;
642 }
643 else
644 {
645 return ipmi::sel::lastEntry;
646 }
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800647}
648
649static int fromHexStr(const std::string hexStr, std::vector<uint8_t>& data)
650{
651 for (unsigned int i = 0; i < hexStr.size(); i += 2)
652 {
653 try
654 {
655 data.push_back(static_cast<uint8_t>(
656 std::stoul(hexStr.substr(i, 2), nullptr, 16)));
657 }
658 catch (std::invalid_argument& e)
659 {
660 phosphor::logging::log<phosphor::logging::level::ERR>(e.what());
661 return -1;
662 }
663 catch (std::out_of_range& e)
664 {
665 phosphor::logging::log<phosphor::logging::level::ERR>(e.what());
666 return -1;
667 }
668 }
669 return 0;
670}
671
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700672ipmi::RspType<uint8_t, // SEL version
673 uint16_t, // SEL entry count
674 uint16_t, // free space
675 uint32_t, // last add timestamp
676 uint32_t, // last erase timestamp
677 uint8_t> // operation support
678 ipmiStorageGetSELInfo()
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800679{
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700680 constexpr uint8_t selVersion = ipmi::sel::selVersion;
681 uint16_t entries = countSELEntries();
682 uint32_t addTimeStamp = intel_oem::ipmi::sel::getFileTimestamp(
683 intel_oem::ipmi::sel::selLogDir / intel_oem::ipmi::sel::selLogFilename);
684 uint32_t eraseTimeStamp = intel_oem::ipmi::sel::erase_time::get();
685 constexpr uint8_t operationSupport =
686 intel_oem::ipmi::sel::selOperationSupport;
687 constexpr uint16_t freeSpace =
688 0xffff; // Spec indicates that more than 64kB is free
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800689
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700690 return ipmi::responseSuccess(selVersion, entries, freeSpace, addTimeStamp,
691 eraseTimeStamp, operationSupport);
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800692}
693
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700694using systemEventType = std::tuple<
695 uint32_t, // Timestamp
696 uint16_t, // Generator ID
697 uint8_t, // EvM Rev
698 uint8_t, // Sensor Type
699 uint8_t, // Sensor Number
700 uint7_t, // Event Type
701 bool, // Event Direction
702 std::array<uint8_t, intel_oem::ipmi::sel::systemEventSize>>; // Event Data
703using oemTsEventType = std::tuple<
704 uint32_t, // Timestamp
705 std::array<uint8_t, intel_oem::ipmi::sel::oemTsEventSize>>; // Event Data
706using oemEventType =
707 std::array<uint8_t, intel_oem::ipmi::sel::oemEventSize>; // Event Data
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800708
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700709ipmi::RspType<uint16_t, // Next Record ID
710 uint16_t, // Record ID
711 uint8_t, // Record Type
712 std::variant<systemEventType, oemTsEventType,
713 oemEventType>> // Record Content
714 ipmiStorageGetSELEntry(uint16_t reservationID, uint16_t targetID,
715 uint8_t offset, uint8_t size)
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800716{
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700717 // Only support getting the entire SEL record. If a partial size or non-zero
718 // offset is requested, return an error
719 if (offset != 0 || size != ipmi::sel::entireRecord)
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800720 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700721 return ipmi::responseRetBytesUnavailable();
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800722 }
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800723
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700724 // Check the reservation ID if one is provided or required (only if the
725 // offset is non-zero)
726 if (reservationID != 0 || offset != 0)
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800727 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700728 if (!checkSELReservation(reservationID))
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800729 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700730 return ipmi::responseInvalidReservationId();
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800731 }
732 }
733
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700734 // Get the ipmi_sel log files
735 std::vector<std::filesystem::path> selLogFiles;
736 if (!getSELLogFiles(selLogFiles))
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800737 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700738 return ipmi::responseSensorInvalid();
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800739 }
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800740
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700741 std::string targetEntry;
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800742
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800743 if (targetID == ipmi::sel::firstEntry)
744 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700745 // The first entry will be at the top of the oldest log file
746 std::ifstream logStream(selLogFiles.back());
747 if (!logStream.is_open())
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800748 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700749 return ipmi::responseUnspecifiedError();
750 }
751
752 if (!std::getline(logStream, targetEntry))
753 {
754 return ipmi::responseUnspecifiedError();
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800755 }
756 }
757 else if (targetID == ipmi::sel::lastEntry)
758 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700759 // The last entry will be at the bottom of the newest log file
760 std::ifstream logStream(selLogFiles.front());
761 if (!logStream.is_open())
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800762 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700763 return ipmi::responseUnspecifiedError();
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800764 }
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700765
766 std::string line;
767 while (std::getline(logStream, line))
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800768 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700769 targetEntry = line;
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800770 }
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800771 }
772 else
773 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700774 if (!findSELEntry(targetID, selLogFiles, targetEntry))
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800775 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700776 return ipmi::responseSensorInvalid();
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800777 }
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800778 }
779
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700780 // The format of the ipmi_sel message is "<timestamp>
781 // <ID>,<Type>,<EventData>,[<Generator ID>,<Path>,<Direction>]". Split it
782 // into the individual fields.
783 std::vector<std::string> targetEntryFields;
784 boost::split(targetEntryFields, targetEntry, boost::is_any_of(" ,"),
785 boost::token_compress_on);
786 if (targetEntryFields.size() < 4)
787 {
788 return ipmi::responseUnspecifiedError();
789 }
790
791 uint16_t recordID = std::stoul(targetEntryFields[1]);
792 uint16_t nextRecordID = getNextRecordID(recordID, selLogFiles);
793 uint8_t recordType = std::stoul(targetEntryFields[2], nullptr, 16);
794 std::vector<uint8_t> eventDataBytes;
795 if (fromHexStr(targetEntryFields[3], eventDataBytes) < 0)
796 {
797 return ipmi::responseUnspecifiedError();
798 }
799
800 if (recordType == intel_oem::ipmi::sel::systemEvent)
801 {
802 // System type events have three additional fields
803 if (targetEntryFields.size() < 7)
804 {
805 return ipmi::responseUnspecifiedError();
806 }
807
808 // Get the timestamp
809 std::tm timeStruct = {};
810 std::istringstream entryStream(targetEntryFields[0]);
811
812 uint32_t timestamp = ipmi::sel::invalidTimeStamp;
813 if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S"))
814 {
815 timestamp = std::mktime(&timeStruct);
816 }
817
818 // Get the generator ID
819 uint16_t generatorID = std::stoul(targetEntryFields[4], nullptr, 16);
820
821 // Set the event message revision
822 uint8_t evmRev = intel_oem::ipmi::sel::eventMsgRev;
823
824 // Get the sensor type, sensor number, and event type for the sensor
825 uint8_t sensorType = getSensorTypeFromPath(targetEntryFields[5]);
826 uint8_t sensorNum = getSensorNumberFromPath(targetEntryFields[5]);
827 uint7_t eventType = getSensorEventTypeFromPath(targetEntryFields[5]);
828
829 // Get the event direction
830 bool eventDir = std::stoul(targetEntryFields[6]) ? 0 : 1;
831
832 // Only keep the eventData bytes that fit in the record
833 std::array<uint8_t, intel_oem::ipmi::sel::systemEventSize> eventData{};
834 std::copy_n(eventDataBytes.begin(),
835 std::min(eventDataBytes.size(), eventData.size()),
836 eventData.begin());
837
838 return ipmi::responseSuccess(
839 nextRecordID, recordID, recordType,
840 systemEventType{timestamp, generatorID, evmRev, sensorType,
841 sensorNum, eventType, eventDir, eventData});
842 }
843 else if (recordType >= intel_oem::ipmi::sel::oemTsEventFirst &&
844 recordType <= intel_oem::ipmi::sel::oemTsEventLast)
845 {
846 // Get the timestamp
847 std::tm timeStruct = {};
848 std::istringstream entryStream(targetEntryFields[0]);
849
850 uint32_t timestamp = ipmi::sel::invalidTimeStamp;
851 if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S"))
852 {
853 timestamp = std::mktime(&timeStruct);
854 }
855
856 // Only keep the bytes that fit in the record
857 std::array<uint8_t, intel_oem::ipmi::sel::oemTsEventSize> eventData{};
858 std::copy_n(eventDataBytes.begin(),
859 std::min(eventDataBytes.size(), eventData.size()),
860 eventData.begin());
861
862 return ipmi::responseSuccess(nextRecordID, recordID, recordType,
863 oemTsEventType{timestamp, eventData});
864 }
865 else if (recordType >= intel_oem::ipmi::sel::oemEventFirst &&
866 recordType <= intel_oem::ipmi::sel::oemEventLast)
867 {
868 // Only keep the bytes that fit in the record
869 std::array<uint8_t, intel_oem::ipmi::sel::oemEventSize> eventData{};
870 std::copy_n(eventDataBytes.begin(),
871 std::min(eventDataBytes.size(), eventData.size()),
872 eventData.begin());
873
874 return ipmi::responseSuccess(nextRecordID, recordID, recordType,
875 eventData);
876 }
877
878 return ipmi::responseUnspecifiedError();
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800879}
880
881ipmi_ret_t ipmiStorageAddSELEntry(ipmi_netfn_t netfn, ipmi_cmd_t cmd,
882 ipmi_request_t request,
883 ipmi_response_t response,
884 ipmi_data_len_t data_len,
885 ipmi_context_t context)
886{
887 static constexpr char const* ipmiSELObject =
888 "xyz.openbmc_project.Logging.IPMI";
889 static constexpr char const* ipmiSELPath =
890 "/xyz/openbmc_project/Logging/IPMI";
891 static constexpr char const* ipmiSELAddInterface =
892 "xyz.openbmc_project.Logging.IPMI";
893 static const std::string ipmiSELAddMessage =
894 "IPMI SEL entry logged using IPMI Add SEL Entry command.";
895 uint16_t recordID = 0;
896 sdbusplus::bus::bus bus{ipmid_get_sd_bus_connection()};
897
898 if (*data_len != sizeof(AddSELRequest))
899 {
900 *data_len = 0;
901 return IPMI_CC_REQ_DATA_LEN_INVALID;
902 }
903 AddSELRequest* req = static_cast<AddSELRequest*>(request);
904
905 // Per the IPMI spec, need to cancel any reservation when a SEL entry is
906 // added
907 cancelSELReservation();
908
909 if (req->recordType == intel_oem::ipmi::sel::systemEvent)
910 {
911 std::string sensorPath =
912 getPathFromSensorNumber(req->record.system.sensorNum);
913 std::vector<uint8_t> eventData(
914 req->record.system.eventData,
915 req->record.system.eventData +
916 intel_oem::ipmi::sel::systemEventSize);
Jason M. Bills4ed6f2c2019-04-02 12:21:25 -0700917 bool assert = !(req->record.system.eventType & deassertionEvent);
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800918 uint16_t genId = req->record.system.generatorID;
919 sdbusplus::message::message writeSEL = bus.new_method_call(
920 ipmiSELObject, ipmiSELPath, ipmiSELAddInterface, "IpmiSelAdd");
921 writeSEL.append(ipmiSELAddMessage, sensorPath, eventData, assert,
922 genId);
923 try
924 {
925 sdbusplus::message::message writeSELResp = bus.call(writeSEL);
926 writeSELResp.read(recordID);
927 }
928 catch (sdbusplus::exception_t& e)
929 {
930 phosphor::logging::log<phosphor::logging::level::ERR>(e.what());
931 *data_len = 0;
932 return IPMI_CC_UNSPECIFIED_ERROR;
933 }
934 }
935 else if (req->recordType >= intel_oem::ipmi::sel::oemTsEventFirst &&
936 req->recordType <= intel_oem::ipmi::sel::oemEventLast)
937 {
938 std::vector<uint8_t> eventData;
939 if (req->recordType <= intel_oem::ipmi::sel::oemTsEventLast)
940 {
941 eventData =
942 std::vector<uint8_t>(req->record.oemTs.eventData,
943 req->record.oemTs.eventData +
944 intel_oem::ipmi::sel::oemTsEventSize);
945 }
946 else
947 {
948 eventData = std::vector<uint8_t>(
949 req->record.oem.eventData,
950 req->record.oem.eventData + intel_oem::ipmi::sel::oemEventSize);
951 }
952 sdbusplus::message::message writeSEL = bus.new_method_call(
953 ipmiSELObject, ipmiSELPath, ipmiSELAddInterface, "IpmiSelAddOem");
954 writeSEL.append(ipmiSELAddMessage, eventData, req->recordType);
955 try
956 {
957 sdbusplus::message::message writeSELResp = bus.call(writeSEL);
958 writeSELResp.read(recordID);
959 }
960 catch (sdbusplus::exception_t& e)
961 {
962 phosphor::logging::log<phosphor::logging::level::ERR>(e.what());
963 *data_len = 0;
964 return IPMI_CC_UNSPECIFIED_ERROR;
965 }
966 }
967 else
968 {
969 *data_len = 0;
970 return IPMI_CC_PARM_OUT_OF_RANGE;
971 }
972
973 *static_cast<uint16_t*>(response) = recordID;
974 *data_len = sizeof(recordID);
975 return IPMI_CC_OK;
976}
977
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700978ipmi::RspType<uint8_t> ipmiStorageClearSEL(ipmi::Context::ptr ctx,
979 uint16_t reservationID,
980 const std::array<uint8_t, 3>& clr,
981 uint8_t eraseOperation)
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800982{
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700983 if (!checkSELReservation(reservationID))
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800984 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700985 return ipmi::responseInvalidReservationId();
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800986 }
987
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700988 static constexpr std::array<uint8_t, 3> clrExpected = {'C', 'L', 'R'};
989 if (clr != clrExpected)
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800990 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700991 return ipmi::responseInvalidFieldRequest();
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800992 }
993
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700994 // Erasure status cannot be fetched, so always return erasure status as
995 // `erase completed`.
996 if (eraseOperation == ipmi::sel::getEraseStatus)
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800997 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -0700998 return ipmi::responseSuccess(ipmi::sel::eraseComplete);
Jason M. Billsc04e2e72018-11-28 15:15:56 -0800999 }
1000
Jason M. Bills1d4d54d2019-04-23 11:26:11 -07001001 // Check that initiate erase is correct
1002 if (eraseOperation != ipmi::sel::initiateErase)
1003 {
1004 return ipmi::responseInvalidFieldRequest();
1005 }
1006
1007 // Per the IPMI spec, need to cancel any reservation when the SEL is
1008 // cleared
Jason M. Billsc04e2e72018-11-28 15:15:56 -08001009 cancelSELReservation();
1010
Jason M. Bills7944c302019-03-20 15:24:05 -07001011 // Save the erase time
1012 intel_oem::ipmi::sel::erase_time::save();
1013
Jason M. Bills1d4d54d2019-04-23 11:26:11 -07001014 // Clear the SEL by deleting the log files
1015 std::vector<std::filesystem::path> selLogFiles;
1016 if (getSELLogFiles(selLogFiles))
Jason M. Billsc04e2e72018-11-28 15:15:56 -08001017 {
Jason M. Bills1d4d54d2019-04-23 11:26:11 -07001018 for (const std::filesystem::path& file : selLogFiles)
1019 {
1020 std::error_code ec;
1021 std::filesystem::remove(file, ec);
1022 }
Jason M. Billsc04e2e72018-11-28 15:15:56 -08001023 }
1024
Jason M. Bills1d4d54d2019-04-23 11:26:11 -07001025 // Reload rsyslog so it knows to start new log files
1026 sdbusplus::message::message rsyslogReload = dbus.new_method_call(
1027 "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
1028 "org.freedesktop.systemd1.Manager", "ReloadUnit");
1029 rsyslogReload.append("rsyslog.service", "replace");
1030 try
1031 {
1032 sdbusplus::message::message reloadResponse = dbus.call(rsyslogReload);
1033 }
1034 catch (sdbusplus::exception_t& e)
1035 {
1036 phosphor::logging::log<phosphor::logging::level::ERR>(e.what());
1037 }
1038
1039 return ipmi::responseSuccess(ipmi::sel::eraseComplete);
Jason M. Billsc04e2e72018-11-28 15:15:56 -08001040}
1041
Jason M. Bills1d4d54d2019-04-23 11:26:11 -07001042ipmi::RspType<> ipmiStorageSetSELTime(uint32_t selTime)
Jason M. Billscac97a52019-01-30 14:43:46 -08001043{
1044 // Set SEL Time is not supported
Jason M. Bills1d4d54d2019-04-23 11:26:11 -07001045 return ipmi::responseInvalidCommand();
Jason M. Billscac97a52019-01-30 14:43:46 -08001046}
1047
Jason M. Billse2d1aee2018-10-03 15:57:18 -07001048void registerStorageFunctions()
1049{
1050 // <Get FRU Inventory Area Info>
1051 ipmiPrintAndRegister(
1052 NETFUN_STORAGE,
1053 static_cast<ipmi_cmd_t>(IPMINetfnStorageCmds::ipmiCmdGetFRUInvAreaInfo),
1054 NULL, ipmiStorageGetFRUInvAreaInfo, PRIVILEGE_OPERATOR);
1055
Jason M. Billsc04e2e72018-11-28 15:15:56 -08001056 // <READ FRU Data>
Jason M. Billse2d1aee2018-10-03 15:57:18 -07001057 ipmiPrintAndRegister(
1058 NETFUN_STORAGE,
1059 static_cast<ipmi_cmd_t>(IPMINetfnStorageCmds::ipmiCmdReadFRUData), NULL,
1060 ipmiStorageReadFRUData, PRIVILEGE_OPERATOR);
1061
Jason M. Billsc04e2e72018-11-28 15:15:56 -08001062 // <WRITE FRU Data>
Jason M. Billse2d1aee2018-10-03 15:57:18 -07001063 ipmiPrintAndRegister(
1064 NETFUN_STORAGE,
1065 static_cast<ipmi_cmd_t>(IPMINetfnStorageCmds::ipmiCmdWriteFRUData),
1066 NULL, ipmiStorageWriteFRUData, PRIVILEGE_OPERATOR);
Jason M. Billsc04e2e72018-11-28 15:15:56 -08001067
1068 // <Get SEL Info>
Jason M. Bills1d4d54d2019-04-23 11:26:11 -07001069 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage,
1070 ipmi::storage::cmdGetSelInfo,
1071 ipmi::Privilege::Operator, ipmiStorageGetSELInfo);
Jason M. Billsc04e2e72018-11-28 15:15:56 -08001072
1073 // <Get SEL Entry>
Jason M. Bills1d4d54d2019-04-23 11:26:11 -07001074 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage,
1075 ipmi::storage::cmdGetSelEntry,
1076 ipmi::Privilege::Operator, ipmiStorageGetSELEntry);
Jason M. Billsc04e2e72018-11-28 15:15:56 -08001077
1078 // <Add SEL Entry>
1079 ipmiPrintAndRegister(
1080 NETFUN_STORAGE,
1081 static_cast<ipmi_cmd_t>(IPMINetfnStorageCmds::ipmiCmdAddSEL), NULL,
1082 ipmiStorageAddSELEntry, PRIVILEGE_OPERATOR);
1083
1084 // <Clear SEL>
Jason M. Bills1d4d54d2019-04-23 11:26:11 -07001085 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage,
1086 ipmi::storage::cmdClearSel, ipmi::Privilege::Operator,
1087 ipmiStorageClearSEL);
Jason M. Billscac97a52019-01-30 14:43:46 -08001088
1089 // <Set SEL Time>
Jason M. Bills1d4d54d2019-04-23 11:26:11 -07001090 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnStorage,
1091 ipmi::storage::cmdSetSelTime,
1092 ipmi::Privilege::Operator, ipmiStorageSetSELTime);
Jason M. Billse2d1aee2018-10-03 15:57:18 -07001093}
Jason M. Bills3f7c5e42018-10-03 14:00:41 -07001094} // namespace storage
Jason M. Billsc04e2e72018-11-28 15:15:56 -08001095} // namespace ipmi