cleanup: move EM service code in EM directory
Improving repository structure increases maintainability.
Change-Id: I7f746a5d491dda256a06143f516e7d078a761c14
Signed-off-by: Christopher Meis <christopher.meis@9elements.com>
diff --git a/src/entity_manager/entity_manager.cpp b/src/entity_manager/entity_manager.cpp
new file mode 100644
index 0000000..5aa5380
--- /dev/null
+++ b/src/entity_manager/entity_manager.cpp
@@ -0,0 +1,709 @@
+/*
+// Copyright (c) 2018 Intel Corporation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+*/
+/// \file entity_manager.cpp
+
+#include "entity_manager.hpp"
+
+#include "../utils.hpp"
+#include "../variant_visitors.hpp"
+#include "configuration.hpp"
+#include "dbus_interface.hpp"
+#include "overlay.hpp"
+#include "perform_scan.hpp"
+#include "topology.hpp"
+
+#include <boost/algorithm/string/case_conv.hpp>
+#include <boost/algorithm/string/classification.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/split.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/post.hpp>
+#include <boost/asio/steady_timer.hpp>
+#include <boost/container/flat_map.hpp>
+#include <boost/container/flat_set.hpp>
+#include <boost/range/iterator_range.hpp>
+#include <nlohmann/json.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+
+#include <charconv>
+#include <filesystem>
+#include <fstream>
+#include <functional>
+#include <iostream>
+#include <map>
+#include <regex>
+#include <variant>
+constexpr const char* tempConfigDir = "/tmp/configuration/";
+constexpr const char* lastConfiguration = "/tmp/configuration/last.json";
+
+static constexpr std::array<const char*, 6> settableInterfaces = {
+ "FanProfile", "Pid", "Pid.Zone", "Stepwise", "Thresholds", "Polling"};
+
+// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
+
+// todo: pass this through nicer
+std::shared_ptr<sdbusplus::asio::connection> systemBus;
+nlohmann::json lastJson;
+Topology topology;
+
+boost::asio::io_context io;
+// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
+
+const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
+const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
+
+sdbusplus::asio::PropertyPermission getPermission(const std::string& interface)
+{
+ return std::find(settableInterfaces.begin(), settableInterfaces.end(),
+ interface) != settableInterfaces.end()
+ ? sdbusplus::asio::PropertyPermission::readWrite
+ : sdbusplus::asio::PropertyPermission::readOnly;
+}
+
+void postToDbus(const nlohmann::json& newConfiguration,
+ nlohmann::json& systemConfiguration,
+ sdbusplus::asio::object_server& objServer)
+
+{
+ std::map<std::string, std::string> newBoards; // path -> name
+
+ // iterate through boards
+ for (const auto& [boardId, boardConfig] : newConfiguration.items())
+ {
+ std::string boardName = boardConfig["Name"];
+ std::string boardNameOrig = boardConfig["Name"];
+ std::string jsonPointerPath = "/" + boardId;
+ // loop through newConfiguration, but use values from system
+ // configuration to be able to modify via dbus later
+ auto boardValues = systemConfiguration[boardId];
+ auto findBoardType = boardValues.find("Type");
+ std::string boardType;
+ if (findBoardType != boardValues.end() &&
+ findBoardType->type() == nlohmann::json::value_t::string)
+ {
+ boardType = findBoardType->get<std::string>();
+ std::regex_replace(boardType.begin(), boardType.begin(),
+ boardType.end(), illegalDbusMemberRegex, "_");
+ }
+ else
+ {
+ std::cerr << "Unable to find type for " << boardName
+ << " reverting to Chassis.\n";
+ boardType = "Chassis";
+ }
+ std::string boardtypeLower = boost::algorithm::to_lower_copy(boardType);
+
+ std::regex_replace(boardName.begin(), boardName.begin(),
+ boardName.end(), illegalDbusMemberRegex, "_");
+ std::string boardPath = "/xyz/openbmc_project/inventory/system/";
+ boardPath += boardtypeLower;
+ boardPath += "/";
+ boardPath += boardName;
+
+ std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface =
+ dbus_interface::createInterface(
+ objServer, boardPath, "xyz.openbmc_project.Inventory.Item",
+ boardName);
+
+ std::shared_ptr<sdbusplus::asio::dbus_interface> boardIface =
+ dbus_interface::createInterface(
+ objServer, boardPath,
+ "xyz.openbmc_project.Inventory.Item." + boardType,
+ boardNameOrig);
+
+ dbus_interface::createAddObjectMethod(
+ jsonPointerPath, boardPath, systemConfiguration, objServer,
+ boardNameOrig);
+
+ dbus_interface::populateInterfaceFromJson(
+ systemConfiguration, jsonPointerPath, boardIface, boardValues,
+ objServer);
+ jsonPointerPath += "/";
+ // iterate through board properties
+ for (const auto& [propName, propValue] : boardValues.items())
+ {
+ if (propValue.type() == nlohmann::json::value_t::object)
+ {
+ std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
+ dbus_interface::createInterface(objServer, boardPath,
+ propName, boardNameOrig);
+
+ dbus_interface::populateInterfaceFromJson(
+ systemConfiguration, jsonPointerPath + propName, iface,
+ propValue, objServer);
+ }
+ }
+
+ auto exposes = boardValues.find("Exposes");
+ if (exposes == boardValues.end())
+ {
+ continue;
+ }
+ // iterate through exposes
+ jsonPointerPath += "Exposes/";
+
+ // store the board level pointer so we can modify it on the way down
+ std::string jsonPointerPathBoard = jsonPointerPath;
+ size_t exposesIndex = -1;
+ for (auto& item : *exposes)
+ {
+ exposesIndex++;
+ jsonPointerPath = jsonPointerPathBoard;
+ jsonPointerPath += std::to_string(exposesIndex);
+
+ auto findName = item.find("Name");
+ if (findName == item.end())
+ {
+ std::cerr << "cannot find name in field " << item << "\n";
+ continue;
+ }
+ auto findStatus = item.find("Status");
+ // if status is not found it is assumed to be status = 'okay'
+ if (findStatus != item.end())
+ {
+ if (*findStatus == "disabled")
+ {
+ continue;
+ }
+ }
+ auto findType = item.find("Type");
+ std::string itemType;
+ if (findType != item.end())
+ {
+ itemType = findType->get<std::string>();
+ std::regex_replace(itemType.begin(), itemType.begin(),
+ itemType.end(), illegalDbusPathRegex, "_");
+ }
+ else
+ {
+ itemType = "unknown";
+ }
+ std::string itemName = findName->get<std::string>();
+ std::regex_replace(itemName.begin(), itemName.begin(),
+ itemName.end(), illegalDbusMemberRegex, "_");
+ std::string ifacePath = boardPath;
+ ifacePath += "/";
+ ifacePath += itemName;
+
+ if (itemType == "BMC")
+ {
+ std::shared_ptr<sdbusplus::asio::dbus_interface> bmcIface =
+ dbus_interface::createInterface(
+ objServer, ifacePath,
+ "xyz.openbmc_project.Inventory.Item.Bmc",
+ boardNameOrig);
+ dbus_interface::populateInterfaceFromJson(
+ systemConfiguration, jsonPointerPath, bmcIface, item,
+ objServer, getPermission(itemType));
+ }
+ else if (itemType == "System")
+ {
+ std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface =
+ dbus_interface::createInterface(
+ objServer, ifacePath,
+ "xyz.openbmc_project.Inventory.Item.System",
+ boardNameOrig);
+ dbus_interface::populateInterfaceFromJson(
+ systemConfiguration, jsonPointerPath, systemIface, item,
+ objServer, getPermission(itemType));
+ }
+
+ for (const auto& [name, config] : item.items())
+ {
+ jsonPointerPath = jsonPointerPathBoard;
+ jsonPointerPath.append(std::to_string(exposesIndex))
+ .append("/")
+ .append(name);
+ if (config.type() == nlohmann::json::value_t::object)
+ {
+ std::string ifaceName =
+ "xyz.openbmc_project.Configuration.";
+ ifaceName.append(itemType).append(".").append(name);
+
+ std::shared_ptr<sdbusplus::asio::dbus_interface>
+ objectIface = dbus_interface::createInterface(
+ objServer, ifacePath, ifaceName, boardNameOrig);
+
+ dbus_interface::populateInterfaceFromJson(
+ systemConfiguration, jsonPointerPath, objectIface,
+ config, objServer, getPermission(name));
+ }
+ else if (config.type() == nlohmann::json::value_t::array)
+ {
+ size_t index = 0;
+ if (config.empty())
+ {
+ continue;
+ }
+ bool isLegal = true;
+ auto type = config[0].type();
+ if (type != nlohmann::json::value_t::object)
+ {
+ continue;
+ }
+
+ // verify legal json
+ for (const auto& arrayItem : config)
+ {
+ if (arrayItem.type() != type)
+ {
+ isLegal = false;
+ break;
+ }
+ }
+ if (!isLegal)
+ {
+ std::cerr << "dbus format error" << config << "\n";
+ break;
+ }
+
+ for (auto& arrayItem : config)
+ {
+ std::string ifaceName =
+ "xyz.openbmc_project.Configuration.";
+ ifaceName.append(itemType).append(".").append(name);
+ ifaceName.append(std::to_string(index));
+
+ std::shared_ptr<sdbusplus::asio::dbus_interface>
+ objectIface = dbus_interface::createInterface(
+ objServer, ifacePath, ifaceName, boardNameOrig);
+
+ dbus_interface::populateInterfaceFromJson(
+ systemConfiguration,
+ jsonPointerPath + "/" + std::to_string(index),
+ objectIface, arrayItem, objServer,
+ getPermission(name));
+ index++;
+ }
+ }
+ }
+
+ std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface =
+ dbus_interface::createInterface(
+ objServer, ifacePath,
+ "xyz.openbmc_project.Configuration." + itemType,
+ boardNameOrig);
+
+ dbus_interface::populateInterfaceFromJson(
+ systemConfiguration, jsonPointerPath, itemIface, item,
+ objServer, getPermission(itemType));
+
+ topology.addBoard(boardPath, boardType, boardNameOrig, item);
+ }
+
+ newBoards.emplace(boardPath, boardNameOrig);
+ }
+
+ for (const auto& [assocPath, assocPropValue] :
+ topology.getAssocs(newBoards))
+ {
+ auto findBoard = newBoards.find(assocPath);
+ if (findBoard == newBoards.end())
+ {
+ continue;
+ }
+
+ auto ifacePtr = dbus_interface::createInterface(
+ objServer, assocPath, "xyz.openbmc_project.Association.Definitions",
+ findBoard->second);
+
+ ifacePtr->register_property("Associations", assocPropValue);
+ dbus_interface::tryIfaceInitialize(ifacePtr);
+ }
+}
+
+static bool deviceRequiresPowerOn(const nlohmann::json& entity)
+{
+ auto powerState = entity.find("PowerState");
+ if (powerState == entity.end())
+ {
+ return false;
+ }
+
+ const auto* ptr = powerState->get_ptr<const std::string*>();
+ if (ptr == nullptr)
+ {
+ return false;
+ }
+
+ return *ptr == "On" || *ptr == "BiosPost";
+}
+
+static void pruneDevice(const nlohmann::json& systemConfiguration,
+ const bool powerOff, const bool scannedPowerOff,
+ const std::string& name, const nlohmann::json& device)
+{
+ if (systemConfiguration.contains(name))
+ {
+ return;
+ }
+
+ if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff))
+ {
+ return;
+ }
+
+ logDeviceRemoved(device);
+}
+
+void startRemovedTimer(boost::asio::steady_timer& timer,
+ nlohmann::json& systemConfiguration)
+{
+ static bool scannedPowerOff = false;
+ static bool scannedPowerOn = false;
+
+ if (systemConfiguration.empty() || lastJson.empty())
+ {
+ return; // not ready yet
+ }
+ if (scannedPowerOn)
+ {
+ return;
+ }
+
+ if (!isPowerOn() && scannedPowerOff)
+ {
+ return;
+ }
+
+ timer.expires_after(std::chrono::seconds(10));
+ timer.async_wait(
+ [&systemConfiguration](const boost::system::error_code& ec) {
+ if (ec == boost::asio::error::operation_aborted)
+ {
+ return;
+ }
+
+ bool powerOff = !isPowerOn();
+ for (const auto& [name, device] : lastJson.items())
+ {
+ pruneDevice(systemConfiguration, powerOff, scannedPowerOff,
+ name, device);
+ }
+
+ scannedPowerOff = true;
+ if (!powerOff)
+ {
+ scannedPowerOn = true;
+ }
+ });
+}
+
+static void pruneConfiguration(nlohmann::json& systemConfiguration,
+ sdbusplus::asio::object_server& objServer,
+ bool powerOff, const std::string& name,
+ const nlohmann::json& device)
+{
+ if (powerOff && deviceRequiresPowerOn(device))
+ {
+ // power not on yet, don't know if it's there or not
+ return;
+ }
+
+ auto& ifaces = dbus_interface::getDeviceInterfaces(device);
+ for (auto& iface : ifaces)
+ {
+ auto sharedPtr = iface.lock();
+ if (!!sharedPtr)
+ {
+ objServer.remove_interface(sharedPtr);
+ }
+ }
+
+ ifaces.clear();
+ systemConfiguration.erase(name);
+ topology.remove(device["Name"].get<std::string>());
+ logDeviceRemoved(device);
+}
+
+static void publishNewConfiguration(
+ const size_t& instance, const size_t count,
+ boost::asio::steady_timer& timer, nlohmann::json& systemConfiguration,
+ // Gerrit discussion:
+ // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6
+ //
+ // Discord discussion:
+ // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854
+ //
+ // NOLINTNEXTLINE(performance-unnecessary-value-param)
+ const nlohmann::json newConfiguration,
+ sdbusplus::asio::object_server& objServer)
+{
+ loadOverlays(newConfiguration);
+
+ boost::asio::post(io, [systemConfiguration]() {
+ if (!configuration::writeJsonFiles(systemConfiguration))
+ {
+ std::cerr << "Error writing json files\n";
+ }
+ });
+
+ boost::asio::post(io, [&instance, count, &timer, newConfiguration,
+ &systemConfiguration, &objServer]() {
+ postToDbus(newConfiguration, systemConfiguration, objServer);
+ if (count == instance)
+ {
+ startRemovedTimer(timer, systemConfiguration);
+ }
+ });
+}
+
+// main properties changed entry
+void propertiesChangedCallback(nlohmann::json& systemConfiguration,
+ sdbusplus::asio::object_server& objServer)
+{
+ static bool inProgress = false;
+ static boost::asio::steady_timer timer(io);
+ static size_t instance = 0;
+ instance++;
+ size_t count = instance;
+
+ timer.expires_after(std::chrono::milliseconds(500));
+
+ // setup an async wait as we normally get flooded with new requests
+ timer.async_wait([&systemConfiguration, &objServer,
+ count](const boost::system::error_code& ec) {
+ if (ec == boost::asio::error::operation_aborted)
+ {
+ // we were cancelled
+ return;
+ }
+ if (ec)
+ {
+ std::cerr << "async wait error " << ec << "\n";
+ return;
+ }
+
+ if (inProgress)
+ {
+ propertiesChangedCallback(systemConfiguration, objServer);
+ return;
+ }
+ inProgress = true;
+
+ nlohmann::json oldConfiguration = systemConfiguration;
+ auto missingConfigurations = std::make_shared<nlohmann::json>();
+ *missingConfigurations = systemConfiguration;
+
+ std::list<nlohmann::json> configurations;
+ if (!configuration::loadConfigurations(configurations))
+ {
+ std::cerr << "Could not load configurations\n";
+ inProgress = false;
+ return;
+ }
+
+ auto perfScan = std::make_shared<scan::PerformScan>(
+ systemConfiguration, *missingConfigurations, configurations,
+ objServer,
+ [&systemConfiguration, &objServer, count, oldConfiguration,
+ missingConfigurations]() {
+ // this is something that since ac has been applied to the bmc
+ // we saw, and we no longer see it
+ bool powerOff = !isPowerOn();
+ for (const auto& [name, device] :
+ missingConfigurations->items())
+ {
+ pruneConfiguration(systemConfiguration, objServer, powerOff,
+ name, device);
+ }
+
+ nlohmann::json newConfiguration = systemConfiguration;
+
+ configuration::deriveNewConfiguration(oldConfiguration,
+ newConfiguration);
+
+ for (const auto& [_, device] : newConfiguration.items())
+ {
+ logDeviceAdded(device);
+ }
+
+ inProgress = false;
+
+ boost::asio::post(
+ io, std::bind_front(
+ publishNewConfiguration, std::ref(instance), count,
+ std::ref(timer), std::ref(systemConfiguration),
+ newConfiguration, std::ref(objServer)));
+ });
+ perfScan->run();
+ });
+}
+
+// Check if InterfacesAdded payload contains an iface that needs probing.
+static bool iaContainsProbeInterface(
+ sdbusplus::message_t& msg, const std::set<std::string>& probeInterfaces)
+{
+ sdbusplus::message::object_path path;
+ DBusObject interfaces;
+ std::set<std::string> interfaceSet;
+ std::set<std::string> intersect;
+
+ msg.read(path, interfaces);
+
+ std::for_each(interfaces.begin(), interfaces.end(),
+ [&interfaceSet](const auto& iface) {
+ interfaceSet.insert(iface.first);
+ });
+
+ std::set_intersection(interfaceSet.begin(), interfaceSet.end(),
+ probeInterfaces.begin(), probeInterfaces.end(),
+ std::inserter(intersect, intersect.end()));
+ return !intersect.empty();
+}
+
+// Check if InterfacesRemoved payload contains an iface that needs probing.
+static bool irContainsProbeInterface(
+ sdbusplus::message_t& msg, const std::set<std::string>& probeInterfaces)
+{
+ sdbusplus::message::object_path path;
+ std::set<std::string> interfaces;
+ std::set<std::string> intersect;
+
+ msg.read(path, interfaces);
+
+ std::set_intersection(interfaces.begin(), interfaces.end(),
+ probeInterfaces.begin(), probeInterfaces.end(),
+ std::inserter(intersect, intersect.end()));
+ return !intersect.empty();
+}
+
+int main()
+{
+ // setup connection to dbus
+ systemBus = std::make_shared<sdbusplus::asio::connection>(io);
+ systemBus->request_name("xyz.openbmc_project.EntityManager");
+
+ // The EntityManager object itself doesn't expose any properties.
+ // No need to set up ObjectManager for the |EntityManager| object.
+ sdbusplus::asio::object_server objServer(systemBus, /*skipManager=*/true);
+
+ // All other objects that EntityManager currently support are under the
+ // inventory subtree.
+ // See the discussion at
+ // https://discord.com/channels/775381525260664832/1018929092009144380
+ objServer.add_manager("/xyz/openbmc_project/inventory");
+
+ std::shared_ptr<sdbusplus::asio::dbus_interface> entityIface =
+ objServer.add_interface("/xyz/openbmc_project/EntityManager",
+ "xyz.openbmc_project.EntityManager");
+
+ // to keep reference to the match / filter objects so they don't get
+ // destroyed
+
+ nlohmann::json systemConfiguration = nlohmann::json::object();
+
+ std::set<std::string> probeInterfaces = configuration::getProbeInterfaces();
+
+ // We need a poke from DBus for static providers that create all their
+ // objects prior to claiming a well-known name, and thus don't emit any
+ // org.freedesktop.DBus.Properties signals. Similarly if a process exits
+ // for any reason, expected or otherwise, we'll need a poke to remove
+ // entities from DBus.
+ sdbusplus::bus::match_t nameOwnerChangedMatch(
+ static_cast<sdbusplus::bus_t&>(*systemBus),
+ sdbusplus::bus::match::rules::nameOwnerChanged(),
+ [&](sdbusplus::message_t& m) {
+ auto [name, oldOwner,
+ newOwner] = m.unpack<std::string, std::string, std::string>();
+
+ if (name.starts_with(':'))
+ {
+ // We should do nothing with unique-name connections.
+ return;
+ }
+
+ propertiesChangedCallback(systemConfiguration, objServer);
+ });
+ // We also need a poke from DBus when new interfaces are created or
+ // destroyed.
+ sdbusplus::bus::match_t interfacesAddedMatch(
+ static_cast<sdbusplus::bus_t&>(*systemBus),
+ sdbusplus::bus::match::rules::interfacesAdded(),
+ [&](sdbusplus::message_t& msg) {
+ if (iaContainsProbeInterface(msg, probeInterfaces))
+ {
+ propertiesChangedCallback(systemConfiguration, objServer);
+ }
+ });
+ sdbusplus::bus::match_t interfacesRemovedMatch(
+ static_cast<sdbusplus::bus_t&>(*systemBus),
+ sdbusplus::bus::match::rules::interfacesRemoved(),
+ [&](sdbusplus::message_t& msg) {
+ if (irContainsProbeInterface(msg, probeInterfaces))
+ {
+ propertiesChangedCallback(systemConfiguration, objServer);
+ }
+ });
+
+ boost::asio::post(io, [&]() {
+ propertiesChangedCallback(systemConfiguration, objServer);
+ });
+
+ entityIface->register_method("ReScan", [&]() {
+ propertiesChangedCallback(systemConfiguration, objServer);
+ });
+ dbus_interface::tryIfaceInitialize(entityIface);
+
+ if (fwVersionIsSame())
+ {
+ if (std::filesystem::is_regular_file(
+ configuration::currentConfiguration))
+ {
+ // this file could just be deleted, but it's nice for debug
+ std::filesystem::create_directory(tempConfigDir);
+ std::filesystem::remove(lastConfiguration);
+ std::filesystem::copy(configuration::currentConfiguration,
+ lastConfiguration);
+ std::filesystem::remove(configuration::currentConfiguration);
+
+ std::ifstream jsonStream(lastConfiguration);
+ if (jsonStream.good())
+ {
+ auto data = nlohmann::json::parse(jsonStream, nullptr, false);
+ if (data.is_discarded())
+ {
+ std::cerr
+ << "syntax error in " << lastConfiguration << "\n";
+ }
+ else
+ {
+ lastJson = std::move(data);
+ }
+ }
+ else
+ {
+ std::cerr << "unable to open " << lastConfiguration << "\n";
+ }
+ }
+ }
+ else
+ {
+ // not an error, just logging at this level to make it in the journal
+ std::cerr << "Clearing previous configuration\n";
+ std::filesystem::remove(configuration::currentConfiguration);
+ }
+
+ // some boards only show up after power is on, we want to not say they are
+ // removed until the same state happens
+ setupPowerMatch(systemBus);
+
+ io.run();
+
+ return 0;
+}