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/MCTPReactor.cpp b/src/mctp/MCTPReactor.cpp
new file mode 100644
index 0000000..9d32682
--- /dev/null
+++ b/src/mctp/MCTPReactor.cpp
@@ -0,0 +1,209 @@
+#include "MCTPReactor.hpp"
+
+#include "MCTPDeviceRepository.hpp"
+#include "MCTPEndpoint.hpp"
+#include "Utils.hpp"
+
+#include <boost/system/detail/error_code.hpp>
+#include <phosphor-logging/lg2.hpp>
+
+#include <cstdlib>
+#include <memory>
+#include <optional>
+#include <string>
+#include <system_error>
+#include <utility>
+#include <vector>
+
+PHOSPHOR_LOG2_USING;
+
+void MCTPReactor::deferSetup(const std::shared_ptr<MCTPDevice>& dev)
+{
+    debug("Deferring setup for MCTP device at [ {MCTP_DEVICE} ]", "MCTP_DEVICE",
+          dev->describe());
+
+    deferred.emplace(dev);
+}
+
+void MCTPReactor::untrackEndpoint(const std::shared_ptr<MCTPEndpoint>& ep)
+{
+    server.disassociate(MCTPDEndpoint::path(ep));
+}
+
+void MCTPReactor::trackEndpoint(const std::shared_ptr<MCTPEndpoint>& ep)
+{
+    info("Added MCTP endpoint to device: [ {MCTP_ENDPOINT} ]", "MCTP_ENDPOINT",
+         ep->describe());
+
+    ep->subscribe(
+        // Degraded
+        [](const std::shared_ptr<MCTPEndpoint>& ep) {
+            debug("Endpoint entered degraded state: [ {MCTP_ENDPOINT} ]",
+                  "MCTP_ENDPOINT", ep->describe());
+        },
+        // Available
+        [](const std::shared_ptr<MCTPEndpoint>& ep) {
+            debug("Endpoint entered available state: [ {MCTP_ENDPOINT} ]",
+                  "MCTP_ENDPOINT", ep->describe());
+        },
+        // Removed
+        [weak{weak_from_this()}](const std::shared_ptr<MCTPEndpoint>& ep) {
+            info("Removed MCTP endpoint from device: [ {MCTP_ENDPOINT} ]",
+                 "MCTP_ENDPOINT", ep->describe());
+            if (auto self = weak.lock())
+            {
+                self->untrackEndpoint(ep);
+                // Only defer the setup if we know inventory is still present
+                if (self->devices.contains(ep->device()))
+                {
+                    self->deferSetup(ep->device());
+                }
+            }
+            else
+            {
+                info(
+                    "The reactor object was destroyed concurrent to the removal of the remove match for the endpoint '{MCTP_ENDPOINT}'",
+                    "MCTP_ENDPOINT", ep->describe());
+            }
+        });
+
+    // Proxy-host the association back to the inventory at the same path as the
+    // endpoint in mctpd.
+    //
+    // clang-format off
+    // ```
+    // # busctl call xyz.openbmc_project.ObjectMapper /xyz/openbmc_project/object_mapper xyz.openbmc_project.ObjectMapper GetAssociatedSubTree ooias /xyz/openbmc_project/mctp/1/9/configured_by / 0 1 xyz.openbmc_project.Configuration.MCTPDevice
+    // a{sa{sas}} 1 "/xyz/openbmc_project/inventory/system/nvme/NVMe_1/NVMe_1_Temp" 1 "xyz.openbmc_project.EntityManager" 1 "xyz.openbmc_project.Configuration.MCTPDevice"
+    // ```
+    // clang-format on
+    std::optional<std::string> item = devices.inventoryFor(ep->device());
+    if (!item)
+    {
+        error("Inventory missing for endpoint: [ {MCTP_ENDPOINT} ]",
+              "MCTP_ENDPOINT", ep->describe());
+        return;
+    }
+    std::vector<Association> associations{
+        {"configured_by", "configures", *item}};
+    server.associate(MCTPDEndpoint::path(ep), associations);
+}
+
+void MCTPReactor::setupEndpoint(const std::shared_ptr<MCTPDevice>& dev)
+{
+    debug(
+        "Attempting to setup up MCTP endpoint for device at [ {MCTP_DEVICE} ]",
+        "MCTP_DEVICE", dev->describe());
+    dev->setup([weak{weak_from_this()},
+                dev](const std::error_code& ec,
+                     const std::shared_ptr<MCTPEndpoint>& ep) mutable {
+        auto self = weak.lock();
+        if (!self)
+        {
+            info(
+                "The reactor object was destroyed concurrent to the completion of the endpoint setup for '{MCTP_ENDPOINT}'",
+                "MCTP_ENDPOINT", ep->describe());
+            return;
+        }
+
+        if (ec)
+        {
+            debug(
+                "Setup failed for MCTP device at [ {MCTP_DEVICE} ]: {ERROR_MESSAGE}",
+                "MCTP_DEVICE", dev->describe(), "ERROR_MESSAGE", ec.message());
+
+            self->deferSetup(dev);
+            return;
+        }
+
+        try
+        {
+            self->trackEndpoint(ep);
+        }
+        catch (const MCTPException& e)
+        {
+            error("Failed to track endpoint '{MCTP_ENDPOINT}': {EXCEPTION}",
+                  "MCTP_ENDPOINT", ep->describe(), "EXCEPTION", e);
+            self->deferSetup(dev);
+        }
+    });
+}
+
+void MCTPReactor::tick()
+{
+    auto toSetup = std::exchange(deferred, {});
+    for (const auto& entry : toSetup)
+    {
+        setupEndpoint(entry);
+    }
+}
+
+void MCTPReactor::manageMCTPDevice(const std::string& path,
+                                   const std::shared_ptr<MCTPDevice>& device)
+{
+    if (!device)
+    {
+        return;
+    }
+
+    try
+    {
+        devices.add(path, device);
+        debug("MCTP device inventory added at '{INVENTORY_PATH}'",
+              "INVENTORY_PATH", path);
+        setupEndpoint(device);
+    }
+    catch (const std::system_error& e)
+    {
+        if (e.code() != std::errc::device_or_resource_busy)
+        {
+            throw e;
+        }
+
+        auto current = devices.deviceFor(path);
+        if (!current)
+        {
+            warning(
+                "Invalid state: Failed to manage device for inventory at '{INVENTORY_PATH}', but the inventory item is unrecognised",
+                "INVENTORY_PATH", path);
+            return;
+        }
+
+        // TODO: Ensure remove completion happens-before add. For now this
+        // happens unsynchronised. Make some noise about it.
+        warning(
+            "Unsynchronised endpoint reinitialsation due to configuration change at '{INVENTORY_PATH}': Removing '{MCTP_DEVICE}'",
+            "INVENTORY_PATH", path, "MCTP_DEVICE", current->describe());
+
+        unmanageMCTPDevice(path);
+
+        devices.add(path, device);
+
+        // Pray (this is the unsynchronised bit)
+        deferSetup(device);
+    }
+}
+
+void MCTPReactor::unmanageMCTPDevice(const std::string& path)
+{
+    auto device = devices.deviceFor(path);
+    if (!device)
+    {
+        debug("Unrecognised inventory item: {INVENTORY_PATH}", "INVENTORY_PATH",
+              path);
+        return;
+    }
+
+    debug("MCTP device inventory removed at '{INVENTORY_PATH}'",
+          "INVENTORY_PATH", path);
+
+    deferred.erase(device);
+
+    // Remove the device from the repository before notifying the device itself
+    // of removal so we don't defer its setup
+    devices.remove(device);
+
+    debug("Stopping management of MCTP device at [ {MCTP_DEVICE} ]",
+          "MCTP_DEVICE", device->describe());
+
+    device->remove();
+}