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/configuration.cpp b/src/entity_manager/configuration.cpp
new file mode 100644
index 0000000..5663ec8
--- /dev/null
+++ b/src/entity_manager/configuration.cpp
@@ -0,0 +1,197 @@
+#include "configuration.hpp"
+
+#include "../utils.hpp"
+#include "perform_probe.hpp"
+
+#include <nlohmann/json.hpp>
+#include <valijson/adapters/nlohmann_json_adapter.hpp>
+#include <valijson/schema.hpp>
+#include <valijson/schema_parser.hpp>
+#include <valijson/validator.hpp>
+
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+#include <list>
+#include <string>
+#include <vector>
+
+namespace configuration
+{
+// writes output files to persist data
+bool writeJsonFiles(const nlohmann::json& systemConfiguration)
+{
+ std::filesystem::create_directory(configurationOutDir);
+ std::ofstream output(currentConfiguration);
+ if (!output.good())
+ {
+ return false;
+ }
+ output << systemConfiguration.dump(4);
+ output.close();
+ return true;
+}
+
+// reads json files out of the filesystem
+bool loadConfigurations(std::list<nlohmann::json>& configurations)
+{
+ // find configuration files
+ std::vector<std::filesystem::path> jsonPaths;
+ if (!findFiles(
+ std::vector<std::filesystem::path>{configurationDirectory,
+ hostConfigurationDirectory},
+ R"(.*\.json)", jsonPaths))
+ {
+ std::cerr << "Unable to find any configuration files in "
+ << configurationDirectory << "\n";
+ return false;
+ }
+
+ std::ifstream schemaStream(
+ std::string(schemaDirectory) + "/" + globalSchema);
+ if (!schemaStream.good())
+ {
+ std::cerr
+ << "Cannot open schema file, cannot validate JSON, exiting\n\n";
+ std::exit(EXIT_FAILURE);
+ return false;
+ }
+ nlohmann::json schema =
+ nlohmann::json::parse(schemaStream, nullptr, false, true);
+ if (schema.is_discarded())
+ {
+ std::cerr
+ << "Illegal schema file detected, cannot validate JSON, exiting\n";
+ std::exit(EXIT_FAILURE);
+ return false;
+ }
+
+ for (auto& jsonPath : jsonPaths)
+ {
+ std::ifstream jsonStream(jsonPath.c_str());
+ if (!jsonStream.good())
+ {
+ std::cerr << "unable to open " << jsonPath.string() << "\n";
+ continue;
+ }
+ auto data = nlohmann::json::parse(jsonStream, nullptr, false, true);
+ if (data.is_discarded())
+ {
+ std::cerr << "syntax error in " << jsonPath.string() << "\n";
+ continue;
+ }
+ /*
+ * todo(james): reenable this once less things are in flight
+ *
+ if (!validateJson(schema, data))
+ {
+ std::cerr << "Error validating " << jsonPath.string() << "\n";
+ continue;
+ }
+ */
+
+ if (data.type() == nlohmann::json::value_t::array)
+ {
+ for (auto& d : data)
+ {
+ configurations.emplace_back(d);
+ }
+ }
+ else
+ {
+ configurations.emplace_back(data);
+ }
+ }
+ return true;
+}
+
+// Iterate over new configuration and erase items from old configuration.
+void deriveNewConfiguration(const nlohmann::json& oldConfiguration,
+ nlohmann::json& newConfiguration)
+{
+ for (auto it = newConfiguration.begin(); it != newConfiguration.end();)
+ {
+ auto findKey = oldConfiguration.find(it.key());
+ if (findKey != oldConfiguration.end())
+ {
+ it = newConfiguration.erase(it);
+ }
+ else
+ {
+ it++;
+ }
+ }
+}
+
+// validates a given input(configuration) with a given json schema file.
+bool validateJson(const nlohmann::json& schemaFile, const nlohmann::json& input)
+{
+ valijson::Schema schema;
+ valijson::SchemaParser parser;
+ valijson::adapters::NlohmannJsonAdapter schemaAdapter(schemaFile);
+ parser.populateSchema(schemaAdapter, schema);
+ valijson::Validator validator;
+ valijson::adapters::NlohmannJsonAdapter targetAdapter(input);
+ return validator.validate(schema, targetAdapter, nullptr);
+}
+
+// Extract the D-Bus interfaces to probe from the JSON config files.
+std::set<std::string> getProbeInterfaces()
+{
+ std::set<std::string> interfaces;
+ std::list<nlohmann::json> configurations;
+ if (!configuration::loadConfigurations(configurations))
+ {
+ return interfaces;
+ }
+
+ for (auto it = configurations.begin(); it != configurations.end();)
+ {
+ auto findProbe = it->find("Probe");
+ if (findProbe == it->end())
+ {
+ std::cerr << "configuration file missing probe:\n " << *it << "\n";
+ it++;
+ continue;
+ }
+
+ nlohmann::json probeCommand;
+ if ((*findProbe).type() != nlohmann::json::value_t::array)
+ {
+ probeCommand = nlohmann::json::array();
+ probeCommand.push_back(*findProbe);
+ }
+ else
+ {
+ probeCommand = *findProbe;
+ }
+
+ for (const nlohmann::json& probeJson : probeCommand)
+ {
+ const std::string* probe = probeJson.get_ptr<const std::string*>();
+ if (probe == nullptr)
+ {
+ std::cerr << "Probe statement wasn't a string, can't parse";
+ continue;
+ }
+ // Skip it if the probe cmd doesn't contain an interface.
+ if (probe::findProbeType(*probe))
+ {
+ continue;
+ }
+
+ // syntax requires probe before first open brace
+ auto findStart = probe->find('(');
+ if (findStart != std::string::npos)
+ {
+ std::string interface = probe->substr(0, findStart);
+ interfaces.emplace(interface);
+ }
+ }
+ it++;
+ }
+
+ return interfaces;
+}
+
+} // namespace configuration
diff --git a/src/entity_manager/configuration.hpp b/src/entity_manager/configuration.hpp
new file mode 100644
index 0000000..b3b2cdc
--- /dev/null
+++ b/src/entity_manager/configuration.hpp
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <nlohmann/json.hpp>
+
+#include <list>
+#include <set>
+
+namespace configuration
+{
+constexpr const char* globalSchema = "global.json";
+constexpr const char* hostConfigurationDirectory = SYSCONF_DIR "configurations";
+constexpr const char* configurationDirectory = PACKAGE_DIR "configurations";
+constexpr const char* currentConfiguration = "/var/configuration/system.json";
+constexpr const char* schemaDirectory = PACKAGE_DIR "configurations/schemas";
+
+bool writeJsonFiles(const nlohmann::json& systemConfiguration);
+
+bool loadConfigurations(std::list<nlohmann::json>& configurations);
+
+template <typename JsonType>
+bool setJsonFromPointer(const std::string& ptrStr, const JsonType& value,
+ nlohmann::json& systemConfiguration)
+{
+ try
+ {
+ nlohmann::json::json_pointer ptr(ptrStr);
+ nlohmann::json& ref = systemConfiguration[ptr];
+ ref = value;
+ return true;
+ }
+ catch (const std::out_of_range&)
+ {
+ return false;
+ }
+}
+
+void deriveNewConfiguration(const nlohmann::json& oldConfiguration,
+ nlohmann::json& newConfiguration);
+
+bool validateJson(const nlohmann::json& schemaFile,
+ const nlohmann::json& input);
+
+std::set<std::string> getProbeInterfaces();
+
+} // namespace configuration
diff --git a/src/entity_manager/dbus_interface.cpp b/src/entity_manager/dbus_interface.cpp
new file mode 100644
index 0000000..f0c904c
--- /dev/null
+++ b/src/entity_manager/dbus_interface.cpp
@@ -0,0 +1,395 @@
+#include "dbus_interface.hpp"
+
+#include "../utils.hpp"
+#include "perform_probe.hpp"
+
+#include <boost/algorithm/string/case_conv.hpp>
+#include <boost/container/flat_map.hpp>
+
+#include <regex>
+#include <string>
+#include <vector>
+
+using JsonVariantType =
+ std::variant<std::vector<std::string>, std::vector<double>, std::string,
+ int64_t, uint64_t, double, int32_t, uint32_t, int16_t,
+ uint16_t, uint8_t, bool>;
+
+namespace dbus_interface
+{
+
+const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
+const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
+
+// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
+// store reference to all interfaces so we can destroy them later
+boost::container::flat_map<
+ std::string, std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>>
+ inventory;
+// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
+
+void tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface>& iface)
+{
+ try
+ {
+ iface->initialize();
+ }
+ catch (std::exception& e)
+ {
+ std::cerr << "Unable to initialize dbus interface : " << e.what()
+ << "\n"
+ << "object Path : " << iface->get_object_path() << "\n"
+ << "interface name : " << iface->get_interface_name() << "\n";
+ }
+}
+
+std::shared_ptr<sdbusplus::asio::dbus_interface> createInterface(
+ sdbusplus::asio::object_server& objServer, const std::string& path,
+ const std::string& interface, const std::string& parent, bool checkNull)
+{
+ // on first add we have no reason to check for null before add, as there
+ // won't be any. For dynamically added interfaces, we check for null so that
+ // a constant delete/add will not create a memory leak
+
+ auto ptr = objServer.add_interface(path, interface);
+ auto& dataVector = inventory[parent];
+ if (checkNull)
+ {
+ auto it = std::find_if(dataVector.begin(), dataVector.end(),
+ [](const auto& p) { return p.expired(); });
+ if (it != dataVector.end())
+ {
+ *it = ptr;
+ return ptr;
+ }
+ }
+ dataVector.emplace_back(ptr);
+ return ptr;
+}
+
+void createDeleteObjectMethod(
+ const std::string& jsonPointerPath,
+ const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
+ sdbusplus::asio::object_server& objServer,
+ nlohmann::json& systemConfiguration)
+{
+ std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface;
+ iface->register_method(
+ "Delete", [&objServer, &systemConfiguration, interface,
+ jsonPointerPath{std::string(jsonPointerPath)}]() {
+ std::shared_ptr<sdbusplus::asio::dbus_interface> dbusInterface =
+ interface.lock();
+ if (!dbusInterface)
+ {
+ // this technically can't happen as the pointer is pointing to
+ // us
+ throw DBusInternalError();
+ }
+ nlohmann::json::json_pointer ptr(jsonPointerPath);
+ systemConfiguration[ptr] = nullptr;
+
+ // todo(james): dig through sdbusplus to find out why we can't
+ // delete it in a method call
+ boost::asio::post(io, [&objServer, dbusInterface]() mutable {
+ objServer.remove_interface(dbusInterface);
+ });
+
+ if (!configuration::writeJsonFiles(systemConfiguration))
+ {
+ std::cerr << "error setting json file\n";
+ throw DBusInternalError();
+ }
+ });
+}
+
+// adds simple json types to interface's properties
+void populateInterfaceFromJson(
+ nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
+ std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
+ nlohmann::json& dict, sdbusplus::asio::object_server& objServer,
+ sdbusplus::asio::PropertyPermission permission)
+{
+ for (const auto& [key, value] : dict.items())
+ {
+ auto type = value.type();
+ bool array = false;
+ if (value.type() == nlohmann::json::value_t::array)
+ {
+ array = true;
+ if (value.empty())
+ {
+ continue;
+ }
+ type = value[0].type();
+ bool isLegal = true;
+ for (const auto& arrayItem : value)
+ {
+ if (arrayItem.type() != type)
+ {
+ isLegal = false;
+ break;
+ }
+ }
+ if (!isLegal)
+ {
+ std::cerr << "dbus format error" << value << "\n";
+ continue;
+ }
+ }
+ if (type == nlohmann::json::value_t::object)
+ {
+ continue; // handled elsewhere
+ }
+
+ std::string path = jsonPointerPath;
+ path.append("/").append(key);
+ if (permission == sdbusplus::asio::PropertyPermission::readWrite)
+ {
+ // all setable numbers are doubles as it is difficult to always
+ // create a configuration file with all whole numbers as decimals
+ // i.e. 1.0
+ if (array)
+ {
+ if (value[0].is_number())
+ {
+ type = nlohmann::json::value_t::number_float;
+ }
+ }
+ else if (value.is_number())
+ {
+ type = nlohmann::json::value_t::number_float;
+ }
+ }
+
+ switch (type)
+ {
+ case (nlohmann::json::value_t::boolean):
+ {
+ if (array)
+ {
+ // todo: array of bool isn't detected correctly by
+ // sdbusplus, change it to numbers
+ addArrayToDbus<uint64_t>(key, value, iface.get(),
+ permission, systemConfiguration,
+ path);
+ }
+
+ else
+ {
+ addProperty(key, value.get<bool>(), iface.get(),
+ systemConfiguration, path, permission);
+ }
+ break;
+ }
+ case (nlohmann::json::value_t::number_integer):
+ {
+ if (array)
+ {
+ addArrayToDbus<int64_t>(key, value, iface.get(), permission,
+ systemConfiguration, path);
+ }
+ else
+ {
+ addProperty(key, value.get<int64_t>(), iface.get(),
+ systemConfiguration, path,
+ sdbusplus::asio::PropertyPermission::readOnly);
+ }
+ break;
+ }
+ case (nlohmann::json::value_t::number_unsigned):
+ {
+ if (array)
+ {
+ addArrayToDbus<uint64_t>(key, value, iface.get(),
+ permission, systemConfiguration,
+ path);
+ }
+ else
+ {
+ addProperty(key, value.get<uint64_t>(), iface.get(),
+ systemConfiguration, path,
+ sdbusplus::asio::PropertyPermission::readOnly);
+ }
+ break;
+ }
+ case (nlohmann::json::value_t::number_float):
+ {
+ if (array)
+ {
+ addArrayToDbus<double>(key, value, iface.get(), permission,
+ systemConfiguration, path);
+ }
+
+ else
+ {
+ addProperty(key, value.get<double>(), iface.get(),
+ systemConfiguration, path, permission);
+ }
+ break;
+ }
+ case (nlohmann::json::value_t::string):
+ {
+ if (array)
+ {
+ addArrayToDbus<std::string>(key, value, iface.get(),
+ permission, systemConfiguration,
+ path);
+ }
+ else
+ {
+ addProperty(key, value.get<std::string>(), iface.get(),
+ systemConfiguration, path, permission);
+ }
+ break;
+ }
+ default:
+ {
+ std::cerr << "Unexpected json type in system configuration "
+ << key << ": " << value.type_name() << "\n";
+ break;
+ }
+ }
+ }
+ if (permission == sdbusplus::asio::PropertyPermission::readWrite)
+ {
+ createDeleteObjectMethod(jsonPointerPath, iface, objServer,
+ systemConfiguration);
+ }
+ tryIfaceInitialize(iface);
+}
+
+void createAddObjectMethod(
+ const std::string& jsonPointerPath, const std::string& path,
+ nlohmann::json& systemConfiguration,
+ sdbusplus::asio::object_server& objServer, const std::string& board)
+{
+ std::shared_ptr<sdbusplus::asio::dbus_interface> iface = createInterface(
+ objServer, path, "xyz.openbmc_project.AddObject", board);
+
+ iface->register_method(
+ "AddObject",
+ [&systemConfiguration, &objServer,
+ jsonPointerPath{std::string(jsonPointerPath)}, path{std::string(path)},
+ board](const boost::container::flat_map<std::string, JsonVariantType>&
+ data) {
+ nlohmann::json::json_pointer ptr(jsonPointerPath);
+ nlohmann::json& base = systemConfiguration[ptr];
+ auto findExposes = base.find("Exposes");
+
+ if (findExposes == base.end())
+ {
+ throw std::invalid_argument("Entity must have children.");
+ }
+
+ // this will throw invalid-argument to sdbusplus if invalid json
+ nlohmann::json newData{};
+ for (const auto& item : data)
+ {
+ nlohmann::json& newJson = newData[item.first];
+ std::visit(
+ [&newJson](auto&& val) {
+ newJson = std::forward<decltype(val)>(val);
+ },
+ item.second);
+ }
+
+ auto findName = newData.find("Name");
+ auto findType = newData.find("Type");
+ if (findName == newData.end() || findType == newData.end())
+ {
+ throw std::invalid_argument("AddObject missing Name or Type");
+ }
+ const std::string* type = findType->get_ptr<const std::string*>();
+ const std::string* name = findName->get_ptr<const std::string*>();
+ if (type == nullptr || name == nullptr)
+ {
+ throw std::invalid_argument("Type and Name must be a string.");
+ }
+
+ bool foundNull = false;
+ size_t lastIndex = 0;
+ // we add in the "exposes"
+ for (const auto& expose : *findExposes)
+ {
+ if (expose.is_null())
+ {
+ foundNull = true;
+ continue;
+ }
+
+ if (expose["Name"] == *name && expose["Type"] == *type)
+ {
+ throw std::invalid_argument(
+ "Field already in JSON, not adding");
+ }
+
+ if (foundNull)
+ {
+ continue;
+ }
+
+ lastIndex++;
+ }
+
+ std::ifstream schemaFile(
+ std::string(configuration::schemaDirectory) + "/" +
+ boost::to_lower_copy(*type) + ".json");
+ // todo(james) we might want to also make a list of 'can add'
+ // interfaces but for now I think the assumption if there is a
+ // schema avaliable that it is allowed to update is fine
+ if (!schemaFile.good())
+ {
+ throw std::invalid_argument(
+ "No schema avaliable, cannot validate.");
+ }
+ nlohmann::json schema =
+ nlohmann::json::parse(schemaFile, nullptr, false, true);
+ if (schema.is_discarded())
+ {
+ std::cerr << "Schema not legal" << *type << ".json\n";
+ throw DBusInternalError();
+ }
+ if (!configuration::validateJson(schema, newData))
+ {
+ throw std::invalid_argument("Data does not match schema");
+ }
+ if (foundNull)
+ {
+ findExposes->at(lastIndex) = newData;
+ }
+ else
+ {
+ findExposes->push_back(newData);
+ }
+ if (!configuration::writeJsonFiles(systemConfiguration))
+ {
+ std::cerr << "Error writing json files\n";
+ throw DBusInternalError();
+ }
+ std::string dbusName = *name;
+
+ std::regex_replace(dbusName.begin(), dbusName.begin(),
+ dbusName.end(), illegalDbusMemberRegex, "_");
+
+ std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
+ createInterface(objServer, path + "/" + dbusName,
+ "xyz.openbmc_project.Configuration." + *type,
+ board, true);
+ // permission is read-write, as since we just created it, must be
+ // runtime modifiable
+ populateInterfaceFromJson(
+ systemConfiguration,
+ jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
+ interface, newData, objServer,
+ sdbusplus::asio::PropertyPermission::readWrite);
+ });
+ tryIfaceInitialize(iface);
+}
+
+std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
+ getDeviceInterfaces(const nlohmann::json& device)
+{
+ return inventory[device["Name"].get<std::string>()];
+}
+
+} // namespace dbus_interface
diff --git a/src/entity_manager/dbus_interface.hpp b/src/entity_manager/dbus_interface.hpp
new file mode 100644
index 0000000..35ea742
--- /dev/null
+++ b/src/entity_manager/dbus_interface.hpp
@@ -0,0 +1,123 @@
+#pragma once
+
+#include "configuration.hpp"
+
+#include <nlohmann/json.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+
+#include <iostream>
+#include <set>
+#include <vector>
+
+namespace dbus_interface
+{
+void tryIfaceInitialize(
+ std::shared_ptr<sdbusplus::asio::dbus_interface>& iface);
+
+std::shared_ptr<sdbusplus::asio::dbus_interface> createInterface(
+ sdbusplus::asio::object_server& objServer, const std::string& path,
+ const std::string& interface, const std::string& parent,
+ bool checkNull = false);
+
+template <typename PropertyType>
+void addArrayToDbus(const std::string& name, const nlohmann::json& array,
+ sdbusplus::asio::dbus_interface* iface,
+ sdbusplus::asio::PropertyPermission permission,
+ nlohmann::json& systemConfiguration,
+ const std::string& jsonPointerString)
+{
+ std::vector<PropertyType> values;
+ for (const auto& property : array)
+ {
+ auto ptr = property.get_ptr<const PropertyType*>();
+ if (ptr != nullptr)
+ {
+ values.emplace_back(*ptr);
+ }
+ }
+
+ if (permission == sdbusplus::asio::PropertyPermission::readOnly)
+ {
+ iface->register_property(name, values);
+ }
+ else
+ {
+ iface->register_property(
+ name, values,
+ [&systemConfiguration,
+ jsonPointerString{std::string(jsonPointerString)}](
+ const std::vector<PropertyType>& newVal,
+ std::vector<PropertyType>& val) {
+ val = newVal;
+ if (!configuration::setJsonFromPointer(jsonPointerString, val,
+ systemConfiguration))
+ {
+ std::cerr << "error setting json field\n";
+ return -1;
+ }
+ if (!configuration::writeJsonFiles(systemConfiguration))
+ {
+ std::cerr << "error setting json file\n";
+ return -1;
+ }
+ return 1;
+ });
+ }
+}
+
+template <typename PropertyType>
+void addProperty(const std::string& name, const PropertyType& value,
+ sdbusplus::asio::dbus_interface* iface,
+ nlohmann::json& systemConfiguration,
+ const std::string& jsonPointerString,
+ sdbusplus::asio::PropertyPermission permission)
+{
+ if (permission == sdbusplus::asio::PropertyPermission::readOnly)
+ {
+ iface->register_property(name, value);
+ return;
+ }
+ iface->register_property(
+ name, value,
+ [&systemConfiguration,
+ jsonPointerString{std::string(jsonPointerString)}](
+ const PropertyType& newVal, PropertyType& val) {
+ val = newVal;
+ if (!configuration::setJsonFromPointer(jsonPointerString, val,
+ systemConfiguration))
+ {
+ std::cerr << "error setting json field\n";
+ return -1;
+ }
+ if (!configuration::writeJsonFiles(systemConfiguration))
+ {
+ std::cerr << "error setting json file\n";
+ return -1;
+ }
+ return 1;
+ });
+}
+
+void createDeleteObjectMethod(
+ const std::string& jsonPointerPath,
+ const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
+ sdbusplus::asio::object_server& objServer,
+ nlohmann::json& systemConfiguration);
+
+void populateInterfaceFromJson(
+ nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
+ std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
+ nlohmann::json& dict, sdbusplus::asio::object_server& objServer,
+ sdbusplus::asio::PropertyPermission permission =
+ sdbusplus::asio::PropertyPermission::readOnly);
+
+void createAddObjectMethod(
+ const std::string& jsonPointerPath, const std::string& path,
+ nlohmann::json& systemConfiguration,
+ sdbusplus::asio::object_server& objServer, const std::string& board);
+
+std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
+ getDeviceInterfaces(const nlohmann::json& device);
+
+} // namespace dbus_interface
diff --git a/src/entity_manager/devices.hpp b/src/entity_manager/devices.hpp
new file mode 100644
index 0000000..28028bb
--- /dev/null
+++ b/src/entity_manager/devices.hpp
@@ -0,0 +1,206 @@
+/*
+// 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 devices.hpp
+
+#pragma once
+#include <boost/container/flat_map.hpp>
+
+namespace devices
+{
+
+struct CmpStr
+{
+ bool operator()(const char* a, const char* b) const
+ {
+ return std::strcmp(a, b) < 0;
+ }
+};
+
+// I2C device drivers may create a /hwmon subdirectory. For example the tmp75
+// driver creates a /sys/bus/i2c/devices/<busnum>-<i2caddr>/hwmon
+// directory. The sensor code relies on the presence of the /hwmon
+// subdirectory to collect sensor readings. Initialization of this subdir is
+// not reliable. I2C devices flagged with hasHWMonDir are tested for correct
+// initialization, and when a failure is detected the device is deleted, and
+// then recreated. The default is to retry 5 times before moving to the next
+// device.
+
+// Devices such as I2C EEPROMs do not generate this file structure. These
+// kinds of devices are flagged using the noHWMonDir enumeration. The
+// expectation is they are created correctly on the first attempt.
+
+// This enumeration class exists to reduce copy/paste errors. It is easy to
+// overlook the trailing parameter in the ExportTemplate structure when it is
+// a simple boolean.
+enum class createsHWMon : bool
+{
+ noHWMonDir,
+ hasHWMonDir
+};
+
+struct ExportTemplate
+{
+ ExportTemplate(const char* params, const char* bus, const char* constructor,
+ const char* destructor, createsHWMon hasHWMonDir) :
+ parameters(params), busPath(bus), add(constructor), remove(destructor),
+ hasHWMonDir(hasHWMonDir) {};
+ const char* parameters;
+ const char* busPath;
+ const char* add;
+ const char* remove;
+ createsHWMon hasHWMonDir;
+};
+
+const boost::container::flat_map<const char*, ExportTemplate, CmpStr>
+ exportTemplates{
+ {{"EEPROM_24C01",
+ ExportTemplate("24c01 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"EEPROM_24C02",
+ ExportTemplate("24c02 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"EEPROM_24C04",
+ ExportTemplate("24c04 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"EEPROM_24C08",
+ ExportTemplate("24c08 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"EEPROM_24C16",
+ ExportTemplate("24c16 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"EEPROM_24C32",
+ ExportTemplate("24c32 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"EEPROM_24C64",
+ ExportTemplate("24c64 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"EEPROM_24C128",
+ ExportTemplate("24c128 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"EEPROM_24C256",
+ ExportTemplate("24c256 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"ADS1015",
+ ExportTemplate("ads1015 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"ADS7828",
+ ExportTemplate("ads7828 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"EEPROM",
+ ExportTemplate("eeprom $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"Gpio", ExportTemplate("$Index", "/sys/class/gpio", "export",
+ "unexport", createsHWMon::noHWMonDir)},
+ {"IPSPS1",
+ ExportTemplate("ipsps1 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::hasHWMonDir)},
+ {"MAX34440",
+ ExportTemplate("max34440 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::hasHWMonDir)},
+ {"PCA9537",
+ ExportTemplate("pca9537 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"PCA9542Mux",
+ ExportTemplate("pca9542 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"PCA9543Mux",
+ ExportTemplate("pca9543 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"PCA9544Mux",
+ ExportTemplate("pca9544 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"PCA9545Mux",
+ ExportTemplate("pca9545 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"PCA9546Mux",
+ ExportTemplate("pca9546 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"PCA9547Mux",
+ ExportTemplate("pca9547 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"PCA9548Mux",
+ ExportTemplate("pca9548 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"PCA9846Mux",
+ ExportTemplate("pca9846 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"PCA9847Mux",
+ ExportTemplate("pca9847 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"PCA9848Mux",
+ ExportTemplate("pca9848 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"PCA9849Mux",
+ ExportTemplate("pca9849 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::noHWMonDir)},
+ {"SIC450",
+ ExportTemplate("sic450 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::hasHWMonDir)},
+ {"Q50SN12072",
+ ExportTemplate("q50sn12072 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::hasHWMonDir)},
+ {"MAX31790",
+ ExportTemplate("max31790 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::hasHWMonDir)},
+ {"PIC32", ExportTemplate("pic32 $Address",
+ "/sys/bus/i2c/devices/i2c-$Bus", "new_device",
+ "delete_device", createsHWMon::hasHWMonDir)},
+ {"INA226",
+ ExportTemplate("ina226 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::hasHWMonDir)},
+ {"RAA229620",
+ ExportTemplate("raa229620 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::hasHWMonDir)},
+ {"RAA229621",
+ ExportTemplate("raa229621 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::hasHWMonDir)},
+ {"PIC32",
+ ExportTemplate("pic32 $Address", "/sys/bus/i2c/devices/i2c-$Bus",
+ "new_device", "delete_device",
+ createsHWMon::hasHWMonDir)}}};
+} // namespace devices
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;
+}
diff --git a/src/entity_manager/entity_manager.hpp b/src/entity_manager/entity_manager.hpp
new file mode 100644
index 0000000..ccd106e
--- /dev/null
+++ b/src/entity_manager/entity_manager.hpp
@@ -0,0 +1,136 @@
+/*
+// 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.hpp
+
+#pragma once
+
+#include "../utils.hpp"
+
+#include <systemd/sd-journal.h>
+
+#include <boost/container/flat_map.hpp>
+#include <nlohmann/json.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+
+#include <string>
+
+inline void logDeviceAdded(const nlohmann::json& record)
+{
+ if (!deviceHasLogging(record))
+ {
+ return;
+ }
+ auto findType = record.find("Type");
+ auto findAsset =
+ record.find("xyz.openbmc_project.Inventory.Decorator.Asset");
+
+ std::string model = "Unknown";
+ std::string type = "Unknown";
+ std::string sn = "Unknown";
+ std::string name = "Unknown";
+
+ if (findType != record.end())
+ {
+ type = findType->get<std::string>();
+ }
+ if (findAsset != record.end())
+ {
+ auto findModel = findAsset->find("Model");
+ auto findSn = findAsset->find("SerialNumber");
+ if (findModel != findAsset->end())
+ {
+ model = findModel->get<std::string>();
+ }
+ if (findSn != findAsset->end())
+ {
+ const std::string* getSn = findSn->get_ptr<const std::string*>();
+ if (getSn != nullptr)
+ {
+ sn = *getSn;
+ }
+ else
+ {
+ sn = findSn->dump();
+ }
+ }
+ }
+
+ auto findName = record.find("Name");
+ if (findName != record.end())
+ {
+ name = findName->get<std::string>();
+ }
+
+ sd_journal_send("MESSAGE=Inventory Added: %s", name.c_str(), "PRIORITY=%i",
+ LOG_INFO, "REDFISH_MESSAGE_ID=%s",
+ "OpenBMC.0.1.InventoryAdded",
+ "REDFISH_MESSAGE_ARGS=%s,%s,%s", model.c_str(),
+ type.c_str(), sn.c_str(), "NAME=%s", name.c_str(), NULL);
+}
+
+inline void logDeviceRemoved(const nlohmann::json& record)
+{
+ if (!deviceHasLogging(record))
+ {
+ return;
+ }
+ auto findType = record.find("Type");
+ auto findAsset =
+ record.find("xyz.openbmc_project.Inventory.Decorator.Asset");
+
+ std::string model = "Unknown";
+ std::string type = "Unknown";
+ std::string sn = "Unknown";
+ std::string name = "Unknown";
+
+ if (findType != record.end())
+ {
+ type = findType->get<std::string>();
+ }
+ if (findAsset != record.end())
+ {
+ auto findModel = findAsset->find("Model");
+ auto findSn = findAsset->find("SerialNumber");
+ if (findModel != findAsset->end())
+ {
+ model = findModel->get<std::string>();
+ }
+ if (findSn != findAsset->end())
+ {
+ const std::string* getSn = findSn->get_ptr<const std::string*>();
+ if (getSn != nullptr)
+ {
+ sn = *getSn;
+ }
+ else
+ {
+ sn = findSn->dump();
+ }
+ }
+ }
+
+ auto findName = record.find("Name");
+ if (findName != record.end())
+ {
+ name = findName->get<std::string>();
+ }
+
+ sd_journal_send("MESSAGE=Inventory Removed: %s", name.c_str(),
+ "PRIORITY=%i", LOG_INFO, "REDFISH_MESSAGE_ID=%s",
+ "OpenBMC.0.1.InventoryRemoved",
+ "REDFISH_MESSAGE_ARGS=%s,%s,%s", model.c_str(),
+ type.c_str(), sn.c_str(), "NAME=%s", name.c_str(), NULL);
+}
diff --git a/src/entity_manager/meson.build b/src/entity_manager/meson.build
new file mode 100644
index 0000000..005a0d0
--- /dev/null
+++ b/src/entity_manager/meson.build
@@ -0,0 +1,23 @@
+executable(
+ 'entity-manager',
+ 'entity_manager.cpp',
+ 'configuration.cpp',
+ '../expression.cpp',
+ 'dbus_interface.cpp',
+ 'perform_scan.cpp',
+ 'perform_probe.cpp',
+ 'overlay.cpp',
+ 'topology.cpp',
+ '../utils.cpp',
+ cpp_args: cpp_args + ['-DBOOST_ASIO_DISABLE_THREADS'],
+ dependencies: [
+ boost,
+ nlohmann_json_dep,
+ phosphor_logging_dep,
+ sdbusplus,
+ valijson,
+ ],
+ install: true,
+ install_dir: installdir,
+)
+
diff --git a/src/entity_manager/overlay.cpp b/src/entity_manager/overlay.cpp
new file mode 100644
index 0000000..8b52fb9
--- /dev/null
+++ b/src/entity_manager/overlay.cpp
@@ -0,0 +1,323 @@
+/*
+// 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 overlay.cpp
+
+#include "overlay.hpp"
+
+#include "../utils.hpp"
+#include "devices.hpp"
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/steady_timer.hpp>
+#include <boost/container/flat_map.hpp>
+#include <boost/container/flat_set.hpp>
+#include <boost/process/child.hpp>
+#include <nlohmann/json.hpp>
+#include <phosphor-logging/lg2.hpp>
+
+#include <filesystem>
+#include <iomanip>
+#include <iostream>
+#include <regex>
+#include <string>
+
+constexpr const char* outputDir = "/tmp/overlays";
+constexpr const char* templateChar = "$";
+constexpr const char* i2CDevsDir = "/sys/bus/i2c/devices";
+constexpr const char* muxSymlinkDir = "/dev/i2c-mux";
+
+const std::regex illegalNameRegex("[^A-Za-z0-9_]");
+
+// helper function to make json types into string
+std::string jsonToString(const nlohmann::json& in)
+{
+ if (in.type() == nlohmann::json::value_t::string)
+ {
+ return in.get<std::string>();
+ }
+ if (in.type() == nlohmann::json::value_t::array)
+ {
+ // remove brackets and comma from array
+ std::string array = in.dump();
+ array = array.substr(1, array.size() - 2);
+ boost::replace_all(array, ",", " ");
+ return array;
+ }
+ return in.dump();
+}
+
+static std::string deviceDirName(uint64_t bus, uint64_t address)
+{
+ std::ostringstream name;
+ name << bus << "-" << std::hex << std::setw(4) << std::setfill('0')
+ << address;
+ return name.str();
+}
+
+void linkMux(const std::string& muxName, uint64_t busIndex, uint64_t address,
+ const std::vector<std::string>& channelNames)
+{
+ std::error_code ec;
+ std::filesystem::path muxSymlinkDirPath(muxSymlinkDir);
+ std::filesystem::create_directory(muxSymlinkDirPath, ec);
+ // ignore error codes here if the directory already exists
+ ec.clear();
+ std::filesystem::path linkDir = muxSymlinkDirPath / muxName;
+ std::filesystem::create_directory(linkDir, ec);
+
+ std::filesystem::path devDir(i2CDevsDir);
+ devDir /= deviceDirName(busIndex, address);
+
+ for (std::size_t channelIndex = 0; channelIndex < channelNames.size();
+ channelIndex++)
+ {
+ const std::string& channelName = channelNames[channelIndex];
+ if (channelName.empty())
+ {
+ continue;
+ }
+
+ std::filesystem::path channelPath =
+ devDir / ("channel-" + std::to_string(channelIndex));
+ if (!is_symlink(channelPath))
+ {
+ std::cerr << channelPath << " for mux channel " << channelName
+ << " doesn't exist!\n";
+ continue;
+ }
+ std::filesystem::path bus = std::filesystem::read_symlink(channelPath);
+
+ std::filesystem::path fp("/dev" / bus.filename());
+ std::filesystem::path link(linkDir / channelName);
+
+ std::filesystem::create_symlink(fp, link, ec);
+ if (ec)
+ {
+ std::cerr << "Failure creating symlink for " << fp << " to " << link
+ << "\n";
+ }
+ }
+}
+
+static int deleteDevice(const std::string& busPath, uint64_t address,
+ const std::string& destructor)
+{
+ std::filesystem::path deviceDestructor(busPath);
+ deviceDestructor /= destructor;
+ std::ofstream deviceFile(deviceDestructor);
+ if (!deviceFile.good())
+ {
+ std::cerr << "Error writing " << deviceDestructor << "\n";
+ return -1;
+ }
+ deviceFile << std::to_string(address);
+ deviceFile.close();
+ return 0;
+}
+
+static int createDevice(const std::string& busPath,
+ const std::string& parameters,
+ const std::string& constructor)
+{
+ std::filesystem::path deviceConstructor(busPath);
+ deviceConstructor /= constructor;
+ std::ofstream deviceFile(deviceConstructor);
+ if (!deviceFile.good())
+ {
+ std::cerr << "Error writing " << deviceConstructor << "\n";
+ return -1;
+ }
+ deviceFile << parameters;
+ deviceFile.close();
+
+ return 0;
+}
+
+static bool deviceIsCreated(const std::string& busPath, uint64_t bus,
+ uint64_t address,
+ const devices::createsHWMon hasHWMonDir)
+{
+ std::filesystem::path dirPath = busPath;
+ dirPath /= deviceDirName(bus, address);
+ if (hasHWMonDir == devices::createsHWMon::hasHWMonDir)
+ {
+ dirPath /= "hwmon";
+ }
+
+ std::error_code ec;
+ // Ignore errors; anything but a clean 'true' is just fine as 'false'
+ return std::filesystem::exists(dirPath, ec);
+}
+
+static int buildDevice(
+ const std::string& name, const std::string& busPath,
+ const std::string& parameters, uint64_t bus, uint64_t address,
+ const std::string& constructor, const std::string& destructor,
+ const devices::createsHWMon hasHWMonDir,
+ std::vector<std::string> channelNames, const size_t retries = 5)
+{
+ if (retries == 0U)
+ {
+ return -1;
+ }
+
+ // If it's already instantiated, we don't need to create it again.
+ if (!deviceIsCreated(busPath, bus, address, hasHWMonDir))
+ {
+ // Try to create the device
+ createDevice(busPath, parameters, constructor);
+
+ // If it didn't work, delete it and try again in 500ms
+ if (!deviceIsCreated(busPath, bus, address, hasHWMonDir))
+ {
+ deleteDevice(busPath, address, destructor);
+
+ std::shared_ptr<boost::asio::steady_timer> createTimer =
+ std::make_shared<boost::asio::steady_timer>(io);
+ createTimer->expires_after(std::chrono::milliseconds(500));
+ createTimer->async_wait(
+ [createTimer, name, busPath, parameters, bus, address,
+ constructor, destructor, hasHWMonDir,
+ channelNames(std::move(channelNames)),
+ retries](const boost::system::error_code& ec) mutable {
+ if (ec)
+ {
+ std::cerr << "Timer error: " << ec << "\n";
+ return -2;
+ }
+ return buildDevice(name, busPath, parameters, bus, address,
+ constructor, destructor, hasHWMonDir,
+ std::move(channelNames), retries - 1);
+ });
+ return -1;
+ }
+ }
+
+ // Link the mux channels if needed once the device is created.
+ if (!channelNames.empty())
+ {
+ linkMux(name, bus, address, channelNames);
+ }
+
+ return 0;
+}
+
+void exportDevice(const std::string& type,
+ const devices::ExportTemplate& exportTemplate,
+ const nlohmann::json& configuration)
+{
+ std::string parameters = exportTemplate.parameters;
+ std::string busPath = exportTemplate.busPath;
+ std::string constructor = exportTemplate.add;
+ std::string destructor = exportTemplate.remove;
+ devices::createsHWMon hasHWMonDir = exportTemplate.hasHWMonDir;
+ std::string name = "unknown";
+ std::optional<uint64_t> bus;
+ std::optional<uint64_t> address;
+ std::vector<std::string> channels;
+
+ for (auto keyPair = configuration.begin(); keyPair != configuration.end();
+ keyPair++)
+ {
+ std::string subsituteString;
+
+ if (keyPair.key() == "Name" &&
+ keyPair.value().type() == nlohmann::json::value_t::string)
+ {
+ subsituteString = std::regex_replace(
+ keyPair.value().get<std::string>(), illegalNameRegex, "_");
+ name = subsituteString;
+ }
+ else
+ {
+ subsituteString = jsonToString(keyPair.value());
+ }
+
+ if (keyPair.key() == "Bus")
+ {
+ bus = keyPair.value().get<uint64_t>();
+ }
+ else if (keyPair.key() == "Address")
+ {
+ address = keyPair.value().get<uint64_t>();
+ }
+ else if (keyPair.key() == "ChannelNames" && type.ends_with("Mux"))
+ {
+ channels = keyPair.value().get<std::vector<std::string>>();
+ }
+ boost::replace_all(parameters, templateChar + keyPair.key(),
+ subsituteString);
+ boost::replace_all(busPath, templateChar + keyPair.key(),
+ subsituteString);
+ }
+
+ if (!bus || !address)
+ {
+ createDevice(busPath, parameters, constructor);
+ return;
+ }
+
+ buildDevice(name, busPath, parameters, *bus, *address, constructor,
+ destructor, hasHWMonDir, std::move(channels));
+}
+
+bool loadOverlays(const nlohmann::json& systemConfiguration)
+{
+ std::filesystem::create_directory(outputDir);
+ for (auto entity = systemConfiguration.begin();
+ entity != systemConfiguration.end(); entity++)
+ {
+ auto findExposes = entity.value().find("Exposes");
+ if (findExposes == entity.value().end() ||
+ findExposes->type() != nlohmann::json::value_t::array)
+ {
+ continue;
+ }
+
+ for (const auto& configuration : *findExposes)
+ {
+ auto findStatus = configuration.find("Status");
+ // status missing is assumed to be 'okay'
+ if (findStatus != configuration.end() && *findStatus == "disabled")
+ {
+ continue;
+ }
+ auto findType = configuration.find("Type");
+ if (findType == configuration.end() ||
+ findType->type() != nlohmann::json::value_t::string)
+ {
+ continue;
+ }
+ std::string type = findType.value().get<std::string>();
+ auto device = devices::exportTemplates.find(type.c_str());
+ if (device != devices::exportTemplates.end())
+ {
+ exportDevice(type, device->second, configuration);
+ continue;
+ }
+
+ // Because many devices are intentionally not exportable,
+ // this error message is not printed in all situations.
+ // If wondering why your device not appearing, add your type to
+ // the exportTemplates array in the devices.hpp file.
+ lg2::debug("Device type {TYPE} not found in export map allowlist",
+ "TYPE", type);
+ }
+ }
+
+ return true;
+}
diff --git a/src/entity_manager/overlay.hpp b/src/entity_manager/overlay.hpp
new file mode 100644
index 0000000..3bc6eb7
--- /dev/null
+++ b/src/entity_manager/overlay.hpp
@@ -0,0 +1,22 @@
+/*
+// 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 overlay.hpp
+
+#pragma once
+#include <nlohmann/json.hpp>
+
+void unloadAllOverlays();
+bool loadOverlays(const nlohmann::json& systemConfiguration);
diff --git a/src/entity_manager/perform_probe.cpp b/src/entity_manager/perform_probe.cpp
new file mode 100644
index 0000000..d7dcd8a
--- /dev/null
+++ b/src/entity_manager/perform_probe.cpp
@@ -0,0 +1,258 @@
+/*
+// 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 perform_probe.cpp
+#include "perform_probe.hpp"
+
+#include "entity_manager.hpp"
+#include "perform_scan.hpp"
+
+#include <boost/algorithm/string/replace.hpp>
+#include <phosphor-logging/lg2.hpp>
+
+#include <regex>
+#include <utility>
+
+// probes dbus interface dictionary for a key with a value that matches a regex
+// When an interface passes a probe, also save its D-Bus path with it.
+bool probeDbus(const std::string& interfaceName,
+ const std::map<std::string, nlohmann::json>& matches,
+ scan::FoundDevices& devices,
+ const std::shared_ptr<scan::PerformScan>& scan, bool& foundProbe)
+{
+ bool foundMatch = false;
+ foundProbe = false;
+
+ for (const auto& [path, interfaces] : scan->dbusProbeObjects)
+ {
+ auto it = interfaces.find(interfaceName);
+ if (it == interfaces.end())
+ {
+ continue;
+ }
+
+ foundProbe = true;
+
+ bool deviceMatches = true;
+ const DBusInterface& interface = it->second;
+
+ for (const auto& [matchProp, matchJSON] : matches)
+ {
+ auto deviceValue = interface.find(matchProp);
+ if (deviceValue != interface.end())
+ {
+ deviceMatches = deviceMatches &&
+ matchProbe(matchJSON, deviceValue->second);
+ }
+ else
+ {
+ // Move on to the next DBus path
+ deviceMatches = false;
+ break;
+ }
+ }
+ if (deviceMatches)
+ {
+ lg2::debug("Found probe match on {PATH} {IFACE}", "PATH", path,
+ "IFACE", interfaceName);
+ devices.emplace_back(interface, path);
+ foundMatch = true;
+ }
+ }
+ return foundMatch;
+}
+
+// default probe entry point, iterates a list looking for specific types to
+// call specific probe functions
+bool doProbe(const std::vector<std::string>& probeCommand,
+ const std::shared_ptr<scan::PerformScan>& scan,
+ scan::FoundDevices& foundDevs)
+{
+ const static std::regex command(R"(\((.*)\))");
+ std::smatch match;
+ bool ret = false;
+ bool matchOne = false;
+ bool cur = true;
+ probe::probe_type_codes lastCommand = probe::probe_type_codes::FALSE_T;
+ bool first = true;
+
+ for (const auto& probe : probeCommand)
+ {
+ probe::FoundProbeTypeT probeType = probe::findProbeType(probe);
+ if (probeType)
+ {
+ switch ((*probeType)->second)
+ {
+ case probe::probe_type_codes::FALSE_T:
+ {
+ cur = false;
+ break;
+ }
+ case probe::probe_type_codes::TRUE_T:
+ {
+ cur = true;
+ break;
+ }
+ case probe::probe_type_codes::MATCH_ONE:
+ {
+ // set current value to last, this probe type shouldn't
+ // affect the outcome
+ cur = ret;
+ matchOne = true;
+ break;
+ }
+ /*case probe::probe_type_codes::AND:
+ break;
+ case probe::probe_type_codes::OR:
+ break;
+ // these are no-ops until the last command switch
+ */
+ case probe::probe_type_codes::FOUND:
+ {
+ if (!std::regex_search(probe, match, command))
+ {
+ std::cerr
+ << "found probe syntax error " << probe << "\n";
+ return false;
+ }
+ std::string commandStr = *(match.begin() + 1);
+ boost::replace_all(commandStr, "'", "");
+ cur = (std::find(scan->passedProbes.begin(),
+ scan->passedProbes.end(), commandStr) !=
+ scan->passedProbes.end());
+ break;
+ }
+ default:
+ {
+ break;
+ }
+ }
+ }
+ // look on dbus for object
+ else
+ {
+ if (!std::regex_search(probe, match, command))
+ {
+ std::cerr << "dbus probe syntax error " << probe << "\n";
+ return false;
+ }
+ std::string commandStr = *(match.begin() + 1);
+ // convert single ticks and single slashes into legal json
+ boost::replace_all(commandStr, "'", "\"");
+ boost::replace_all(commandStr, R"(\)", R"(\\)");
+ auto json = nlohmann::json::parse(commandStr, nullptr, false, true);
+ if (json.is_discarded())
+ {
+ std::cerr << "dbus command syntax error " << commandStr << "\n";
+ return false;
+ }
+ // we can match any (string, variant) property. (string, string)
+ // does a regex
+ std::map<std::string, nlohmann::json> dbusProbeMap =
+ json.get<std::map<std::string, nlohmann::json>>();
+ auto findStart = probe.find('(');
+ if (findStart == std::string::npos)
+ {
+ return false;
+ }
+ bool foundProbe = !!probeType;
+ std::string probeInterface = probe.substr(0, findStart);
+ cur = probeDbus(probeInterface, dbusProbeMap, foundDevs, scan,
+ foundProbe);
+ }
+
+ // some functions like AND and OR only take affect after the
+ // fact
+ if (lastCommand == probe::probe_type_codes::AND)
+ {
+ ret = cur && ret;
+ }
+ else if (lastCommand == probe::probe_type_codes::OR)
+ {
+ ret = cur || ret;
+ }
+
+ if (first)
+ {
+ ret = cur;
+ first = false;
+ }
+ lastCommand = probeType ? (*probeType)->second
+ : probe::probe_type_codes::FALSE_T;
+ }
+
+ // probe passed, but empty device
+ if (ret && foundDevs.empty())
+ {
+ foundDevs.emplace_back(
+ boost::container::flat_map<std::string, DBusValueVariant>{},
+ std::string{});
+ }
+ if (matchOne && ret)
+ {
+ // match the last one
+ auto last = foundDevs.back();
+ foundDevs.clear();
+
+ foundDevs.emplace_back(std::move(last));
+ }
+ return ret;
+}
+
+namespace probe
+{
+
+PerformProbe::PerformProbe(nlohmann::json& recordRef,
+ const std::vector<std::string>& probeCommand,
+ std::string probeName,
+ std::shared_ptr<scan::PerformScan>& scanPtr) :
+ recordRef(recordRef), _probeCommand(probeCommand),
+ probeName(std::move(probeName)), scan(scanPtr)
+{}
+
+PerformProbe::~PerformProbe()
+{
+ scan::FoundDevices foundDevs;
+ if (doProbe(_probeCommand, scan, foundDevs))
+ {
+ scan->updateSystemConfiguration(recordRef, probeName, foundDevs);
+ }
+}
+
+FoundProbeTypeT findProbeType(const std::string& probe)
+{
+ const boost::container::flat_map<const char*, probe_type_codes, CmpStr>
+ probeTypes{{{"FALSE", probe_type_codes::FALSE_T},
+ {"TRUE", probe_type_codes::TRUE_T},
+ {"AND", probe_type_codes::AND},
+ {"OR", probe_type_codes::OR},
+ {"FOUND", probe_type_codes::FOUND},
+ {"MATCH_ONE", probe_type_codes::MATCH_ONE}}};
+
+ boost::container::flat_map<const char*, probe_type_codes,
+ CmpStr>::const_iterator probeType;
+ for (probeType = probeTypes.begin(); probeType != probeTypes.end();
+ ++probeType)
+ {
+ if (probe.find(probeType->first) != std::string::npos)
+ {
+ return probeType;
+ }
+ }
+
+ return std::nullopt;
+}
+
+} // namespace probe
diff --git a/src/entity_manager/perform_probe.hpp b/src/entity_manager/perform_probe.hpp
new file mode 100644
index 0000000..f979c52
--- /dev/null
+++ b/src/entity_manager/perform_probe.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "perform_scan.hpp"
+
+#include <boost/container/flat_map.hpp>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace probe
+{
+struct CmpStr
+{
+ bool operator()(const char* a, const char* b) const
+ {
+ return std::strcmp(a, b) < 0;
+ }
+};
+
+// underscore T for collison with dbus c api
+enum class probe_type_codes
+{
+ FALSE_T,
+ TRUE_T,
+ AND,
+ OR,
+ FOUND,
+ MATCH_ONE
+};
+
+using FoundProbeTypeT = std::optional<boost::container::flat_map<
+ const char*, probe_type_codes, CmpStr>::const_iterator>;
+
+FoundProbeTypeT findProbeType(const std::string& probe);
+
+// this class finds the needed dbus fields and on destruction runs the probe
+struct PerformProbe : std::enable_shared_from_this<PerformProbe>
+{
+ PerformProbe(nlohmann::json& recordRef,
+ const std::vector<std::string>& probeCommand,
+ std::string probeName,
+ std::shared_ptr<scan::PerformScan>& scanPtr);
+ virtual ~PerformProbe();
+
+ nlohmann::json& recordRef;
+ std::vector<std::string> _probeCommand;
+ std::string probeName;
+ std::shared_ptr<scan::PerformScan> scan;
+};
+
+} // namespace probe
diff --git a/src/entity_manager/perform_scan.cpp b/src/entity_manager/perform_scan.cpp
new file mode 100644
index 0000000..d05312d
--- /dev/null
+++ b/src/entity_manager/perform_scan.cpp
@@ -0,0 +1,664 @@
+/*
+// 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 perform_scan.cpp
+#include "perform_scan.hpp"
+
+#include "entity_manager.hpp"
+#include "perform_probe.hpp"
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/asio/steady_timer.hpp>
+#include <boost/container/flat_map.hpp>
+#include <boost/container/flat_set.hpp>
+#include <phosphor-logging/lg2.hpp>
+
+#include <charconv>
+
+/* Hacks from splitting entity_manager.cpp */
+// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
+extern std::shared_ptr<sdbusplus::asio::connection> systemBus;
+extern nlohmann::json lastJson;
+extern void propertiesChangedCallback(
+ nlohmann::json& systemConfiguration,
+ sdbusplus::asio::object_server& objServer);
+// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
+
+using GetSubTreeType = std::vector<
+ std::pair<std::string,
+ std::vector<std::pair<std::string, std::vector<std::string>>>>>;
+
+constexpr const int32_t maxMapperDepth = 0;
+
+struct DBusInterfaceInstance
+{
+ std::string busName;
+ std::string path;
+ std::string interface;
+};
+
+void getInterfaces(
+ const DBusInterfaceInstance& instance,
+ const std::vector<std::shared_ptr<probe::PerformProbe>>& probeVector,
+ const std::shared_ptr<scan::PerformScan>& scan, size_t retries = 5)
+{
+ if (retries == 0U)
+ {
+ std::cerr << "retries exhausted on " << instance.busName << " "
+ << instance.path << " " << instance.interface << "\n";
+ return;
+ }
+
+ systemBus->async_method_call(
+ [instance, scan, probeVector,
+ retries](boost::system::error_code& errc, const DBusInterface& resp) {
+ if (errc)
+ {
+ std::cerr << "error calling getall on " << instance.busName
+ << " " << instance.path << " "
+ << instance.interface << "\n";
+
+ auto timer = std::make_shared<boost::asio::steady_timer>(io);
+ timer->expires_after(std::chrono::seconds(2));
+
+ timer->async_wait([timer, instance, scan, probeVector,
+ retries](const boost::system::error_code&) {
+ getInterfaces(instance, probeVector, scan, retries - 1);
+ });
+ return;
+ }
+
+ scan->dbusProbeObjects[instance.path][instance.interface] = resp;
+ },
+ instance.busName, instance.path, "org.freedesktop.DBus.Properties",
+ "GetAll", instance.interface);
+}
+
+static void registerCallback(nlohmann::json& systemConfiguration,
+ sdbusplus::asio::object_server& objServer,
+ const std::string& path)
+{
+ static boost::container::flat_map<std::string, sdbusplus::bus::match_t>
+ dbusMatches;
+
+ auto find = dbusMatches.find(path);
+ if (find != dbusMatches.end())
+ {
+ return;
+ }
+
+ std::function<void(sdbusplus::message_t & message)> eventHandler =
+ [&](sdbusplus::message_t&) {
+ propertiesChangedCallback(systemConfiguration, objServer);
+ };
+
+ sdbusplus::bus::match_t match(
+ static_cast<sdbusplus::bus_t&>(*systemBus),
+ "type='signal',member='PropertiesChanged',path='" + path + "'",
+ eventHandler);
+ dbusMatches.emplace(path, std::move(match));
+}
+
+static void processDbusObjects(
+ std::vector<std::shared_ptr<probe::PerformProbe>>& probeVector,
+ const std::shared_ptr<scan::PerformScan>& scan,
+ const GetSubTreeType& interfaceSubtree)
+{
+ for (const auto& [path, object] : interfaceSubtree)
+ {
+ // Get a PropertiesChanged callback for all interfaces on this path.
+ registerCallback(scan->_systemConfiguration, scan->objServer, path);
+
+ for (const auto& [busname, ifaces] : object)
+ {
+ for (const std::string& iface : ifaces)
+ {
+ // The 3 default org.freedeskstop interfaces (Peer,
+ // Introspectable, and Properties) are returned by
+ // the mapper but don't have properties, so don't bother
+ // with the GetAll call to save some cycles.
+ if (!boost::algorithm::starts_with(iface, "org.freedesktop"))
+ {
+ getInterfaces({busname, path, iface}, probeVector, scan);
+ }
+ }
+ }
+ }
+}
+
+// Populates scan->dbusProbeObjects with all interfaces and properties
+// for the paths that own the interfaces passed in.
+void findDbusObjects(
+ std::vector<std::shared_ptr<probe::PerformProbe>>&& probeVector,
+ boost::container::flat_set<std::string>&& interfaces,
+ const std::shared_ptr<scan::PerformScan>& scan, size_t retries = 5)
+{
+ // Filter out interfaces already obtained.
+ for (const auto& [path, probeInterfaces] : scan->dbusProbeObjects)
+ {
+ for (const auto& [interface, _] : probeInterfaces)
+ {
+ interfaces.erase(interface);
+ }
+ }
+ if (interfaces.empty())
+ {
+ return;
+ }
+
+ // find all connections in the mapper that expose a specific type
+ systemBus->async_method_call(
+ [interfaces, probeVector{std::move(probeVector)}, scan,
+ retries](boost::system::error_code& ec,
+ const GetSubTreeType& interfaceSubtree) mutable {
+ if (ec)
+ {
+ if (ec.value() == ENOENT)
+ {
+ return; // wasn't found by mapper
+ }
+ std::cerr << "Error communicating to mapper.\n";
+
+ if (retries == 0U)
+ {
+ // if we can't communicate to the mapper something is very
+ // wrong
+ std::exit(EXIT_FAILURE);
+ }
+
+ auto timer = std::make_shared<boost::asio::steady_timer>(io);
+ timer->expires_after(std::chrono::seconds(10));
+
+ timer->async_wait(
+ [timer, interfaces{std::move(interfaces)}, scan,
+ probeVector{std::move(probeVector)},
+ retries](const boost::system::error_code&) mutable {
+ findDbusObjects(std::move(probeVector),
+ std::move(interfaces), scan,
+ retries - 1);
+ });
+ return;
+ }
+
+ processDbusObjects(probeVector, scan, interfaceSubtree);
+ },
+ "xyz.openbmc_project.ObjectMapper",
+ "/xyz/openbmc_project/object_mapper",
+ "xyz.openbmc_project.ObjectMapper", "GetSubTree", "/", maxMapperDepth,
+ interfaces);
+}
+
+static std::string getRecordName(const DBusInterface& probe,
+ const std::string& probeName)
+{
+ if (probe.empty())
+ {
+ return probeName;
+ }
+
+ // use an array so alphabetical order from the flat_map is maintained
+ auto device = nlohmann::json::array();
+ for (const auto& devPair : probe)
+ {
+ device.push_back(devPair.first);
+ std::visit([&device](auto&& v) { device.push_back(v); },
+ devPair.second);
+ }
+
+ // hashes are hard to distinguish, use the non-hashed version if we want
+ // debug
+ // return probeName + device.dump();
+
+ return std::to_string(std::hash<std::string>{}(probeName + device.dump()));
+}
+
+scan::PerformScan::PerformScan(nlohmann::json& systemConfiguration,
+ nlohmann::json& missingConfigurations,
+ std::list<nlohmann::json>& configurations,
+ sdbusplus::asio::object_server& objServerIn,
+ std::function<void()>&& callback) :
+ _systemConfiguration(systemConfiguration),
+ _missingConfigurations(missingConfigurations),
+ _configurations(configurations), objServer(objServerIn),
+ _callback(std::move(callback))
+{}
+
+static void pruneRecordExposes(nlohmann::json& record)
+{
+ auto findExposes = record.find("Exposes");
+ if (findExposes == record.end())
+ {
+ return;
+ }
+
+ auto copy = nlohmann::json::array();
+ for (auto& expose : *findExposes)
+ {
+ if (!expose.is_null())
+ {
+ copy.emplace_back(expose);
+ }
+ }
+ *findExposes = copy;
+}
+
+static void recordDiscoveredIdentifiers(
+ std::set<nlohmann::json>& usedNames, std::list<size_t>& indexes,
+ const std::string& probeName, const nlohmann::json& record)
+{
+ size_t indexIdx = probeName.find('$');
+ if (indexIdx == std::string::npos)
+ {
+ return;
+ }
+
+ auto nameIt = record.find("Name");
+ if (nameIt == record.end())
+ {
+ std::cerr << "Last JSON Illegal\n";
+ return;
+ }
+
+ int index = 0;
+ auto str = nameIt->get<std::string>().substr(indexIdx);
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
+ const char* endPtr = str.data() + str.size();
+ auto [p, ec] = std::from_chars(str.data(), endPtr, index);
+ if (ec != std::errc())
+ {
+ return; // non-numeric replacement
+ }
+
+ usedNames.insert(nameIt.value());
+
+ auto usedIt = std::find(indexes.begin(), indexes.end(), index);
+ if (usedIt != indexes.end())
+ {
+ indexes.erase(usedIt);
+ }
+}
+
+static bool extractExposeActionRecordNames(std::vector<std::string>& matches,
+ nlohmann::json::iterator& keyPair)
+{
+ if (keyPair.value().is_string())
+ {
+ matches.emplace_back(keyPair.value());
+ return true;
+ }
+
+ if (keyPair.value().is_array())
+ {
+ for (const auto& value : keyPair.value())
+ {
+ if (!value.is_string())
+ {
+ std::cerr << "Value is invalid type " << value << "\n";
+ break;
+ }
+ matches.emplace_back(value);
+ }
+
+ return true;
+ }
+
+ std::cerr << "Value is invalid type " << keyPair.key() << "\n";
+
+ return false;
+}
+
+static std::optional<std::vector<std::string>::iterator> findExposeActionRecord(
+ std::vector<std::string>& matches, const nlohmann::json& record)
+{
+ const auto& name = (record)["Name"].get_ref<const std::string&>();
+ auto compare = [&name](const std::string& s) { return s == name; };
+ auto matchIt = std::find_if(matches.begin(), matches.end(), compare);
+
+ if (matchIt == matches.end())
+ {
+ return std::nullopt;
+ }
+
+ return matchIt;
+}
+
+static void applyBindExposeAction(nlohmann::json& exposedObject,
+ nlohmann::json& expose,
+ const std::string& propertyName)
+{
+ if (boost::starts_with(propertyName, "Bind"))
+ {
+ std::string bind = propertyName.substr(sizeof("Bind") - 1);
+ exposedObject["Status"] = "okay";
+ expose[bind] = exposedObject;
+ }
+}
+
+static void applyDisableExposeAction(nlohmann::json& exposedObject,
+ const std::string& propertyName)
+{
+ if (propertyName == "DisableNode")
+ {
+ exposedObject["Status"] = "disabled";
+ }
+}
+
+static void applyConfigExposeActions(
+ std::vector<std::string>& matches, nlohmann::json& expose,
+ const std::string& propertyName, nlohmann::json& configExposes)
+{
+ for (auto& exposedObject : configExposes)
+ {
+ auto match = findExposeActionRecord(matches, exposedObject);
+ if (match)
+ {
+ matches.erase(*match);
+ applyBindExposeAction(exposedObject, expose, propertyName);
+ applyDisableExposeAction(exposedObject, propertyName);
+ }
+ }
+}
+
+static void applyExposeActions(
+ nlohmann::json& systemConfiguration, const std::string& recordName,
+ nlohmann::json& expose, nlohmann::json::iterator& keyPair)
+{
+ bool isBind = boost::starts_with(keyPair.key(), "Bind");
+ bool isDisable = keyPair.key() == "DisableNode";
+ bool isExposeAction = isBind || isDisable;
+
+ if (!isExposeAction)
+ {
+ return;
+ }
+
+ std::vector<std::string> matches;
+
+ if (!extractExposeActionRecordNames(matches, keyPair))
+ {
+ return;
+ }
+
+ for (const auto& [configId, config] : systemConfiguration.items())
+ {
+ // don't disable ourselves
+ if (isDisable && configId == recordName)
+ {
+ continue;
+ }
+
+ auto configListFind = config.find("Exposes");
+ if (configListFind == config.end())
+ {
+ continue;
+ }
+
+ if (!configListFind->is_array())
+ {
+ continue;
+ }
+
+ applyConfigExposeActions(matches, expose, keyPair.key(),
+ *configListFind);
+ }
+
+ if (!matches.empty())
+ {
+ std::cerr << "configuration file dependency error, could not find "
+ << keyPair.key() << " " << keyPair.value() << "\n";
+ }
+}
+
+static std::string generateDeviceName(
+ const std::set<nlohmann::json>& usedNames, const DBusObject& dbusObject,
+ size_t foundDeviceIdx, const std::string& nameTemplate,
+ std::optional<std::string>& replaceStr)
+{
+ nlohmann::json copyForName = {{"Name", nameTemplate}};
+ nlohmann::json::iterator copyIt = copyForName.begin();
+ std::optional<std::string> replaceVal =
+ templateCharReplace(copyIt, dbusObject, foundDeviceIdx, replaceStr);
+
+ if (!replaceStr && replaceVal)
+ {
+ if (usedNames.find(copyIt.value()) != usedNames.end())
+ {
+ replaceStr = replaceVal;
+ copyForName = {{"Name", nameTemplate}};
+ copyIt = copyForName.begin();
+ templateCharReplace(copyIt, dbusObject, foundDeviceIdx, replaceStr);
+ }
+ }
+
+ if (replaceStr)
+ {
+ std::cerr << "Duplicates found, replacing " << *replaceStr
+ << " with found device index.\n Consider "
+ "fixing template to not have duplicates\n";
+ }
+
+ return copyIt.value();
+}
+
+void scan::PerformScan::updateSystemConfiguration(
+ const nlohmann::json& recordRef, const std::string& probeName,
+ FoundDevices& foundDevices)
+{
+ _passed = true;
+ passedProbes.push_back(probeName);
+
+ std::set<nlohmann::json> usedNames;
+ std::list<size_t> indexes(foundDevices.size());
+ std::iota(indexes.begin(), indexes.end(), 1);
+
+ // copy over persisted configurations and make sure we remove
+ // indexes that are already used
+ for (auto itr = foundDevices.begin(); itr != foundDevices.end();)
+ {
+ std::string recordName = getRecordName(itr->interface, probeName);
+
+ auto record = _systemConfiguration.find(recordName);
+ if (record == _systemConfiguration.end())
+ {
+ record = lastJson.find(recordName);
+ if (record == lastJson.end())
+ {
+ itr++;
+ continue;
+ }
+
+ pruneRecordExposes(*record);
+
+ _systemConfiguration[recordName] = *record;
+ }
+ _missingConfigurations.erase(recordName);
+
+ // We've processed the device, remove it and advance the
+ // iterator
+ itr = foundDevices.erase(itr);
+ recordDiscoveredIdentifiers(usedNames, indexes, probeName, *record);
+ }
+
+ std::optional<std::string> replaceStr;
+
+ DBusObject emptyObject;
+ DBusInterface emptyInterface;
+ emptyObject.emplace(std::string{}, emptyInterface);
+
+ for (const auto& [foundDevice, path] : foundDevices)
+ {
+ // Need all interfaces on this path so that template
+ // substitutions can be done with any of the contained
+ // properties. If the probe that passed didn't use an
+ // interface, such as if it was just TRUE, then
+ // templateCharReplace will just get passed in an empty
+ // map.
+ auto objectIt = dbusProbeObjects.find(path);
+ const DBusObject& dbusObject = (objectIt == dbusProbeObjects.end())
+ ? emptyObject
+ : objectIt->second;
+
+ nlohmann::json record = recordRef;
+ std::string recordName = getRecordName(foundDevice, probeName);
+ size_t foundDeviceIdx = indexes.front();
+ indexes.pop_front();
+
+ // check name first so we have no duplicate names
+ auto getName = record.find("Name");
+ if (getName == record.end())
+ {
+ std::cerr << "Record Missing Name! " << record.dump();
+ continue; // this should be impossible at this level
+ }
+
+ std::string deviceName = generateDeviceName(
+ usedNames, dbusObject, foundDeviceIdx, getName.value(), replaceStr);
+ getName.value() = deviceName;
+ usedNames.insert(deviceName);
+
+ for (auto keyPair = record.begin(); keyPair != record.end(); keyPair++)
+ {
+ if (keyPair.key() != "Name")
+ {
+ templateCharReplace(keyPair, dbusObject, foundDeviceIdx,
+ replaceStr);
+ }
+ }
+
+ // insert into configuration temporarily to be able to
+ // reference ourselves
+
+ _systemConfiguration[recordName] = record;
+
+ auto findExpose = record.find("Exposes");
+ if (findExpose == record.end())
+ {
+ continue;
+ }
+
+ for (auto& expose : *findExpose)
+ {
+ for (auto keyPair = expose.begin(); keyPair != expose.end();
+ keyPair++)
+ {
+ templateCharReplace(keyPair, dbusObject, foundDeviceIdx,
+ replaceStr);
+
+ applyExposeActions(_systemConfiguration, recordName, expose,
+ keyPair);
+ }
+ }
+
+ // overwrite ourselves with cleaned up version
+ _systemConfiguration[recordName] = record;
+ _missingConfigurations.erase(recordName);
+ }
+}
+
+void scan::PerformScan::run()
+{
+ boost::container::flat_set<std::string> dbusProbeInterfaces;
+ std::vector<std::shared_ptr<probe::PerformProbe>> dbusProbePointers;
+
+ for (auto it = _configurations.begin(); it != _configurations.end();)
+ {
+ // check for poorly formatted fields, probe must be an array
+ auto findProbe = it->find("Probe");
+ if (findProbe == it->end())
+ {
+ std::cerr << "configuration file missing probe:\n " << *it << "\n";
+ it = _configurations.erase(it);
+ continue;
+ }
+
+ auto findName = it->find("Name");
+ if (findName == it->end())
+ {
+ std::cerr << "configuration file missing name:\n " << *it << "\n";
+ it = _configurations.erase(it);
+ continue;
+ }
+ std::string probeName = *findName;
+
+ if (std::find(passedProbes.begin(), passedProbes.end(), probeName) !=
+ passedProbes.end())
+ {
+ it = _configurations.erase(it);
+ continue;
+ }
+
+ nlohmann::json& recordRef = *it;
+ nlohmann::json probeCommand;
+ if ((*findProbe).type() != nlohmann::json::value_t::array)
+ {
+ probeCommand = nlohmann::json::array();
+ probeCommand.push_back(*findProbe);
+ }
+ else
+ {
+ probeCommand = *findProbe;
+ }
+
+ // store reference to this to children to makes sure we don't get
+ // destroyed too early
+ auto thisRef = shared_from_this();
+ auto probePointer = std::make_shared<probe::PerformProbe>(
+ recordRef, probeCommand, probeName, thisRef);
+
+ // parse out dbus probes by discarding other probe types, store in a
+ // map
+ for (const nlohmann::json& probeJson : probeCommand)
+ {
+ const std::string* probe = probeJson.get_ptr<const std::string*>();
+ if (probe == nullptr)
+ {
+ std::cerr << "Probe statement wasn't a string, can't parse";
+ continue;
+ }
+ if (probe::findProbeType(*probe))
+ {
+ continue;
+ }
+ // syntax requires probe before first open brace
+ auto findStart = probe->find('(');
+ std::string interface = probe->substr(0, findStart);
+ dbusProbeInterfaces.emplace(interface);
+ dbusProbePointers.emplace_back(probePointer);
+ }
+ it++;
+ }
+
+ // probe vector stores a shared_ptr to each PerformProbe that cares
+ // about a dbus interface
+ findDbusObjects(std::move(dbusProbePointers),
+ std::move(dbusProbeInterfaces), shared_from_this());
+}
+
+scan::PerformScan::~PerformScan()
+{
+ if (_passed)
+ {
+ auto nextScan = std::make_shared<PerformScan>(
+ _systemConfiguration, _missingConfigurations, _configurations,
+ objServer, std::move(_callback));
+ nextScan->passedProbes = std::move(passedProbes);
+ nextScan->dbusProbeObjects = std::move(dbusProbeObjects);
+ nextScan->run();
+ }
+ else
+ {
+ _callback();
+ }
+}
diff --git a/src/entity_manager/perform_scan.hpp b/src/entity_manager/perform_scan.hpp
new file mode 100644
index 0000000..ae57582
--- /dev/null
+++ b/src/entity_manager/perform_scan.hpp
@@ -0,0 +1,48 @@
+#pragma once
+
+#include "../utils.hpp"
+
+#include <systemd/sd-journal.h>
+
+#include <boost/container/flat_map.hpp>
+#include <nlohmann/json.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+
+#include <functional>
+#include <list>
+#include <vector>
+
+namespace scan
+{
+struct DBusDeviceDescriptor
+{
+ DBusInterface interface;
+ std::string path;
+};
+
+using FoundDevices = std::vector<DBusDeviceDescriptor>;
+
+struct PerformScan : std::enable_shared_from_this<PerformScan>
+{
+ PerformScan(nlohmann::json& systemConfiguration,
+ nlohmann::json& missingConfigurations,
+ std::list<nlohmann::json>& configurations,
+ sdbusplus::asio::object_server& objServer,
+ std::function<void()>&& callback);
+
+ void updateSystemConfiguration(const nlohmann::json& recordRef,
+ const std::string& probeName,
+ FoundDevices& foundDevices);
+ void run();
+ virtual ~PerformScan();
+ nlohmann::json& _systemConfiguration;
+ nlohmann::json& _missingConfigurations;
+ std::list<nlohmann::json> _configurations;
+ sdbusplus::asio::object_server& objServer;
+ std::function<void()> _callback;
+ bool _passed = false;
+ MapperGetSubTreeResponse dbusProbeObjects;
+ std::vector<std::string> passedProbes;
+};
+
+} // namespace scan
diff --git a/src/entity_manager/topology.cpp b/src/entity_manager/topology.cpp
new file mode 100644
index 0000000..ed827ad
--- /dev/null
+++ b/src/entity_manager/topology.cpp
@@ -0,0 +1,139 @@
+#include "topology.hpp"
+
+#include <iostream>
+
+void Topology::addBoard(const std::string& path, const std::string& boardType,
+ const std::string& boardName,
+ const nlohmann::json& exposesItem)
+{
+ auto findType = exposesItem.find("Type");
+ if (findType == exposesItem.end())
+ {
+ return;
+ }
+
+ boardNames.try_emplace(boardName, path);
+
+ PortType exposesType = findType->get<std::string>();
+
+ if (exposesType == "DownstreamPort")
+ {
+ auto findConnectsTo = exposesItem.find("ConnectsToType");
+ if (findConnectsTo == exposesItem.end())
+ {
+ std::cerr << "Board at path " << path
+ << " is missing ConnectsToType" << std::endl;
+ return;
+ }
+ PortType connectsTo = findConnectsTo->get<std::string>();
+
+ downstreamPorts[connectsTo].emplace_back(path);
+ boardTypes[path] = boardType;
+ auto findPoweredBy = exposesItem.find("PowerPort");
+ if (findPoweredBy != exposesItem.end())
+ {
+ powerPaths.insert(path);
+ }
+ }
+ else if (exposesType.ends_with("Port"))
+ {
+ upstreamPorts[exposesType].emplace_back(path);
+ boardTypes[path] = boardType;
+ }
+}
+
+std::unordered_map<std::string, std::vector<Association>> Topology::getAssocs(
+ const std::map<Path, BoardName>& boards)
+{
+ std::unordered_map<std::string, std::vector<Association>> result;
+
+ // look at each upstream port type
+ for (const auto& upstreamPortPair : upstreamPorts)
+ {
+ auto downstreamMatch = downstreamPorts.find(upstreamPortPair.first);
+
+ if (downstreamMatch == downstreamPorts.end())
+ {
+ // no match
+ continue;
+ }
+
+ for (const Path& upstream : upstreamPortPair.second)
+ {
+ if (boardTypes[upstream] == "Chassis" ||
+ boardTypes[upstream] == "Board")
+ {
+ for (const Path& downstream : downstreamMatch->second)
+ {
+ // The downstream path must be one we care about.
+ if (boards.find(downstream) != boards.end())
+ {
+ result[downstream].emplace_back("contained_by",
+ "containing", upstream);
+ if (powerPaths.find(downstream) != powerPaths.end())
+ {
+ result[upstream].emplace_back(
+ "powered_by", "powering", downstream);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+void Topology::remove(const std::string& boardName)
+{
+ // Remove the board from boardNames, and then using the path
+ // found in boardNames remove it from upstreamPorts and
+ // downstreamPorts.
+ auto boardFind = boardNames.find(boardName);
+ if (boardFind == boardNames.end())
+ {
+ return;
+ }
+
+ std::string boardPath = boardFind->second;
+
+ boardNames.erase(boardFind);
+
+ for (auto it = upstreamPorts.begin(); it != upstreamPorts.end();)
+ {
+ auto pathIt =
+ std::find(it->second.begin(), it->second.end(), boardPath);
+ if (pathIt != it->second.end())
+ {
+ it->second.erase(pathIt);
+ }
+
+ if (it->second.empty())
+ {
+ it = upstreamPorts.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ for (auto it = downstreamPorts.begin(); it != downstreamPorts.end();)
+ {
+ auto pathIt =
+ std::find(it->second.begin(), it->second.end(), boardPath);
+ if (pathIt != it->second.end())
+ {
+ it->second.erase(pathIt);
+ }
+
+ if (it->second.empty())
+ {
+ it = downstreamPorts.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+}
diff --git a/src/entity_manager/topology.hpp b/src/entity_manager/topology.hpp
new file mode 100644
index 0000000..816704a
--- /dev/null
+++ b/src/entity_manager/topology.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <nlohmann/json.hpp>
+
+#include <set>
+#include <unordered_map>
+
+using Association = std::tuple<std::string, std::string, std::string>;
+
+class Topology
+{
+ public:
+ explicit Topology() = default;
+
+ void addBoard(const std::string& path, const std::string& boardType,
+ const std::string& boardName,
+ const nlohmann::json& exposesItem);
+ std::unordered_map<std::string, std::vector<Association>> getAssocs(
+ const std::map<std::string, std::string>& boards);
+ void remove(const std::string& boardName);
+
+ private:
+ using Path = std::string;
+ using BoardType = std::string;
+ using BoardName = std::string;
+ using PortType = std::string;
+
+ std::unordered_map<PortType, std::vector<Path>> upstreamPorts;
+ std::unordered_map<PortType, std::vector<Path>> downstreamPorts;
+ std::set<Path> powerPaths;
+ std::unordered_map<Path, BoardType> boardTypes;
+ std::unordered_map<BoardName, Path> boardNames;
+};