blob: d2215ca77f5b59ecdd97b3341473c35ab65df572 [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 <fmt/format.h>
#include <nlohmann/json.hpp>
#include <phosphor-logging/log.hpp>
#include <sdbusplus/bus.hpp>
#include <sdeventplus/source/signal.hpp>
#include <filesystem>
#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 confCompatIntf =
"xyz.openbmc_project.Configuration.IBMCompatibleSystem";
constexpr auto confCompatProp = "Names";
class JsonConfig
{
public:
using ConfFileReadyFunc = std::function<void(const std::string&)>;
/**
* @brief Constructor
*
* Looks for the JSON config file. If it can't find one, then it
* will watch entity-manager for the IBMCompatibleSystem interface
* to show up and then use that data to try again. If the config
* file is initially present, the callback function is executed
* with the path to the file.
*
* @param[in] bus - The dbus bus object
* @param[in] appName - The appName portion of the config file path
* @param[in] fileName - Application's configuration file's name
* @param[in] func - The function to call when the config file
* is found.
*/
JsonConfig(sdbusplus::bus::bus& bus, const std::string& appName,
const std::string& fileName, ConfFileReadyFunc func) :
_appName(appName),
_fileName(fileName), _readyFunc(func)
{
_match = std::make_unique<sdbusplus::server::match::match>(
bus,
sdbusplus::bus::match::rules::interfacesAdded() +
sdbusplus::bus::match::rules::sender(
"xyz.openbmc_project.EntityManager"),
std::bind(&JsonConfig::ifacesAddedCallback, this,
std::placeholders::_1));
try
{
_confFile = getConfFile(bus, _appName, _fileName);
}
catch (const std::runtime_error& e)
{
// No conf file found, so let the interfacesAdded
// match callback handle finding it.
}
if (!_confFile.empty())
{
_match.reset();
_readyFunc(_confFile);
}
}
/**
* @brief The interfacesAdded callback function that looks for
* the IBMCompatibleSystem interface. If it finds it,
* it uses the Names property in the interface to find
* the JSON config file to use. If it finds one, it calls
* the _readyFunc function with the config file path.
*
* @param[in] msg - The D-Bus message contents
*/
void ifacesAddedCallback(sdbusplus::message::message& msg)
{
sdbusplus::message::object_path path;
std::map<std::string,
std::map<std::string, std::variant<std::vector<std::string>>>>
interfaces;
msg.read(path, interfaces);
if (interfaces.find(confCompatIntf) == interfaces.end())
{
return;
}
const auto& properties = interfaces.at(confCompatIntf);
auto names =
std::get<std::vector<std::string>>(properties.at(confCompatProp));
auto it =
std::find_if(names.begin(), names.end(), [this](auto const& name) {
auto confFile =
fs::path{confBasePath} / _appName / name / _fileName;
if (fs::exists(confFile))
{
_confFile = confFile;
return true;
}
return false;
});
if (it != names.end())
{
_readyFunc(_confFile);
_match.reset();
}
}
/**
* 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 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
* 3.) *DEFAULT* - From the confBasePath location
*
* @brief Get the configuration file to be used
*
* @param[in] bus - The dbus bus object
* @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(sdbusplus::bus::bus& bus,
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;
}
confFile.clear();
// Get all objects implementing the compatible interface
auto objects =
util::SDBusPlus::getSubTreePathsRaw(bus, "/", confCompatIntf, 0);
for (auto& path : objects)
{
try
{
// Retrieve json config compatible relative path locations
auto confCompatValue =
util::SDBusPlus::getProperty<std::vector<std::string>>(
bus, path, confCompatIntf, confCompatProp);
// Look for a config file at each entry relative to the base
// path and use the first one found
auto it = std::find_if(
confCompatValue.begin(), confCompatValue.end(),
[&confFile, &appName, &fileName](auto const& entry) {
confFile =
fs::path{confBasePath} / appName / entry / fileName;
return fs::exists(confFile);
});
if (it != confCompatValue.end())
{
// Use the first config file found at a listed location
break;
}
confFile.clear();
}
catch (const util::DBusError&)
{
// Property unavailable on object.
}
}
if (confFile.empty() && !isOptional)
{
throw std::runtime_error("No JSON config file found");
}
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>(
fmt::format("Loading configuration from {}", confFile.string())
.c_str());
file.open(confFile);
try
{
jsonConf = json::parse(file);
}
catch (std::exception& e)
{
log<level::ERR>(
fmt::format(
"Failed to parse JSON config file: {}, error: {}",
confFile.string(), e.what())
.c_str());
throw std::runtime_error(
fmt::format(
"Failed to parse JSON config file: {}, error: {}",
confFile.string(), e.what())
.c_str());
}
}
else
{
log<level::ERR>(fmt::format("Unable to open JSON config file: {}",
confFile.string())
.c_str());
throw std::runtime_error(
fmt::format("Unable to open JSON config file: {}",
confFile.string())
.c_str());
}
return jsonConf;
}
private:
/**
* @brief The 'appName' portion of the config file path.
*/
const std::string _appName;
/**
* @brief The config file name.
*/
const std::string _fileName;
/**
* @brief The function to call when the config file is available.
*/
ConfFileReadyFunc _readyFunc;
/**
* @brief The JSON config file
*/
fs::path _confFile;
/**
* @brief The interfacesAdded match that is used to wait
* for the IBMCompatibleSystem interface to show up.
*/
std::unique_ptr<sdbusplus::server::match::match> _match;
};
} // namespace phosphor::fan