Add mctpreactor for dynamic configuration of MCTP networks

While mctpd[1] may see heavy use in projects such as OpenBMC, it
implements generic functionality necessary to operate MCTP as a
protocol. It therefore should be easy to use in other contexts, and so
it feels unwise to embed OpenBMC-specific details in its implementation.

Conversely, entity-manager's scope is to expose inventory and board
configuration. It externalises all other responsibilities for the sake
of stability and maintenance. While entity-manager is central to
OpenBMC's implementation and has little use in other contexts, embedding
details of how to configure mctpd in entity-manager exceeds its scope.

Thus we reach the design point of mctpreactor, an intermediary process
that encapsulates OpenBMC-specific and mctpd-specific behaviors to
constrain their dispersion in either direction. The design-point was
reached via discussion at [2].

mctpreactor tracks instances of transport-specific MCTP device
configurations[3] appearing as a result of inventory changes, and uses
them to assign endpoint IDs via mctpd.

The lifecycle of an MCTP device can be quite dynamic - mctpd provides
behaviors to recover[4] or remove endpoints from the network. Their
presence cannot be assumed. mctpreactor handles these events: If
a device is removed at the MCTP layer (as it may be unresponsive),
mctpreactor will periodically attempt to re-establish it as an endpoint
so long as the associated configuration on the entity-manager inventory
object remains exposed.

[1]: https://github.com/CodeConstruct/mctp/
[2]: https://github.com/CodeConstruct/mctp/pull/17
[3]: https://gerrit.openbmc.org/c/openbmc/entity-manager/+/70628
[4]: https://github.com/CodeConstruct/mctp/blob/7ec2f8daa3a8948066390aee621d6afa03f6ecd9/docs/endpoint-recovery.md

Change-Id: I5e362cf6e5ce80ce282bab48d912a1038003e236
Signed-off-by: Andrew Jeffery <andrew@codeconstruct.com.au>
diff --git a/src/mctp/MCTPReactorMain.cpp b/src/mctp/MCTPReactorMain.cpp
new file mode 100644
index 0000000..9abc0fc
--- /dev/null
+++ b/src/mctp/MCTPReactorMain.cpp
@@ -0,0 +1,249 @@
+#include "MCTPEndpoint.hpp"
+#include "MCTPReactor.hpp"
+#include "Utils.hpp"
+
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/post.hpp>
+#include <boost/asio/steady_timer.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/message.hpp>
+#include <sdbusplus/message/native_types.hpp>
+
+#include <chrono>
+#include <cstdlib>
+#include <format>
+#include <functional>
+#include <map>
+#include <memory>
+#include <optional>
+#include <set>
+#include <stdexcept>
+#include <system_error>
+#include <vector>
+
+PHOSPHOR_LOG2_USING;
+
+class DBusAssociationServer : public AssociationServer
+{
+  public:
+    DBusAssociationServer() = delete;
+    DBusAssociationServer(const DBusAssociationServer&) = delete;
+    DBusAssociationServer(DBusAssociationServer&&) = delete;
+    explicit DBusAssociationServer(
+        const std::shared_ptr<sdbusplus::asio::connection>& connection) :
+        server(connection)
+    {
+        server.add_manager("/xyz/openbmc_project/mctp");
+    }
+    ~DBusAssociationServer() override = default;
+    DBusAssociationServer& operator=(const DBusAssociationServer&) = delete;
+    DBusAssociationServer& operator=(DBusAssociationServer&&) = delete;
+
+    void associate(const std::string& path,
+                   const std::vector<Association>& associations) override
+    {
+        auto [entry, _] = objects.emplace(
+            path, server.add_interface(path, association::interface));
+        std::shared_ptr<sdbusplus::asio::dbus_interface> iface = entry->second;
+        iface->register_property("Associations", associations);
+        iface->initialize();
+    }
+
+    void disassociate(const std::string& path) override
+    {
+        const auto entry = objects.find(path);
+        if (entry == objects.end())
+        {
+            throw std::logic_error(std::format(
+                "Attempted to untrack path that was not tracked: {}", path));
+        }
+        std::shared_ptr<sdbusplus::asio::dbus_interface> iface = entry->second;
+        server.remove_interface(entry->second);
+        objects.erase(entry);
+    }
+
+  private:
+    std::shared_ptr<sdbusplus::asio::connection> connection;
+    sdbusplus::asio::object_server server;
+    std::map<std::string, std::shared_ptr<sdbusplus::asio::dbus_interface>>
+        objects;
+};
+
+static std::shared_ptr<MCTPDevice> deviceFromConfig(
+    const std::shared_ptr<sdbusplus::asio::connection>& connection,
+    const SensorData& config)
+{
+    try
+    {
+        std::optional<SensorBaseConfigMap> iface;
+        // NOLINTNEXTLINE(bugprone-assignment-in-if-condition)
+        if ((iface = I2CMCTPDDevice::match(config)))
+        {
+            return I2CMCTPDDevice::from(connection, *iface);
+        }
+    }
+    catch (const std::invalid_argument& ex)
+    {
+        error("Unable to create device: {EXCEPTION}", "EXCEPTION", ex);
+    }
+
+    return {};
+}
+
+static void addInventory(
+    const std::shared_ptr<sdbusplus::asio::connection>& connection,
+    const std::shared_ptr<MCTPReactor>& reactor, sdbusplus::message_t& msg)
+{
+    auto [path,
+          exposed] = msg.unpack<sdbusplus::message::object_path, SensorData>();
+    try
+    {
+        reactor->manageMCTPDevice(path, deviceFromConfig(connection, exposed));
+    }
+    catch (const std::logic_error& e)
+    {
+        error(
+            "Addition of inventory at '{INVENTORY_PATH}' caused an invalid program state: {EXCEPTION}",
+            "INVENTORY_PATH", path, "EXCEPTION", e);
+    }
+    catch (const std::system_error& e)
+    {
+        error(
+            "Failed to manage device described by inventory at '{INVENTORY_PATH}: {EXCEPTION}'",
+            "INVENTORY_PATH", path, "EXCEPTION", e);
+    }
+}
+
+static void removeInventory(const std::shared_ptr<MCTPReactor>& reactor,
+                            sdbusplus::message_t& msg)
+{
+    auto [path, removed] =
+        msg.unpack<sdbusplus::message::object_path, std::set<std::string>>();
+    try
+    {
+        if (I2CMCTPDDevice::match(removed))
+        {
+            reactor->unmanageMCTPDevice(path.str);
+        }
+    }
+    catch (const std::logic_error& e)
+    {
+        error(
+            "Removal of inventory at '{INVENTORY_PATH}' caused an invalid program state: {EXCEPTION}",
+            "INVENTORY_PATH", path, "EXCEPTION", e);
+    }
+    catch (const std::system_error& e)
+    {
+        error(
+            "Failed to unmanage device described by inventory at '{INVENTORY_PATH}: {EXCEPTION}'",
+            "INVENTORY_PATH", path, "EXCEPTION", e);
+    }
+}
+
+static void manageMCTPEntity(
+    const std::shared_ptr<sdbusplus::asio::connection>& connection,
+    const std::shared_ptr<MCTPReactor>& reactor, ManagedObjectType& entities)
+{
+    for (const auto& [path, config] : entities)
+    {
+        try
+        {
+            reactor->manageMCTPDevice(path,
+                                      deviceFromConfig(connection, config));
+        }
+        catch (const std::logic_error& e)
+        {
+            error(
+                "Addition of inventory at '{INVENTORY_PATH}' caused an invalid program state: {EXCEPTION}",
+                "INVENTORY_PATH", path, "EXCEPTION", e);
+        }
+        catch (const std::system_error& e)
+        {
+            error(
+                "Failed to manage device described by inventory at '{INVENTORY_PATH}: {EXCEPTION}'",
+                "INVENTORY_PATH", path, "EXCEPTION", e);
+        }
+    }
+}
+
+static void exitReactor(boost::asio::io_context* io, sdbusplus::message_t& msg)
+{
+    auto name = msg.unpack<std::string>();
+    info("Shutting down mctpreactor, lost dependency '{SERVICE_NAME}'",
+         "SERVICE_NAME", name);
+    io->stop();
+}
+
+int main()
+{
+    constexpr std::chrono::seconds period(5);
+
+    boost::asio::io_context io;
+    auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
+    DBusAssociationServer associationServer(systemBus);
+    auto reactor = std::make_shared<MCTPReactor>(associationServer);
+    boost::asio::steady_timer clock(io);
+
+    std::function<void(const boost::system::error_code&)> alarm =
+        [&](const boost::system::error_code& ec) {
+            if (ec)
+            {
+                return;
+            }
+            clock.expires_after(period);
+            clock.async_wait(alarm);
+            reactor->tick();
+        };
+    clock.expires_after(period);
+    clock.async_wait(alarm);
+
+    using namespace sdbusplus::bus::match;
+
+    const std::string entityManagerNameLostSpec =
+        rules::nameOwnerChanged("xyz.openbmc_project.EntityManager");
+
+    auto entityManagerNameLostMatch = sdbusplus::bus::match_t(
+        static_cast<sdbusplus::bus_t&>(*systemBus), entityManagerNameLostSpec,
+        std::bind_front(exitReactor, &io));
+
+    const std::string mctpdNameLostSpec =
+        rules::nameOwnerChanged("xyz.openbmc_project.MCTP");
+
+    auto mctpdNameLostMatch = sdbusplus::bus::match_t(
+        static_cast<sdbusplus::bus_t&>(*systemBus), mctpdNameLostSpec,
+        std::bind_front(exitReactor, &io));
+
+    const std::string interfacesRemovedMatchSpec =
+        rules::sender("xyz.openbmc_project.EntityManager") +
+        // Trailing slash on path: Listen for signals on the inventory subtree
+        rules::interfacesRemovedAtPath("/xyz/openbmc_project/inventory/");
+
+    auto interfacesRemovedMatch = sdbusplus::bus::match_t(
+        static_cast<sdbusplus::bus_t&>(*systemBus), interfacesRemovedMatchSpec,
+        std::bind_front(removeInventory, reactor));
+
+    const std::string interfacesAddedMatchSpec =
+        rules::sender("xyz.openbmc_project.EntityManager") +
+        // Trailing slash on path: Listen for signals on the inventory subtree
+        rules::interfacesAddedAtPath("/xyz/openbmc_project/inventory/");
+
+    auto interfacesAddedMatch = sdbusplus::bus::match_t(
+        static_cast<sdbusplus::bus_t&>(*systemBus), interfacesAddedMatchSpec,
+        std::bind_front(addInventory, systemBus, reactor));
+
+    systemBus->request_name("xyz.openbmc_project.MCTPReactor");
+
+    boost::asio::post(io, [reactor, systemBus]() {
+        auto gsc = std::make_shared<GetSensorConfiguration>(
+            systemBus, std::bind_front(manageMCTPEntity, systemBus, reactor));
+        gsc->getConfiguration({"MCTPI2CTarget"});
+    });
+
+    io.run();
+
+    return EXIT_SUCCESS;
+}