Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 1 | /** |
| 2 | * Copyright © 2020 IBM Corporation |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | #pragma once |
| 17 | |
| 18 | #include "sdbusplus.hpp" |
| 19 | |
Matthew Barth | 1826c73 | 2020-08-28 08:40:59 -0500 | [diff] [blame] | 20 | #include <fmt/format.h> |
| 21 | |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 22 | #include <nlohmann/json.hpp> |
| 23 | #include <phosphor-logging/log.hpp> |
| 24 | #include <sdbusplus/bus.hpp> |
| 25 | #include <sdeventplus/source/signal.hpp> |
| 26 | |
| 27 | #include <filesystem> |
| 28 | #include <fstream> |
| 29 | |
| 30 | namespace phosphor::fan |
| 31 | { |
| 32 | |
| 33 | namespace fs = std::filesystem; |
| 34 | using json = nlohmann::json; |
| 35 | using namespace phosphor::logging; |
| 36 | |
| 37 | constexpr auto confOverridePath = "/etc/phosphor-fan-presence"; |
| 38 | constexpr auto confBasePath = "/usr/share/phosphor-fan-presence"; |
Matthew Barth | fcbdc0e | 2020-10-28 14:11:07 -0500 | [diff] [blame] | 39 | constexpr auto confCompatIntf = |
| 40 | "xyz.openbmc_project.Configuration.IBMCompatibleSystem"; |
| 41 | constexpr auto confCompatProp = "Names"; |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 42 | |
| 43 | class JsonConfig |
| 44 | { |
| 45 | public: |
Matt Spinler | 59850df | 2021-01-06 16:56:06 -0600 | [diff] [blame] | 46 | using ConfFileReadyFunc = std::function<void(const std::string&)>; |
| 47 | |
| 48 | /** |
| 49 | * @brief Constructor |
| 50 | * |
| 51 | * Looks for the JSON config file. If it can't find one, then it |
| 52 | * will watch entity-manager for the IBMCompatibleSystem interface |
| 53 | * to show up and then use that data to try again. If the config |
| 54 | * file is initially present, the callback function is executed |
| 55 | * with the path to the file. |
| 56 | * |
| 57 | * @param[in] bus - The dbus bus object |
| 58 | * @param[in] appName - The appName portion of the config file path |
| 59 | * @param[in] fileName - Application's configuration file's name |
| 60 | * @param[in] func - The function to call when the config file |
| 61 | * is found. |
| 62 | */ |
| 63 | JsonConfig(sdbusplus::bus::bus& bus, const std::string& appName, |
| 64 | const std::string& fileName, ConfFileReadyFunc func) : |
| 65 | _appName(appName), |
| 66 | _fileName(fileName), _readyFunc(func) |
| 67 | { |
| 68 | _match = std::make_unique<sdbusplus::server::match::match>( |
| 69 | bus, |
| 70 | sdbusplus::bus::match::rules::interfacesAdded() + |
| 71 | sdbusplus::bus::match::rules::sender( |
| 72 | "xyz.openbmc_project.EntityManager"), |
| 73 | std::bind(&JsonConfig::ifacesAddedCallback, this, |
| 74 | std::placeholders::_1)); |
| 75 | try |
| 76 | { |
| 77 | _confFile = getConfFile(bus, _appName, _fileName); |
| 78 | } |
| 79 | catch (const std::runtime_error& e) |
| 80 | { |
| 81 | // No conf file found, so let the interfacesAdded |
| 82 | // match callback handle finding it. |
| 83 | } |
| 84 | |
| 85 | if (!_confFile.empty()) |
| 86 | { |
| 87 | _match.reset(); |
| 88 | _readyFunc(_confFile); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * @brief The interfacesAdded callback function that looks for |
| 94 | * the IBMCompatibleSystem interface. If it finds it, |
| 95 | * it uses the Names property in the interface to find |
| 96 | * the JSON config file to use. If it finds one, it calls |
| 97 | * the _readyFunc function with the config file path. |
| 98 | * |
| 99 | * @param[in] msg - The D-Bus message contents |
| 100 | */ |
| 101 | void ifacesAddedCallback(sdbusplus::message::message& msg) |
| 102 | { |
| 103 | sdbusplus::message::object_path path; |
| 104 | std::map<std::string, |
| 105 | std::map<std::string, std::variant<std::vector<std::string>>>> |
| 106 | interfaces; |
| 107 | |
| 108 | msg.read(path, interfaces); |
| 109 | |
| 110 | if (interfaces.find(confCompatIntf) == interfaces.end()) |
| 111 | { |
| 112 | return; |
| 113 | } |
| 114 | |
| 115 | const auto& properties = interfaces.at(confCompatIntf); |
| 116 | auto names = |
| 117 | std::get<std::vector<std::string>>(properties.at(confCompatProp)); |
| 118 | |
| 119 | auto it = |
| 120 | std::find_if(names.begin(), names.end(), [this](auto const& name) { |
| 121 | auto confFile = |
| 122 | fs::path{confBasePath} / _appName / name / _fileName; |
| 123 | if (fs::exists(confFile)) |
| 124 | { |
| 125 | _confFile = confFile; |
| 126 | return true; |
| 127 | } |
| 128 | return false; |
| 129 | }); |
| 130 | |
| 131 | if (it != names.end()) |
| 132 | { |
| 133 | _readyFunc(_confFile); |
| 134 | _match.reset(); |
| 135 | } |
Matt Spinler | 4031f10 | 2021-04-22 16:42:20 -0500 | [diff] [blame] | 136 | else |
| 137 | { |
| 138 | log<level::ERR>(fmt::format("Could not find fan {} conf file {} " |
| 139 | "even after {} iface became available", |
| 140 | _appName, _fileName, confCompatIntf) |
| 141 | .c_str()); |
| 142 | } |
Matt Spinler | 59850df | 2021-01-06 16:56:06 -0600 | [diff] [blame] | 143 | } |
| 144 | |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 145 | /** |
| 146 | * Get the json configuration file. The first location found to contain |
| 147 | * the json config file for the given fan application is used from the |
| 148 | * following locations in order. |
| 149 | * 1.) From the confOverridePath location |
Matthew Barth | fcbdc0e | 2020-10-28 14:11:07 -0500 | [diff] [blame] | 150 | * 2.) From config file found using an entry from a list obtained from an |
| 151 | * interface's property as a relative path extension on the base path where: |
| 152 | * interface = Interface set in confCompatIntf with the property |
| 153 | * property = Property set in confCompatProp containing a list of |
| 154 | * subdirectories in priority order to find a config |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 155 | * 3.) *DEFAULT* - From the confBasePath location |
| 156 | * |
| 157 | * @brief Get the configuration file to be used |
| 158 | * |
| 159 | * @param[in] bus - The dbus bus object |
| 160 | * @param[in] appName - The phosphor-fan-presence application name |
| 161 | * @param[in] fileName - Application's configuration file's name |
Matthew Barth | b599102 | 2020-08-05 11:08:50 -0500 | [diff] [blame] | 162 | * @param[in] isOptional - Config file is optional, default to 'false' |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 163 | * |
| 164 | * @return filesystem path |
| 165 | * The filesystem path to the configuration file to use |
| 166 | */ |
| 167 | static const fs::path getConfFile(sdbusplus::bus::bus& bus, |
| 168 | const std::string& appName, |
Matthew Barth | b599102 | 2020-08-05 11:08:50 -0500 | [diff] [blame] | 169 | const std::string& fileName, |
| 170 | bool isOptional = false) |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 171 | { |
| 172 | // Check override location |
| 173 | fs::path confFile = fs::path{confOverridePath} / appName / fileName; |
| 174 | if (fs::exists(confFile)) |
| 175 | { |
| 176 | return confFile; |
| 177 | } |
| 178 | |
Matt Spinler | 59850df | 2021-01-06 16:56:06 -0600 | [diff] [blame] | 179 | // If the default file is there, use it |
Matthew Barth | fcbdc0e | 2020-10-28 14:11:07 -0500 | [diff] [blame] | 180 | confFile = fs::path{confBasePath} / appName / fileName; |
Matt Spinler | 59850df | 2021-01-06 16:56:06 -0600 | [diff] [blame] | 181 | if (fs::exists(confFile)) |
| 182 | { |
| 183 | return confFile; |
| 184 | } |
| 185 | confFile.clear(); |
Matthew Barth | fcbdc0e | 2020-10-28 14:11:07 -0500 | [diff] [blame] | 186 | |
| 187 | // Get all objects implementing the compatible interface |
| 188 | auto objects = |
| 189 | util::SDBusPlus::getSubTreePathsRaw(bus, "/", confCompatIntf, 0); |
| 190 | for (auto& path : objects) |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 191 | { |
Matthew Barth | fcbdc0e | 2020-10-28 14:11:07 -0500 | [diff] [blame] | 192 | try |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 193 | { |
Matthew Barth | fcbdc0e | 2020-10-28 14:11:07 -0500 | [diff] [blame] | 194 | // Retrieve json config compatible relative path locations |
| 195 | auto confCompatValue = |
| 196 | util::SDBusPlus::getProperty<std::vector<std::string>>( |
| 197 | bus, path, confCompatIntf, confCompatProp); |
| 198 | // Look for a config file at each entry relative to the base |
| 199 | // path and use the first one found |
| 200 | auto it = std::find_if( |
| 201 | confCompatValue.begin(), confCompatValue.end(), |
| 202 | [&confFile, &appName, &fileName](auto const& entry) { |
| 203 | confFile = |
| 204 | fs::path{confBasePath} / appName / entry / fileName; |
| 205 | return fs::exists(confFile); |
| 206 | }); |
| 207 | if (it != confCompatValue.end()) |
| 208 | { |
| 209 | // Use the first config file found at a listed location |
| 210 | break; |
| 211 | } |
Matt Spinler | 59850df | 2021-01-06 16:56:06 -0600 | [diff] [blame] | 212 | confFile.clear(); |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 213 | } |
Matthew Barth | fcbdc0e | 2020-10-28 14:11:07 -0500 | [diff] [blame] | 214 | catch (const util::DBusError&) |
| 215 | { |
| 216 | // Property unavailable on object. |
Matthew Barth | fcbdc0e | 2020-10-28 14:11:07 -0500 | [diff] [blame] | 217 | } |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 218 | } |
| 219 | |
Matt Spinler | 4031f10 | 2021-04-22 16:42:20 -0500 | [diff] [blame] | 220 | if (!isOptional && confFile.empty() && !objects.empty()) |
| 221 | { |
| 222 | log<level::ERR>(fmt::format("Could not find fan {} conf file {}", |
| 223 | appName, fileName) |
| 224 | .c_str()); |
| 225 | } |
| 226 | |
Matt Spinler | 59850df | 2021-01-06 16:56:06 -0600 | [diff] [blame] | 227 | if (confFile.empty() && !isOptional) |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 228 | { |
Matt Spinler | 59850df | 2021-01-06 16:56:06 -0600 | [diff] [blame] | 229 | throw std::runtime_error("No JSON config file found"); |
Matthew Barth | b599102 | 2020-08-05 11:08:50 -0500 | [diff] [blame] | 230 | } |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 231 | |
| 232 | return confFile; |
| 233 | } |
| 234 | |
| 235 | /** |
| 236 | * @brief Load the JSON config file |
| 237 | * |
| 238 | * @param[in] confFile - File system path of the configuration file to load |
| 239 | * |
| 240 | * @return Parsed JSON object |
| 241 | * The parsed JSON configuration file object |
| 242 | */ |
| 243 | static const json load(const fs::path& confFile) |
| 244 | { |
| 245 | std::ifstream file; |
| 246 | json jsonConf; |
| 247 | |
Matthew Barth | b599102 | 2020-08-05 11:08:50 -0500 | [diff] [blame] | 248 | if (!confFile.empty() && fs::exists(confFile)) |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 249 | { |
Matthew Barth | 1826c73 | 2020-08-28 08:40:59 -0500 | [diff] [blame] | 250 | log<level::INFO>( |
| 251 | fmt::format("Loading configuration from {}", confFile.string()) |
| 252 | .c_str()); |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 253 | file.open(confFile); |
| 254 | try |
| 255 | { |
| 256 | jsonConf = json::parse(file); |
| 257 | } |
| 258 | catch (std::exception& e) |
| 259 | { |
Matthew Barth | 1826c73 | 2020-08-28 08:40:59 -0500 | [diff] [blame] | 260 | log<level::ERR>( |
| 261 | fmt::format( |
| 262 | "Failed to parse JSON config file: {}, error: {}", |
| 263 | confFile.string(), e.what()) |
| 264 | .c_str()); |
| 265 | throw std::runtime_error( |
| 266 | fmt::format( |
| 267 | "Failed to parse JSON config file: {}, error: {}", |
| 268 | confFile.string(), e.what()) |
| 269 | .c_str()); |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 270 | } |
| 271 | } |
| 272 | else |
| 273 | { |
Matthew Barth | 1826c73 | 2020-08-28 08:40:59 -0500 | [diff] [blame] | 274 | log<level::ERR>(fmt::format("Unable to open JSON config file: {}", |
| 275 | confFile.string()) |
| 276 | .c_str()); |
| 277 | throw std::runtime_error( |
| 278 | fmt::format("Unable to open JSON config file: {}", |
| 279 | confFile.string()) |
| 280 | .c_str()); |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 281 | } |
| 282 | |
| 283 | return jsonConf; |
| 284 | } |
Matt Spinler | 59850df | 2021-01-06 16:56:06 -0600 | [diff] [blame] | 285 | |
| 286 | private: |
| 287 | /** |
| 288 | * @brief The 'appName' portion of the config file path. |
| 289 | */ |
| 290 | const std::string _appName; |
| 291 | |
| 292 | /** |
| 293 | * @brief The config file name. |
| 294 | */ |
| 295 | const std::string _fileName; |
| 296 | |
| 297 | /** |
| 298 | * @brief The function to call when the config file is available. |
| 299 | */ |
| 300 | ConfFileReadyFunc _readyFunc; |
| 301 | |
| 302 | /** |
| 303 | * @brief The JSON config file |
| 304 | */ |
| 305 | fs::path _confFile; |
| 306 | |
| 307 | /** |
| 308 | * @brief The interfacesAdded match that is used to wait |
| 309 | * for the IBMCompatibleSystem interface to show up. |
| 310 | */ |
| 311 | std::unique_ptr<sdbusplus::server::match::match> _match; |
Matthew Barth | 11547c9 | 2020-05-05 10:34:27 -0500 | [diff] [blame] | 312 | }; |
| 313 | |
| 314 | } // namespace phosphor::fan |