From 0ac206aa23e735042af75a8321aaa1432732a035 Mon Sep 17 00:00:00 2001 From: Andrew Jeffery Date: Wed, 31 Jan 2024 12:41:14 +1030 Subject: [PATCH] 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 is implemented in terms of two new expose schemas in entity-manager: - MCTPInterface - MCTPDevice mctpreactor tracks instances of both appearing as a result of inventory changes, and uses the provided information to dynamically configure the endpoints via mctpd. This dynamic configuration may include assignment of static endpoint IDs to the devices as they appear. The lifecycle of an MCTP device can be quite dynamic - mctpd provides behaviors to recover[3] 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://github.com/CodeConstruct/mctp/blob/7ec2f8daa3a8948066390aee621d6afa03f6ecd9/docs/endpoint-recovery.md Change-Id: I5e362cf6e5ce80ce282bab48d912a1038003e236 Signed-off-by: Andrew Jeffery --- meson.options | 1 + service_files/meson.build | 1 + .../xyz.openbmc_project.mctpreactor.service | 13 + src/MCTPDeviceRepository.hpp | 93 ++++++ src/MCTPEndpoint.cpp | 308 ++++++++++++++++++ src/MCTPEndpoint.hpp | 303 +++++++++++++++++ src/MCTPReactor.cpp | 278 ++++++++++++++++ src/MCTPReactor.hpp | 68 ++++ src/MCTPReactorMain.cpp | 195 +++++++++++ src/meson.build | 11 + tests/meson.build | 13 + tests/test_MCTPReactor.cpp | 255 +++++++++++++++ 12 files changed, 1539 insertions(+) create mode 100644 service_files/xyz.openbmc_project.mctpreactor.service create mode 100644 src/MCTPDeviceRepository.hpp create mode 100644 src/MCTPEndpoint.cpp create mode 100644 src/MCTPEndpoint.hpp create mode 100644 src/MCTPReactor.cpp create mode 100644 src/MCTPReactor.hpp create mode 100644 src/MCTPReactorMain.cpp create mode 100644 tests/test_MCTPReactor.cpp diff --git a/meson.options b/meson.options index d6a8b966d..f8119309b 100644 --- a/meson.options +++ b/meson.options @@ -5,6 +5,7 @@ option('fan', type: 'feature', value: 'enabled', description: 'Enable fan sensor option('hwmon-temp', type: 'feature', value: 'enabled', description: 'Enable HWMON temperature sensor.',) option('intrusion', type: 'feature', value: 'enabled', description: 'Enable intrusion sensor.',) option('ipmb', type: 'feature', value: 'enabled', description: 'Enable IPMB sensor.',) +option('mctp', type: 'feature', value: 'enabled', description: 'Enable MCTP endpoint management') option('mcu', type: 'feature', value: 'enabled', description: 'Enable MCU sensor.',) option('nvme', type: 'feature', value: 'enabled', description: 'Enable NVMe sensor.',) option('psu', type: 'feature', value: 'enabled', description: 'Enable PSU sensor.',) diff --git a/service_files/meson.build b/service_files/meson.build index 20bd84adb..c3b23e037 100644 --- a/service_files/meson.build +++ b/service_files/meson.build @@ -12,6 +12,7 @@ unit_files = [ ['hwmon-temp', 'xyz.openbmc_project.hwmontempsensor.service'], ['ipmb', 'xyz.openbmc_project.ipmbsensor.service'], ['intrusion', 'xyz.openbmc_project.intrusionsensor.service'], + ['mctp', 'xyz.openbmc_project.mctpreactor.service'], ['mcu', 'xyz.openbmc_project.mcutempsensor.service'], ['nvme', 'xyz.openbmc_project.nvmesensor.service'], ['psu', 'xyz.openbmc_project.psusensor.service'], diff --git a/service_files/xyz.openbmc_project.mctpreactor.service b/service_files/xyz.openbmc_project.mctpreactor.service new file mode 100644 index 000000000..ed55557f5 --- /dev/null +++ b/service_files/xyz.openbmc_project.mctpreactor.service @@ -0,0 +1,13 @@ +[Unit] +Description=MCTP device configuration +StopWhenUnneeded=false +Requires=xyz.openbmc_project.EntityManager.service +After=xyz.openbmc_project.EntityManager.service + +[Service] +Restart=always +RestartSec=5 +ExecStart=/usr/bin/mctpreactor + +[Install] +WantedBy=multi-user.target diff --git a/src/MCTPDeviceRepository.hpp b/src/MCTPDeviceRepository.hpp new file mode 100644 index 000000000..c9b99def9 --- /dev/null +++ b/src/MCTPDeviceRepository.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include "MCTPEndpoint.hpp" + +class MCTPDeviceRepository +{ + private: + // FIXME: Ugh, hack. Figure out a better data structure? + std::map> devices; + + auto lookup(const std::shared_ptr& 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& device) + { + auto [_, fresh] = devices.emplace(inventory, device); + if (!fresh) + { + throw std::logic_error( + std::format("Tried to add entry for existing device: {}", + device->describe())); + } + } + + void remove(const std::shared_ptr& device) + { + auto entry = lookup(device); + if (entry == devices.end()) + { + throw std::logic_error( + std::format("Trying to remove unknown device: {}", + entry->second->describe())); + } + devices.erase(entry); + } + + void remove(const std::string& inventory) + { + auto entry = devices.find(inventory); + if (entry == devices.end()) + { + throw std::logic_error(std::format( + "Trying to remove unknown inventory: {}", inventory)); + } + devices.erase(entry); + } + + bool contains(const std::string& inventory) + { + return devices.contains(inventory); + } + + bool contains(const std::shared_ptr& device) + { + return lookup(device) != devices.end(); + } + + const std::string& inventoryFor(const std::shared_ptr& device) + { + auto entry = lookup(device); + if (entry == devices.end()) + { + throw std::logic_error( + std::format("Cannot retrieve inventory for unknown device: {}", + device->describe())); + } + return entry->first; + } + + const std::shared_ptr& deviceFor(const std::string& inventory) + { + auto entry = devices.find(inventory); + if (entry == devices.end()) + { + throw std::logic_error(std::format( + "Cannot retrieve device for unknown inventory: {}", inventory)); + } + return entry->second; + } +}; diff --git a/src/MCTPEndpoint.cpp b/src/MCTPEndpoint.cpp new file mode 100644 index 000000000..51384754d --- /dev/null +++ b/src/MCTPEndpoint.cpp @@ -0,0 +1,308 @@ +#include "MCTPEndpoint.hpp" + +#include "Utils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +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& connection, + const std::string& interface, const std::vector& physaddr, + std::optional eid) : + connection(connection), + interface(interface), physaddr(physaddr), eid{eid} +{} + +void MCTPDDevice::onEndpointInterfacesRemoved( + const std::weak_ptr& weak, const std::string& objpath, + sdbusplus::message_t& msg) +{ + auto path = msg.unpack(); + if (path.str != objpath) + { + return; + } + + auto removedIfaces = msg.unpack>(); + if (!removedIfaces.contains(mctpdEndpointControlInterface)) + { + return; + } + + if (auto self = weak.lock()) + { + self->endpointRemoved(); + } +} + +void MCTPDDevice::finaliseEndpoint( + const std::string& objpath, uint8_t eid, int network, + std::function& ep)>&& added) +{ + const auto matchSpec = + sdbusplus::bus::match::rules::interfacesRemovedAtPath(objpath); + removeMatch = std::make_unique( + *connection, matchSpec, + std::bind_front(MCTPDDevice::onEndpointInterfacesRemoved, + weak_from_this(), objpath)); + endpoint = std::make_shared(shared_from_this(), connection, + objpath, network, eid); + added({}, endpoint); +} + +void MCTPDDevice::setup( + std::function& 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, std::move(added)); + } + }; + try + { + if (eid) + { + connection->async_method_call( + onSetup, mctpdBusName, mctpdControlPath, mctpdControlInterface, + "AssignEndpointStatic", interface, physaddr, *eid); + } + else + { + connection->async_method_call( + onSetup, mctpdBusName, mctpdControlPath, mctpdControlInterface, + "SetupEndpoint", interface, physaddr); + } + } + catch (const sdbusplus::exception::SdBusError& err) + { + debug("Caught exception while configuring endpoint: {EXCEPTION}", + "EXEPTION", err); + auto errc = std::errc::no_such_device_or_address; + auto ec = std::make_error_code(errc); + added(ec, {}); + } +} + +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& 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::vector>(); + if (iface != mctpdEndpointControlInterface) + { + return; + } + + auto it = changed.find("Connectivity"); + if (it == changed.end()) + { + return; + } + + updateEndpointConnectivity(std::get(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 = degraded; + this->notifyAvailable = available; + this->notifyRemoved = removed; + + try + { + connectivityMatch.emplace( + static_cast(*connection), matchSpec, + [weak{weak_from_this()}](sdbusplus::message_t& msg) { + if (auto self = weak.lock()) + { + self->onMctpEndpointChange(msg); + } + }); + connection->async_method_call( + [weak{weak_from_this()}](const boost::system::error_code& ec, + const std::variant& 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(value); + self->updateEndpointConnectivity(connectivity); + } + }, + 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() +{ + try + { + 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"); + } + catch (const sdbusplus::exception::SdBusError& err) + { + std::throw_with_nested( + MCTPException("Failed schedule endpoint removal")); + } +} + +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 MCTPDEndpoint::device() const +{ + return dev; +} diff --git a/src/MCTPEndpoint.hpp b/src/MCTPEndpoint.hpp new file mode 100644 index 000000000..e282d478c --- /dev/null +++ b/src/MCTPEndpoint.hpp @@ -0,0 +1,303 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +/** + * @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& ep)>; + using Result = std::function; + + 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 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& 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 +{ + public: + static std::string path(const std::shared_ptr& ep); + + MCTPDEndpoint() = delete; + MCTPDEndpoint( + const std::shared_ptr& dev, + const std::shared_ptr& 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( + std::function& ep)>&& degraded, + std::function& ep)>&& + available, + std::function& ep)>&& removed) + override; + void remove() override; + + std::string describe() const override; + + std::shared_ptr 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 dev; + std::shared_ptr connection; + sdbusplus::message::object_path objpath; + struct + { + int network; + uint8_t eid; + } mctp; + MCTPEndpoint::Event notifyAvailable; + MCTPEndpoint::Event notifyDegraded; + MCTPEndpoint::Event notifyRemoved; + std::optional 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 +{ + public: + MCTPDDevice() = delete; + MCTPDDevice(const std::shared_ptr& connection, + const std::string& interface, + const std::vector& physaddr, + std::optional eid = {}); + MCTPDDevice(const MCTPDDevice& other) = delete; + MCTPDDevice(MCTPDDevice&& other) = delete; + ~MCTPDDevice() override = default; + + void setup(std::function& ep)>&& + added) override; + void remove() override; + std::string describe() const override; + + private: + static void + onEndpointInterfacesRemoved(const std::weak_ptr& weak, + const std::string& objpath, + sdbusplus::message_t& msg); + + std::shared_ptr connection; + const std::string interface; + const std::vector physaddr; + const std::optional eid; + std::shared_ptr endpoint; + std::unique_ptr 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& ep)>&& added); + void endpointRemoved(); +}; diff --git a/src/MCTPReactor.cpp b/src/MCTPReactor.cpp new file mode 100644 index 000000000..9da3d144b --- /dev/null +++ b/src/MCTPReactor.cpp @@ -0,0 +1,278 @@ +#include "MCTPReactor.hpp" + +#include "MCTPDeviceRepository.hpp" +#include "MCTPEndpoint.hpp" +#include "Utils.hpp" +#include "VariantVisitors.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +PHOSPHOR_LOG2_USING; + +void MCTPReactor::deferSetup(const std::shared_ptr& dev) +{ + info("Deferring setup for MCTP device at [ {MCTP_DEVICE} ]", "MCTP_DEVICE", + dev->describe()); + + deferred.emplace(dev); +} + +void MCTPReactor::untrackEndpoint(const std::shared_ptr& ep) +{ + server.disassociate(MCTPDEndpoint::path(ep)); +} + +void MCTPReactor::trackEndpoint(const std::shared_ptr& ep) +{ + info("Device endpoint configured at [ {MCTP_ENDPOINT} ]", "MCTP_ENDPOINT", + ep->describe()); + + ep->subscribe( + // Degraded + [](const std::shared_ptr&) {}, + // Available + [](const std::shared_ptr&) {}, + // Removed + [weak{weak_from_this()}](const std::shared_ptr& ep) { + info("Removed endpoint from MCTP device at [ {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()); + } + } + }); + + // 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 + const std::string& item = devices.inventoryFor(ep->device()); + std::vector associations{ + {"configured_by", "configures", item}}; + server.associate(MCTPDEndpoint::path(ep), associations); +} + +void MCTPReactor::setupEndpoint(const std::shared_ptr& dev) +{ + info("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& ep) mutable { + auto self = weak.lock(); + if (!self) + { + return; + } + + if (ec) + { + error( + "Setup failed for MCTP device at [ {MCTP_DEVICE} ], deferring: {ERROR_MESSAGE}", + "MCTP_DEVICE", dev->describe(), "ERROR_MESSAGE", ec.message()); + + self->deferSetup(dev); + return; + } + + self->trackEndpoint(ep); + }); +} + +static std::vector encodeDeviceAddress(MCTPTransport transport, + const std::string& address) +{ + if (transport == MCTPTransport::SMBus) + { + std::uint8_t res{}; + auto [_, ec] = std::from_chars(address.data(), + address.data() + address.size(), res); + if (ec == std::errc()) + { + return {res}; + } + error("Invalid address: {ERRC_DESCRIPTION}", "ERRC", + static_cast(ec), "ERRC_DESCRIPTION", + std::make_error_code(ec).message()); + } + + return {}; +} + +void MCTPReactor::tick() +{ + auto toSetup = std::exchange(deferred, {}); + for (const auto& entry : toSetup) + { + setupEndpoint(entry); + } +} + +void MCTPReactor::manageMCTPDevice(const std::string& path, + const SensorBaseConfigMap& iface) +{ + auto mAddress = iface.find("Address"); + auto mInterface = iface.find("Interface"); + if (mAddress == iface.end() || mInterface == iface.end()) + { + error( + "Configuration object at '{INVENTORY_PATH}' violates the MCTPDevice schema", + "INVENTORY_PATH", path); + return; + } + + auto interface = std::visit(VariantToStringVisitor(), mInterface->second); + auto interfaceEntry = interfaceConfiguration.find(interface); + if (interfaceEntry == interfaceConfiguration.end()) + { + info( + "Device at '{INVENTORY_PATH}' specified unconfigured MCTP interface '{MCTP_INTERFACE}', deferring setup", + "INVENTORY_PATH", path, "MCTP_INTERFACE", interface); + unreachable.emplace(path, iface); + return; + } + + MCTPTransport transport = interfaceEntry->second.transport; + auto address = std::visit(VariantToStringVisitor(), mAddress->second); + std::vector encoded = encodeDeviceAddress(transport, address); + if (encoded.empty()) + { + error( + "Unable to encode {MCTP_TRANSPORT} address '{MCTP_DEVICE_ADDRESS}' for device at '{INVENTORY_PATH}'", + "MCTP_TRANSPORT", std::string("SMBus"), "MCTP_DEVICE_ADDRESS", + address, "INVENTORY_PATH", path); + return; + } + + info("MCTP device inventory added at '{INVENTORY_PATH}'", "INVENTORY_PATH", + path); + + auto mStaticEndpointId = iface.find("StaticEndpointID"); + std::optional eid{}; + if (mStaticEndpointId != iface.end()) + { + auto eids = std::visit(VariantToStringVisitor(), + mStaticEndpointId->second); + std::uint8_t eidv{}; + auto [_, ec] = std::from_chars(eids.data(), eids.data() + eids.size(), + eidv); + if (ec == std::errc()) + { + eid = eidv; + } + else + { + error("Invalid static endpoint ID: {ERROR_MESSAGE}", "ERROR_CODE", + static_cast(ec), "ERROR_MESSAGE", + std::make_error_code(ec).message()); + } + } + + auto device = createDevice(interface, encoded, eid); + devices.add(path, device); + + info("Starting management of MCTP device at [ {MCTP_DEVICE} ]", + "MCTP_DEVICE", device->describe()); + + setupEndpoint(device); +} + +void MCTPReactor::unmanageMCTPDevice(const std::string& path) +{ + if (!devices.contains(path)) + { + return; + } + + std::shared_ptr device = devices.deviceFor(path); + + info("MCTP device inventory removed at '{INVENTORY_PATH}'", + "INVENTORY_PATH", path); + + unreachable.erase(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); + + info("Stopping management of MCTP device at [ {MCTP_DEVICE} ]", + "MCTP_DEVICE", device->describe()); + + device->remove(); +} + +void MCTPReactor::trackMCTPInterface(const std::string& path, + const SensorBaseConfigMap& iface) +{ + auto mName = iface.find("Name"); + auto mTransport = iface.find("Transport"); + + if (mName == iface.end() || mTransport == iface.end()) + { + throw std::invalid_argument( + "Provided object violates MCTPInterface schema"); + } + + auto name = std::visit(VariantToStringVisitor(), mName->second); + auto transport = std::visit(VariantToStringVisitor(), mTransport->second); + if (transport != "SMBus") + { + throw std::invalid_argument( + std::format("Unsupported MCTP transport: {}", transport)); + } + + interfaceConfiguration.emplace(std::pair( + name, {name, MCTPTransport::SMBus})); + interfaceInventory.emplace(path, name); + + info("Tracking MCTP interface '{MCTP_INTERFACE}' ({MCTP_TRANSPORT})", + "MCTP_INTERFACE", name, "MCTP_TRANSPORT", transport); + + // XXX: Figure out a more effective data structure? + auto candidates = std::exchange(unreachable, {}); + for (const auto& [path, config] : candidates) + { + manageMCTPDevice(path, config); + } +} + +void MCTPReactor::untrackMCTPInterface(const std::string& path) +{ + auto inventoryEntry = interfaceInventory.find(path); + if (inventoryEntry == interfaceInventory.end()) + { + return; + } + + info("Untracking MCTP interface '{MCTP_INTERFACE}'", "MCTP_INTERFACE", + inventoryEntry->second); + + interfaceConfiguration.erase(inventoryEntry->second); + interfaceInventory.erase(inventoryEntry); +} diff --git a/src/MCTPReactor.hpp b/src/MCTPReactor.hpp new file mode 100644 index 000000000..725bec311 --- /dev/null +++ b/src/MCTPReactor.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include "MCTPDeviceRepository.hpp" +#include "MCTPEndpoint.hpp" +#include "Utils.hpp" + +#include +#include + +struct AssociationServer +{ + virtual ~AssociationServer() = default; + + virtual void associate(const std::string& path, + const std::vector& associations) = 0; + virtual void disassociate(const std::string& path) = 0; +}; + +class MCTPReactor : public std::enable_shared_from_this +{ + using MCTPDeviceFactory = std::function( + const std::string& interface, const std::vector& physaddr, + std::optional eid)>; + + public: + MCTPReactor() = delete; + MCTPReactor(const MCTPReactor&) = delete; + MCTPReactor(MCTPReactor&&) = delete; + MCTPReactor(MCTPDeviceFactory&& createDevice, AssociationServer& server) : + createDevice(createDevice), server(server) + {} + ~MCTPReactor() = default; + MCTPReactor& operator=(const MCTPReactor&) = delete; + MCTPReactor& operator=(MCTPReactor&&) = delete; + + void tick(); + + void manageMCTPDevice(const std::string& path, + const SensorBaseConfigMap& iface); + void unmanageMCTPDevice(const std::string& path); + void trackMCTPInterface(const std::string& path, + const SensorBaseConfigMap& iface); + void untrackMCTPInterface(const std::string& path); + + private: + MCTPDeviceFactory createDevice; + AssociationServer& server; + + // Maps the inventory DBus object path exposing an MCTP interface to an MCTP + // interface name + std::map interfaceInventory; + // Maps an MCTP interface name to the interface's properties + std::map interfaceConfiguration; + + MCTPDeviceRepository devices; + + // Maps the inventory DBus object path exposing an MCTP device to the + // device's MCTP properties + std::map unreachable; + + // Tracks MCTP devices that have failed their setup + std::set> deferred; + + void deferSetup(const std::shared_ptr& dev); + void setupEndpoint(const std::shared_ptr& dev); + void trackEndpoint(const std::shared_ptr& ep); + void untrackEndpoint(const std::shared_ptr& ep); +}; diff --git a/src/MCTPReactorMain.cpp b/src/MCTPReactorMain.cpp new file mode 100644 index 000000000..83b5a6ed9 --- /dev/null +++ b/src/MCTPReactorMain.cpp @@ -0,0 +1,195 @@ +#include "MCTPReactor.hpp" + +#include +#include + +PHOSPHOR_LOG2_USING; + +static constexpr const char* const mctpInterfaceInterface = + "xyz.openbmc_project.Configuration.MCTPInterface"; + +static constexpr const char* const mctpDeviceInterface = + "xyz.openbmc_project.Configuration.MCTPDevice"; + +class DBusAssociationServer : public AssociationServer +{ + public: + DBusAssociationServer() = delete; + DBusAssociationServer(const DBusAssociationServer&) = delete; + DBusAssociationServer(DBusAssociationServer&&) = delete; + explicit DBusAssociationServer( + const std::shared_ptr& 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& associations) override + { + auto [entry, _] = objects.emplace( + path, server.add_interface(path, association::interface)); + std::shared_ptr 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 iface = entry->second; + server.remove_interface(entry->second); + objects.erase(entry); + } + + private: + std::shared_ptr connection; + sdbusplus::asio::object_server server; + std::map> + objects; +}; + +static void addInventory(const std::shared_ptr& reactor, + sdbusplus::message_t& msg) +{ + auto [path, + exposed] = msg.unpack(); + auto interfaceEntry = exposed.find(mctpInterfaceInterface); + if (interfaceEntry != exposed.end()) + { + try + { + reactor->trackMCTPInterface(path, interfaceEntry->second); + } + catch (const std::invalid_argument& ex) + { + warning( + "Cannot track MCTP interface defined at '{INVENTORY_PATH}': {EXCEPTION}", + "INVENTORY_PATH", path, "EXCEPTION", ex); + } + } + + auto deviceEntry = exposed.find(mctpDeviceInterface); + if (deviceEntry != exposed.end()) + { + try + { + reactor->manageMCTPDevice(path, deviceEntry->second); + } + catch (const std::invalid_argument& ex) + { + warning( + "Cannot manage MCTP device defined at '{INVENTORY_PATH}': {EXCEPTION}", + "INVENTORY_PATH", path, "EXCEPTION", ex); + } + } +} + +static void removeInventory(const std::shared_ptr& reactor, + sdbusplus::message_t& msg) +{ + auto [path, removed] = + msg.unpack>(); + if (removed.contains(mctpDeviceInterface)) + { + reactor->unmanageMCTPDevice(path.str); + } + + if (removed.contains(mctpInterfaceInterface)) + { + reactor->untrackMCTPInterface(path.str); + } +} + +static void manageMCTPEntity(const std::shared_ptr& reactor, + ManagedObjectType& entities) +{ + for (const auto& [path, config] : entities) + { + auto interfaceEntry = config.find(mctpInterfaceInterface); + if (interfaceEntry != config.end()) + { + reactor->trackMCTPInterface(path, interfaceEntry->second); + } + + auto deviceEntry = config.find(mctpDeviceInterface); + if (deviceEntry != config.end()) + { + reactor->manageMCTPDevice(path, deviceEntry->second); + } + } +} + +static auto createMCTPDDevice( + const std::shared_ptr& connection, + const std::string& interface, const std::vector& physaddr, + std::optional eid) +{ + return std::make_shared(connection, interface, physaddr, eid); +} + +int main() +{ + constexpr std::chrono::seconds period(5); + + boost::asio::io_context io; + auto systemBus = std::make_shared(io); + DBusAssociationServer associationServer(systemBus); + auto createMCTPDevice = std::bind_front(createMCTPDDevice, systemBus); + auto reactor = std::make_shared(std::move(createMCTPDevice), + associationServer); + boost::asio::steady_timer clock(io); + + std::function 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); + + systemBus->request_name("xyz.openbmc_project.MCTPReactor"); + + using namespace sdbusplus::bus::match; + + 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(*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(*systemBus), interfacesAddedMatchSpec, + std::bind_front(addInventory, reactor)); + + boost::asio::post(io, [reactor, systemBus]() { + auto gsc = std::make_shared( + systemBus, std::bind_front(manageMCTPEntity, reactor)); + gsc->getConfiguration({"MCTPInterface", "MCTPDevice"}); + }); + + io.run(); + + return EXIT_SUCCESS; +} diff --git a/src/meson.build b/src/meson.build index 429f7c5de..9c20eb34f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -177,6 +177,17 @@ if get_option('ipmb').allowed() ) endif +if get_option('mctp').allowed() + executable( + 'mctpreactor', + 'MCTPReactorMain.cpp', + 'MCTPReactor.cpp', + 'MCTPEndpoint.cpp', + dependencies: [ default_deps, utils_dep ], + install: true + ) +endif + if get_option('mcu').allowed() executable( 'mcutempsensor', diff --git a/tests/meson.build b/tests/meson.build index e5ad2ea11..d0ddca540 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -36,3 +36,16 @@ test( include_directories: '../src', ), ) + +test( + 'MCTPReactor', + executable( + 'test_MCTPReactor', + 'test_MCTPReactor.cpp', + '../src/MCTPReactor.cpp', + '../src/MCTPEndpoint.cpp', + dependencies: [ ut_deps_list, gmock_dep ], + implicit_include_directories: false, + include_directories: '../src', + ), +) diff --git a/tests/test_MCTPReactor.cpp b/tests/test_MCTPReactor.cpp new file mode 100644 index 000000000..c654472b6 --- /dev/null +++ b/tests/test_MCTPReactor.cpp @@ -0,0 +1,255 @@ +#include "MCTPReactor.hpp" + +#include +#include + +#include +#include + +class MockMCTPDevice : public MCTPDevice +{ + public: + ~MockMCTPDevice() override = default; + + MOCK_METHOD(void, setup, + (std::function& 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, device, (), (const, override)); +}; + +class MockAssociationServer : public AssociationServer +{ + public: + ~MockAssociationServer() override = default; + + MOCK_METHOD(void, associate, + (const std::string& path, + const std::vector& associations), + (override)); + MOCK_METHOD(void, disassociate, (const std::string& path), (override)); +}; + +static std::shared_ptr createNullMCTPDevice( + [[maybe_unused]] const std::string& interface, + [[maybe_unused]] const std::vector& physaddr, + [[maybe_unused]] std::optional eid) +{ + return {}; +} + +class NullDeviceTest : public testing::Test +{ + protected: + void SetUp() override + { + reactor = std::make_shared(createNullMCTPDevice, + mockServer); + } + + MockAssociationServer mockServer; + std::shared_ptr reactor; +}; + +TEST_F(NullDeviceTest, trackInvalidConfiguration) +{ + EXPECT_THROW(reactor->trackMCTPInterface("/mctpi2c0", {}), + std::invalid_argument); +} + +TEST_F(NullDeviceTest, trackUnsupportedTransport) +{ + EXPECT_THROW(reactor->trackMCTPInterface("/mctpi2c0", + { + {"Name", {"mctpi2c0"}}, + {"Transport", {"Unsupported"}}, + }), + std::invalid_argument); +} + +TEST_F(NullDeviceTest, trackValidInterface) +{ + reactor->trackMCTPInterface("/mctpi2c0", { + {"Name", {"mctpi2c0"}}, + {"Transport", {"SMBus"}}, + }); +} + +TEST_F(NullDeviceTest, removeUntrackedInterface) +{ + reactor->untrackMCTPInterface("/untracked"); +} + +TEST_F(NullDeviceTest, removeTrackedInterface) +{ + reactor->trackMCTPInterface("/mctpi2c0", { + {"Name", {"mctpi2c0"}}, + {"Transport", {"SMBus"}}, + }); + reactor->untrackMCTPInterface("/mctpi2c0"); +} + +TEST_F(NullDeviceTest, manageMCTPDeviceUntrackedInterface) +{ + reactor->manageMCTPDevice("/NVMe", { + {"Address", {"0x1d"}}, + {"Interface", {"untracked"}}, + {"Name", {"NVMe"}}, + {"Type", {"MCTPDevice"}}, + }); +} + +class MockDeviceTest : public testing::Test +{ + protected: + std::shared_ptr + createMockDevice(const std::string& interface, + const std::vector& physaddr, + [[maybe_unused]] std::optional eid) + { + auto endpointDescription = std::format("network: 1, eid: 9"); + EXPECT_CALL(*endpoint, describe()) + .WillRepeatedly(testing::Return(endpointDescription)); + + auto deviceDescription = std::format("interface: {}, physaddr: {}", + interface, physaddr.at(0)); + EXPECT_CALL(*device, describe()) + .WillRepeatedly(testing::Return(deviceDescription)); + + return device; + } + + void SetUp() override + { + device = std::make_shared(); + endpoint = std::make_shared(); + + EXPECT_CALL(*endpoint, network()).WillRepeatedly(testing::Return(1)); + EXPECT_CALL(*endpoint, eid()).WillRepeatedly(testing::Return(9)); + EXPECT_CALL(*endpoint, subscribe(testing::_, testing::_, testing::_)) + .WillRepeatedly(testing::SaveArg<2>(&removeHandler)); + EXPECT_CALL(*endpoint, device()) + .WillRepeatedly(testing::Return(device)); + EXPECT_CALL(*endpoint, remove()).WillRepeatedly(testing::Invoke([&]() { + mockServer.disassociate("/xyz/openbmc_project/mctp/1/9"); + })); + + EXPECT_CALL(*device, setup(testing::_)) + .WillRepeatedly( + testing::InvokeArgument<0>(std::error_code(), endpoint)); + EXPECT_CALL(*device, remove()).WillRepeatedly(testing::Invoke([&]() { + endpoint->remove(); + })); + + reactor = std::make_shared( + std::bind_front(&MockDeviceTest::createMockDevice, this), + mockServer); + } + + void TearDown() override + { + // https://stackoverflow.com/a/10289205 + EXPECT_TRUE(testing::Mock::VerifyAndClearExpectations(endpoint.get())); + EXPECT_TRUE(testing::Mock::VerifyAndClearExpectations(device.get())); + } + + MockAssociationServer mockServer; + std::function& ep)> removeHandler; + std::shared_ptr device; + std::shared_ptr endpoint; + std::shared_ptr reactor; +}; + +TEST_F(MockDeviceTest, manageInterfaceBeforeDevice) +{ + EXPECT_CALL(mockServer, + associate("/xyz/openbmc_project/mctp/1/9", testing::_)) + .Times(1); + EXPECT_CALL(mockServer, disassociate("/xyz/openbmc_project/mctp/1/9")) + .Times(1); + + reactor->trackMCTPInterface( + "/mctpi2c0", {{"Name", {"mctpi2c0"}}, {"Transport", {"SMBus"}}}); + reactor->manageMCTPDevice("/NVMe", { + {"Address", {"0x1d"}}, + {"Interface", {"mctpi2c0"}}, + {"Name", {"NVMe"}}, + {"Type", {"MCTPDevice"}}, + }); + reactor->unmanageMCTPDevice("/NVMe"); +} + +TEST_F(MockDeviceTest, manageDeviceBeforeInterface) +{ + EXPECT_CALL(mockServer, + associate("/xyz/openbmc_project/mctp/1/9", testing::_)) + .Times(1); + EXPECT_CALL(mockServer, disassociate("/xyz/openbmc_project/mctp/1/9")) + .Times(1); + + reactor->manageMCTPDevice("/NVMe", { + {"Address", {"0x1d"}}, + {"Interface", {"mctpi2c0"}}, + {"Name", {"NVMe"}}, + {"Type", {"MCTPDevice"}}, + }); + reactor->trackMCTPInterface( + "/mctpi2c0", {{"Name", {"mctpi2c0"}}, {"Transport", {"SMBus"}}}); + reactor->unmanageMCTPDevice("/NVMe"); +} + +TEST_F(MockDeviceTest, manageDeviceNoInterface) +{ + EXPECT_CALL(mockServer, + associate("/xyz/openbmc_project/mctp/1/9", testing::_)) + .Times(0); + EXPECT_CALL(mockServer, disassociate("/xyz/openbmc_project/mctp/1/9")) + .Times(0); + + reactor->manageMCTPDevice("/NVMe", { + {"Address", {"0x1d"}}, + {"Interface", {"mctpi2c0"}}, + {"Name", {"NVMe"}}, + {"Type", {"MCTPDevice"}}, + }); + reactor->unmanageMCTPDevice("/NVMe"); +} + +TEST_F(MockDeviceTest, manageDeviceWithRemove) +{ + EXPECT_CALL(mockServer, + associate("/xyz/openbmc_project/mctp/1/9", testing::_)) + .Times(2); + EXPECT_CALL(mockServer, disassociate("/xyz/openbmc_project/mctp/1/9")) + .Times(2); + + reactor->trackMCTPInterface( + "/mctpi2c0", {{"Name", {"mctpi2c0"}}, {"Transport", {"SMBus"}}}); + reactor->manageMCTPDevice("/NVMe", { + {"Address", {"0x1d"}}, + {"Interface", {"mctpi2c0"}}, + {"Name", {"NVMe"}}, + {"Type", {"MCTPDevice"}}, + }); + ASSERT_NE(removeHandler, nullptr); + removeHandler(endpoint); + reactor->tick(); + reactor->unmanageMCTPDevice("/NVMe"); +}