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