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