| /* |
| // 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 "absl/strings/match.h" |
| |
| #include "MctpEndpoint.hpp" |
| #include "MctpReactorDevice.hpp" |
| #include "MctpUtil.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 NVMeSubsystemMap = std::map<std::string, std::shared_ptr<NVMeSubsystem>>; |
| static NVMeSubsystemMap nvmeSubsystems; |
| |
| // a map with key value of {path, NVMeDevice} |
| using NVMeDeviceMap = std::map<std::string, std::shared_ptr<NVMeDevice>>; |
| static NVMeDeviceMap 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 std::vector<BusInfo> extractNVMeBusInfo(const SensorData& configData) |
| { |
| std::vector<BusInfo> busInfoList; |
| std::map<int, BusInfo> indexedBusInfo; |
| BusInfo legacyBusInfo; |
| bool hasLegacy = false; |
| |
| for (const auto& pair : configData) |
| { |
| const std::string& intf = pair.first; |
| const SensorBaseConfigMap& props = pair.second; |
| if (absl::StrContains( |
| intf, "xyz.openbmc_project.Configuration.NVME1000.BusInfo")) |
| { |
| BusInfo busInfo; |
| std::string busType; |
| auto it = props.find("BusType"); |
| if (it != props.end()) |
| { |
| busType = std::visit(VariantToStringVisitor(), it->second); |
| busInfo["BusType"] = busType; |
| } |
| |
| auto extractProp = [&](const std::string& key) { |
| auto it = props.find(key); |
| if (it != props.end()) |
| { |
| busInfo[key] = std::visit(VariantToStringVisitor(), |
| it->second); |
| } |
| }; |
| |
| if (busType == "USB") |
| { |
| extractProp("RootHubPath"); |
| extractProp("Port"); |
| extractProp("InterfaceNum"); |
| extractProp("Configuration"); |
| |
| if (busInfo.find("Configuration") == busInfo.end()) |
| { |
| busInfo["Configuration"] = "1"; |
| } |
| |
| if (busInfo.find("InterfaceNum") == busInfo.end()) |
| { |
| busInfo["InterfaceNum"] = "0"; |
| } |
| } |
| else if (busType == "I2C") |
| { |
| extractProp("Bus"); |
| extractProp("Address"); |
| } |
| else if (busType == "MCTP Bridge") |
| { |
| extractProp("EIDOffset"); |
| } |
| else if (!busType.empty()) |
| { |
| lg2::warning("Unsupported BusType {TYPE} in NVME1000 config", |
| "TYPE", busType); |
| } |
| |
| std::string suffix = intf.substr( |
| std::string( |
| "xyz.openbmc_project.Configuration.NVME1000.BusInfo") |
| .length()); |
| if (suffix.empty()) |
| { |
| legacyBusInfo = busInfo; |
| hasLegacy = true; |
| } |
| else |
| { |
| try |
| { |
| int index = std::stoi(suffix); |
| indexedBusInfo[index] = busInfo; |
| } |
| catch (const std::exception& e) |
| { |
| lg2::error( |
| "Failed to parse index from interface {INTF}: {ERROR}", |
| "INTF", intf, "ERROR", e.what()); |
| } |
| } |
| } |
| } |
| |
| if (hasLegacy) |
| { |
| busInfoList.push_back(legacyBusInfo); |
| } |
| else |
| { |
| for (const auto& [index, busInfo] : indexedBusInfo) |
| { |
| busInfoList.push_back(busInfo); |
| } |
| } |
| |
| return busInfoList; |
| } |
| |
| 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<std::string> nvmeProtocol = |
| extractProtocol(nvmeObjectPath, sensorConfig); |
| if (!nvmeProtocol) |
| { |
| nvmeProtocol.emplace("mi_basic"); |
| } |
| |
| std::optional<int> busNumber; |
| std::optional<int> address; |
| std::vector<BusInfo> busInfoList; |
| std::string endpointPath; |
| |
| if (*nvmeProtocol == "mi_basic" || *nvmeProtocol == "mi_i2c") |
| { |
| busNumber = extractBusNumber(nvmeObjectPath, sensorConfig); |
| address = extractAddress(nvmeObjectPath, sensorConfig); |
| if (!busNumber) |
| { |
| continue; |
| } |
| } |
| else if (*nvmeProtocol == "mi_mctp") |
| { |
| busInfoList = extractNVMeBusInfo(configData); |
| if (busInfoList.empty()) |
| { |
| lg2::error("Missing BusInfo for mi_mctp device {PATH}", "PATH", |
| nvmeObjectPath.str); |
| continue; |
| } |
| |
| // Matching is now handled in MctpReactorDevice::setup() |
| |
| if (!busInfoList.empty()) |
| { |
| const auto& rootBusInfo = busInfoList.front(); |
| auto itType = rootBusInfo.find("BusType"); |
| if (itType != rootBusInfo.end() && itType->second == "I2C") |
| { |
| auto itBus = rootBusInfo.find("Bus"); |
| if (itBus != rootBusInfo.end()) |
| { |
| busNumber = std::stoi(itBus->second); |
| } |
| auto itAddr = rootBusInfo.find("Address"); |
| if (itAddr != rootBusInfo.end()) |
| { |
| address = std::stoi(itAddr->second, nullptr, 0); |
| } |
| } |
| } |
| } |
| |
| std::optional<std::string> sensorName = extractName(nvmeObjectPath, |
| sensorConfig); |
| std::optional<std::string> mctpReactorConfigPath = |
| extractMctpReactorConfigPath(sensorConfig); |
| |
| bool enableFeatureLockdown = hasLockdownFeature(nvmeObjectPath, |
| sensorConfig); |
| |
| if (!sensorName) |
| { |
| continue; |
| } |
| |
| const bool isMctpReactorDevice = mctpReactorConfigPath.has_value() || |
| *nvmeProtocol == "mi_mctp"; |
| |
| if (busNumber && bannedBuses.contains(*busNumber)) |
| { |
| lg2::info("Skip banned i2c bus: {BUS}", "BUS", *busNumber); |
| 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(*sensorName, nvmeIntf); |
| |
| auto [subsysEntry, subsysAdded] = |
| nvmeSubsystems.try_emplace(nvmeObjectPath, nvmeSubsys); |
| if (subsysAdded) |
| { |
| subsysEntry->second->start(); |
| } |
| |
| auto [devEntry, devAdded] = |
| nvmeDevices.try_emplace(nvmeObjectPath, nvmeDev); |
| if (devAdded) |
| { |
| devEntry->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" || *nvmeProtocol == "mi_mctp") |
| { |
| // defualt i2c nvme-mi port is 0x1d |
| if (!address) |
| { |
| address.emplace(0x1d); |
| } |
| PowerState powerState = getPowerState(sensorConfig); |
| |
| std::shared_ptr<NVMeMiWorker> worker; |
| if (singleWorkerFeature && busNumber.has_value()) |
| { |
| 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 |
| { |
| std::shared_ptr<MctpDevice> mctpDev; |
| if (*nvmeProtocol == "mi_mctp") |
| { |
| mctpDev = std::make_shared<MctpReactorDevice>( |
| dbusConnection, nvmeObjectPath.str, busInfoList, |
| busNumber, address); |
| } |
| else 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, *sensorName, mctpDev, |
| nvmeIntf); |
| |
| auto [subsysEntry, subsysAdded] = |
| nvmeSubsystems.try_emplace(nvmeObjectPath, nvmeSubsys); |
| if (subsysAdded) |
| { |
| subsysEntry->second->start(); |
| } |
| |
| auto [devEntry, devAdded] = |
| nvmeDevices.try_emplace(nvmeObjectPath, nvmeDev); |
| if (devAdded) |
| { |
| devEntry->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(); |
| |
| for (auto& [_, nvmeSubsys] : nvmeSubsystems) |
| { |
| nvmeSubsys->stop(); |
| } |
| nvmeSubsystems.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) |
| { |
| 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 = nvmeDevices.find(path); |
| if (device != nvmeDevices.end()) |
| { |
| device->second->stop(); |
| nvmeDevices.erase(device); |
| } |
| |
| auto subsys = nvmeSubsystems.find(path); |
| if (subsys != nvmeSubsystems.end()) |
| { |
| subsys->second->stop(); |
| nvmeSubsystems.erase(subsys); |
| } |
| } |
| |
| 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"); |
| |
| setupMctpEndpointListener(systemBus, MctpMessageType::NVME_MI); |
| |
| 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); }); |
| |
| 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); |
| } |
| } |