xref: /openbmc/entity-manager/src/perform_scan.cpp (revision a41f014cbec2e48be2ca9debaf48b97dcc87b210)
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
50      getInterfaces(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