xref: /openbmc/entity-manager/src/entity_manager/entity_manager.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 entity_manager.cpp
17 
18 #include "entity_manager.hpp"
19 
20 #include "../utils.hpp"
21 #include "../variant_visitors.hpp"
22 #include "configuration.hpp"
23 #include "dbus_interface.hpp"
24 #include "overlay.hpp"
25 #include "perform_scan.hpp"
26 #include "topology.hpp"
27 
28 #include <boost/algorithm/string/case_conv.hpp>
29 #include <boost/algorithm/string/classification.hpp>
30 #include <boost/algorithm/string/predicate.hpp>
31 #include <boost/algorithm/string/replace.hpp>
32 #include <boost/algorithm/string/split.hpp>
33 #include <boost/asio/io_context.hpp>
34 #include <boost/asio/post.hpp>
35 #include <boost/asio/steady_timer.hpp>
36 #include <boost/container/flat_map.hpp>
37 #include <boost/container/flat_set.hpp>
38 #include <boost/range/iterator_range.hpp>
39 #include <nlohmann/json.hpp>
40 #include <sdbusplus/asio/connection.hpp>
41 #include <sdbusplus/asio/object_server.hpp>
42 
43 #include <charconv>
44 #include <filesystem>
45 #include <fstream>
46 #include <functional>
47 #include <iostream>
48 #include <map>
49 #include <regex>
50 #include <variant>
51 constexpr const char* tempConfigDir = "/tmp/configuration/";
52 constexpr const char* lastConfiguration = "/tmp/configuration/last.json";
53 
54 static constexpr std::array<const char*, 6> settableInterfaces = {
55     "FanProfile", "Pid", "Pid.Zone", "Stepwise", "Thresholds", "Polling"};
56 
57 // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
58 
59 // todo: pass this through nicer
60 std::shared_ptr<sdbusplus::asio::connection> systemBus;
61 nlohmann::json lastJson;
62 Topology topology;
63 
64 boost::asio::io_context io;
65 // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
66 
67 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
68 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
69 
70 sdbusplus::asio::PropertyPermission getPermission(const std::string& interface)
71 {
72     return std::find(settableInterfaces.begin(), settableInterfaces.end(),
73                      interface) != settableInterfaces.end()
74                ? sdbusplus::asio::PropertyPermission::readWrite
75                : sdbusplus::asio::PropertyPermission::readOnly;
76 }
77 
78 void postToDbus(const nlohmann::json& newConfiguration,
79                 nlohmann::json& systemConfiguration,
80                 sdbusplus::asio::object_server& objServer)
81 
82 {
83     std::map<std::string, std::string> newBoards; // path -> name
84 
85     // iterate through boards
86     for (const auto& [boardId, boardConfig] : newConfiguration.items())
87     {
88         std::string boardName = boardConfig["Name"];
89         std::string boardNameOrig = boardConfig["Name"];
90         std::string jsonPointerPath = "/" + boardId;
91         // loop through newConfiguration, but use values from system
92         // configuration to be able to modify via dbus later
93         auto boardValues = systemConfiguration[boardId];
94         auto findBoardType = boardValues.find("Type");
95         std::string boardType;
96         if (findBoardType != boardValues.end() &&
97             findBoardType->type() == nlohmann::json::value_t::string)
98         {
99             boardType = findBoardType->get<std::string>();
100             std::regex_replace(boardType.begin(), boardType.begin(),
101                                boardType.end(), illegalDbusMemberRegex, "_");
102         }
103         else
104         {
105             std::cerr << "Unable to find type for " << boardName
106                       << " reverting to Chassis.\n";
107             boardType = "Chassis";
108         }
109         std::string boardtypeLower = boost::algorithm::to_lower_copy(boardType);
110 
111         std::regex_replace(boardName.begin(), boardName.begin(),
112                            boardName.end(), illegalDbusMemberRegex, "_");
113         std::string boardPath = "/xyz/openbmc_project/inventory/system/";
114         boardPath += boardtypeLower;
115         boardPath += "/";
116         boardPath += boardName;
117 
118         std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface =
119             dbus_interface::createInterface(
120                 objServer, boardPath, "xyz.openbmc_project.Inventory.Item",
121                 boardName);
122 
123         std::shared_ptr<sdbusplus::asio::dbus_interface> boardIface =
124             dbus_interface::createInterface(
125                 objServer, boardPath,
126                 "xyz.openbmc_project.Inventory.Item." + boardType,
127                 boardNameOrig);
128 
129         dbus_interface::createAddObjectMethod(
130             jsonPointerPath, boardPath, systemConfiguration, objServer,
131             boardNameOrig);
132 
133         dbus_interface::populateInterfaceFromJson(
134             systemConfiguration, jsonPointerPath, boardIface, boardValues,
135             objServer);
136         jsonPointerPath += "/";
137         // iterate through board properties
138         for (const auto& [propName, propValue] : boardValues.items())
139         {
140             if (propValue.type() == nlohmann::json::value_t::object)
141             {
142                 std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
143                     dbus_interface::createInterface(objServer, boardPath,
144                                                     propName, boardNameOrig);
145 
146                 dbus_interface::populateInterfaceFromJson(
147                     systemConfiguration, jsonPointerPath + propName, iface,
148                     propValue, objServer);
149             }
150         }
151 
152         auto exposes = boardValues.find("Exposes");
153         if (exposes == boardValues.end())
154         {
155             continue;
156         }
157         // iterate through exposes
158         jsonPointerPath += "Exposes/";
159 
160         // store the board level pointer so we can modify it on the way down
161         std::string jsonPointerPathBoard = jsonPointerPath;
162         size_t exposesIndex = -1;
163         for (auto& item : *exposes)
164         {
165             exposesIndex++;
166             jsonPointerPath = jsonPointerPathBoard;
167             jsonPointerPath += std::to_string(exposesIndex);
168 
169             auto findName = item.find("Name");
170             if (findName == item.end())
171             {
172                 std::cerr << "cannot find name in field " << item << "\n";
173                 continue;
174             }
175             auto findStatus = item.find("Status");
176             // if status is not found it is assumed to be status = 'okay'
177             if (findStatus != item.end())
178             {
179                 if (*findStatus == "disabled")
180                 {
181                     continue;
182                 }
183             }
184             auto findType = item.find("Type");
185             std::string itemType;
186             if (findType != item.end())
187             {
188                 itemType = findType->get<std::string>();
189                 std::regex_replace(itemType.begin(), itemType.begin(),
190                                    itemType.end(), illegalDbusPathRegex, "_");
191             }
192             else
193             {
194                 itemType = "unknown";
195             }
196             std::string itemName = findName->get<std::string>();
197             std::regex_replace(itemName.begin(), itemName.begin(),
198                                itemName.end(), illegalDbusMemberRegex, "_");
199             std::string ifacePath = boardPath;
200             ifacePath += "/";
201             ifacePath += itemName;
202 
203             if (itemType == "BMC")
204             {
205                 std::shared_ptr<sdbusplus::asio::dbus_interface> bmcIface =
206                     dbus_interface::createInterface(
207                         objServer, ifacePath,
208                         "xyz.openbmc_project.Inventory.Item.Bmc",
209                         boardNameOrig);
210                 dbus_interface::populateInterfaceFromJson(
211                     systemConfiguration, jsonPointerPath, bmcIface, item,
212                     objServer, getPermission(itemType));
213             }
214             else if (itemType == "System")
215             {
216                 std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface =
217                     dbus_interface::createInterface(
218                         objServer, ifacePath,
219                         "xyz.openbmc_project.Inventory.Item.System",
220                         boardNameOrig);
221                 dbus_interface::populateInterfaceFromJson(
222                     systemConfiguration, jsonPointerPath, systemIface, item,
223                     objServer, getPermission(itemType));
224             }
225 
226             for (const auto& [name, config] : item.items())
227             {
228                 jsonPointerPath = jsonPointerPathBoard;
229                 jsonPointerPath.append(std::to_string(exposesIndex))
230                     .append("/")
231                     .append(name);
232                 if (config.type() == nlohmann::json::value_t::object)
233                 {
234                     std::string ifaceName =
235                         "xyz.openbmc_project.Configuration.";
236                     ifaceName.append(itemType).append(".").append(name);
237 
238                     std::shared_ptr<sdbusplus::asio::dbus_interface>
239                         objectIface = dbus_interface::createInterface(
240                             objServer, ifacePath, ifaceName, boardNameOrig);
241 
242                     dbus_interface::populateInterfaceFromJson(
243                         systemConfiguration, jsonPointerPath, objectIface,
244                         config, objServer, getPermission(name));
245                 }
246                 else if (config.type() == nlohmann::json::value_t::array)
247                 {
248                     size_t index = 0;
249                     if (config.empty())
250                     {
251                         continue;
252                     }
253                     bool isLegal = true;
254                     auto type = config[0].type();
255                     if (type != nlohmann::json::value_t::object)
256                     {
257                         continue;
258                     }
259 
260                     // verify legal json
261                     for (const auto& arrayItem : config)
262                     {
263                         if (arrayItem.type() != type)
264                         {
265                             isLegal = false;
266                             break;
267                         }
268                     }
269                     if (!isLegal)
270                     {
271                         std::cerr << "dbus format error" << config << "\n";
272                         break;
273                     }
274 
275                     for (auto& arrayItem : config)
276                     {
277                         std::string ifaceName =
278                             "xyz.openbmc_project.Configuration.";
279                         ifaceName.append(itemType).append(".").append(name);
280                         ifaceName.append(std::to_string(index));
281 
282                         std::shared_ptr<sdbusplus::asio::dbus_interface>
283                             objectIface = dbus_interface::createInterface(
284                                 objServer, ifacePath, ifaceName, boardNameOrig);
285 
286                         dbus_interface::populateInterfaceFromJson(
287                             systemConfiguration,
288                             jsonPointerPath + "/" + std::to_string(index),
289                             objectIface, arrayItem, objServer,
290                             getPermission(name));
291                         index++;
292                     }
293                 }
294             }
295 
296             std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface =
297                 dbus_interface::createInterface(
298                     objServer, ifacePath,
299                     "xyz.openbmc_project.Configuration." + itemType,
300                     boardNameOrig);
301 
302             dbus_interface::populateInterfaceFromJson(
303                 systemConfiguration, jsonPointerPath, itemIface, item,
304                 objServer, getPermission(itemType));
305 
306             topology.addBoard(boardPath, boardType, boardNameOrig, item);
307         }
308 
309         newBoards.emplace(boardPath, boardNameOrig);
310     }
311 
312     for (const auto& [assocPath, assocPropValue] :
313          topology.getAssocs(newBoards))
314     {
315         auto findBoard = newBoards.find(assocPath);
316         if (findBoard == newBoards.end())
317         {
318             continue;
319         }
320 
321         auto ifacePtr = dbus_interface::createInterface(
322             objServer, assocPath, "xyz.openbmc_project.Association.Definitions",
323             findBoard->second);
324 
325         ifacePtr->register_property("Associations", assocPropValue);
326         dbus_interface::tryIfaceInitialize(ifacePtr);
327     }
328 }
329 
330 static bool deviceRequiresPowerOn(const nlohmann::json& entity)
331 {
332     auto powerState = entity.find("PowerState");
333     if (powerState == entity.end())
334     {
335         return false;
336     }
337 
338     const auto* ptr = powerState->get_ptr<const std::string*>();
339     if (ptr == nullptr)
340     {
341         return false;
342     }
343 
344     return *ptr == "On" || *ptr == "BiosPost";
345 }
346 
347 static void pruneDevice(const nlohmann::json& systemConfiguration,
348                         const bool powerOff, const bool scannedPowerOff,
349                         const std::string& name, const nlohmann::json& device)
350 {
351     if (systemConfiguration.contains(name))
352     {
353         return;
354     }
355 
356     if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff))
357     {
358         return;
359     }
360 
361     logDeviceRemoved(device);
362 }
363 
364 void startRemovedTimer(boost::asio::steady_timer& timer,
365                        nlohmann::json& systemConfiguration)
366 {
367     static bool scannedPowerOff = false;
368     static bool scannedPowerOn = false;
369 
370     if (systemConfiguration.empty() || lastJson.empty())
371     {
372         return; // not ready yet
373     }
374     if (scannedPowerOn)
375     {
376         return;
377     }
378 
379     if (!isPowerOn() && scannedPowerOff)
380     {
381         return;
382     }
383 
384     timer.expires_after(std::chrono::seconds(10));
385     timer.async_wait(
386         [&systemConfiguration](const boost::system::error_code& ec) {
387             if (ec == boost::asio::error::operation_aborted)
388             {
389                 return;
390             }
391 
392             bool powerOff = !isPowerOn();
393             for (const auto& [name, device] : lastJson.items())
394             {
395                 pruneDevice(systemConfiguration, powerOff, scannedPowerOff,
396                             name, device);
397             }
398 
399             scannedPowerOff = true;
400             if (!powerOff)
401             {
402                 scannedPowerOn = true;
403             }
404         });
405 }
406 
407 static void pruneConfiguration(nlohmann::json& systemConfiguration,
408                                sdbusplus::asio::object_server& objServer,
409                                bool powerOff, const std::string& name,
410                                const nlohmann::json& device)
411 {
412     if (powerOff && deviceRequiresPowerOn(device))
413     {
414         // power not on yet, don't know if it's there or not
415         return;
416     }
417 
418     auto& ifaces = dbus_interface::getDeviceInterfaces(device);
419     for (auto& iface : ifaces)
420     {
421         auto sharedPtr = iface.lock();
422         if (!!sharedPtr)
423         {
424             objServer.remove_interface(sharedPtr);
425         }
426     }
427 
428     ifaces.clear();
429     systemConfiguration.erase(name);
430     topology.remove(device["Name"].get<std::string>());
431     logDeviceRemoved(device);
432 }
433 
434 static void publishNewConfiguration(
435     const size_t& instance, const size_t count,
436     boost::asio::steady_timer& timer, nlohmann::json& systemConfiguration,
437     // Gerrit discussion:
438     // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6
439     //
440     // Discord discussion:
441     // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854
442     //
443     // NOLINTNEXTLINE(performance-unnecessary-value-param)
444     const nlohmann::json newConfiguration,
445     sdbusplus::asio::object_server& objServer)
446 {
447     loadOverlays(newConfiguration);
448 
449     boost::asio::post(io, [systemConfiguration]() {
450         if (!configuration::writeJsonFiles(systemConfiguration))
451         {
452             std::cerr << "Error writing json files\n";
453         }
454     });
455 
456     boost::asio::post(io, [&instance, count, &timer, newConfiguration,
457                            &systemConfiguration, &objServer]() {
458         postToDbus(newConfiguration, systemConfiguration, objServer);
459         if (count == instance)
460         {
461             startRemovedTimer(timer, systemConfiguration);
462         }
463     });
464 }
465 
466 // main properties changed entry
467 void propertiesChangedCallback(nlohmann::json& systemConfiguration,
468                                sdbusplus::asio::object_server& objServer)
469 {
470     static bool inProgress = false;
471     static boost::asio::steady_timer timer(io);
472     static size_t instance = 0;
473     instance++;
474     size_t count = instance;
475 
476     timer.expires_after(std::chrono::milliseconds(500));
477 
478     // setup an async wait as we normally get flooded with new requests
479     timer.async_wait([&systemConfiguration, &objServer,
480                       count](const boost::system::error_code& ec) {
481         if (ec == boost::asio::error::operation_aborted)
482         {
483             // we were cancelled
484             return;
485         }
486         if (ec)
487         {
488             std::cerr << "async wait error " << ec << "\n";
489             return;
490         }
491 
492         if (inProgress)
493         {
494             propertiesChangedCallback(systemConfiguration, objServer);
495             return;
496         }
497         inProgress = true;
498 
499         nlohmann::json oldConfiguration = systemConfiguration;
500         auto missingConfigurations = std::make_shared<nlohmann::json>();
501         *missingConfigurations = systemConfiguration;
502 
503         std::list<nlohmann::json> configurations;
504         if (!configuration::loadConfigurations(configurations))
505         {
506             std::cerr << "Could not load configurations\n";
507             inProgress = false;
508             return;
509         }
510 
511         auto perfScan = std::make_shared<scan::PerformScan>(
512             systemConfiguration, *missingConfigurations, configurations,
513             objServer,
514             [&systemConfiguration, &objServer, count, oldConfiguration,
515              missingConfigurations]() {
516                 // this is something that since ac has been applied to the bmc
517                 // we saw, and we no longer see it
518                 bool powerOff = !isPowerOn();
519                 for (const auto& [name, device] :
520                      missingConfigurations->items())
521                 {
522                     pruneConfiguration(systemConfiguration, objServer, powerOff,
523                                        name, device);
524                 }
525 
526                 nlohmann::json newConfiguration = systemConfiguration;
527 
528                 configuration::deriveNewConfiguration(oldConfiguration,
529                                                       newConfiguration);
530 
531                 for (const auto& [_, device] : newConfiguration.items())
532                 {
533                     logDeviceAdded(device);
534                 }
535 
536                 inProgress = false;
537 
538                 boost::asio::post(
539                     io, std::bind_front(
540                             publishNewConfiguration, std::ref(instance), count,
541                             std::ref(timer), std::ref(systemConfiguration),
542                             newConfiguration, std::ref(objServer)));
543             });
544         perfScan->run();
545     });
546 }
547 
548 // Check if InterfacesAdded payload contains an iface that needs probing.
549 static bool iaContainsProbeInterface(
550     sdbusplus::message_t& msg, const std::set<std::string>& probeInterfaces)
551 {
552     sdbusplus::message::object_path path;
553     DBusObject interfaces;
554     std::set<std::string> interfaceSet;
555     std::set<std::string> intersect;
556 
557     msg.read(path, interfaces);
558 
559     std::for_each(interfaces.begin(), interfaces.end(),
560                   [&interfaceSet](const auto& iface) {
561                       interfaceSet.insert(iface.first);
562                   });
563 
564     std::set_intersection(interfaceSet.begin(), interfaceSet.end(),
565                           probeInterfaces.begin(), probeInterfaces.end(),
566                           std::inserter(intersect, intersect.end()));
567     return !intersect.empty();
568 }
569 
570 // Check if InterfacesRemoved payload contains an iface that needs probing.
571 static bool irContainsProbeInterface(
572     sdbusplus::message_t& msg, const std::set<std::string>& probeInterfaces)
573 {
574     sdbusplus::message::object_path path;
575     std::set<std::string> interfaces;
576     std::set<std::string> intersect;
577 
578     msg.read(path, interfaces);
579 
580     std::set_intersection(interfaces.begin(), interfaces.end(),
581                           probeInterfaces.begin(), probeInterfaces.end(),
582                           std::inserter(intersect, intersect.end()));
583     return !intersect.empty();
584 }
585 
586 int main()
587 {
588     // setup connection to dbus
589     systemBus = std::make_shared<sdbusplus::asio::connection>(io);
590     systemBus->request_name("xyz.openbmc_project.EntityManager");
591 
592     // The EntityManager object itself doesn't expose any properties.
593     // No need to set up ObjectManager for the |EntityManager| object.
594     sdbusplus::asio::object_server objServer(systemBus, /*skipManager=*/true);
595 
596     // All other objects that EntityManager currently support are under the
597     // inventory subtree.
598     // See the discussion at
599     // https://discord.com/channels/775381525260664832/1018929092009144380
600     objServer.add_manager("/xyz/openbmc_project/inventory");
601 
602     std::shared_ptr<sdbusplus::asio::dbus_interface> entityIface =
603         objServer.add_interface("/xyz/openbmc_project/EntityManager",
604                                 "xyz.openbmc_project.EntityManager");
605 
606     // to keep reference to the match / filter objects so they don't get
607     // destroyed
608 
609     nlohmann::json systemConfiguration = nlohmann::json::object();
610 
611     std::set<std::string> probeInterfaces = configuration::getProbeInterfaces();
612 
613     // We need a poke from DBus for static providers that create all their
614     // objects prior to claiming a well-known name, and thus don't emit any
615     // org.freedesktop.DBus.Properties signals.  Similarly if a process exits
616     // for any reason, expected or otherwise, we'll need a poke to remove
617     // entities from DBus.
618     sdbusplus::bus::match_t nameOwnerChangedMatch(
619         static_cast<sdbusplus::bus_t&>(*systemBus),
620         sdbusplus::bus::match::rules::nameOwnerChanged(),
621         [&](sdbusplus::message_t& m) {
622             auto [name, oldOwner,
623                   newOwner] = m.unpack<std::string, std::string, std::string>();
624 
625             if (name.starts_with(':'))
626             {
627                 // We should do nothing with unique-name connections.
628                 return;
629             }
630 
631             propertiesChangedCallback(systemConfiguration, objServer);
632         });
633     // We also need a poke from DBus when new interfaces are created or
634     // destroyed.
635     sdbusplus::bus::match_t interfacesAddedMatch(
636         static_cast<sdbusplus::bus_t&>(*systemBus),
637         sdbusplus::bus::match::rules::interfacesAdded(),
638         [&](sdbusplus::message_t& msg) {
639             if (iaContainsProbeInterface(msg, probeInterfaces))
640             {
641                 propertiesChangedCallback(systemConfiguration, objServer);
642             }
643         });
644     sdbusplus::bus::match_t interfacesRemovedMatch(
645         static_cast<sdbusplus::bus_t&>(*systemBus),
646         sdbusplus::bus::match::rules::interfacesRemoved(),
647         [&](sdbusplus::message_t& msg) {
648             if (irContainsProbeInterface(msg, probeInterfaces))
649             {
650                 propertiesChangedCallback(systemConfiguration, objServer);
651             }
652         });
653 
654     boost::asio::post(io, [&]() {
655         propertiesChangedCallback(systemConfiguration, objServer);
656     });
657 
658     entityIface->register_method("ReScan", [&]() {
659         propertiesChangedCallback(systemConfiguration, objServer);
660     });
661     dbus_interface::tryIfaceInitialize(entityIface);
662 
663     if (fwVersionIsSame())
664     {
665         if (std::filesystem::is_regular_file(
666                 configuration::currentConfiguration))
667         {
668             // this file could just be deleted, but it's nice for debug
669             std::filesystem::create_directory(tempConfigDir);
670             std::filesystem::remove(lastConfiguration);
671             std::filesystem::copy(configuration::currentConfiguration,
672                                   lastConfiguration);
673             std::filesystem::remove(configuration::currentConfiguration);
674 
675             std::ifstream jsonStream(lastConfiguration);
676             if (jsonStream.good())
677             {
678                 auto data = nlohmann::json::parse(jsonStream, nullptr, false);
679                 if (data.is_discarded())
680                 {
681                     std::cerr
682                         << "syntax error in " << lastConfiguration << "\n";
683                 }
684                 else
685                 {
686                     lastJson = std::move(data);
687                 }
688             }
689             else
690             {
691                 std::cerr << "unable to open " << lastConfiguration << "\n";
692             }
693         }
694     }
695     else
696     {
697         // not an error, just logging at this level to make it in the journal
698         std::cerr << "Clearing previous configuration\n";
699         std::filesystem::remove(configuration::currentConfiguration);
700     }
701 
702     // some boards only show up after power is on, we want to not say they are
703     // removed until the same state happens
704     setupPowerMatch(systemBus);
705 
706     io.run();
707 
708     return 0;
709 }
710