/** * 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 #include #include #include #include #include #include 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& getCompatObjPaths() { using SubTreeMap = std::map>>; SubTreeMap subTreeObjs = util::SDBusPlus::getSubTreeRaw( util::SDBusPlus::getBus(), "/", confCompatIntf, 0); static std::vector 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 func) : _loadFunc(func) { std::vector compatObjPaths; _match = std::make_unique( 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>( 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>>> 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>(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( 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( 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(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& - The values */ static const std::vector& getCompatValues() { return _confCompatValues; } private: /* Load function to call for a fan app to load its config file(s). */ std::function _loadFunc; /** * @brief The interfacesAdded match that is used to wait * for the Inventory.Decorator.Compatible interface to show up. */ std::unique_ptr _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 _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