blob: c770c7ba181c0fa820f3f9b5a58b736c28f768f2 [file]
/*
// Copyright (c) 2019 Intel Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
*/
#include "MctpEndpoint.hpp"
#include "MctpReactorDevice.hpp"
#include "NVMeBasic.hpp"
#include "NVMeDevice.hpp"
#include "NVMeIntf.hpp"
#include "NVMeMi.hpp"
#include "NVMePlugin.hpp"
#include "NVMeSubsys.hpp"
#include "Utils.hpp"
#include <dlfcn.h>
#include <boost/algorithm/string.hpp>
#include <boost/asio/steady_timer.hpp>
#include <phosphor-logging/lg2.hpp>
#include <filesystem>
#include <fstream>
#include <optional>
#include <regex>
#include <system_error>
#include <unordered_set>
// a map with key value of {path, NVMeSubsystem}
using NVMEMap = std::map<std::string, std::shared_ptr<NVMeDevice>>;
static NVMEMap nvmeDevices;
// A map from root bus number to the Worker
// This map means to reuse the same worker for all NVMe EP under the same
// I2C root bus. There is no real physical concurrency among the i2c/mctp
// devices on the same bus. Though mctp kernel drive can schedule and
// sequencialize the transactions but assigning individual worker thread to
// each EP makes no sense.
static std::map<int, std::weak_ptr<NVMeMiWorker>> i2cWorkerMap{};
std::unordered_map<std::string, void*> pluginLibMap = {};
static std::unordered_set<int> bannedBuses;
static void initBannedI2cBus()
{
const std::string script = "/usr/bin/init-banned-i2c-bus.sh";
const std::string confPath = "/var/run/nvmed/banned-i2c-bus.conf";
if (!std::filesystem::exists(script))
{
lg2::error("Script {SCRIPT} doesn't exist", "SCRIPT", script);
return;
}
lg2::info("Begin to execute {SCRIPT}", "SCRIPT", script);
// TODO: (b/376270522) cert-env33-c error: calling 'system' uses a command
// processor
int rc = std::system(script.c_str()); // NOLINT(cert-env33-c)
lg2::info("Shell script rc = {RC}", "RC", rc);
if (!std::filesystem::exists(confPath))
{
lg2::warning("Warning: {CONF_PATH} doesn't exist.", "CONF_PATH",
confPath);
// Be optimistic,assume no bus is banned
return;
}
std::ifstream file;
file.open(confPath);
if (!file.is_open())
{
lg2::error("Error: cannot open {CONF_PATH}", "CONF_PATH", confPath);
// Be optimistic,assume no bus is banned
return;
}
bannedBuses.clear();
int i2cBus{0};
while (file >> i2cBus)
{
lg2::info("Banned i2c bus: {BUS}", "BUS", i2cBus);
bannedBuses.insert(i2cBus);
}
file.close();
}
static std::optional<int>
extractBusNumber(const std::string& path,
const SensorBaseConfigMap& properties)
{
auto findBus = properties.find("Bus");
if (findBus == properties.end())
{
lg2::error("could not determine bus number for '{PATH}'", "PATH", path);
return std::nullopt;
}
return std::visit(VariantToIntVisitor(), findBus->second);
}
static std::optional<int> extractAddress(const std::string& path,
const SensorBaseConfigMap& properties)
{
auto findAddr = properties.find("Address");
if (findAddr == properties.end())
{
lg2::error("could not determine address for '{PATH}'", "PATH", path);
return std::nullopt;
}
return std::visit(VariantToIntVisitor(), findAddr->second);
}
static std::optional<std::string>
extractName(const std::string& path, const SensorBaseConfigMap& properties)
{
auto findName = properties.find("Name");
if (findName == properties.end())
{
lg2::error("could not determine configuration name for '{PATH}'",
"PATH", path);
return std::nullopt;
}
return std::get<std::string>(findName->second);
}
static std::optional<std::string>
extractProtocol(const std::string& path,
const SensorBaseConfigMap& properties)
{
auto findProtocol = properties.find("Protocol");
if (findProtocol == properties.end())
{
lg2::error("could not determine nvme protocol for '{PATH}'", "PATH",
path);
return std::nullopt;
}
return std::get<std::string>(findProtocol->second);
}
static std::optional<std::string>
extractMctpReactorConfigPath(const SensorBaseConfigMap& properties)
{
auto findMctpReactorConfigPath = properties.find("MctpReactorConfigPath");
if (findMctpReactorConfigPath == properties.end())
{
return std::nullopt;
}
return std::get<std::string>(findMctpReactorConfigPath->second);
}
static bool hasLockdownFeature(const std::string& path,
const SensorBaseConfigMap& properties)
{
auto it = properties.find("SupportedFeatures");
if (it == properties.end())
{
return false;
}
const auto& value = it->second;
if (std::holds_alternative<std::vector<std::string>>(value))
{
const auto& features =
std::get<std::vector<std::string>>(value); // Safe now
for (const auto& feature : features)
{
if (feature == "Lockdown")
{
return true;
}
}
}
else
{
lg2::warning(
"'{PATH}': 'SupportedFeatures' is not an array of strings.", "PATH",
path);
}
return false;
}
static void handleConfigurations(
boost::asio::io_context& io, sdbusplus::asio::object_server& objectServer,
std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
const ManagedObjectType& nvmeConfigurations)
{
// Initialize banned i2c bus info on every configuration change
initBannedI2cBus();
for (const auto& [nvmeObjectPath, configData] : nvmeConfigurations)
{
// find base configuration
auto sensorBase =
configData.find(configInterfaceName(nvme::sensorType));
if (sensorBase == configData.end())
{
continue;
}
const SensorBaseConfigMap& sensorConfig = sensorBase->second;
std::optional<int> busNumber = extractBusNumber(nvmeObjectPath,
sensorConfig);
std::optional<int> address = extractAddress(nvmeObjectPath,
sensorConfig);
std::optional<std::string> sensorName = extractName(nvmeObjectPath,
sensorConfig);
std::optional<std::string> nvmeProtocol =
extractProtocol(nvmeObjectPath, sensorConfig);
std::optional<std::string> mctpReactorConfigPath =
extractMctpReactorConfigPath(sensorConfig);
bool enableFeatureLockdown = hasLockdownFeature(nvmeObjectPath,
sensorConfig);
if (!sensorName)
{
continue;
}
const bool isMctpReactorDevice = mctpReactorConfigPath.has_value();
// Some NVMe devices are accessible over I2C because they're connected
// over SMBus directly to the BMC. In other cases, they might be
// connected to the BMC through a bridge (such as
// MCTP USB <-> I2C bridge).
const bool isI2cAccessible = busNumber.has_value();
// If a device is not an mctpreactor device, it must be accessible over
// I2C directly.
if (!isMctpReactorDevice && !isI2cAccessible)
{
lg2::info("Found an mctpd device that isn't I2C accessible,"
"but it must be: {PATH}",
"PATH", nvmeObjectPath.str);
continue;
}
if (busNumber && bannedBuses.contains(*busNumber))
{
lg2::info("Skip banned i2c bus: {BUS}", "BUS", *busNumber);
continue;
}
// the default protocol is mi_basic
if (!nvmeProtocol)
{
nvmeProtocol.emplace("mi_basic");
}
// Support for non-I2C-accessible devices doesn't exist for mi_basic
// protocol because we don't need it for now.
if (*nvmeProtocol == "mi_basic" && !isI2cAccessible)
{
lg2::info("Skipping an mi_basic device that's not accessible "
"over i2c bus.");
continue;
}
if (*nvmeProtocol == "mi_basic")
{
// defualt i2c basic port is 0x6a
if (!address)
{
address.emplace(0x6a);
}
try
{
NVMeIntf nvmeIntf = NVMeIntf::create<NVMeBasic>(io, *busNumber,
*address);
auto nvmeSubsys = NVMeSubsystem::create(
io, objectServer, dbusConnection, nvmeObjectPath,
*sensorName, configData, nvmeIntf, enableFeatureLockdown);
auto nvmeDev = NVMeDevice::create(std::move(nvmeIntf),
nvmeSubsys);
auto [entry, added] = nvmeDevices.try_emplace(nvmeObjectPath,
nvmeDev);
if (added)
{
entry->second->start();
}
}
catch (std::exception& ex)
{
lg2::error(
"Failed to add nvme basic interface for '{PATH}': {ERROR}",
"PATH", nvmeObjectPath.str, "ERROR", ex.what());
}
}
else if (*nvmeProtocol == "mi_i2c")
{
// defualt i2c nvme-mi port is 0x1d
if (!address)
{
address.emplace(0x1d);
}
PowerState powerState = getPowerState(sensorConfig);
std::shared_ptr<NVMeMiWorker> worker;
if (singleWorkerFeature && isI2cAccessible)
{
auto root = deriveRootBus(*busNumber);
if (!root || *root < 0)
{
throw std::runtime_error("invalid root bus number");
}
auto res = i2cWorkerMap.find(*root);
if (res == i2cWorkerMap.end() || res->second.expired())
{
worker = NVMeMiWorker::create(io);
i2cWorkerMap[*root] = worker;
}
else
{
worker = res->second.lock();
}
}
else
{
worker = NVMeMiWorker::create(io);
}
try
{
// Note that in this codepath, despite using "mi_i2c" protocol,
// we can be talking to NVMe devices not accessible through I2C
// directly.
std::shared_ptr<MctpDevice> mctpDev;
if (!isMctpReactorDevice)
{
mctpDev = std::make_shared<SmbusMctpdDevice>(
dbusConnection, *busNumber, *address);
}
else // isMctpReactorDevice
{
mctpDev = std::make_shared<MctpReactorDevice>(
dbusConnection, *mctpReactorConfigPath, busNumber,
address);
}
NVMeIntf nvmeIntf = NVMeIntf::create<NVMeMi>(
io, dbusConnection, mctpDev, worker, powerState);
auto nvmeSubsys = NVMeSubsystem::create(
io, objectServer, dbusConnection, nvmeObjectPath,
*sensorName, configData, nvmeIntf, enableFeatureLockdown);
auto nvmeDev = NVMeDevice::create(
io, mctpDev, std::move(nvmeIntf), nvmeSubsys);
auto [entry, added] = nvmeDevices.try_emplace(nvmeObjectPath,
nvmeDev);
if (added)
{
entry->second->start();
}
}
catch (std::exception& ex)
{
lg2::error(
"Failed to add nvme mi interface for '{PATH}': {ERROR}",
"PATH", nvmeObjectPath.str, "ERROR", ex.what());
}
}
}
}
void createNVMeSubsystems(
boost::asio::io_context& io, sdbusplus::asio::object_server& objectServer,
std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
{
// todo: it'd be better to only update the ones we care about
for (auto& [_, nvmeDev] : nvmeDevices)
{
nvmeDev->stop();
}
nvmeDevices.clear();
static int count = 0;
static ManagedObjectType configs;
count += 2;
auto getter = std::make_shared<GetSensorConfiguration>(
dbusConnection, [&io, &objectServer, &dbusConnection](
const ManagedObjectType& nvmeConfigurations) {
configs = nvmeConfigurations;
count--;
if (count == 0)
{
handleConfigurations(io, objectServer, dbusConnection, configs);
}
else
{
lg2::error("more than one `handleConfigurations` has been "
"scheduled, cancel the current one");
}
});
auto timer = std::make_shared<boost::asio::steady_timer>(
io, std::chrono::seconds(10));
timer->async_wait([&io, &objectServer, &dbusConnection,
timer](const boost::system::error_code& ec) {
count--;
if (ec)
{
return;
}
if (count == 0)
{
handleConfigurations(io, objectServer, dbusConnection, configs);
}
else
{
lg2::error("`handleConfigurations` has not been triggered, "
"cancel the timer");
}
});
getter->getConfiguration(std::vector<std::string>{nvme::sensorType});
}
static void interfaceRemoved(sdbusplus::message_t& message, NVMEMap& devices)
{
if (message.is_method_error())
{
lg2::error("interfacesRemoved callback method error");
return;
}
sdbusplus::message::object_path path;
std::vector<std::string> interfaces;
message.read(path, interfaces);
auto interface = std::find(interfaces.begin(), interfaces.end(),
configInterfaceName(nvme::sensorType));
if (interface == interfaces.end())
{
return;
}
auto device = devices.find(path);
if (device == devices.end())
{
return;
}
device->second->stop();
devices.erase(device);
}
int main(int argc, char** argv)
{
for (int i = 1; i < argc; ++i)
{
std::string_view arg(argv[i]);
if (arg == "--enable-metrics")
{
setMetricStoreEnabled(true);
lg2::info("Metric store runtime override: enabled via CLI");
}
else if (arg == "--disable-metrics")
{
setMetricStoreEnabled(false);
lg2::info("Metric store runtime override: disabled via CLI");
}
}
if (singleWorkerFeature)
{
lg2::info("singleWorkerFeature on");
}
// Load plugin shared libraries
try
{
for (const auto& entry :
std::filesystem::directory_iterator(NVMePlugin::libraryPath))
{
void* lib = dlopen(entry.path().c_str(), RTLD_NOW);
if (lib != nullptr)
{
pluginLibMap.emplace(entry.path().filename().string(), lib);
}
else
{
lg2::error("could not load the plugin: {ERROR}", "ERROR",
dlerror());
}
}
}
catch (const std::filesystem::filesystem_error& e)
{
lg2::error("failed to open plugin folder: {ERROR}", "ERROR", e.what());
}
// TODO: set single thread mode according to input parameters
boost::asio::io_context io;
auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
systemBus->request_name("xyz.openbmc_project.NVMe");
sdbusplus::asio::object_server objectServer(systemBus, true);
objectServer.add_manager("/xyz/openbmc_project/sensors");
objectServer.add_manager("/xyz/openbmc_project/inventory");
boost::asio::post(
io, [&]() { createNVMeSubsystems(io, objectServer, systemBus); });
boost::asio::steady_timer filterTimer(io);
std::function<void(sdbusplus::message_t&)> eventHandler =
[&filterTimer, &io, &objectServer, &systemBus](sdbusplus::message_t&) {
// this implicitly cancels the timer
filterTimer.expires_after(std::chrono::seconds(1));
filterTimer.async_wait([&](const boost::system::error_code& ec) {
if (ec == boost::asio::error::operation_aborted)
{
return; // we're being canceled
}
if (ec)
{
lg2::error("Error: {ERROR}", "ERROR", ec.message());
return;
}
createNVMeSubsystems(io, objectServer, systemBus);
});
};
std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
setupPropertiesChangedMatches(
*systemBus, std::to_array<const char*>({NVMeSensor::sensorType}),
eventHandler);
// Watch for entity-manager to remove configuration interfaces
// so the corresponding sensors can be removed.
auto ifaceRemovedMatch = std::make_unique<sdbusplus::bus::match_t>(
static_cast<sdbusplus::bus_t&>(*systemBus),
"type='signal',member='InterfacesRemoved',arg0path='" +
std::string(inventoryPath) + "/'",
[](sdbusplus::message_t& msg) { interfaceRemoved(msg, nvmeDevices); });
setupManufacturingModeMatch(*systemBus);
// The NVMe controller used pipe to transfer raw data. The pipe could be
// closed by the client. It should not be considered as an error.
boost::asio::signal_set signals(io, SIGPIPE);
signals.async_wait(
[](const boost::system::error_code& error, int signalNumber) {
lg2::info("signal: {SIGNAL}, {ERROR}", "SIGNAL",
strsignal(signalNumber), "ERROR", error.message());
});
io.run();
for (const auto& [_, lib] : pluginLibMap)
{
dlclose(lib);
}
}