cleanup: move EM service code in EM directory

Improving repository structure increases maintainability.

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