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