| /* |
| // 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); |
| } |
| } |