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..1c58e5ef7 --- /dev/null +++ b/src/MCTPEndpoint.cpp @@ -0,0 +1,418 @@ +#include "MCTPEndpoint.hpp" + +#include "Utils.hpp" +#include "VariantVisitors.hpp" + +#include +#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, + "AssignEndpoint", interface, physaddr); + } + } + catch (const sdbusplus::exception::SdBusError& err) + { + debug("Caught exception while configuring endpoint: {EXCEPTION}", + "EXCEPTION", 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; +} + +std::optional + 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& interfaces) +{ + return interfaces.contains(configInterfaceName(configType)); +} + +std::shared_ptr I2CMCTPDDevice::from( + const std::shared_ptr& 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"); + } + + 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 + { + warning("Invalid static endpoint ID: {ERROR_MESSAGE}", "ERROR_CODE", + static_cast(ec), "ERROR_MESSAGE", + std::make_error_code(ec).message()); + } + } + + try + { + return std::make_shared(connection, bus, address, eid); + } + catch (const std::runtime_error& 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)) + { + throw std::runtime_error("Bus is not configured as an MCTP interface"); + } + + return it->path().filename(); +} diff --git a/src/MCTPEndpoint.hpp b/src/MCTPEndpoint.hpp new file mode 100644 index 000000000..cd4199ec3 --- /dev/null +++ b/src/MCTPEndpoint.hpp @@ -0,0 +1,324 @@ +#pragma once + +#include "Utils.hpp" + +#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(Event&& degraded, Event&& available, + Event&& 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(); +}; + +class I2CMCTPDDevice : public MCTPDDevice +{ + public: + static std::optional match(const SensorData& config); + static bool match(const std::set& interfaces); + static std::shared_ptr + from(const std::shared_ptr& connection, + const SensorBaseConfigMap& iface); + + I2CMCTPDDevice() = delete; + I2CMCTPDDevice( + const std::shared_ptr& connection, int bus, + uint8_t physaddr, std::optional eid) : + MCTPDDevice(connection, interfaceFromBus(bus), {physaddr}, eid) + {} + ~I2CMCTPDDevice() override = default; + + private: + static constexpr const char* configType = "MCTPI2CTarget"; + + static std::string interfaceFromBus(int bus); +}; diff --git a/src/MCTPReactor.cpp b/src/MCTPReactor.cpp new file mode 100644 index 000000000..0f0ede913 --- /dev/null +++ b/src/MCTPReactor.cpp @@ -0,0 +1,157 @@ +#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 +#include +#include + +PHOSPHOR_LOG2_USING; + +void MCTPReactor::deferSetup(const std::shared_ptr& dev) +{ + debug("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("Added MCTP endpoint to device: [ {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 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()); + } + } + }); + + // 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) +{ + 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& ep) mutable { + auto self = weak.lock(); + if (!self) + { + 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; + } + + self->trackEndpoint(ep); + }); +} + +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& device) +{ + if (!device) + { + return; + } + + debug("MCTP device inventory added at '{INVENTORY_PATH}'", "INVENTORY_PATH", + path); + + devices.add(path, device); + + setupEndpoint(device); +} + +void MCTPReactor::unmanageMCTPDevice(const std::string& path) +{ + if (!devices.contains(path)) + { + return; + } + + std::shared_ptr device = devices.deviceFor(path); + + 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/MCTPReactor.hpp b/src/MCTPReactor.hpp new file mode 100644 index 000000000..ca20b456a --- /dev/null +++ b/src/MCTPReactor.hpp @@ -0,0 +1,53 @@ +#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; + 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& device); + void unmanageMCTPDevice(const std::string& path); + + private: + static std::optional findSMBusInterface(int bus); + + AssociationServer& server; + MCTPDeviceRepository devices; + + // 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..ccbbdf5f9 --- /dev/null +++ b/src/MCTPReactorMain.cpp @@ -0,0 +1,161 @@ +#include "MCTPEndpoint.hpp" +#include "MCTPReactor.hpp" +#include "Utils.hpp" + +#include +#include + +PHOSPHOR_LOG2_USING; + +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 std::shared_ptr deviceFromConfig( + const std::shared_ptr& connection, + const SensorData& config) +{ + try + { + std::optional iface; + 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& connection, + const std::shared_ptr& reactor, + sdbusplus::message_t& msg) +{ + auto [path, + exposed] = msg.unpack(); + reactor->manageMCTPDevice(path, deviceFromConfig(connection, exposed)); +} + +static void removeInventory(const std::shared_ptr& reactor, + sdbusplus::message_t& msg) +{ + auto [path, removed] = + msg.unpack>(); + if (I2CMCTPDDevice::match(removed)) + { + reactor->unmanageMCTPDevice(path.str); + } +} + +static void manageMCTPEntity( + const std::shared_ptr& connection, + const std::shared_ptr& reactor, ManagedObjectType& entities) +{ + for (const auto& [path, config] : entities) + { + reactor->manageMCTPDevice(path, deviceFromConfig(connection, config)); + } +} + +int main() +{ + constexpr std::chrono::seconds period(5); + + boost::asio::io_context io; + auto systemBus = std::make_shared(io); + DBusAssociationServer associationServer(systemBus); + auto reactor = std::make_shared(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, systemBus, reactor)); + + boost::asio::post(io, [reactor, systemBus]() { + auto gsc = std::make_shared( + systemBus, std::bind_front(manageMCTPEntity, systemBus, reactor)); + gsc->getConfiguration({"MCTPI2CTarget"}); + }); + + 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..b77fb3e36 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -36,3 +36,28 @@ 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' + ) +) + +test( + 'MCTPEndpoint', + executable( + 'test_MCTPEndpoint', + 'test_MCTPEndpoint.cpp', + '../src/MCTPEndpoint.cpp', + dependencies: [ ut_deps_list, gmock_dep ], + implicit_include_directories: false, + include_directories: '../src' + ) +) diff --git a/tests/test_MCTPEndpoint.cpp b/tests/test_MCTPEndpoint.cpp new file mode 100644 index 000000000..a0a6c6f9b --- /dev/null +++ b/tests/test_MCTPEndpoint.cpp @@ -0,0 +1,85 @@ +#include "MCTPEndpoint.hpp" + +#include + +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/tests/test_MCTPReactor.cpp b/tests/test_MCTPReactor.cpp new file mode 100644 index 000000000..082eed088 --- /dev/null +++ b/tests/test_MCTPReactor.cpp @@ -0,0 +1,185 @@ +#include "MCTPReactor.hpp" + +#include +#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)); +}; + +class MCTPReactorFixture : public testing::Test +{ + protected: + void SetUp() override + { + reactor = std::make_shared(assoc); + device = std::make_shared(); + EXPECT_CALL(*device, describe()) + .WillRepeatedly(testing::Return("mock device")); + + endpoint = std::make_shared(); + 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 reactor; + std::shared_ptr device; + std::shared_ptr 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& ep)> removeHandler; + + std::vector 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& ep)> removeHandler; + + std::vector 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& ep)> removeHandler; + + std::vector 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"); +}