| /** |
| * Copyright © 2020 IBM 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. |
| */ |
| #pragma once |
| |
| #include "sdbusplus.hpp" |
| |
| #include <nlohmann/json.hpp> |
| #include <phosphor-logging/log.hpp> |
| #include <sdbusplus/bus.hpp> |
| #include <sdeventplus/source/signal.hpp> |
| |
| #include <filesystem> |
| #include <format> |
| #include <fstream> |
| |
| namespace phosphor::fan |
| { |
| |
| namespace fs = std::filesystem; |
| using json = nlohmann::json; |
| using namespace phosphor::logging; |
| |
| constexpr auto confOverridePath = "/etc/phosphor-fan-presence"; |
| constexpr auto confBasePath = "/usr/share/phosphor-fan-presence"; |
| constexpr auto confCompatServ = "xyz.openbmc_project.EntityManager"; |
| constexpr auto confCompatIntf = |
| "xyz.openbmc_project.Inventory.Decorator.Compatible"; |
| constexpr auto confCompatProp = "Names"; |
| |
| /** |
| * @class NoConfigFound - A no JSON configuration found exception |
| * |
| * A no JSON configuration found exception that is used to denote that a JSON |
| * configuration has not been found yet. |
| */ |
| class NoConfigFound : public std::runtime_error |
| { |
| public: |
| NoConfigFound() = delete; |
| NoConfigFound(const NoConfigFound&) = delete; |
| NoConfigFound(NoConfigFound&&) = delete; |
| NoConfigFound& operator=(const NoConfigFound&) = delete; |
| NoConfigFound& operator=(NoConfigFound&&) = delete; |
| ~NoConfigFound() = default; |
| |
| /** |
| * @brief No JSON configuration found exception object |
| * |
| * When attempting to find the JSON configuration file(s), a NoConfigFound |
| * exception can be thrown to denote that at that time finding/loading the |
| * JSON configuration file(s) for a fan application failed. Details on what |
| * application and JSON configuration file that failed to be found will be |
| * logged resulting in the application being terminated. |
| * |
| * @param[in] details - Additional details |
| */ |
| NoConfigFound(const std::string& appName, const std::string& fileName) : |
| std::runtime_error(std::format("JSON configuration not found [Could " |
| "not find fan {} conf file {}]", |
| appName, fileName) |
| .c_str()) |
| {} |
| }; |
| |
| class JsonConfig |
| { |
| public: |
| /** |
| * @brief Get the object paths with the compatible interface |
| * |
| * Retrieve all the object paths implementing the compatible interface for |
| * configuration file loading. |
| */ |
| std::vector<std::string>& getCompatObjPaths() |
| { |
| using SubTreeMap = |
| std::map<std::string, |
| std::map<std::string, std::vector<std::string>>>; |
| SubTreeMap subTreeObjs = util::SDBusPlus::getSubTreeRaw( |
| util::SDBusPlus::getBus(), "/", confCompatIntf, 0); |
| |
| static std::vector<std::string> paths; |
| for (auto& [path, serviceMap] : subTreeObjs) |
| { |
| // Only save objects under confCompatServ |
| if (serviceMap.find(confCompatServ) != serviceMap.end()) |
| { |
| paths.emplace_back(path); |
| } |
| } |
| return paths; |
| } |
| |
| /** |
| * @brief Constructor |
| * |
| * Attempts to set the list of compatible values from the compatible |
| * interface and call the fan app's function to load its config file(s). If |
| * the compatible interface is not found, it subscribes to the |
| * interfacesAdded signal for that interface on the compatible service |
| * defined above. |
| * |
| * @param[in] func - Fan app function to call to load its config file(s) |
| */ |
| JsonConfig(std::function<void()> func) : _loadFunc(func) |
| { |
| std::vector<std::string> compatObjPaths; |
| |
| _match = std::make_unique<sdbusplus::bus::match_t>( |
| util::SDBusPlus::getBus(), |
| sdbusplus::bus::match::rules::interfacesAdded() + |
| sdbusplus::bus::match::rules::sender(confCompatServ), |
| std::bind(&JsonConfig::compatIntfAdded, this, |
| std::placeholders::_1)); |
| |
| try |
| { |
| compatObjPaths = getCompatObjPaths(); |
| } |
| catch (const util::DBusMethodError&) |
| { |
| // Compatible interface does not exist on any dbus object yet |
| } |
| |
| if (!compatObjPaths.empty()) |
| { |
| for (auto& path : compatObjPaths) |
| { |
| try |
| { |
| // Retrieve json config compatible relative path |
| // locations (last one found will be what's used if more |
| // than one dbus object implementing the compatible |
| // interface exists). |
| _confCompatValues = |
| util::SDBusPlus::getProperty<std::vector<std::string>>( |
| util::SDBusPlus::getBus(), path, confCompatIntf, |
| confCompatProp); |
| } |
| catch (const util::DBusError&) |
| { |
| // Compatible property unavailable on this dbus object |
| // path's compatible interface, ignore |
| } |
| } |
| try |
| { |
| _loadFunc(); |
| } |
| catch (const NoConfigFound&) |
| { |
| // The Decorator.Compatible interface is not unique to one |
| // single object on DBus so this should not be treated as a |
| // failure, wait for interfacesAdded signal. |
| } |
| } |
| else |
| { |
| // Check if required config(s) are found not needing the |
| // compatible interface, otherwise this is intended to catch the |
| // exception thrown by the getConfFile function when the |
| // required config file was not found. This would then result in |
| // waiting for the compatible interfacesAdded signal |
| try |
| { |
| _loadFunc(); |
| } |
| catch (const NoConfigFound&) |
| { |
| // Wait for compatible interfacesAdded signal |
| } |
| } |
| } |
| |
| /** |
| * @brief InterfacesAdded callback function for the compatible interface. |
| * |
| * @param[in] msg - The D-Bus message contents |
| * |
| * If the compatible interface is found, it uses the compatible property on |
| * the interface to set the list of compatible values to be used when |
| * attempting to get a configuration file. Once the list of compatible |
| * values has been updated, it calls the load function. |
| */ |
| void compatIntfAdded(sdbusplus::message_t& msg) |
| { |
| if (!_compatibleName.empty()) |
| { |
| // Do not process the interfaceAdded signal if one compatible name |
| // has been successfully used to get config files |
| return; |
| } |
| sdbusplus::message::object_path op; |
| std::map<std::string, |
| std::map<std::string, std::variant<std::vector<std::string>>>> |
| intfProps; |
| |
| msg.read(op, intfProps); |
| |
| if (intfProps.find(confCompatIntf) == intfProps.end()) |
| { |
| return; |
| } |
| |
| const auto& props = intfProps.at(confCompatIntf); |
| // Only one dbus object with the compatible interface is used at a time |
| _confCompatValues = |
| std::get<std::vector<std::string>>(props.at(confCompatProp)); |
| _loadFunc(); |
| } |
| |
| /** |
| * Get the json configuration file. The first location found to contain |
| * the json config file for the given fan application is used from the |
| * following locations in order. |
| * 1.) From the confOverridePath location |
| * 2.) From the default confBasePath location |
| * 3.) From config file found using an entry from a list obtained from an |
| * interface's property as a relative path extension on the base path where: |
| * interface = Interface set in confCompatIntf with the property |
| * property = Property set in confCompatProp containing a list of |
| * subdirectories in priority order to find a config |
| * |
| * @brief Get the configuration file to be used |
| * |
| * @param[in] appName - The phosphor-fan-presence application name |
| * @param[in] fileName - Application's configuration file's name |
| * @param[in] isOptional - Config file is optional, default to 'false' |
| * |
| * @return filesystem path |
| * The filesystem path to the configuration file to use |
| */ |
| static const fs::path getConfFile(const std::string& appName, |
| const std::string& fileName, |
| bool isOptional = false) |
| { |
| // Check override location |
| fs::path confFile = fs::path{confOverridePath} / appName / fileName; |
| if (fs::exists(confFile)) |
| { |
| return confFile; |
| } |
| |
| // If the default file is there, use it |
| confFile = fs::path{confBasePath} / appName / fileName; |
| if (fs::exists(confFile)) |
| { |
| return confFile; |
| } |
| |
| // Look for a config file at each entry relative to the base |
| // path and use the first one found |
| auto it = std::find_if( |
| _confCompatValues.begin(), _confCompatValues.end(), |
| [&confFile, &appName, &fileName](const auto& value) { |
| confFile = fs::path{confBasePath} / appName / value / fileName; |
| _compatibleName = value; |
| return fs::exists(confFile); |
| }); |
| if (it == _confCompatValues.end()) |
| { |
| confFile.clear(); |
| _compatibleName.clear(); |
| } |
| |
| if (confFile.empty() && !isOptional) |
| { |
| throw NoConfigFound(appName, fileName); |
| } |
| |
| return confFile; |
| } |
| |
| /** |
| * @brief Load the JSON config file |
| * |
| * @param[in] confFile - File system path of the configuration file to load |
| * |
| * @return Parsed JSON object |
| * The parsed JSON configuration file object |
| */ |
| static const json load(const fs::path& confFile) |
| { |
| std::ifstream file; |
| json jsonConf; |
| |
| if (!confFile.empty() && fs::exists(confFile)) |
| { |
| log<level::INFO>( |
| std::format("Loading configuration from {}", confFile.string()) |
| .c_str()); |
| file.open(confFile); |
| try |
| { |
| // Enable ignoring `//` or `/* */` comments |
| jsonConf = json::parse(file, nullptr, true, true); |
| } |
| catch (const std::exception& e) |
| { |
| log<level::ERR>( |
| std::format( |
| "Failed to parse JSON config file: {}, error: {}", |
| confFile.string(), e.what()) |
| .c_str()); |
| throw std::runtime_error( |
| std::format( |
| "Failed to parse JSON config file: {}, error: {}", |
| confFile.string(), e.what()) |
| .c_str()); |
| } |
| } |
| else |
| { |
| log<level::ERR>(std::format("Unable to open JSON config file: {}", |
| confFile.string()) |
| .c_str()); |
| throw std::runtime_error( |
| std::format("Unable to open JSON config file: {}", |
| confFile.string()) |
| .c_str()); |
| } |
| |
| return jsonConf; |
| } |
| |
| /** |
| * @brief Return the compatible values property |
| * |
| * @return const std::vector<std::string>& - The values |
| */ |
| static const std::vector<std::string>& getCompatValues() |
| { |
| return _confCompatValues; |
| } |
| |
| private: |
| /* Load function to call for a fan app to load its config file(s). */ |
| std::function<void()> _loadFunc; |
| |
| /** |
| * @brief The interfacesAdded match that is used to wait |
| * for the Inventory.Decorator.Compatible interface to show up. |
| */ |
| std::unique_ptr<sdbusplus::bus::match_t> _match; |
| |
| /** |
| * @brief List of compatible values from the compatible interface |
| * |
| * Only supports a single instance of the compatible interface on a dbus |
| * object. If more than one dbus object exists with the compatible |
| * interface, the last one found will be the list of compatible values used. |
| */ |
| inline static std::vector<std::string> _confCompatValues; |
| |
| /** |
| * @brief The compatible value that is currently used to load configuration |
| * |
| * The value extracted from the achieved property value list that is used |
| * as a sub-folder to append to the configuration location and really |
| * contains the configruation files |
| */ |
| |
| inline static std::string _compatibleName; |
| }; |
| |
| } // namespace phosphor::fan |