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