xref: /openbmc/entity-manager/src/entity_manager/perform_scan.cpp (revision dbf95b2c54c5a40d1ea44d650eb6aab2a4c34ba5)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright 2018 Intel Corporation
3 
4 #include "perform_scan.hpp"
5 
6 #include "perform_probe.hpp"
7 #include "utils.hpp"
8 
9 #include <boost/asio/steady_timer.hpp>
10 #include <phosphor-logging/lg2.hpp>
11 
12 #include <charconv>
13 #include <flat_map>
14 #include <flat_set>
15 
16 using GetSubTreeType = std::vector<
17     std::pair<std::string,
18               std::vector<std::pair<std::string, std::vector<std::string>>>>>;
19 
20 constexpr const int32_t maxMapperDepth = 0;
21 
22 struct DBusInterfaceInstance
23 {
24     std::string busName;
25     std::string path;
26     std::string interface;
27 };
28 
29 void getInterfaces(
30     const DBusInterfaceInstance& instance,
31     const std::vector<std::shared_ptr<probe::PerformProbe>>& probeVector,
32     const std::shared_ptr<scan::PerformScan>& scan, boost::asio::io_context& io,
33     size_t retries = 5)
34 {
35     if (retries == 0U)
36     {
37         lg2::error("retries exhausted on {BUSNAME} {PATH} {INTF}", "BUSNAME",
38                    instance.busName, "PATH", instance.path, "INTF",
39                    instance.interface);
40         return;
41     }
42 
43     scan->_em.systemBus->async_method_call(
44         [instance, scan, probeVector, retries,
45          &io](boost::system::error_code& errc,
46               const DBusInterface& resp) mutable {
47             if (errc)
48             {
49                 lg2::error("error calling getall on {BUSNAME} {PATH} {INTF}",
50                            "BUSNAME", instance.busName, "PATH", instance.path,
51                            "INTF", instance.interface);
52 
53                 auto timer = std::make_shared<boost::asio::steady_timer>(io);
54                 timer->expires_after(std::chrono::seconds(2));
55 
56                 timer->async_wait([timer, instance, scan, probeVector, retries,
57                                    &io](const boost::system::error_code&) {
58                     getInterfaces(instance, probeVector, scan, io, retries - 1);
59                 });
60                 return;
61             }
62 
63             scan->dbusProbeObjects[std::string(instance.path)]
64                                   [std::string(instance.interface)] = resp;
65         },
66         instance.busName, instance.path, "org.freedesktop.DBus.Properties",
67         "GetAll", instance.interface);
68 }
69 
70 static void processDbusObjects(
71     std::vector<std::shared_ptr<probe::PerformProbe>>& probeVector,
72     const std::shared_ptr<scan::PerformScan>& scan,
73     const GetSubTreeType& interfaceSubtree, boost::asio::io_context& io)
74 {
75     for (const auto& [path, object] : interfaceSubtree)
76     {
77         // Get a PropertiesChanged callback for all interfaces on this path.
78         scan->_em.registerCallback(path);
79 
80         for (const auto& [busname, ifaces] : object)
81         {
82             for (const std::string& iface : ifaces)
83             {
84                 // The 3 default org.freedeskstop interfaces (Peer,
85                 // Introspectable, and Properties) are returned by
86                 // the mapper but don't have properties, so don't bother
87                 // with the GetAll call to save some cycles.
88                 if (!iface.starts_with("org.freedesktop"))
89                 {
90                     getInterfaces({busname, path, iface}, probeVector, scan,
91                                   io);
92                 }
93             }
94         }
95     }
96 }
97 
98 // Populates scan->dbusProbeObjects with all interfaces and properties
99 // for the paths that own the interfaces passed in.
100 void findDbusObjects(
101     std::vector<std::shared_ptr<probe::PerformProbe>>&& probeVector,
102     std::flat_set<std::string, std::less<>>&& interfaces,
103     const std::shared_ptr<scan::PerformScan>& scan, boost::asio::io_context& io,
104     size_t retries = 5)
105 {
106     // Filter out interfaces already obtained.
107     for (const auto& [path, probeInterfaces] : scan->dbusProbeObjects)
108     {
109         for (const auto& [interface, _] : probeInterfaces)
110         {
111             interfaces.erase(interface);
112         }
113     }
114     if (interfaces.empty())
115     {
116         return;
117     }
118 
119     // find all connections in the mapper that expose a specific type
120     scan->_em.systemBus->async_method_call(
121         [interfaces, probeVector{std::move(probeVector)}, scan, retries,
122          &io](boost::system::error_code& ec,
123               const GetSubTreeType& interfaceSubtree) mutable {
124             if (ec)
125             {
126                 if (ec.value() == ENOENT)
127                 {
128                     return; // wasn't found by mapper
129                 }
130                 lg2::error("Error communicating to mapper.");
131 
132                 if (retries == 0U)
133                 {
134                     // if we can't communicate to the mapper something is very
135                     // wrong
136                     std::exit(EXIT_FAILURE);
137                 }
138 
139                 auto timer = std::make_shared<boost::asio::steady_timer>(io);
140                 timer->expires_after(std::chrono::seconds(10));
141 
142                 timer->async_wait(
143                     [timer, interfaces{std::move(interfaces)}, scan,
144                      probeVector{std::move(probeVector)}, retries,
145                      &io](const boost::system::error_code&) mutable {
146                         findDbusObjects(std::move(probeVector),
147                                         std::move(interfaces), scan, io,
148                                         retries - 1);
149                     });
150                 return;
151             }
152 
153             processDbusObjects(probeVector, scan, interfaceSubtree, io);
154         },
155         "xyz.openbmc_project.ObjectMapper",
156         "/xyz/openbmc_project/object_mapper",
157         "xyz.openbmc_project.ObjectMapper", "GetSubTree", "/", maxMapperDepth,
158         interfaces);
159 }
160 
161 static std::string getRecordName(const DBusInterface& probe,
162                                  const std::string& probeName)
163 {
164     if (probe.empty())
165     {
166         return probeName;
167     }
168 
169     // use an array so alphabetical order from the flat_map is maintained
170     auto device = nlohmann::json::array();
171     for (const auto& devPair : probe)
172     {
173         device.push_back(devPair.first);
174         std::visit([&device](auto&& v) { device.push_back(v); },
175                    devPair.second);
176     }
177 
178     // hashes are hard to distinguish, use the non-hashed version if we want
179     // debug
180     // return probeName + device.dump();
181 
182     return std::to_string(std::hash<std::string>{}(probeName + device.dump()));
183 }
184 
185 scan::PerformScan::PerformScan(
186     EntityManager& em, nlohmann::json& missingConfigurations,
187     std::vector<nlohmann::json>& configurations, boost::asio::io_context& io,
188     std::function<void()>&& callback) :
189     _em(em), _missingConfigurations(missingConfigurations),
190     _configurations(configurations), _callback(std::move(callback)), io(io)
191 {}
192 
193 static void pruneRecordExposes(nlohmann::json& record)
194 {
195     auto findExposes = record.find("Exposes");
196     if (findExposes == record.end())
197     {
198         return;
199     }
200 
201     auto copy = nlohmann::json::array();
202     for (auto& expose : *findExposes)
203     {
204         if (!expose.is_null())
205         {
206             copy.emplace_back(expose);
207         }
208     }
209     *findExposes = copy;
210 }
211 
212 static void recordDiscoveredIdentifiers(
213     std::set<nlohmann::json>& usedNames, std::list<size_t>& indexes,
214     const std::string& probeName, const nlohmann::json& record)
215 {
216     size_t indexIdx = probeName.find('$');
217     if (indexIdx == std::string::npos)
218     {
219         return;
220     }
221 
222     auto nameIt = record.find("Name");
223     if (nameIt == record.end())
224     {
225         lg2::error("Last JSON Illegal");
226         return;
227     }
228 
229     int index = 0;
230     auto str = nameIt->get<std::string>().substr(indexIdx);
231     // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
232     const char* endPtr = str.data() + str.size();
233     auto [p, ec] = std::from_chars(str.data(), endPtr, index);
234     if (ec != std::errc())
235     {
236         return; // non-numeric replacement
237     }
238 
239     usedNames.insert(nameIt.value());
240 
241     auto usedIt = std::find(indexes.begin(), indexes.end(), index);
242     if (usedIt != indexes.end())
243     {
244         indexes.erase(usedIt);
245     }
246 }
247 
248 static bool extractExposeActionRecordNames(std::vector<std::string>& matches,
249                                            nlohmann::json::iterator& keyPair)
250 {
251     if (keyPair.value().is_string())
252     {
253         matches.emplace_back(keyPair.value());
254         return true;
255     }
256 
257     if (keyPair.value().is_array())
258     {
259         for (const auto& value : keyPair.value())
260         {
261             if (!value.is_string())
262             {
263                 lg2::error("Value is invalid type {VALUE}", "VALUE", value);
264                 break;
265             }
266             matches.emplace_back(value);
267         }
268 
269         return true;
270     }
271 
272     lg2::error("Value is invalid type {KEY}", "KEY", keyPair.key());
273 
274     return false;
275 }
276 
277 static std::optional<std::vector<std::string>::iterator> findExposeActionRecord(
278     std::vector<std::string>& matches, const nlohmann::json& record)
279 {
280     const auto& name = (record)["Name"].get_ref<const std::string&>();
281     auto compare = [&name](const std::string& s) { return s == name; };
282     auto matchIt = std::find_if(matches.begin(), matches.end(), compare);
283 
284     if (matchIt == matches.end())
285     {
286         return std::nullopt;
287     }
288 
289     return matchIt;
290 }
291 
292 static void applyBindExposeAction(nlohmann::json& exposedObject,
293                                   nlohmann::json& expose,
294                                   const std::string& propertyName)
295 {
296     if (propertyName.starts_with("Bind"))
297     {
298         std::string bind = propertyName.substr(sizeof("Bind") - 1);
299         exposedObject["Status"] = "okay";
300         expose[bind] = exposedObject;
301     }
302 }
303 
304 static void applyDisableExposeAction(nlohmann::json& exposedObject,
305                                      const std::string& propertyName)
306 {
307     if (propertyName == "DisableNode")
308     {
309         exposedObject["Status"] = "disabled";
310     }
311 }
312 
313 static void applyConfigExposeActions(
314     std::vector<std::string>& matches, nlohmann::json& expose,
315     const std::string& propertyName, nlohmann::json& configExposes)
316 {
317     for (auto& exposedObject : configExposes)
318     {
319         auto match = findExposeActionRecord(matches, exposedObject);
320         if (match)
321         {
322             matches.erase(*match);
323             applyBindExposeAction(exposedObject, expose, propertyName);
324             applyDisableExposeAction(exposedObject, propertyName);
325         }
326     }
327 }
328 
329 static void applyExposeActions(
330     nlohmann::json& systemConfiguration, const std::string& recordName,
331     nlohmann::json& expose, nlohmann::json::iterator& keyPair)
332 {
333     bool isBind = keyPair.key().starts_with("Bind");
334     bool isDisable = keyPair.key() == "DisableNode";
335     bool isExposeAction = isBind || isDisable;
336 
337     if (!isExposeAction)
338     {
339         return;
340     }
341 
342     std::vector<std::string> matches;
343 
344     if (!extractExposeActionRecordNames(matches, keyPair))
345     {
346         return;
347     }
348 
349     for (const auto& [configId, config] : systemConfiguration.items())
350     {
351         // don't disable ourselves
352         if (isDisable && configId == recordName)
353         {
354             continue;
355         }
356 
357         auto configListFind = config.find("Exposes");
358         if (configListFind == config.end())
359         {
360             continue;
361         }
362 
363         if (!configListFind->is_array())
364         {
365             continue;
366         }
367 
368         applyConfigExposeActions(matches, expose, keyPair.key(),
369                                  *configListFind);
370     }
371 
372     if (!matches.empty())
373     {
374         lg2::error(
375             "configuration file dependency error, could not find {KEY} {VALUE}",
376             "KEY", keyPair.key(), "VALUE", keyPair.value());
377     }
378 }
379 
380 static std::string generateDeviceName(
381     const std::set<nlohmann::json>& usedNames, const DBusObject& dbusObject,
382     size_t foundDeviceIdx, const std::string& nameTemplate,
383     std::optional<std::string>& replaceStr)
384 {
385     nlohmann::json copyForName = {{"Name", nameTemplate}};
386     nlohmann::json::iterator copyIt = copyForName.begin();
387     std::optional<std::string> replaceVal = em_utils::templateCharReplace(
388         copyIt, dbusObject, foundDeviceIdx, replaceStr);
389 
390     if (!replaceStr && replaceVal)
391     {
392         if (usedNames.contains(copyIt.value()))
393         {
394             replaceStr = replaceVal;
395             copyForName = {{"Name", nameTemplate}};
396             copyIt = copyForName.begin();
397             em_utils::templateCharReplace(copyIt, dbusObject, foundDeviceIdx,
398                                           replaceStr);
399         }
400     }
401 
402     if (replaceStr)
403     {
404         lg2::error(
405             "Duplicates found, replacing {STR} with found device index. Consider fixing template to not have duplicates",
406             "STR", *replaceStr);
407     }
408 
409     return copyIt.value();
410 }
411 
412 void scan::PerformScan::updateSystemConfiguration(
413     const nlohmann::json& recordRef, const std::string& probeName,
414     FoundDevices& foundDevices)
415 {
416     _passed = true;
417     passedProbes.push_back(probeName);
418 
419     std::set<nlohmann::json> usedNames;
420     std::list<size_t> indexes(foundDevices.size());
421     std::iota(indexes.begin(), indexes.end(), 1);
422 
423     // copy over persisted configurations and make sure we remove
424     // indexes that are already used
425     for (auto itr = foundDevices.begin(); itr != foundDevices.end();)
426     {
427         std::string recordName = getRecordName(itr->interface, probeName);
428 
429         auto record = _em.systemConfiguration.find(recordName);
430         if (record == _em.systemConfiguration.end())
431         {
432             record = _em.lastJson.find(recordName);
433             if (record == _em.lastJson.end())
434             {
435                 itr++;
436                 continue;
437             }
438 
439             pruneRecordExposes(*record);
440 
441             _em.systemConfiguration[recordName] = *record;
442         }
443         _missingConfigurations.erase(recordName);
444 
445         // We've processed the device, remove it and advance the
446         // iterator
447         itr = foundDevices.erase(itr);
448         recordDiscoveredIdentifiers(usedNames, indexes, probeName, *record);
449     }
450 
451     std::optional<std::string> replaceStr;
452 
453     DBusObject emptyObject;
454     DBusInterface emptyInterface;
455     emptyObject.emplace(std::string{}, emptyInterface);
456 
457     for (const auto& [foundDevice, path] : foundDevices)
458     {
459         // Need all interfaces on this path so that template
460         // substitutions can be done with any of the contained
461         // properties.  If the probe that passed didn't use an
462         // interface, such as if it was just TRUE, then
463         // templateCharReplace will just get passed in an empty
464         // map.
465         auto objectIt = dbusProbeObjects.find(path);
466         const DBusObject& dbusObject = (objectIt == dbusProbeObjects.end())
467                                            ? emptyObject
468                                            : objectIt->second;
469 
470         nlohmann::json record = recordRef;
471         std::string recordName = getRecordName(foundDevice, probeName);
472         size_t foundDeviceIdx = indexes.front();
473         indexes.pop_front();
474 
475         // check name first so we have no duplicate names
476         auto getName = record.find("Name");
477         if (getName == record.end())
478         {
479             lg2::error("Record Missing Name! {JSON}", "JSON", record.dump());
480             continue; // this should be impossible at this level
481         }
482 
483         std::string deviceName = generateDeviceName(
484             usedNames, dbusObject, foundDeviceIdx, getName.value(), replaceStr);
485         getName.value() = deviceName;
486         usedNames.insert(deviceName);
487 
488         for (auto keyPair = record.begin(); keyPair != record.end(); keyPair++)
489         {
490             if (keyPair.key() != "Name")
491             {
492                 // "Probe" string does not contain template variables
493                 // Handle left-over variables for "Exposes" later below
494                 const bool handleLeftOver =
495                     (keyPair.key() != "Probe") && (keyPair.key() != "Exposes");
496                 em_utils::templateCharReplace(keyPair, dbusObject,
497                                               foundDeviceIdx, replaceStr,
498                                               handleLeftOver);
499             }
500         }
501 
502         // insert into configuration temporarily to be able to
503         // reference ourselves
504 
505         _em.systemConfiguration[recordName] = record;
506 
507         auto findExpose = record.find("Exposes");
508         if (findExpose == record.end())
509         {
510             continue;
511         }
512 
513         for (auto& expose : *findExpose)
514         {
515             for (auto keyPair = expose.begin(); keyPair != expose.end();
516                  keyPair++)
517             {
518                 em_utils::templateCharReplace(keyPair, dbusObject,
519                                               foundDeviceIdx, replaceStr);
520 
521                 applyExposeActions(_em.systemConfiguration, recordName, expose,
522                                    keyPair);
523             }
524         }
525 
526         // If we end up here and the path is empty, we have Probe: "True"
527         // and we dont want that to show up in the associations.
528         if (!path.empty())
529         {
530             auto boardType = record.find("Type")->get<std::string>();
531             auto boardName = record.find("Name")->get<std::string>();
532             std::string boardInventoryPath =
533                 em_utils::buildInventorySystemPath(boardName, boardType);
534             _em.topology.addProbePath(boardInventoryPath, path);
535         }
536 
537         // overwrite ourselves with cleaned up version
538         _em.systemConfiguration[recordName] = record;
539         _missingConfigurations.erase(recordName);
540     }
541 }
542 
543 void scan::PerformScan::run()
544 {
545     std::flat_set<std::string, std::less<>> dbusProbeInterfaces;
546     std::vector<std::shared_ptr<probe::PerformProbe>> dbusProbePointers;
547 
548     for (auto it = _configurations.begin(); it != _configurations.end();)
549     {
550         // check for poorly formatted fields, probe must be an array
551         auto findProbe = it->find("Probe");
552         if (findProbe == it->end())
553         {
554             lg2::error("configuration file missing probe:\n {JSON}", "JSON",
555                        *it);
556             it = _configurations.erase(it);
557             continue;
558         }
559 
560         auto findName = it->find("Name");
561         if (findName == it->end())
562         {
563             lg2::error("configuration file missing name:\n {JSON}", "JSON",
564                        *it);
565             it = _configurations.erase(it);
566             continue;
567         }
568         std::string probeName = *findName;
569 
570         if (std::find(passedProbes.begin(), passedProbes.end(), probeName) !=
571             passedProbes.end())
572         {
573             it = _configurations.erase(it);
574             continue;
575         }
576 
577         nlohmann::json& recordRef = *it;
578         nlohmann::json probeCommand;
579         if ((*findProbe).type() != nlohmann::json::value_t::array)
580         {
581             probeCommand = nlohmann::json::array();
582             probeCommand.push_back(*findProbe);
583         }
584         else
585         {
586             probeCommand = *findProbe;
587         }
588 
589         // store reference to this to children to makes sure we don't get
590         // destroyed too early
591         auto thisRef = shared_from_this();
592         auto probePointer = std::make_shared<probe::PerformProbe>(
593             recordRef, probeCommand, probeName, thisRef);
594 
595         // parse out dbus probes by discarding other probe types, store in a
596         // map
597         for (const nlohmann::json& probeJson : probeCommand)
598         {
599             const std::string* probe = probeJson.get_ptr<const std::string*>();
600             if (probe == nullptr)
601             {
602                 lg2::error("Probe statement wasn't a string, can't parse");
603                 continue;
604             }
605             if (probe::findProbeType(*probe))
606             {
607                 continue;
608             }
609             // syntax requires probe before first open brace
610             auto findStart = probe->find('(');
611             std::string interface = probe->substr(0, findStart);
612             dbusProbeInterfaces.emplace(interface);
613             dbusProbePointers.emplace_back(probePointer);
614         }
615         it++;
616     }
617 
618     // probe vector stores a shared_ptr to each PerformProbe that cares
619     // about a dbus interface
620     findDbusObjects(std::move(dbusProbePointers),
621                     std::move(dbusProbeInterfaces), shared_from_this(), io);
622 }
623 
624 scan::PerformScan::~PerformScan()
625 {
626     if (_passed)
627     {
628         auto nextScan = std::make_shared<PerformScan>(
629             _em, _missingConfigurations, _configurations, io,
630             std::move(_callback));
631         nextScan->passedProbes = std::move(passedProbes);
632         nextScan->dbusProbeObjects = std::move(dbusProbeObjects);
633         nextScan->run();
634     }
635     else
636     {
637         _callback();
638     }
639 }
640