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;
+ }
+};
diff --git a/src/mctp/MCTPEndpoint.cpp b/src/mctp/MCTPEndpoint.cpp
new file mode 100644
index 0000000..a8fcc91
--- /dev/null
+++ b/src/mctp/MCTPEndpoint.cpp
@@ -0,0 +1,407 @@
+#include "MCTPEndpoint.hpp"
+
+#include "Utils.hpp"
+#include "VariantVisitors.hpp"
+
+#include <bits/fs_dir.h>
+
+#include <boost/system/detail/errc.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/exception.hpp>
+#include <sdbusplus/message.hpp>
+#include <sdbusplus/message/native_types.hpp>
+
+#include <cassert>
+#include <charconv>
+#include <cstdint>
+#include <exception>
+#include <filesystem>
+#include <format>
+#include <functional>
+#include <map>
+#include <memory>
+#include <optional>
+#include <set>
+#include <stdexcept>
+#include <string>
+#include <system_error>
+#include <utility>
+#include <variant>
+#include <vector>
+
+PHOSPHOR_LOG2_USING;
+
+static constexpr const char* mctpdBusName = "xyz.openbmc_project.MCTP";
+static constexpr const char* mctpdControlPath = "/xyz/openbmc_project/mctp";
+static constexpr const char* mctpdControlInterface =
+ "au.com.CodeConstruct.MCTP";
+static constexpr const char* mctpdEndpointControlInterface =
+ "au.com.CodeConstruct.MCTP.Endpoint";
+
+MCTPDDevice::MCTPDDevice(
+ const std::shared_ptr<sdbusplus::asio::connection>& connection,
+ const std::string& interface, const std::vector<uint8_t>& physaddr) :
+ connection(connection), interface(interface), physaddr(physaddr)
+{}
+
+void MCTPDDevice::onEndpointInterfacesRemoved(
+ const std::weak_ptr<MCTPDDevice>& weak, const std::string& objpath,
+ sdbusplus::message_t& msg)
+{
+ auto path = msg.unpack<sdbusplus::message::object_path>();
+ assert(path.str == objpath);
+
+ auto removedIfaces = msg.unpack<std::set<std::string>>();
+ if (!removedIfaces.contains(mctpdEndpointControlInterface))
+ {
+ return;
+ }
+
+ if (auto self = weak.lock())
+ {
+ self->endpointRemoved();
+ }
+ else
+ {
+ info(
+ "Device for inventory at '{INVENTORY_PATH}' was destroyed concurrent to endpoint removal",
+ "INVENTORY_PATH", objpath);
+ }
+}
+
+void MCTPDDevice::finaliseEndpoint(
+ const std::string& objpath, uint8_t eid, int network,
+ std::function<void(const std::error_code& ec,
+ const std::shared_ptr<MCTPEndpoint>& ep)>& added)
+{
+ const auto matchSpec =
+ sdbusplus::bus::match::rules::interfacesRemovedAtPath(objpath);
+ removeMatch = std::make_unique<sdbusplus::bus::match_t>(
+ *connection, matchSpec,
+ std::bind_front(MCTPDDevice::onEndpointInterfacesRemoved,
+ weak_from_this(), objpath));
+ endpoint = std::make_shared<MCTPDEndpoint>(shared_from_this(), connection,
+ objpath, network, eid);
+ added({}, endpoint);
+}
+
+void MCTPDDevice::setup(
+ std::function<void(const std::error_code& ec,
+ const std::shared_ptr<MCTPEndpoint>& ep)>&& added)
+{
+ // Use a lambda to separate state validation from business logic,
+ // where the business logic for a successful setup() is encoded in
+ // MctpdDevice::finaliseEndpoint()
+ auto onSetup = [weak{weak_from_this()}, added{std::move(added)}](
+ const boost::system::error_code& ec, uint8_t eid,
+ int network, const std::string& objpath,
+ bool allocated [[maybe_unused]]) mutable {
+ if (ec)
+ {
+ added(ec, {});
+ return;
+ }
+
+ if (auto self = weak.lock())
+ {
+ self->finaliseEndpoint(objpath, eid, network, added);
+ }
+ else
+ {
+ info(
+ "Device object for inventory at '{INVENTORY_PATH}' was destroyed concurrent to completion of its endpoint setup",
+ "INVENTORY_PATH", objpath);
+ }
+ };
+ connection->async_method_call(onSetup, mctpdBusName, mctpdControlPath,
+ mctpdControlInterface, "AssignEndpoint",
+ interface, physaddr);
+}
+
+void MCTPDDevice::endpointRemoved()
+{
+ if (endpoint)
+ {
+ debug("Endpoint removed @ [ {MCTP_ENDPOINT} ]", "MCTP_ENDPOINT",
+ endpoint->describe());
+ removeMatch.reset();
+ endpoint->removed();
+ endpoint.reset();
+ }
+}
+
+void MCTPDDevice::remove()
+{
+ if (endpoint)
+ {
+ debug("Removing endpoint @ [ {MCTP_ENDPOINT} ]", "MCTP_ENDPOINT",
+ endpoint->describe());
+ endpoint->remove();
+ }
+}
+
+std::string MCTPDDevice::describe() const
+{
+ std::string description = std::format("interface: {}", interface);
+ if (!physaddr.empty())
+ {
+ description.append(", address: 0x [ ");
+ auto it = physaddr.begin();
+ for (; it != physaddr.end() - 1; it++)
+ {
+ description.append(std::format("{:02x} ", *it));
+ }
+ description.append(std::format("{:02x} ]", *it));
+ }
+ return description;
+}
+
+std::string MCTPDEndpoint::path(const std::shared_ptr<MCTPEndpoint>& ep)
+{
+ return std::format("/xyz/openbmc_project/mctp/{}/{}", ep->network(),
+ ep->eid());
+}
+
+void MCTPDEndpoint::onMctpEndpointChange(sdbusplus::message_t& msg)
+{
+ auto [iface, changed, _] =
+ msg.unpack<std::string, std::map<std::string, BasicVariantType>,
+ std::vector<std::string>>();
+ if (iface != mctpdEndpointControlInterface)
+ {
+ return;
+ }
+
+ auto it = changed.find("Connectivity");
+ if (it == changed.end())
+ {
+ return;
+ }
+
+ updateEndpointConnectivity(std::get<std::string>(it->second));
+}
+
+void MCTPDEndpoint::updateEndpointConnectivity(const std::string& connectivity)
+{
+ if (connectivity == "Degraded")
+ {
+ if (notifyDegraded)
+ {
+ notifyDegraded(shared_from_this());
+ }
+ }
+ else if (connectivity == "Available")
+ {
+ if (notifyAvailable)
+ {
+ notifyAvailable(shared_from_this());
+ }
+ }
+ else
+ {
+ debug("Unrecognised connectivity state: '{CONNECTIVITY_STATE}'",
+ "CONNECTIVITY_STATE", connectivity);
+ }
+}
+
+int MCTPDEndpoint::network() const
+{
+ return mctp.network;
+}
+
+uint8_t MCTPDEndpoint::eid() const
+{
+ return mctp.eid;
+}
+
+void MCTPDEndpoint::subscribe(Event&& degraded, Event&& available,
+ Event&& removed)
+{
+ const auto matchSpec =
+ sdbusplus::bus::match::rules::propertiesChangedNamespace(
+ objpath.str, mctpdEndpointControlInterface);
+
+ this->notifyDegraded = std::move(degraded);
+ this->notifyAvailable = std::move(available);
+ this->notifyRemoved = std::move(removed);
+
+ try
+ {
+ connectivityMatch.emplace(
+ static_cast<sdbusplus::bus_t&>(*connection), matchSpec,
+ [weak{weak_from_this()},
+ path{objpath.str}](sdbusplus::message_t& msg) {
+ if (auto self = weak.lock())
+ {
+ self->onMctpEndpointChange(msg);
+ }
+ else
+ {
+ info(
+ "The endpoint for the device at inventory path '{INVENTORY_PATH}' was destroyed concurrent to the removal of its state change match",
+ "INVENTORY_PATH", path);
+ }
+ });
+ connection->async_method_call(
+ [weak{weak_from_this()},
+ path{objpath.str}](const boost::system::error_code& ec,
+ const std::variant<std::string>& value) {
+ if (ec)
+ {
+ debug(
+ "Failed to get current connectivity state: {ERROR_MESSAGE}",
+ "ERROR_MESSAGE", ec.message(), "ERROR_CATEGORY",
+ ec.category().name(), "ERROR_CODE", ec.value());
+ return;
+ }
+
+ if (auto self = weak.lock())
+ {
+ const std::string& connectivity =
+ std::get<std::string>(value);
+ self->updateEndpointConnectivity(connectivity);
+ }
+ else
+ {
+ info(
+ "The endpoint for the device at inventory path '{INVENTORY_PATH}' was destroyed concurrent to the completion of its connectivity state query",
+ "INVENTORY_PATH", path);
+ }
+ },
+ mctpdBusName, objpath.str, "org.freedesktop.DBus.Properties", "Get",
+ mctpdEndpointControlInterface, "Connectivity");
+ }
+ catch (const sdbusplus::exception::SdBusError& err)
+ {
+ this->notifyDegraded = nullptr;
+ this->notifyAvailable = nullptr;
+ this->notifyRemoved = nullptr;
+ std::throw_with_nested(
+ MCTPException("Failed to register connectivity signal match"));
+ }
+}
+
+void MCTPDEndpoint::remove()
+{
+ connection->async_method_call(
+ [self{shared_from_this()}](const boost::system::error_code& ec) {
+ if (ec)
+ {
+ debug("Failed to remove endpoint @ [ {MCTP_ENDPOINT} ]",
+ "MCTP_ENDPOINT", self->describe());
+ return;
+ }
+ },
+ mctpdBusName, objpath.str, mctpdEndpointControlInterface, "Remove");
+}
+
+void MCTPDEndpoint::removed()
+{
+ if (notifyRemoved)
+ {
+ notifyRemoved(shared_from_this());
+ }
+}
+
+std::string MCTPDEndpoint::describe() const
+{
+ return std::format("network: {}, EID: {} | {}", mctp.network, mctp.eid,
+ dev->describe());
+}
+
+std::shared_ptr<MCTPDevice> MCTPDEndpoint::device() const
+{
+ return dev;
+}
+
+std::optional<SensorBaseConfigMap>
+ I2CMCTPDDevice::match(const SensorData& config)
+{
+ auto iface = config.find(configInterfaceName(configType));
+ if (iface == config.end())
+ {
+ return std::nullopt;
+ }
+ return iface->second;
+}
+
+bool I2CMCTPDDevice::match(const std::set<std::string>& interfaces)
+{
+ return interfaces.contains(configInterfaceName(configType));
+}
+
+std::shared_ptr<I2CMCTPDDevice> I2CMCTPDDevice::from(
+ const std::shared_ptr<sdbusplus::asio::connection>& connection,
+ const SensorBaseConfigMap& iface)
+{
+ auto mType = iface.find("Type");
+ if (mType == iface.end())
+ {
+ throw std::invalid_argument(
+ "No 'Type' member found for provided configuration object");
+ }
+
+ auto type = std::visit(VariantToStringVisitor(), mType->second);
+ if (type != configType)
+ {
+ throw std::invalid_argument("Not an SMBus device");
+ }
+
+ auto mAddress = iface.find("Address");
+ auto mBus = iface.find("Bus");
+ auto mName = iface.find("Name");
+ if (mAddress == iface.end() || mBus == iface.end() || mName == iface.end())
+ {
+ throw std::invalid_argument(
+ "Configuration object violates MCTPI2CTarget schema");
+ }
+
+ auto sAddress = std::visit(VariantToStringVisitor(), mAddress->second);
+ std::uint8_t address{};
+ auto [aptr, aec] = std::from_chars(
+ sAddress.data(), sAddress.data() + sAddress.size(), address);
+ if (aec != std::errc{})
+ {
+ throw std::invalid_argument("Bad device address");
+ }
+
+ auto sBus = std::visit(VariantToStringVisitor(), mBus->second);
+ int bus{};
+ auto [bptr,
+ bec] = std::from_chars(sBus.data(), sBus.data() + sBus.size(), bus);
+ if (bec != std::errc{})
+ {
+ throw std::invalid_argument("Bad bus index");
+ }
+
+ try
+ {
+ return std::make_shared<I2CMCTPDDevice>(connection, bus, address);
+ }
+ catch (const MCTPException& ex)
+ {
+ warning(
+ "Failed to create I2CMCTPDDevice at [ bus: {I2C_BUS}, address: {I2C_ADDRESS} ]: {EXCEPTION}",
+ "I2C_BUS", bus, "I2C_ADDRESS", address, "EXCEPTION", ex);
+ return {};
+ }
+}
+
+std::string I2CMCTPDDevice::interfaceFromBus(int bus)
+{
+ std::filesystem::path netdir =
+ std::format("/sys/bus/i2c/devices/i2c-{}/net", bus);
+ std::error_code ec;
+ std::filesystem::directory_iterator it(netdir, ec);
+ if (ec || it == std::filesystem::end(it))
+ {
+ error("No net device associated with I2C bus {I2C_BUS} at {NET_DEVICE}",
+ "I2C_BUS", bus, "NET_DEVICE", netdir);
+ throw MCTPException("Bus is not configured as an MCTP interface");
+ }
+
+ return it->path().filename();
+}
diff --git a/src/mctp/MCTPEndpoint.hpp b/src/mctp/MCTPEndpoint.hpp
new file mode 100644
index 0000000..02322d1
--- /dev/null
+++ b/src/mctp/MCTPEndpoint.hpp
@@ -0,0 +1,321 @@
+#pragma once
+
+#include "Utils.hpp"
+
+#include <boost/asio/steady_timer.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/message.hpp>
+#include <sdbusplus/message/native_types.hpp>
+
+#include <cstdint>
+#include <iostream>
+
+/**
+ * @file
+ * @brief Abstract and concrete classes representing MCTP concepts and
+ * behaviours.
+ */
+
+/**
+ * @brief An exception type that may be thrown by implementations of the MCTP
+ * abstract classes.
+ *
+ * This exception should be the basis for all exceptions thrown out of the MCTP
+ * APIs, and should capture any other exceptions that occur.
+ */
+class MCTPException : public std::exception
+{
+ public:
+ MCTPException() = delete;
+ explicit MCTPException(const char* desc) : desc(desc) {}
+ const char* what() const noexcept override
+ {
+ return desc;
+ }
+
+ private:
+ const char* desc;
+};
+
+/**
+ * @brief An enum of the MCTP transports described in DSP0239 v1.10.0 Section 7.
+ *
+ * https://www.dmtf.org/sites/default/files/standards/documents/DSP0239_1.10.0.pdf
+ */
+enum class MCTPTransport
+{
+ Reserved = 0x00,
+ SMBus = 0x01,
+};
+
+/**
+ * @brief Captures properties of MCTP interfaces.
+ *
+ * https://github.com/CodeConstruct/mctp/blob/v1.1/src/mctp.c#L672-L703
+ */
+struct MCTPInterface
+{
+ std::string name;
+ MCTPTransport transport;
+
+ auto operator<=>(const MCTPInterface& r) const = default;
+};
+
+class MCTPDevice;
+
+/**
+ * @brief Captures the behaviour of an endpoint at the MCTP layer
+ *
+ * The lifetime of an instance of MctpEndpoint is proportional to the lifetime
+ * of the endpoint configuration. If an endpoint is deconfigured such that its
+ * device has no assigned EID, then any related MctpEndpoint instance must be
+ * destructed as a consequence.
+ */
+class MCTPEndpoint
+{
+ public:
+ using Event = std::function<void(const std::shared_ptr<MCTPEndpoint>& ep)>;
+ using Result = std::function<void(const std::error_code& ec)>;
+
+ virtual ~MCTPEndpoint() = default;
+
+ /**
+ * @return The Linux network ID of the network in which the endpoint
+ participates
+ */
+ virtual int network() const = 0;
+
+ /**
+ * @return The MCTP endpoint ID of the endpoint in its network
+ */
+ virtual uint8_t eid() const = 0;
+
+ /**
+ * @brief Subscribe to events produced by an endpoint object across its
+ * lifecycle
+ *
+ * @param degraded The callback to execute when the MCTP layer indicates the
+ * endpoint is unresponsive
+ *
+ * @param available The callback to execute when the MCTP layer indicates
+ * that communication with the degraded endpoint has been
+ * recovered
+ *
+ * @param removed The callback to execute when the MCTP layer indicates the
+ * endpoint has been removed.
+ */
+ virtual void subscribe(Event&& degraded, Event&& available,
+ Event&& removed) = 0;
+
+ /**
+ * @brief Remove the endpoint from its associated network
+ */
+ virtual void remove() = 0;
+
+ /**
+ * @return A formatted string representing the endpoint in terms of its
+ * address properties
+ */
+ virtual std::string describe() const = 0;
+
+ /**
+ * @return A shared pointer to the device instance associated with the
+ * endpoint.
+ */
+ virtual std::shared_ptr<MCTPDevice> device() const = 0;
+};
+
+/**
+ * @brief Represents an MCTP-capable device on a bus.
+ *
+ * It is often known that an MCTP-capable device exists on a bus prior to the
+ * MCTP stack configuring the device for communication. MctpDevice exposes the
+ * ability to set-up the endpoint device for communication.
+ *
+ * The lifetime of an MctpDevice instance is proportional to the existence of an
+ * MCTP-capable device in the system. If a device represented by an MctpDevice
+ * instance is removed from the system then any related MctpDevice instance must
+ * be destructed a consequence.
+ *
+ * Successful set-up of the device as an endpoint yields an MctpEndpoint
+ * instance. The lifetime of the MctpEndpoint instance produced must not exceed
+ * the lifetime of its parent MctpDevice.
+ */
+class MCTPDevice
+{
+ public:
+ virtual ~MCTPDevice() = default;
+
+ /**
+ * @brief Configure the device for MCTP communication
+ *
+ * @param added The callback to invoke once the setup process has
+ * completed. The provided error code @p ec must be
+ * checked as the request may not have succeeded. If
+ * the request was successful then @p ep contains a
+ * valid MctpEndpoint instance.
+ */
+ virtual void
+ setup(std::function<void(const std::error_code& ec,
+ const std::shared_ptr<MCTPEndpoint>& ep)>&&
+ added) = 0;
+
+ /**
+ * @brief Remove the device and any associated endpoint from the MCTP stack.
+ */
+ virtual void remove() = 0;
+
+ /**
+ * @return A formatted string representing the device in terms of its
+ * address properties.
+ */
+ virtual std::string describe() const = 0;
+};
+
+class MCTPDDevice;
+
+/**
+ * @brief An implementation of MctpEndpoint in terms of the D-Bus interfaces
+ * exposed by @c mctpd.
+ *
+ * The lifetime of an MctpdEndpoint is proportional to the lifetime of the
+ * endpoint object exposed by @c mctpd. The lifecycle of @c mctpd endpoint
+ * objects is discussed here:
+ *
+ * https://github.com/CodeConstruct/mctp/pull/23/files#diff-00234f5f2543b8b9b8a419597e55121fe1cc57cf1c7e4ff9472bed83096bd28e
+ */
+class MCTPDEndpoint :
+ public MCTPEndpoint,
+ public std::enable_shared_from_this<MCTPDEndpoint>
+{
+ public:
+ static std::string path(const std::shared_ptr<MCTPEndpoint>& ep);
+
+ MCTPDEndpoint() = delete;
+ MCTPDEndpoint(
+ const std::shared_ptr<MCTPDDevice>& dev,
+ const std::shared_ptr<sdbusplus::asio::connection>& connection,
+ sdbusplus::message::object_path objpath, int network, uint8_t eid) :
+ dev(dev), connection(connection), objpath(std::move(objpath)),
+ mctp{network, eid}
+ {}
+ MCTPDEndpoint& McptdEndpoint(const MCTPDEndpoint& other) = delete;
+ MCTPDEndpoint(MCTPDEndpoint&& other) noexcept = default;
+ ~MCTPDEndpoint() override = default;
+
+ int network() const override;
+ uint8_t eid() const override;
+ void subscribe(Event&& degraded, Event&& available,
+ Event&& removed) override;
+ void remove() override;
+
+ std::string describe() const override;
+
+ std::shared_ptr<MCTPDevice> device() const override;
+
+ /**
+ * @brief Indicate the endpoint has been removed
+ *
+ * Called from the implementation of MctpdDevice for resource cleanup
+ * prior to destruction. Resource cleanup is delegated by invoking the
+ * notifyRemoved() callback. As the actions may be abitrary we avoid
+ * invoking notifyRemoved() in the destructor.
+ */
+ void removed();
+
+ private:
+ std::shared_ptr<MCTPDDevice> dev;
+ std::shared_ptr<sdbusplus::asio::connection> connection;
+ sdbusplus::message::object_path objpath;
+ struct
+ {
+ int network;
+ uint8_t eid;
+ } mctp;
+ MCTPEndpoint::Event notifyAvailable;
+ MCTPEndpoint::Event notifyDegraded;
+ MCTPEndpoint::Event notifyRemoved;
+ std::optional<sdbusplus::bus::match_t> connectivityMatch;
+
+ void onMctpEndpointChange(sdbusplus::message_t& msg);
+ void updateEndpointConnectivity(const std::string& connectivity);
+};
+
+/**
+ * @brief An implementation of MctpDevice in terms of D-Bus interfaces exposed
+ * by @c mctpd.
+ *
+ * The construction or destruction of an MctpdDevice is not required to be
+ * correlated with signals from @c mctpd. For instance, EntityManager may expose
+ * the existance of an MCTP-capable device through its usual configuration
+ * mechanisms.
+ */
+class MCTPDDevice :
+ public MCTPDevice,
+ public std::enable_shared_from_this<MCTPDDevice>
+{
+ public:
+ MCTPDDevice() = delete;
+ MCTPDDevice(const std::shared_ptr<sdbusplus::asio::connection>& connection,
+ const std::string& interface,
+ const std::vector<uint8_t>& physaddr);
+ MCTPDDevice(const MCTPDDevice& other) = delete;
+ MCTPDDevice(MCTPDDevice&& other) = delete;
+ ~MCTPDDevice() override = default;
+
+ void setup(std::function<void(const std::error_code& ec,
+ const std::shared_ptr<MCTPEndpoint>& ep)>&&
+ added) override;
+ void remove() override;
+ std::string describe() const override;
+
+ private:
+ static void onEndpointInterfacesRemoved(
+ const std::weak_ptr<MCTPDDevice>& weak, const std::string& objpath,
+ sdbusplus::message_t& msg);
+
+ std::shared_ptr<sdbusplus::asio::connection> connection;
+ const std::string interface;
+ const std::vector<uint8_t> physaddr;
+ std::shared_ptr<MCTPDEndpoint> endpoint;
+ std::unique_ptr<sdbusplus::bus::match_t> removeMatch;
+
+ /**
+ * @brief Actions to perform once endpoint setup has succeeded
+ *
+ * Now that the endpoint exists two tasks remain:
+ *
+ * 1. Setup the match capturing removal of the endpoint object by mctpd
+ * 2. Invoke the callback to notify the requester that setup has completed,
+ * providing the MctpEndpoint instance associated with the MctpDevice.
+ */
+ void finaliseEndpoint(
+ const std::string& objpath, uint8_t eid, int network,
+ std::function<void(const std::error_code& ec,
+ const std::shared_ptr<MCTPEndpoint>& ep)>& added);
+ void endpointRemoved();
+};
+
+class I2CMCTPDDevice : public MCTPDDevice
+{
+ public:
+ static std::optional<SensorBaseConfigMap> match(const SensorData& config);
+ static bool match(const std::set<std::string>& interfaces);
+ static std::shared_ptr<I2CMCTPDDevice>
+ from(const std::shared_ptr<sdbusplus::asio::connection>& connection,
+ const SensorBaseConfigMap& iface);
+
+ I2CMCTPDDevice() = delete;
+ I2CMCTPDDevice(
+ const std::shared_ptr<sdbusplus::asio::connection>& connection, int bus,
+ uint8_t physaddr) :
+ MCTPDDevice(connection, interfaceFromBus(bus), {physaddr})
+ {}
+ ~I2CMCTPDDevice() override = default;
+
+ private:
+ static constexpr const char* configType = "MCTPI2CTarget";
+
+ static std::string interfaceFromBus(int bus);
+};
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();
+}
diff --git a/src/mctp/MCTPReactor.hpp b/src/mctp/MCTPReactor.hpp
new file mode 100644
index 0000000..ca20b45
--- /dev/null
+++ b/src/mctp/MCTPReactor.hpp
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "MCTPDeviceRepository.hpp"
+#include "MCTPEndpoint.hpp"
+#include "Utils.hpp"
+
+#include <string>
+#include <vector>
+
+struct AssociationServer
+{
+ virtual ~AssociationServer() = default;
+
+ virtual void associate(const std::string& path,
+ const std::vector<Association>& associations) = 0;
+ virtual void disassociate(const std::string& path) = 0;
+};
+
+class MCTPReactor : public std::enable_shared_from_this<MCTPReactor>
+{
+ using MCTPDeviceFactory = std::function<std::shared_ptr<MCTPDevice>(
+ const std::string& interface, const std::vector<std::uint8_t>& physaddr,
+ std::optional<std::uint8_t> eid)>;
+
+ public:
+ MCTPReactor() = delete;
+ MCTPReactor(const MCTPReactor&) = delete;
+ MCTPReactor(MCTPReactor&&) = delete;
+ explicit MCTPReactor(AssociationServer& server) : server(server) {}
+ ~MCTPReactor() = default;
+ MCTPReactor& operator=(const MCTPReactor&) = delete;
+ MCTPReactor& operator=(MCTPReactor&&) = delete;
+
+ void tick();
+
+ void manageMCTPDevice(const std::string& path,
+ const std::shared_ptr<MCTPDevice>& device);
+ void unmanageMCTPDevice(const std::string& path);
+
+ private:
+ static std::optional<std::string> findSMBusInterface(int bus);
+
+ AssociationServer& server;
+ MCTPDeviceRepository devices;
+
+ // Tracks MCTP devices that have failed their setup
+ std::set<std::shared_ptr<MCTPDevice>> deferred;
+
+ void deferSetup(const std::shared_ptr<MCTPDevice>& dev);
+ void setupEndpoint(const std::shared_ptr<MCTPDevice>& dev);
+ void trackEndpoint(const std::shared_ptr<MCTPEndpoint>& ep);
+ void untrackEndpoint(const std::shared_ptr<MCTPEndpoint>& ep);
+};
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;
+}
diff --git a/src/mctp/meson.build b/src/mctp/meson.build
new file mode 100644
index 0000000..2b10004
--- /dev/null
+++ b/src/mctp/meson.build
@@ -0,0 +1,8 @@
+executable(
+ 'mctpreactor',
+ 'MCTPReactorMain.cpp',
+ 'MCTPReactor.cpp',
+ 'MCTPEndpoint.cpp',
+ dependencies: [ default_deps, utils_dep ],
+ install: true
+)
diff --git a/src/meson.build b/src/meson.build
index 64d156d..85b754b 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -101,6 +101,10 @@
subdir('ipmb')
endif
+if get_option('mctp').allowed()
+ subdir('mctp')
+endif
+
if get_option('mcu').allowed()
subdir('mcu')
endif
diff --git a/src/tests/meson.build b/src/tests/meson.build
index 0415e96..c477843 100644
--- a/src/tests/meson.build
+++ b/src/tests/meson.build
@@ -56,3 +56,28 @@
include_directories: src_inc,
),
)
+
+test(
+ 'MCTPReactor',
+ executable(
+ 'test_MCTPReactor',
+ 'test_MCTPReactor.cpp',
+ '../mctp/MCTPReactor.cpp',
+ '../mctp/MCTPEndpoint.cpp',
+ dependencies: [ gmock_dep, ut_deps_list, utils_dep ],
+ implicit_include_directories: false,
+ include_directories: '../mctp'
+ )
+)
+
+test(
+ 'MCTPEndpoint',
+ executable(
+ 'test_MCTPEndpoint',
+ 'test_MCTPEndpoint.cpp',
+ '../mctp/MCTPEndpoint.cpp',
+ dependencies: [ gmock_dep, ut_deps_list, utils_dep ],
+ implicit_include_directories: false,
+ include_directories: '../mctp'
+ )
+)
diff --git a/src/tests/test_MCTPEndpoint.cpp b/src/tests/test_MCTPEndpoint.cpp
new file mode 100644
index 0000000..36b7d3c
--- /dev/null
+++ b/src/tests/test_MCTPEndpoint.cpp
@@ -0,0 +1,88 @@
+#include "MCTPEndpoint.hpp"
+#include "Utils.hpp"
+
+#include <stdexcept>
+
+#include <gtest/gtest.h>
+
+TEST(I2CMCTPDDevice, matchEmptyConfig)
+{
+ SensorData config{};
+ EXPECT_FALSE(I2CMCTPDDevice::match(config));
+}
+
+TEST(I2CMCTPDDevice, matchIrrelevantConfig)
+{
+ SensorData config{{"xyz.openbmc_project.Configuration.NVME1000", {}}};
+ EXPECT_FALSE(I2CMCTPDDevice::match(config));
+}
+
+TEST(I2CMCTPDDevice, matchRelevantConfig)
+{
+ SensorData config{{"xyz.openbmc_project.Configuration.MCTPI2CTarget", {}}};
+ EXPECT_TRUE(I2CMCTPDDevice::match(config));
+}
+
+TEST(I2CMCTPDDevice, fromBadIfaceNoType)
+{
+ SensorBaseConfigMap iface{{}};
+ EXPECT_THROW(I2CMCTPDDevice::from({}, iface), std::invalid_argument);
+}
+
+TEST(I2CMCTPDDevice, fromBadIfaceWrongType)
+{
+ SensorBaseConfigMap iface{{"Type", "NVME1000"}};
+ EXPECT_THROW(I2CMCTPDDevice::from({}, iface), std::invalid_argument);
+}
+
+TEST(I2CMCTPDDevice, fromBadIfaceNoAddress)
+{
+ SensorBaseConfigMap iface{
+ {"Bus", "0"},
+ {"Name", "test"},
+ {"Type", "MCTPI2CTarget"},
+ };
+ EXPECT_THROW(I2CMCTPDDevice::from({}, iface), std::invalid_argument);
+}
+
+TEST(I2CMCTPDDevice, fromBadIfaceBadAddress)
+{
+ SensorBaseConfigMap iface{
+ {"Address", "not a number"},
+ {"Bus", "0"},
+ {"Name", "test"},
+ {"Type", "MCTPI2CTarget"},
+ };
+ EXPECT_THROW(I2CMCTPDDevice::from({}, iface), std::invalid_argument);
+}
+
+TEST(I2CMCTPDDevice, fromBadIfaceNoBus)
+{
+ SensorBaseConfigMap iface{
+ {"Address", "0x1d"},
+ {"Name", "test"},
+ {"Type", "MCTPI2CTarget"},
+ };
+ EXPECT_THROW(I2CMCTPDDevice::from({}, iface), std::invalid_argument);
+}
+
+TEST(I2CMCTPDDevice, fromBadIfaceBadBus)
+{
+ SensorBaseConfigMap iface{
+ {"Address", "0x1d"},
+ {"Bus", "not a number"},
+ {"Name", "test"},
+ {"Type", "MCTPI2CTarget"},
+ };
+ EXPECT_THROW(I2CMCTPDDevice::from({}, iface), std::invalid_argument);
+}
+
+TEST(I2CMCTPDDevice, fromBadIfaceNoName)
+{
+ SensorBaseConfigMap iface{
+ {"Address", "0x1d"},
+ {"Bus", "0"},
+ {"Type", "MCTPI2CTarget"},
+ };
+ EXPECT_THROW(I2CMCTPDDevice::from({}, iface), std::invalid_argument);
+}
diff --git a/src/tests/test_MCTPReactor.cpp b/src/tests/test_MCTPReactor.cpp
new file mode 100644
index 0000000..abdd9e9
--- /dev/null
+++ b/src/tests/test_MCTPReactor.cpp
@@ -0,0 +1,250 @@
+#include "MCTPEndpoint.hpp"
+#include "MCTPReactor.hpp"
+#include "Utils.hpp"
+
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <string>
+#include <system_error>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+class MockMCTPDevice : public MCTPDevice
+{
+ public:
+ ~MockMCTPDevice() override = default;
+
+ MOCK_METHOD(void, setup,
+ (std::function<void(const std::error_code& ec,
+ const std::shared_ptr<MCTPEndpoint>& ep)> &&
+ added),
+ (override));
+ MOCK_METHOD(void, remove, (), (override));
+ MOCK_METHOD(std::string, describe, (), (const, override));
+};
+
+class MockMCTPEndpoint : public MCTPEndpoint
+{
+ public:
+ ~MockMCTPEndpoint() override = default;
+
+ MOCK_METHOD(int, network, (), (const, override));
+ MOCK_METHOD(uint8_t, eid, (), (const, override));
+ MOCK_METHOD(void, subscribe,
+ (Event && degraded, Event&& available, Event&& removed),
+ (override));
+ MOCK_METHOD(void, remove, (), (override));
+ MOCK_METHOD(std::string, describe, (), (const, override));
+ MOCK_METHOD(std::shared_ptr<MCTPDevice>, device, (), (const, override));
+};
+
+class MockAssociationServer : public AssociationServer
+{
+ public:
+ ~MockAssociationServer() override = default;
+
+ MOCK_METHOD(void, associate,
+ (const std::string& path,
+ const std::vector<Association>& associations),
+ (override));
+ MOCK_METHOD(void, disassociate, (const std::string& path), (override));
+};
+
+class MCTPReactorFixture : public testing::Test
+{
+ protected:
+ void SetUp() override
+ {
+ reactor = std::make_shared<MCTPReactor>(assoc);
+ device = std::make_shared<MockMCTPDevice>();
+ EXPECT_CALL(*device, describe())
+ .WillRepeatedly(testing::Return("mock device"));
+
+ endpoint = std::make_shared<MockMCTPEndpoint>();
+ EXPECT_CALL(*endpoint, device())
+ .WillRepeatedly(testing::Return(device));
+ EXPECT_CALL(*endpoint, describe())
+ .WillRepeatedly(testing::Return("mock endpoint"));
+ EXPECT_CALL(*endpoint, eid()).WillRepeatedly(testing::Return(9));
+ EXPECT_CALL(*endpoint, network()).WillRepeatedly(testing::Return(1));
+ }
+
+ void TearDown() override
+ {
+ // https://stackoverflow.com/a/10289205
+ EXPECT_TRUE(testing::Mock::VerifyAndClearExpectations(endpoint.get()));
+ EXPECT_TRUE(testing::Mock::VerifyAndClearExpectations(device.get()));
+ }
+
+ MockAssociationServer assoc;
+ std::shared_ptr<MCTPReactor> reactor;
+ std::shared_ptr<MockMCTPDevice> device;
+ std::shared_ptr<MockMCTPEndpoint> endpoint;
+};
+
+TEST_F(MCTPReactorFixture, manageNullDevice)
+{
+ reactor->manageMCTPDevice("/test", {});
+ reactor->unmanageMCTPDevice("/test");
+}
+
+TEST_F(MCTPReactorFixture, manageMockDeviceSetupFailure)
+{
+ EXPECT_CALL(*device, remove());
+ EXPECT_CALL(*device, setup(testing::_))
+ .WillOnce(testing::InvokeArgument<0>(
+ std::make_error_code(std::errc::permission_denied), endpoint));
+
+ reactor->manageMCTPDevice("/test", device);
+ reactor->unmanageMCTPDevice("/test");
+}
+
+TEST_F(MCTPReactorFixture, manageMockDevice)
+{
+ std::function<void(const std::shared_ptr<MCTPEndpoint>& ep)> removeHandler;
+
+ std::vector<Association> requiredAssociation{
+ {"configured_by", "configures", "/test"}};
+ EXPECT_CALL(assoc, associate("/xyz/openbmc_project/mctp/1/9",
+ requiredAssociation));
+ EXPECT_CALL(assoc, disassociate("/xyz/openbmc_project/mctp/1/9"));
+
+ EXPECT_CALL(*endpoint, remove()).WillOnce(testing::Invoke([&]() {
+ removeHandler(endpoint);
+ }));
+ EXPECT_CALL(*endpoint, subscribe(testing::_, testing::_, testing::_))
+ .WillOnce(testing::SaveArg<2>(&removeHandler));
+
+ EXPECT_CALL(*device, remove()).WillOnce(testing::Invoke([&]() {
+ endpoint->remove();
+ }));
+ EXPECT_CALL(*device, setup(testing::_))
+ .WillOnce(testing::InvokeArgument<0>(std::error_code(), endpoint));
+
+ reactor->manageMCTPDevice("/test", device);
+ reactor->unmanageMCTPDevice("/test");
+}
+
+TEST_F(MCTPReactorFixture, manageMockDeviceDeferredSetup)
+{
+ std::function<void(const std::shared_ptr<MCTPEndpoint>& ep)> removeHandler;
+
+ std::vector<Association> requiredAssociation{
+ {"configured_by", "configures", "/test"}};
+ EXPECT_CALL(assoc, associate("/xyz/openbmc_project/mctp/1/9",
+ requiredAssociation));
+ EXPECT_CALL(assoc, disassociate("/xyz/openbmc_project/mctp/1/9"));
+
+ EXPECT_CALL(*endpoint, remove()).WillOnce(testing::Invoke([&]() {
+ removeHandler(endpoint);
+ }));
+ EXPECT_CALL(*endpoint, subscribe(testing::_, testing::_, testing::_))
+ .WillOnce(testing::SaveArg<2>(&removeHandler));
+
+ EXPECT_CALL(*device, remove()).WillOnce(testing::Invoke([&]() {
+ endpoint->remove();
+ }));
+ EXPECT_CALL(*device, setup(testing::_))
+ .WillOnce(testing::InvokeArgument<0>(
+ std::make_error_code(std::errc::permission_denied), endpoint))
+ .WillOnce(testing::InvokeArgument<0>(std::error_code(), endpoint));
+
+ reactor->manageMCTPDevice("/test", device);
+ reactor->tick();
+ reactor->unmanageMCTPDevice("/test");
+}
+
+TEST_F(MCTPReactorFixture, manageMockDeviceRemoved)
+{
+ std::function<void(const std::shared_ptr<MCTPEndpoint>& ep)> removeHandler;
+
+ std::vector<Association> requiredAssociation{
+ {"configured_by", "configures", "/test"}};
+ EXPECT_CALL(assoc,
+ associate("/xyz/openbmc_project/mctp/1/9", requiredAssociation))
+ .Times(2);
+ EXPECT_CALL(assoc, disassociate("/xyz/openbmc_project/mctp/1/9")).Times(2);
+
+ EXPECT_CALL(*endpoint, remove()).WillOnce(testing::Invoke([&]() {
+ removeHandler(endpoint);
+ }));
+ EXPECT_CALL(*endpoint, subscribe(testing::_, testing::_, testing::_))
+ .Times(2)
+ .WillRepeatedly(testing::SaveArg<2>(&removeHandler));
+
+ EXPECT_CALL(*device, remove()).WillOnce(testing::Invoke([&]() {
+ endpoint->remove();
+ }));
+ EXPECT_CALL(*device, setup(testing::_))
+ .Times(2)
+ .WillRepeatedly(
+ testing::InvokeArgument<0>(std::error_code(), endpoint));
+
+ reactor->manageMCTPDevice("/test", device);
+ removeHandler(endpoint);
+ reactor->tick();
+ reactor->unmanageMCTPDevice("/test");
+}
+
+TEST(MCTPReactor, replaceConfiguration)
+{
+ MockAssociationServer assoc{};
+ auto reactor = std::make_shared<MCTPReactor>(assoc);
+ std::function<void(const std::shared_ptr<MCTPEndpoint>& ep)> removeHandler;
+
+ std::vector<Association> requiredAssociation{
+ {"configured_by", "configures", "/test"}};
+
+ EXPECT_CALL(assoc,
+ associate("/xyz/openbmc_project/mctp/1/9", requiredAssociation))
+ .Times(2);
+ EXPECT_CALL(assoc, disassociate("/xyz/openbmc_project/mctp/1/9")).Times(2);
+
+ auto endpoint = std::make_shared<MockMCTPEndpoint>();
+ EXPECT_CALL(*endpoint, describe())
+ .WillRepeatedly(testing::Return("mock endpoint"));
+ EXPECT_CALL(*endpoint, eid()).WillRepeatedly(testing::Return(9));
+ EXPECT_CALL(*endpoint, network()).WillRepeatedly(testing::Return(1));
+ EXPECT_CALL(*endpoint, remove())
+ .Times(2)
+ .WillRepeatedly(testing::Invoke([&]() { removeHandler(endpoint); }));
+ EXPECT_CALL(*endpoint, subscribe(testing::_, testing::_, testing::_))
+ .Times(2)
+ .WillRepeatedly(testing::SaveArg<2>(&removeHandler));
+
+ auto initial = std::make_shared<MockMCTPDevice>();
+ EXPECT_CALL(*initial, describe())
+ .WillRepeatedly(testing::Return("mock device: initial"));
+ EXPECT_CALL(*initial, setup(testing::_))
+ .WillOnce(testing::InvokeArgument<0>(std::error_code(), endpoint));
+ EXPECT_CALL(*initial, remove()).WillOnce(testing::Invoke([&]() {
+ endpoint->remove();
+ }));
+
+ auto replacement = std::make_shared<MockMCTPDevice>();
+ EXPECT_CALL(*replacement, describe())
+ .WillRepeatedly(testing::Return("mock device: replacement"));
+ EXPECT_CALL(*replacement, setup(testing::_))
+ .WillOnce(testing::InvokeArgument<0>(std::error_code(), endpoint));
+ EXPECT_CALL(*replacement, remove()).WillOnce(testing::Invoke([&]() {
+ endpoint->remove();
+ }));
+
+ EXPECT_CALL(*endpoint, device())
+ .WillOnce(testing::Return(initial))
+ .WillOnce(testing::Return(initial))
+ .WillOnce(testing::Return(replacement))
+ .WillOnce(testing::Return(replacement));
+
+ reactor->manageMCTPDevice("/test", initial);
+ reactor->manageMCTPDevice("/test", replacement);
+ reactor->tick();
+ reactor->unmanageMCTPDevice("/test");
+
+ EXPECT_TRUE(testing::Mock::VerifyAndClearExpectations(initial.get()));
+ EXPECT_TRUE(testing::Mock::VerifyAndClearExpectations(replacement.get()));
+ EXPECT_TRUE(testing::Mock::VerifyAndClearExpectations(endpoint.get()));
+}