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/MCTPDeviceRepository.hpp b/src/mctp/MCTPDeviceRepository.hpp
new file mode 100644
index 0000000..475acbe
--- /dev/null
+++ b/src/mctp/MCTPDeviceRepository.hpp
@@ -0,0 +1,77 @@
+#pragma once
+
+#include "MCTPEndpoint.hpp"
+
+class MCTPDeviceRepository
+{
+  private:
+    // FIXME: Ugh, hack. Figure out a better data structure?
+    std::map<std::string, std::shared_ptr<MCTPDevice>> devices;
+
+    auto lookup(const std::shared_ptr<MCTPDevice>& device)
+    {
+        auto pred = [&device](const auto& it) { return it.second == device; };
+        return std::ranges::find_if(devices, pred);
+    }
+
+  public:
+    MCTPDeviceRepository() = default;
+    MCTPDeviceRepository(const MCTPDeviceRepository&) = delete;
+    MCTPDeviceRepository(MCTPDeviceRepository&&) = delete;
+    ~MCTPDeviceRepository() = default;
+
+    MCTPDeviceRepository& operator=(const MCTPDeviceRepository&) = delete;
+    MCTPDeviceRepository& operator=(MCTPDeviceRepository&&) = delete;
+
+    void add(const std::string& inventory,
+             const std::shared_ptr<MCTPDevice>& device)
+    {
+        auto [entry, fresh] = devices.emplace(inventory, device);
+        if (!fresh && entry->second.get() != device.get())
+        {
+            throw std::system_error(
+                std::make_error_code(std::errc::device_or_resource_busy),
+                std::format("Tried to add entry for existing device: {}",
+                            device->describe()));
+        }
+    }
+
+    void remove(const std::shared_ptr<MCTPDevice>& device)
+    {
+        auto entry = lookup(device);
+        if (entry == devices.end())
+        {
+            throw std::system_error(
+                std::make_error_code(std::errc::no_such_device),
+                std::format("Trying to remove unknown device: {}",
+                            entry->second->describe()));
+        }
+        devices.erase(entry);
+    }
+
+    bool contains(const std::shared_ptr<MCTPDevice>& device)
+    {
+        return lookup(device) != devices.end();
+    }
+
+    std::optional<std::string>
+        inventoryFor(const std::shared_ptr<MCTPDevice>& device)
+    {
+        auto entry = lookup(device);
+        if (entry == devices.end())
+        {
+            return {};
+        }
+        return entry->first;
+    }
+
+    std::shared_ptr<MCTPDevice> deviceFor(const std::string& inventory)
+    {
+        auto entry = devices.find(inventory);
+        if (entry == devices.end())
+        {
+            return {};
+        }
+        return entry->second;
+    }
+};