xref: /openbmc/phosphor-host-ipmid/dbus-sdr/sdrutils.cpp (revision 653aec092d86597aa7d7d1259eee58e12310f202)
1 /*
2 // Copyright (c) 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 
17 #include "dbus-sdr/sdrutils.hpp"
18 
19 #include <ipmid/utils.hpp>
20 #include <nlohmann/json.hpp>
21 #include <phosphor-logging/lg2.hpp>
22 #include <xyz/openbmc_project/ObjectMapper/common.hpp>
23 #include <xyz/openbmc_project/Sensor/Threshold/Critical/common.hpp>
24 #include <xyz/openbmc_project/Sensor/Threshold/Warning/common.hpp>
25 #include <xyz/openbmc_project/Sensor/Value/common.hpp>
26 
27 #include <fstream>
28 #include <optional>
29 #include <unordered_set>
30 
31 using ObjectMapper = sdbusplus::common::xyz::openbmc_project::ObjectMapper;
32 using SensorValue = sdbusplus::common::xyz::openbmc_project::sensor::Value;
33 using SensorThresholdWarning =
34     sdbusplus::common::xyz::openbmc_project::sensor::threshold::Warning;
35 using SensorThresholdCritical =
36     sdbusplus::common::xyz::openbmc_project::sensor::threshold::Critical;
37 
38 #ifdef FEATURE_HYBRID_SENSORS
39 
40 #include <ipmid/utils.hpp>
41 namespace ipmi
42 {
43 namespace sensor
44 {
45 extern const IdInfoMap sensors;
46 } // namespace sensor
47 } // namespace ipmi
48 
49 #endif
50 
51 boost::container::flat_map<
52     const char*, std::pair<SensorTypeCodes, SensorEventTypeCodes>, CmpStr>
53     sensorTypes{
54         {{"temperature", std::make_pair(SensorTypeCodes::temperature,
55                                         SensorEventTypeCodes::threshold)},
56          {"voltage", std::make_pair(SensorTypeCodes::voltage,
57                                     SensorEventTypeCodes::threshold)},
58          {"current", std::make_pair(SensorTypeCodes::current,
59                                     SensorEventTypeCodes::threshold)},
60          {"fan_tach", std::make_pair(SensorTypeCodes::fan,
61                                      SensorEventTypeCodes::threshold)},
62          {"fan_pwm", std::make_pair(SensorTypeCodes::fan,
63                                     SensorEventTypeCodes::threshold)},
64          {"intrusion", std::make_pair(SensorTypeCodes::physicalSecurity,
65                                       SensorEventTypeCodes::sensorSpecified)},
66          {"processor", std::make_pair(SensorTypeCodes::processor,
67                                       SensorEventTypeCodes::sensorSpecified)},
68          {"power", std::make_pair(SensorTypeCodes::other,
69                                   SensorEventTypeCodes::threshold)},
70          {"memory", std::make_pair(SensorTypeCodes::memory,
71                                    SensorEventTypeCodes::sensorSpecified)},
72          {"state", std::make_pair(SensorTypeCodes::powerUnit,
73                                   SensorEventTypeCodes::sensorSpecified)},
74          {"buttons", std::make_pair(SensorTypeCodes::buttons,
75                                     SensorEventTypeCodes::sensorSpecified)},
76          {"watchdog", std::make_pair(SensorTypeCodes::watchdog2,
77                                      SensorEventTypeCodes::sensorSpecified)},
78          {"entity", std::make_pair(SensorTypeCodes::entity,
79                                    SensorEventTypeCodes::sensorSpecified)},
80          {"energy", std::make_pair(SensorTypeCodes::other,
81                                    SensorEventTypeCodes::threshold)}}};
82 
83 namespace details
84 {
85 
86 // IPMI supports a smaller number of sensors than are available via Redfish.
87 // Trim the list of sensors, via a configuration file.
88 // Read the IPMI Sensor Filtering section in docs/configuration.md for
89 // a more detailed description.
filterSensors(SensorSubTree & subtree)90 static void filterSensors(SensorSubTree& subtree)
91 {
92     constexpr const char* filterFilename =
93         "/usr/share/ipmi-providers/sensor_filter.json";
94     std::ifstream filterFile(filterFilename);
95     if (!filterFile.good())
96     {
97         return;
98     }
99     nlohmann::json sensorFilterJSON =
100         nlohmann::json::parse(filterFile, nullptr, false);
101     nlohmann::json::iterator svcFilterit =
102         sensorFilterJSON.find("ServiceFilter");
103     if (svcFilterit == sensorFilterJSON.end())
104     {
105         return;
106     }
107 
108     subtree.erase(std::remove_if(subtree.begin(), subtree.end(),
109                                  [svcFilterit](SensorSubTree::value_type& kv) {
110                                      auto& [_, serviceToIfaces] = kv;
111 
112                                      for (auto service = svcFilterit->begin();
113                                           service != svcFilterit->end();
114                                           ++service)
115                                      {
116                                          serviceToIfaces.erase(*service);
117                                      }
118                                      return serviceToIfaces.empty();
119                                  }),
120                   subtree.end());
121 }
122 
getSensorSubtree(std::shared_ptr<SensorSubTree> & subtree)123 uint16_t getSensorSubtree(std::shared_ptr<SensorSubTree>& subtree)
124 {
125     static std::shared_ptr<SensorSubTree> sensorTreePtr;
126     static uint16_t sensorUpdatedIndex = 0;
127     std::shared_ptr<sdbusplus::asio::connection> dbus = getSdBus();
128     static sdbusplus::bus::match_t sensorAdded(
129         *dbus,
130         "type='signal',member='InterfacesAdded',arg0path='/xyz/openbmc_project/"
131         "sensors/'",
132         [](sdbusplus::message_t&) { sensorTreePtr.reset(); });
133 
134     static sdbusplus::bus::match_t sensorRemoved(
135         *dbus,
136         "type='signal',member='InterfacesRemoved',arg0path='/xyz/"
137         "openbmc_project/sensors/'",
138         [](sdbusplus::message_t&) { sensorTreePtr.reset(); });
139 
140     if (sensorTreePtr)
141     {
142         subtree = sensorTreePtr;
143         return sensorUpdatedIndex;
144     }
145 
146     sensorTreePtr = std::make_shared<SensorSubTree>();
147 
148     static constexpr const int32_t depth = 2;
149 
150     auto lbdUpdateSensorTree = [&dbus](const char* path,
151                                        const auto& interfaces) {
152         auto mapperCall = dbus->new_method_call(
153             ObjectMapper::default_service, ObjectMapper::instance_path,
154             ObjectMapper::interface, ObjectMapper::method_names::get_sub_tree);
155         SensorSubTree sensorTreePartial;
156 
157         mapperCall.append(path, depth, interfaces);
158 
159         try
160         {
161             auto mapperReply = dbus->call(mapperCall);
162             mapperReply.read(sensorTreePartial);
163         }
164         catch (const sdbusplus::exception_t& e)
165         {
166             lg2::error("Failed to update subtree, path: {PATH}, error: {ERROR}",
167                        "PATH", path, "ERROR", e);
168             return false;
169         }
170         if constexpr (debug)
171         {
172             std::fprintf(stderr, "IPMI updated: %zu sensors under %s\n",
173                          sensorTreePartial.size(), path);
174         }
175         sensorTreePtr->merge(std::move(sensorTreePartial));
176         return true;
177     };
178 
179     // Add sensors to SensorTree
180     static constexpr const std::array sensorInterfaces = {
181         SensorValue::interface, "xyz.openbmc_project.Sensor.ValueMutability",
182         SensorThresholdWarning::interface, SensorThresholdCritical::interface};
183     static constexpr const std::array vrInterfaces = {
184         "xyz.openbmc_project.Control.VoltageRegulatorMode"};
185 
186     bool sensorRez =
187         lbdUpdateSensorTree("/xyz/openbmc_project/sensors", sensorInterfaces);
188 
189 #ifdef FEATURE_HYBRID_SENSORS
190 
191     if (!ipmi::sensor::sensors.empty())
192     {
193         for (const auto& sensor : ipmi::sensor::sensors)
194         {
195             // Threshold sensors should not be emplaced in here.
196             if (sensor.second.sensorPath.starts_with(
197                     "/xyz/openbmc_project/sensors/"))
198             {
199                 continue;
200             }
201 
202             // The bus service name is not listed in ipmi::sensor::Info. Give it
203             // an empty string. For those function using non-threshold sensors,
204             // the bus service name will be retrieved in an alternative way.
205             boost::container::flat_map<std::string, std::vector<std::string>>
206                 connectionMap{
207                     {"", {sensor.second.propertyInterfaces.begin()->first}}};
208             sensorTreePtr->emplace(sensor.second.sensorPath, connectionMap);
209         }
210     }
211 
212 #endif
213 
214     // Error if searching for sensors failed.
215     if (!sensorRez)
216     {
217         return sensorUpdatedIndex;
218     }
219 
220     filterSensors(*sensorTreePtr);
221     // Add VR control as optional search path.
222     (void)lbdUpdateSensorTree("/xyz/openbmc_project/vr", vrInterfaces);
223 
224     subtree = sensorTreePtr;
225     sensorUpdatedIndex++;
226     // The SDR is being regenerated, wipe the old stats
227     sdrStatsTable.wipeTable();
228     sdrWriteTable.wipeTable();
229     return sensorUpdatedIndex;
230 }
231 
getSensorNumMap(std::shared_ptr<SensorNumMap> & sensorNumMap)232 bool getSensorNumMap(std::shared_ptr<SensorNumMap>& sensorNumMap)
233 {
234     static std::shared_ptr<SensorNumMap> sensorNumMapPtr;
235     bool sensorNumMapUpated = false;
236     static uint16_t prevSensorUpdatedIndex = 0;
237     std::shared_ptr<SensorSubTree> sensorTree;
238     uint16_t curSensorUpdatedIndex = details::getSensorSubtree(sensorTree);
239     if (!sensorTree)
240     {
241         return sensorNumMapUpated;
242     }
243 
244     if ((curSensorUpdatedIndex == prevSensorUpdatedIndex) && sensorNumMapPtr)
245     {
246         sensorNumMap = sensorNumMapPtr;
247         return sensorNumMapUpated;
248     }
249     prevSensorUpdatedIndex = curSensorUpdatedIndex;
250 
251     sensorNumMapPtr = std::make_shared<SensorNumMap>();
252 
253     uint16_t sensorNum = 0;
254     uint16_t sensorIndex = 0;
255     for (const auto& sensor : *sensorTree)
256     {
257         sensorNumMapPtr->insert(
258             SensorNumMap::value_type(sensorNum, sensor.first));
259         sensorIndex++;
260         if (sensorIndex == maxSensorsPerLUN)
261         {
262             sensorIndex = lun1Sensor0;
263         }
264         else if (sensorIndex == (lun1Sensor0 | maxSensorsPerLUN))
265         {
266             // Skip assigning LUN 0x2 any sensors
267             sensorIndex = lun3Sensor0;
268         }
269         else if (sensorIndex == (lun3Sensor0 | maxSensorsPerLUN))
270         {
271             // this is an error, too many IPMI sensors
272             throw std::out_of_range("Maximum number of IPMI sensors exceeded.");
273         }
274         sensorNum = sensorIndex;
275     }
276     sensorNumMap = sensorNumMapPtr;
277     sensorNumMapUpated = true;
278     return sensorNumMapUpated;
279 }
280 } // namespace details
281 
getSensorSubtree(SensorSubTree & subtree)282 bool getSensorSubtree(SensorSubTree& subtree)
283 {
284     std::shared_ptr<SensorSubTree> sensorTree;
285     details::getSensorSubtree(sensorTree);
286     if (!sensorTree)
287     {
288         return false;
289     }
290 
291     subtree = *sensorTree;
292     return true;
293 }
294 
295 #ifdef FEATURE_HYBRID_SENSORS
296 // Static sensors are listed in sensor-gen.cpp.
findStaticSensor(const std::string & path)297 ipmi::sensor::IdInfoMap::const_iterator findStaticSensor(
298     const std::string& path)
299 {
300     return std::find_if(
301         ipmi::sensor::sensors.begin(), ipmi::sensor::sensors.end(),
302         [&path](const ipmi::sensor::IdInfoMap::value_type& findSensor) {
303             return findSensor.second.sensorPath == path;
304         });
305 }
306 #endif
307 
getSensorTypeStringFromPath(const std::string & path)308 std::string getSensorTypeStringFromPath(const std::string& path)
309 {
310     // get sensor type string from path, path is defined as
311     // /xyz/openbmc_project/sensors/<type>/label
312     size_t typeEnd = path.rfind("/");
313     if (typeEnd == std::string::npos)
314     {
315         return path;
316     }
317     size_t typeStart = path.rfind("/", typeEnd - 1);
318     if (typeStart == std::string::npos)
319     {
320         return path;
321     }
322     // Start at the character after the '/'
323     typeStart++;
324     return path.substr(typeStart, typeEnd - typeStart);
325 }
326 
getSensorTypeFromPath(const std::string & path)327 uint8_t getSensorTypeFromPath(const std::string& path)
328 {
329     uint8_t sensorType = 0;
330     std::string type = getSensorTypeStringFromPath(path);
331     auto findSensor = sensorTypes.find(type.c_str());
332     if (findSensor != sensorTypes.end())
333     {
334         sensorType =
335             static_cast<uint8_t>(std::get<sensorTypeCodes>(findSensor->second));
336     } // else default 0x0 RESERVED
337 
338     return sensorType;
339 }
340 
getSensorNumberFromPath(const std::string & path)341 uint16_t getSensorNumberFromPath(const std::string& path)
342 {
343     std::shared_ptr<SensorNumMap> sensorNumMapPtr;
344     details::getSensorNumMap(sensorNumMapPtr);
345     if (!sensorNumMapPtr)
346     {
347         return invalidSensorNumber;
348     }
349 
350     try
351     {
352         return sensorNumMapPtr->right.at(path);
353     }
354     catch (const std::out_of_range& e)
355     {
356         return invalidSensorNumber;
357     }
358 }
359 
getSensorEventTypeFromPath(const std::string & path)360 uint8_t getSensorEventTypeFromPath(const std::string& path)
361 {
362     uint8_t sensorEventType = 0;
363     std::string type = getSensorTypeStringFromPath(path);
364     auto findSensor = sensorTypes.find(type.c_str());
365     if (findSensor != sensorTypes.end())
366     {
367         sensorEventType = static_cast<uint8_t>(
368             std::get<sensorEventTypeCodes>(findSensor->second));
369     }
370 
371     return sensorEventType;
372 }
373 
getPathFromSensorNumber(uint16_t sensorNum)374 std::string getPathFromSensorNumber(uint16_t sensorNum)
375 {
376     std::shared_ptr<SensorNumMap> sensorNumMapPtr;
377     details::getSensorNumMap(sensorNumMapPtr);
378     if (!sensorNumMapPtr)
379     {
380         return std::string();
381     }
382 
383     try
384     {
385         return sensorNumMapPtr->left.at(sensorNum);
386     }
387     catch (const std::out_of_range& e)
388     {
389         return std::string();
390     }
391 }
392 
393 namespace ipmi
394 {
395 
396 std::optional<std::map<std::string, std::vector<std::string>>>
getObjectInterfaces(const char * path)397     getObjectInterfaces(const char* path)
398 {
399     std::map<std::string, std::vector<std::string>> interfacesResponse;
400     std::vector<std::string> interfaces;
401     std::shared_ptr<sdbusplus::asio::connection> dbus = getSdBus();
402 
403     sdbusplus::message_t getObjectMessage = dbus->new_method_call(
404         ObjectMapper::default_service, ObjectMapper::instance_path,
405         ObjectMapper::interface, ObjectMapper::method_names::get_object);
406     getObjectMessage.append(path, interfaces);
407 
408     try
409     {
410         sdbusplus::message_t response = dbus->call(getObjectMessage);
411         response.read(interfacesResponse);
412     }
413     catch (const std::exception& e)
414     {
415         return std::nullopt;
416     }
417 
418     return interfacesResponse;
419 }
420 
getEntityManagerProperties(const char * path,const char * interface)421 std::map<std::string, Value> getEntityManagerProperties(const char* path,
422                                                         const char* interface)
423 {
424     std::map<std::string, Value> properties;
425     std::shared_ptr<sdbusplus::asio::connection> dbus = getSdBus();
426 
427     sdbusplus::message_t getProperties =
428         dbus->new_method_call("xyz.openbmc_project.EntityManager", path,
429                               "org.freedesktop.DBus.Properties", "GetAll");
430     getProperties.append(interface);
431 
432     try
433     {
434         sdbusplus::message_t response = dbus->call(getProperties);
435         response.read(properties);
436     }
437     catch (const std::exception& e)
438     {
439         lg2::error("Failed to GetAll, path: {PATH}, interface: {INTERFACE}, "
440                    "error: {ERROR}",
441                    "PATH", path, "INTERFACE", interface, "ERROR", e);
442     }
443 
444     return properties;
445 }
446 
447 // Fetch the ipmiDecoratorPaths to get the list of dbus objects that
448 // have ipmi decorator to prevent unnessary dbus call to fetch the info
getIpmiDecoratorPaths(const std::optional<ipmi::Context::ptr> & ctx)449 std::optional<std::unordered_set<std::string>>& getIpmiDecoratorPaths(
450     const std::optional<ipmi::Context::ptr>& ctx)
451 {
452     static std::optional<std::unordered_set<std::string>> ipmiDecoratorPaths;
453 
454     if (!ctx.has_value() || ipmiDecoratorPaths != std::nullopt)
455     {
456         return ipmiDecoratorPaths;
457     }
458 
459     using Paths = std::vector<std::string>;
460     boost::system::error_code ec;
461     Paths paths = ipmi::callDbusMethod<Paths>(
462         *ctx, ec, ObjectMapper::default_service, ObjectMapper::instance_path,
463         ObjectMapper::interface, ObjectMapper::method_names::get_sub_tree_paths,
464         "/", int32_t(0),
465         std::array<const char*, 1>{
466             "xyz.openbmc_project.Inventory.Decorator.Ipmi"});
467 
468     if (ec)
469     {
470         return ipmiDecoratorPaths;
471     }
472 
473     ipmiDecoratorPaths =
474         std::unordered_set<std::string>(paths.begin(), paths.end());
475     return ipmiDecoratorPaths;
476 }
477 
getSensorConfigurationInterface(const std::map<std::string,std::vector<std::string>> & sensorInterfacesResponse)478 const std::string* getSensorConfigurationInterface(
479     const std::map<std::string, std::vector<std::string>>&
480         sensorInterfacesResponse)
481 {
482     auto entityManagerService =
483         sensorInterfacesResponse.find("xyz.openbmc_project.EntityManager");
484     if (entityManagerService == sensorInterfacesResponse.end())
485     {
486         return nullptr;
487     }
488 
489     // Find the fan configuration first (fans can have multiple configuration
490     // interfaces).
491     for (const auto& entry : entityManagerService->second)
492     {
493         if (entry == "xyz.openbmc_project.Configuration.AspeedFan" ||
494             entry == "xyz.openbmc_project.Configuration.I2CFan" ||
495             entry == "xyz.openbmc_project.Configuration.NuvotonFan")
496         {
497             return &entry;
498         }
499     }
500 
501     for (const auto& entry : entityManagerService->second)
502     {
503         if (entry.starts_with("xyz.openbmc_project.Configuration."))
504         {
505             return &entry;
506         }
507     }
508 
509     return nullptr;
510 }
511 
512 // Follow Association properties for Sensor back to the Board dbus object to
513 // check for an EntityId and EntityInstance property.
updateIpmiFromAssociation(const std::string & path,const std::unordered_set<std::string> & ipmiDecoratorPaths,const DbusInterfaceMap & sensorMap,uint8_t & entityId,uint8_t & entityInstance)514 void updateIpmiFromAssociation(
515     const std::string& path,
516     const std::unordered_set<std::string>& ipmiDecoratorPaths,
517     const DbusInterfaceMap& sensorMap, uint8_t& entityId,
518     uint8_t& entityInstance)
519 {
520     namespace fs = std::filesystem;
521 
522     auto sensorAssociationObject =
523         sensorMap.find("xyz.openbmc_project.Association.Definitions");
524     if (sensorAssociationObject == sensorMap.end())
525     {
526         if constexpr (debug)
527         {
528             std::fprintf(stderr, "path=%s, no association interface found\n",
529                          path.c_str());
530         }
531 
532         return;
533     }
534 
535     auto associationObject =
536         sensorAssociationObject->second.find("Associations");
537     if (associationObject == sensorAssociationObject->second.end())
538     {
539         if constexpr (debug)
540         {
541             std::fprintf(stderr, "path=%s, no association records found\n",
542                          path.c_str());
543         }
544 
545         return;
546     }
547 
548     std::vector<Association> associationValues =
549         std::get<std::vector<Association>>(associationObject->second);
550 
551     // loop through the Associations looking for the right one:
552     for (const auto& entry : associationValues)
553     {
554         // forward, reverse, endpoint
555         const std::string& forward = std::get<0>(entry);
556         const std::string& reverse = std::get<1>(entry);
557         const std::string& endpoint = std::get<2>(entry);
558 
559         // We only currently concern ourselves with chassis+all_sensors.
560         if (!(forward == "chassis" && reverse == "all_sensors"))
561         {
562             continue;
563         }
564 
565         // the endpoint is the board entry provided by
566         // Entity-Manager. so let's grab its properties if it has
567         // the right interface.
568 
569         // just try grabbing the properties first.
570         ipmi::PropertyMap::iterator entityIdProp;
571         ipmi::PropertyMap::iterator entityInstanceProp;
572         if (ipmiDecoratorPaths.contains(endpoint))
573         {
574             std::map<std::string, Value> ipmiProperties =
575                 getEntityManagerProperties(
576                     endpoint.c_str(),
577                     "xyz.openbmc_project.Inventory.Decorator.Ipmi");
578 
579             entityIdProp = ipmiProperties.find("EntityId");
580             entityInstanceProp = ipmiProperties.find("EntityInstance");
581             if (entityIdProp != ipmiProperties.end())
582             {
583                 entityId = static_cast<uint8_t>(
584                     std::get<uint64_t>(entityIdProp->second));
585             }
586             if (entityInstanceProp != ipmiProperties.end())
587             {
588                 entityInstance = static_cast<uint8_t>(
589                     std::get<uint64_t>(entityInstanceProp->second));
590             }
591         }
592 
593         // Now check the entity-manager entry for this sensor to see
594         // if it has its own value and use that instead.
595         //
596         // In theory, checking this first saves us from checking
597         // both, except in most use-cases identified, there won't be
598         // a per sensor override, so we need to always check both.
599         std::string sensorNameFromPath = fs::path(path).filename();
600 
601         std::string sensorConfigPath = endpoint + "/" + sensorNameFromPath;
602 
603         // Download the interfaces for the sensor from
604         // Entity-Manager to find the name of the configuration
605         // interface.
606         std::optional<std::map<std::string, std::vector<std::string>>>
607             sensorInterfacesResponseOpt =
608                 getObjectInterfaces(sensorConfigPath.c_str());
609 
610         if (!sensorInterfacesResponseOpt.has_value())
611         {
612             lg2::debug("Failed to GetObject, path: {PATH}", "PATH",
613                        sensorConfigPath);
614             continue;
615         }
616 
617         const std::string* configurationInterface =
618             getSensorConfigurationInterface(
619                 sensorInterfacesResponseOpt.value());
620 
621         // If there are multi association path settings and only one path exist,
622         // we need to continue if cannot find configuration interface for this
623         // sensor.
624         if (!configurationInterface)
625         {
626             continue;
627         }
628 
629         // We found a configuration interface.
630         std::map<std::string, Value> configurationProperties =
631             getEntityManagerProperties(sensorConfigPath.c_str(),
632                                        configurationInterface->c_str());
633 
634         entityIdProp = configurationProperties.find("EntityId");
635         entityInstanceProp = configurationProperties.find("EntityInstance");
636         if (entityIdProp != configurationProperties.end())
637         {
638             entityId =
639                 static_cast<uint8_t>(std::get<uint64_t>(entityIdProp->second));
640         }
641         if (entityInstanceProp != configurationProperties.end())
642         {
643             entityInstance = static_cast<uint8_t>(
644                 std::get<uint64_t>(entityInstanceProp->second));
645         }
646 
647         // stop searching Association records.
648         break;
649     } // end for Association vectors.
650 
651     if constexpr (debug)
652     {
653         std::fprintf(stderr, "path=%s, entityId=%d, entityInstance=%d\n",
654                      path.c_str(), entityId, entityInstance);
655     }
656 }
657 
658 } // namespace ipmi
659