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;
+};