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 20 #include <fmt/format.h> 21 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"; 39 constexpr auto confCompatServ = "xyz.openbmc_project.EntityManager"; 40 constexpr auto confCompatIntf = 41 "xyz.openbmc_project.Configuration.IBMCompatibleSystem"; 42 constexpr auto confCompatProp = "Names"; 43 44 class JsonConfig 45 { 46 public: 47 using ConfFileReadyFunc = std::function<void(const std::string&)>; 48 49 /** 50 * @brief Get the object paths with the compatible interface 51 * 52 * Retrieve all the object paths implementing the compatible interface for 53 * configuration file loading. 54 */ 55 static auto& getCompatObjPaths() __attribute__((pure)) 56 { 57 static auto paths = util::SDBusPlus::getSubTreePathsRaw( 58 util::SDBusPlus::getBus(), "/", confCompatIntf, 0); 59 return paths; 60 } 61 62 /** 63 * @brief Constructor 64 * 65 * Looks for the JSON config file. If it can't find one, then it 66 * will watch entity-manager for the IBMCompatibleSystem interface 67 * to show up and then use that data to try again. If the config 68 * file is initially present, the callback function is executed 69 * with the path to the file. 70 * 71 * @param[in] bus - The dbus bus object 72 * @param[in] appName - The appName portion of the config file path 73 * @param[in] fileName - Application's configuration file's name 74 * @param[in] func - The function to call when the config file 75 * is found. 76 */ 77 JsonConfig(sdbusplus::bus::bus& bus, const std::string& appName, 78 const std::string& fileName, ConfFileReadyFunc func) : 79 _appName(appName), 80 _fileName(fileName), _readyFunc(func) 81 { 82 _match = std::make_unique<sdbusplus::server::match::match>( 83 bus, 84 sdbusplus::bus::match::rules::interfacesAdded() + 85 sdbusplus::bus::match::rules::sender( 86 "xyz.openbmc_project.EntityManager"), 87 std::bind(&JsonConfig::ifacesAddedCallback, this, 88 std::placeholders::_1)); 89 try 90 { 91 auto compatObjPaths = getCompatObjPaths(); 92 if (!compatObjPaths.empty()) 93 { 94 for (auto& path : compatObjPaths) 95 { 96 try 97 { 98 // Retrieve json config compatible relative path 99 // locations (last one found will be what's used if more 100 // than one dbus object implementing the comptaible 101 // interface exists). 102 _confCompatValues = util::SDBusPlus::getProperty< 103 std::vector<std::string>>(bus, path, confCompatIntf, 104 confCompatProp); 105 } 106 catch (const util::DBusError&) 107 { 108 // Property unavailable on this dbus object path. 109 } 110 } 111 } 112 _confFile = getConfFile(bus, _appName, _fileName); 113 } 114 catch (const std::runtime_error& e) 115 { 116 // No conf file found, so let the interfacesAdded 117 // match callback handle finding it. 118 } 119 120 if (!_confFile.empty()) 121 { 122 _match.reset(); 123 _readyFunc(_confFile); 124 } 125 } 126 127 /** 128 * @brief Constructor 129 * 130 * Attempts to set the list of compatible values from the compatible 131 * interface and call the fan app's function to load its config file(s). If 132 * the compatible interface is not found, it subscribes to the 133 * interfacesAdded signal for that interface on the compatible service 134 * defined above. 135 * 136 * @param[in] func - Fan app function to call to load its config file(s) 137 */ 138 JsonConfig(std::function<void()> func) : _loadFunc(func) 139 { 140 _match = std::make_unique<sdbusplus::server::match::match>( 141 util::SDBusPlus::getBus(), 142 sdbusplus::bus::match::rules::interfacesAdded() + 143 sdbusplus::bus::match::rules::sender(confCompatServ), 144 std::bind(&JsonConfig::compatIntfAdded, this, 145 std::placeholders::_1)); 146 try 147 { 148 auto compatObjPaths = getCompatObjPaths(); 149 if (!compatObjPaths.empty()) 150 { 151 for (auto& path : compatObjPaths) 152 { 153 try 154 { 155 // Retrieve json config compatible relative path 156 // locations (last one found will be what's used if more 157 // than one dbus object implementing the comptaible 158 // interface exists). 159 _confCompatValues = util::SDBusPlus::getProperty< 160 std::vector<std::string>>(util::SDBusPlus::getBus(), 161 path, confCompatIntf, 162 confCompatProp); 163 } 164 catch (const util::DBusError&) 165 { 166 // Compatible property unavailable on this dbus object 167 // path's compatible interface, ignore 168 } 169 } 170 _loadFunc(); 171 _match.reset(); 172 } 173 else 174 { 175 // Check if required config(s) are found not needing the 176 // compatible interface, otherwise this is intended to catch the 177 // exception thrown by the getConfFile function when the 178 // required config file was not found. This would then result in 179 // waiting for the compatible interfacesAdded signal 180 try 181 { 182 _loadFunc(); 183 _match.reset(); 184 } 185 catch (const std::runtime_error&) 186 { 187 // Wait for compatible interfacesAdded signal 188 } 189 } 190 } 191 catch (const std::runtime_error&) 192 { 193 // Wait for compatible interfacesAdded signal 194 } 195 } 196 197 /** 198 * @brief The interfacesAdded callback function that looks for 199 * the IBMCompatibleSystem interface. If it finds it, 200 * it uses the Names property in the interface to find 201 * the JSON config file to use. If it finds one, it calls 202 * the _readyFunc function with the config file path. 203 * 204 * @param[in] msg - The D-Bus message contents 205 */ 206 void ifacesAddedCallback(sdbusplus::message::message& msg) 207 { 208 sdbusplus::message::object_path path; 209 std::map<std::string, 210 std::map<std::string, std::variant<std::vector<std::string>>>> 211 interfaces; 212 213 msg.read(path, interfaces); 214 215 if (interfaces.find(confCompatIntf) == interfaces.end()) 216 { 217 return; 218 } 219 220 const auto& properties = interfaces.at(confCompatIntf); 221 auto names = 222 std::get<std::vector<std::string>>(properties.at(confCompatProp)); 223 224 auto it = 225 std::find_if(names.begin(), names.end(), [this](auto const& name) { 226 auto confFile = 227 fs::path{confBasePath} / _appName / name / _fileName; 228 if (fs::exists(confFile)) 229 { 230 _confFile = confFile; 231 return true; 232 } 233 return false; 234 }); 235 236 if (it != names.end()) 237 { 238 _readyFunc(_confFile); 239 _match.reset(); 240 } 241 else 242 { 243 log<level::ERR>(fmt::format("Could not find fan {} conf file {} " 244 "even after {} iface became available", 245 _appName, _fileName, confCompatIntf) 246 .c_str()); 247 } 248 } 249 250 /** 251 * @brief InterfacesAdded callback function for the compatible interface. 252 * 253 * @param[in] msg - The D-Bus message contents 254 * 255 * If the compatible interface is found, it uses the compatible property on 256 * the interface to set the list of compatible values to be used when 257 * attempting to get a configuration file. Once the list of compatible 258 * values has been updated, it calls the load function. 259 */ 260 void compatIntfAdded(sdbusplus::message::message& msg) 261 { 262 sdbusplus::message::object_path op; 263 std::map<std::string, 264 std::map<std::string, std::variant<std::vector<std::string>>>> 265 intfProps; 266 267 msg.read(op, intfProps); 268 269 if (intfProps.find(confCompatIntf) == intfProps.end()) 270 { 271 return; 272 } 273 274 const auto& props = intfProps.at(confCompatIntf); 275 // Only one dbus object with the compatible interface is used at a time 276 _confCompatValues = 277 std::get<std::vector<std::string>>(props.at(confCompatProp)); 278 _loadFunc(); 279 } 280 281 /** 282 * Get the json configuration file. The first location found to contain 283 * the json config file for the given fan application is used from the 284 * following locations in order. 285 * 1.) From the confOverridePath location 286 * 2.) From the default confBasePath location 287 * 3.) From config file found using an entry from a list obtained from an 288 * interface's property as a relative path extension on the base path where: 289 * interface = Interface set in confCompatIntf with the property 290 * property = Property set in confCompatProp containing a list of 291 * subdirectories in priority order to find a config 292 * 293 * @brief Get the configuration file to be used 294 * 295 * @param[in] bus - The dbus bus object 296 * @param[in] appName - The phosphor-fan-presence application name 297 * @param[in] fileName - Application's configuration file's name 298 * @param[in] isOptional - Config file is optional, default to 'false' 299 * 300 * @return filesystem path 301 * The filesystem path to the configuration file to use 302 */ 303 static const fs::path getConfFile(sdbusplus::bus::bus& bus, 304 const std::string& appName, 305 const std::string& fileName, 306 bool isOptional = false) 307 { 308 // Check override location 309 fs::path confFile = fs::path{confOverridePath} / appName / fileName; 310 if (fs::exists(confFile)) 311 { 312 return confFile; 313 } 314 315 // If the default file is there, use it 316 confFile = fs::path{confBasePath} / appName / fileName; 317 if (fs::exists(confFile)) 318 { 319 return confFile; 320 } 321 322 // Look for a config file at each entry relative to the base 323 // path and use the first one found 324 auto it = std::find_if( 325 _confCompatValues.begin(), _confCompatValues.end(), 326 [&confFile, &appName, &fileName](const auto& value) { 327 confFile = fs::path{confBasePath} / appName / value / fileName; 328 return fs::exists(confFile); 329 }); 330 if (it == _confCompatValues.end()) 331 { 332 confFile.clear(); 333 } 334 335 if (!isOptional && confFile.empty() && !_confCompatValues.empty()) 336 { 337 log<level::ERR>(fmt::format("Could not find fan {} conf file {}", 338 appName, fileName) 339 .c_str()); 340 } 341 342 if (confFile.empty() && !isOptional) 343 { 344 throw std::runtime_error("No JSON config file found"); 345 } 346 347 return confFile; 348 } 349 350 /** 351 * @brief Load the JSON config file 352 * 353 * @param[in] confFile - File system path of the configuration file to load 354 * 355 * @return Parsed JSON object 356 * The parsed JSON configuration file object 357 */ 358 static const json load(const fs::path& confFile) 359 { 360 std::ifstream file; 361 json jsonConf; 362 363 if (!confFile.empty() && fs::exists(confFile)) 364 { 365 log<level::INFO>( 366 fmt::format("Loading configuration from {}", confFile.string()) 367 .c_str()); 368 file.open(confFile); 369 try 370 { 371 jsonConf = json::parse(file); 372 } 373 catch (std::exception& e) 374 { 375 log<level::ERR>( 376 fmt::format( 377 "Failed to parse JSON config file: {}, error: {}", 378 confFile.string(), e.what()) 379 .c_str()); 380 throw std::runtime_error( 381 fmt::format( 382 "Failed to parse JSON config file: {}, error: {}", 383 confFile.string(), e.what()) 384 .c_str()); 385 } 386 } 387 else 388 { 389 log<level::ERR>(fmt::format("Unable to open JSON config file: {}", 390 confFile.string()) 391 .c_str()); 392 throw std::runtime_error( 393 fmt::format("Unable to open JSON config file: {}", 394 confFile.string()) 395 .c_str()); 396 } 397 398 return jsonConf; 399 } 400 401 private: 402 /** 403 * @brief The 'appName' portion of the config file path. 404 */ 405 const std::string _appName; 406 407 /** 408 * @brief The config file name. 409 */ 410 const std::string _fileName; 411 412 /** 413 * @brief The function to call when the config file is available. 414 */ 415 ConfFileReadyFunc _readyFunc; 416 417 /* Load function to call for a fan app to load its config file(s). */ 418 std::function<void()> _loadFunc; 419 420 /** 421 * @brief The JSON config file 422 */ 423 fs::path _confFile; 424 425 /** 426 * @brief The interfacesAdded match that is used to wait 427 * for the IBMCompatibleSystem interface to show up. 428 */ 429 std::unique_ptr<sdbusplus::server::match::match> _match; 430 431 /** 432 * @brief List of compatible values from the compatible interface 433 * 434 * Only supports a single instance of the compatible interface on a dbus 435 * object. If more than one dbus object exists with the compatible 436 * interface, the last one found will be the list of compatible values used. 437 */ 438 inline static std::vector<std::string> _confCompatValues; 439 }; 440 441 } // namespace phosphor::fan 442