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