blob: 04deb6bebb084f01d72f8fe9076e21d1f9ed6b8f [file] [log] [blame]
/**
* 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