blob: 70efb3619a7eca181da4dfcb95c415f4c808e9fe [file]
#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 <array>
#include <cassert>
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <filesystem>
#include <format>
#include <fstream>
#include <functional>
#include <iterator>
#include <map>
#include <memory>
#include <numeric>
#include <optional>
#include <set>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <variant>
#include <vector>
PHOSPHOR_LOG2_USING;
static constexpr const char* mctpdBusName = "au.com.codeconstruct.MCTP1";
static constexpr const char* mctpdControlPath = "/au/com/codeconstruct/mctp1";
static constexpr const char* mctpdControlInterface =
"au.com.codeconstruct.MCTP.BusOwner1";
static constexpr const char* mctpdEndpointControlInterface =
"au.com.codeconstruct.MCTP.Endpoint1";
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::object_path>();
assert(path.string() == 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::match_rules::interfacesRemovedAtPath(objpath);
removeMatch = std::make_unique<sdbusplus::match>(
*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 + std::string("/interfaces/") + interface,
mctpdControlInterface, "AssignEndpoint", 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;
}
// https://en.cppreference.com/w/cpp/utility/hash/operator().html
//
// https://en.wikipedia.org/w/index.php?title=Fowler%E2%80%93Noll%E2%80%93Vo_hash_function&oldid=1312413750#FNV_hash_parameters
template <typename T, typename V, T p>
static std::size_t fnv1a(T h, V e);
template <std::size_t S>
static std::size_t fnv1a(const std::vector<std::uint8_t>& d);
template <typename V>
static std::size_t fnv1a(std::uint32_t h, V v)
{
constexpr std::uint32_t p = 0x01000193;
return (h ^ v) * p;
}
template <>
[[maybe_unused]] std::size_t fnv1a<4UL>(const std::vector<std::uint8_t>& d)
{
std::uint32_t h = 0x811c9dc5;
for (const auto& v : d)
{
h = fnv1a(h, v);
}
return h;
}
template <typename V>
static std::size_t fnv1a(std::uint64_t h, V v)
{
constexpr std::uint64_t p = 0x00000100000001b3;
return (h ^ v) * p;
}
template <>
[[maybe_unused]] std::size_t fnv1a<8UL>(const std::vector<std::uint8_t>& d)
{
std::uint64_t h = 0xcbf29ce484222325;
for (const auto& v : d)
{
h = fnv1a(h, v);
}
return h;
}
static std::size_t fnv1aHash(const std::vector<std::uint8_t>& d)
{
return fnv1a<sizeof(std::size_t)>(d);
}
std::size_t MCTPDDevice::id() const
{
std::size_t h1 = std::hash<std::string>{}(interface);
std::size_t h2 = fnv1aHash(physaddr);
return h1 ^ (h2 << 1);
}
std::string MCTPDEndpoint::path(const std::shared_ptr<MCTPEndpoint>& ep)
{
return std::format("{}/networks/{}/endpoints/{}", mctpdControlPath,
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::match_rules::propertiesChangedNamespace(
objpath.string(), 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.string()}](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.string()}](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.string(), "org.freedesktop.DBus.Properties",
"Get", mctpdEndpointControlInterface, "Connectivity");
}
catch (const sdbusplus::exception::internal_exception& 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.string(), 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;
}
std::optional<SensorBaseConfigMap> I3CMCTPDDevice::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));
}
bool I3CMCTPDDevice::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::shared_ptr<I3CMCTPDDevice> I3CMCTPDDevice::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 I3C 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 MCTPI3CTarget schema");
}
auto address = std::visit(VariantToNumArrayVisitor<uint8_t, uint64_t>(),
mAddress->second);
if (address.empty())
{
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<I3CMCTPDDevice>(connection, bus, address);
}
catch (const MCTPException& ex)
{
warning(
"Failed to create I3CMCTPDDevice at [ bus: {I3C_BUS} ]: {EXCEPTION}",
"I3C_BUS", bus, "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();
}
std::string I3CMCTPDDevice::interfaceFromBus(int bus)
{
std::filesystem::path netdir = std::format("/sys/devices/virtual/net");
std::error_code ec;
std::filesystem::directory_iterator it(netdir, ec);
if (ec || it == std::filesystem::end(it))
{
error("No net device associated with I3C bus {I3C_BUS} at {NET_DEVICE}",
"I3C_BUS", bus, "NET_DEVICE", netdir);
throw MCTPException("Bus is not configured as an MCTP interface");
}
std::string targetInterface = std::format("mctpi3c{}", bus);
for (const auto& entry : std::filesystem::directory_iterator(netdir))
{
if (entry.is_directory() && entry.path().filename() == targetInterface)
{
return targetInterface;
}
}
error("No matching net device found for I3C bus {I3C_BUS} at {NET_DEVICE}",
"I3C_BUS", bus, "NET_DEVICE", netdir);
throw MCTPException("No matching net device found for the specified bus");
}
// The addresses of the USB host controllers belong to the SOC, so a root hub
// position only names one once the SOC is known.
static std::span<const std::string_view> rootHubsForSoc(
const std::string& socFamily)
{
// The AST2620 is the AST2600 with the same controller addresses. The UHCI
// companion at 1e6b0000 is not listed: it serves the same ports at lower
// speed rather than being a host controller of its own.
static constexpr std::array<std::string_view, 2> ast2600RootHubs = {
"/sys/devices/platform/ahb/1e6a1000.usb",
"/sys/devices/platform/ahb/1e6a3000.usb"};
if (socFamily.find("AST2600") != std::string::npos ||
socFamily.find("AST2620") != std::string::npos)
{
return ast2600RootHubs;
}
return {};
}
// Resolve a root hub position to its host controller's sysfs path. Returns
// nullopt, having logged why, if the SOC or the position is not supported.
static std::optional<std::string_view> rootHubPathForPosition(size_t position)
{
std::ifstream socFamilyFile("/sys/bus/soc/devices/soc0/family");
if (!socFamilyFile)
{
error("Unable to read SOC family information");
return std::nullopt;
}
std::string socFamily;
std::getline(socFamilyFile, socFamily);
const std::span<const std::string_view> rootHubs =
rootHubsForSoc(socFamily);
if (rootHubs.empty())
{
error("Unsupported SOC for USB MCTP: {SOC_FAMILY}", "SOC_FAMILY",
socFamily);
return std::nullopt;
}
if (position >= rootHubs.size())
{
error(
"RootHubPosition {POSITION} names no host controller on {SOC_FAMILY}",
"POSITION", position, "SOC_FAMILY", socFamily);
return std::nullopt;
}
return rootHubs[position];
}
/*
* Given the sysfs path for a USB root hub, determines the bus number. An
* example root hub path on an AST2600 is
* /sys/devices/platform/ahb/1e6a3000.usb. This function identifies the
* correct bus device directory (e.g. "usb1") by looking for a "busnum"
* attribute file within it, and returns the content of that file.
*/
std::string USBMCTPDDevice::busFromRootHubPath(
const std::filesystem::path& rootHubPath)
{
std::error_code ec;
std::filesystem::directory_iterator it(rootHubPath, ec);
if (ec)
{
error("Unable to open RootHubPath {PATH}", "PATH", rootHubPath);
throw MCTPException("Invalid RootHubPath");
}
for (const auto& entry : it)
{
const auto& path = entry.path();
if (!entry.is_directory() ||
!path.filename().string().starts_with("usb"))
{
continue;
}
std::filesystem::path busnumPath = path / "busnum";
if (!std::filesystem::exists(busnumPath))
{
continue;
}
std::ifstream busnumFile(busnumPath);
std::string busnum;
if (busnumFile >> busnum && !busnum.empty())
{
return busnum;
}
}
error(
"No directory containing a 'busnum' file found under RootHubPath {PATH}",
"PATH", rootHubPath);
throw MCTPException("Could not determine bus number");
}
/*
* Given the components of a USB device's sysfs path, construct the full path
* and determine the network interface name associated with it.
*/
std::string USBMCTPDDevice::interfaceFromSysfs(
const std::string& bus, const std::string& port, uint8_t configuration,
uint8_t interfaceNum)
{
std::filesystem::path netdir =
std::format("/sys/bus/usb/devices/{}-{}/{}-{}:{}.{}/net", bus, port,
bus, port, configuration, interfaceNum);
std::error_code ec;
std::filesystem::directory_iterator it(netdir, ec);
if (ec || it == std::filesystem::end(it))
{
error("No net device associated with USB device at {NET_DEVICE}",
"NET_DEVICE", netdir);
throw MCTPException("Device is not configured as an MCTP interface");
}
return it->path().filename();
}
std::optional<SensorBaseConfigMap> USBMCTPDDevice::match(
const SensorData& config)
{
auto iface = config.find(configInterfaceName(configType));
if (iface == config.end())
{
return std::nullopt;
}
return iface->second;
}
bool USBMCTPDDevice::match(const std::set<std::string>& interfaces)
{
return interfaces.contains(configInterfaceName(configType));
}
std::shared_ptr<USBMCTPDDevice> USBMCTPDDevice::from(
const std::shared_ptr<sdbusplus::asio::connection>& connection,
const SensorBaseConfigMap& iface)
{
auto mName = iface.find("Name");
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 USB device");
}
auto mRootHubPosition = iface.find("RootHubPosition");
auto mPort = iface.find("Port");
auto mConfiguration = iface.find("Configuration");
auto mInterface = iface.find("Interface");
if (mName == iface.end() || mRootHubPosition == iface.end() ||
mPort == iface.end() || mConfiguration == iface.end() ||
mInterface == iface.end())
{
throw std::invalid_argument(
"Configuration object violates MCTPUSBDevice schema");
}
auto sRootHubPosition =
std::visit(VariantToStringVisitor(), mRootHubPosition->second);
uint8_t rootHubPosition{};
auto [rptr, rec] = std::from_chars(
sRootHubPosition.data(),
sRootHubPosition.data() + sRootHubPosition.size(), rootHubPosition);
if (rec != std::errc{} ||
rptr != sRootHubPosition.data() + sRootHubPosition.size())
{
throw std::invalid_argument("Bad RootHubPosition value");
}
auto portArray = std::visit(VariantToNumArrayVisitor<uint8_t, uint64_t>(),
mPort->second);
if (portArray.empty())
{
throw std::invalid_argument("Bad Port value");
}
// Convert port array to dot-separated string (e.g., [1, 2, 1] -> "1.2.1")
std::string port = std::accumulate(
std::next(portArray.begin()), portArray.end(),
std::to_string(portArray[0]), [](const std::string& acc, uint8_t val) {
return acc + "." + std::to_string(val);
});
auto sConfiguration =
std::visit(VariantToStringVisitor(), mConfiguration->second);
uint8_t configuration{};
auto [cptr, cec] = std::from_chars(
sConfiguration.data(), sConfiguration.data() + sConfiguration.size(),
configuration);
if (cec != std::errc{} ||
cptr != sConfiguration.data() + sConfiguration.size())
{
throw std::invalid_argument("Bad Configuration value");
}
auto sInterface = std::visit(VariantToStringVisitor(), mInterface->second);
uint8_t interfaceNum{};
auto [iptr, iec] = std::from_chars(
sInterface.data(), sInterface.data() + sInterface.size(), interfaceNum);
if (iec != std::errc{} || iptr != sInterface.data() + sInterface.size())
{
throw std::invalid_argument("Bad Interface value");
}
std::optional<std::string_view> rootHubPath =
rootHubPathForPosition(rootHubPosition);
if (!rootHubPath)
{
return {};
}
try
{
std::string bus = busFromRootHubPath(std::string(*rootHubPath));
std::string interface =
interfaceFromSysfs(bus, port, configuration, interfaceNum);
return std::make_shared<USBMCTPDDevice>(connection, interface);
}
catch (const MCTPException& ex)
{
warning(
"Failed to create MCTPUSBDevice at [ RootHubPosition: {POSITION}, port: {USB_PORT} ]: {EXCEPTION}",
"POSITION", sRootHubPosition, "USB_PORT", port, "EXCEPTION", ex);
return {};
}
}