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 <nlohmann/json.hpp> 21 #include <phosphor-logging/log.hpp> 22 #include <sdbusplus/bus.hpp> 23 #include <sdeventplus/source/signal.hpp> 24 25 #include <filesystem> 26 #include <format> 27 #include <fstream> 28 29 namespace phosphor::fan 30 { 31 32 namespace fs = std::filesystem; 33 using json = nlohmann::json; 34 using namespace phosphor::logging; 35 36 constexpr auto confOverridePath = "/etc/phosphor-fan-presence"; 37 constexpr auto confBasePath = "/usr/share/phosphor-fan-presence"; 38 constexpr auto confCompatServ = "xyz.openbmc_project.EntityManager"; 39 constexpr auto confCompatIntf = 40 "xyz.openbmc_project.Inventory.Decorator.Compatible"; 41 constexpr auto confCompatProp = "Names"; 42 43 /** 44 * @class NoConfigFound - A no JSON configuration found exception 45 * 46 * A no JSON configuration found exception that is used to denote that a JSON 47 * configuration has not been found yet. 48 */ 49 class NoConfigFound : public std::runtime_error 50 { 51 public: 52 NoConfigFound() = delete; 53 NoConfigFound(const NoConfigFound&) = delete; 54 NoConfigFound(NoConfigFound&&) = delete; 55 NoConfigFound& operator=(const NoConfigFound&) = delete; 56 NoConfigFound& operator=(NoConfigFound&&) = delete; 57 ~NoConfigFound() = default; 58 59 /** 60 * @brief No JSON configuration found exception object 61 * 62 * When attempting to find the JSON configuration file(s), a NoConfigFound 63 * exception can be thrown to denote that at that time finding/loading the 64 * JSON configuration file(s) for a fan application failed. Details on what 65 * application and JSON configuration file that failed to be found will be 66 * logged resulting in the application being terminated. 67 * 68 * @param[in] details - Additional details 69 */ NoConfigFound(const std::string & appName,const std::string & fileName)70 NoConfigFound(const std::string& appName, const std::string& fileName) : 71 std::runtime_error(std::format("JSON configuration not found [Could " 72 "not find fan {} conf file {}]", 73 appName, fileName) 74 .c_str()) 75 {} 76 }; 77 78 class JsonConfig 79 { 80 public: 81 /** 82 * @brief Get the object paths with the compatible interface 83 * 84 * Retrieve all the object paths implementing the compatible interface for 85 * configuration file loading. 86 */ getCompatObjPaths()87 std::vector<std::string>& getCompatObjPaths() 88 { 89 using SubTreeMap = 90 std::map<std::string, 91 std::map<std::string, std::vector<std::string>>>; 92 SubTreeMap subTreeObjs = util::SDBusPlus::getSubTreeRaw( 93 util::SDBusPlus::getBus(), "/", confCompatIntf, 0); 94 95 static std::vector<std::string> paths; 96 for (auto& [path, serviceMap] : subTreeObjs) 97 { 98 // Only save objects under confCompatServ 99 if (serviceMap.find(confCompatServ) != serviceMap.end()) 100 { 101 paths.emplace_back(path); 102 } 103 } 104 return paths; 105 } 106 107 /** 108 * @brief Constructor 109 * 110 * Attempts to set the list of compatible values from the compatible 111 * interface and call the fan app's function to load its config file(s). If 112 * the compatible interface is not found, it subscribes to the 113 * interfacesAdded signal for that interface on the compatible service 114 * defined above. 115 * 116 * @param[in] func - Fan app function to call to load its config file(s) 117 */ JsonConfig(std::function<void ()> func)118 JsonConfig(std::function<void()> func) : _loadFunc(func) 119 { 120 std::vector<std::string> compatObjPaths; 121 122 _match = std::make_unique<sdbusplus::bus::match_t>( 123 util::SDBusPlus::getBus(), 124 sdbusplus::bus::match::rules::interfacesAdded() + 125 sdbusplus::bus::match::rules::sender(confCompatServ), 126 std::bind(&JsonConfig::compatIntfAdded, this, 127 std::placeholders::_1)); 128 129 try 130 { 131 compatObjPaths = getCompatObjPaths(); 132 } 133 catch (const util::DBusMethodError&) 134 { 135 // Compatible interface does not exist on any dbus object yet 136 } 137 138 if (!compatObjPaths.empty()) 139 { 140 for (auto& path : compatObjPaths) 141 { 142 try 143 { 144 // Retrieve json config compatible relative path 145 // locations (last one found will be what's used if more 146 // than one dbus object implementing the compatible 147 // interface exists). 148 _confCompatValues = 149 util::SDBusPlus::getProperty<std::vector<std::string>>( 150 util::SDBusPlus::getBus(), path, confCompatIntf, 151 confCompatProp); 152 } 153 catch (const util::DBusError&) 154 { 155 // Compatible property unavailable on this dbus object 156 // path's compatible interface, ignore 157 } 158 } 159 try 160 { 161 _loadFunc(); 162 } 163 catch (const NoConfigFound&) 164 { 165 // The Decorator.Compatible interface is not unique to one 166 // single object on DBus so this should not be treated as a 167 // failure, wait for interfacesAdded signal. 168 } 169 } 170 else 171 { 172 // Check if required config(s) are found not needing the 173 // compatible interface, otherwise this is intended to catch the 174 // exception thrown by the getConfFile function when the 175 // required config file was not found. This would then result in 176 // waiting for the compatible interfacesAdded signal 177 try 178 { 179 _loadFunc(); 180 } 181 catch (const NoConfigFound&) 182 { 183 // Wait for compatible interfacesAdded signal 184 } 185 } 186 } 187 188 /** 189 * @brief InterfacesAdded callback function for the compatible interface. 190 * 191 * @param[in] msg - The D-Bus message contents 192 * 193 * If the compatible interface is found, it uses the compatible property on 194 * the interface to set the list of compatible values to be used when 195 * attempting to get a configuration file. Once the list of compatible 196 * values has been updated, it calls the load function. 197 */ compatIntfAdded(sdbusplus::message_t & msg)198 void compatIntfAdded(sdbusplus::message_t& msg) 199 { 200 if (!_compatibleName.empty()) 201 { 202 // Do not process the interfaceAdded signal if one compatible name 203 // has been successfully used to get config files 204 return; 205 } 206 sdbusplus::message::object_path op; 207 std::map<std::string, 208 std::map<std::string, std::variant<std::vector<std::string>>>> 209 intfProps; 210 211 msg.read(op, intfProps); 212 213 if (intfProps.find(confCompatIntf) == intfProps.end()) 214 { 215 return; 216 } 217 218 const auto& props = intfProps.at(confCompatIntf); 219 // Only one dbus object with the compatible interface is used at a time 220 _confCompatValues = 221 std::get<std::vector<std::string>>(props.at(confCompatProp)); 222 _loadFunc(); 223 } 224 225 /** 226 * Get the json configuration file. The first location found to contain 227 * the json config file for the given fan application is used from the 228 * following locations in order. 229 * 1.) From the confOverridePath location 230 * 2.) From the default confBasePath location 231 * 3.) From config file found using an entry from a list obtained from an 232 * interface's property as a relative path extension on the base path where: 233 * interface = Interface set in confCompatIntf with the property 234 * property = Property set in confCompatProp containing a list of 235 * subdirectories in priority order to find a config 236 * 237 * @brief Get the configuration file to be used 238 * 239 * @param[in] appName - The phosphor-fan-presence application name 240 * @param[in] fileName - Application's configuration file's name 241 * @param[in] isOptional - Config file is optional, default to 'false' 242 * 243 * @return filesystem path 244 * The filesystem path to the configuration file to use 245 */ getConfFile(const std::string & appName,const std::string & fileName,bool isOptional=false)246 static const fs::path getConfFile(const std::string& appName, 247 const std::string& fileName, 248 bool isOptional = false) 249 { 250 // Check override location 251 fs::path confFile = fs::path{confOverridePath} / appName / fileName; 252 if (fs::exists(confFile)) 253 { 254 return confFile; 255 } 256 257 // If the default file is there, use it 258 confFile = fs::path{confBasePath} / appName / fileName; 259 if (fs::exists(confFile)) 260 { 261 return confFile; 262 } 263 264 // Look for a config file at each entry relative to the base 265 // path and use the first one found 266 auto it = std::find_if( 267 _confCompatValues.begin(), _confCompatValues.end(), 268 [&confFile, &appName, &fileName](const auto& value) { 269 confFile = fs::path{confBasePath} / appName / value / fileName; 270 _compatibleName = value; 271 return fs::exists(confFile); 272 }); 273 if (it == _confCompatValues.end()) 274 { 275 confFile.clear(); 276 _compatibleName.clear(); 277 } 278 279 if (confFile.empty() && !isOptional) 280 { 281 throw NoConfigFound(appName, fileName); 282 } 283 284 return confFile; 285 } 286 287 /** 288 * @brief Load the JSON config file 289 * 290 * @param[in] confFile - File system path of the configuration file to load 291 * 292 * @return Parsed JSON object 293 * The parsed JSON configuration file object 294 */ load(const fs::path & confFile)295 static const json load(const fs::path& confFile) 296 { 297 std::ifstream file; 298 json jsonConf; 299 300 if (!confFile.empty() && fs::exists(confFile)) 301 { 302 log<level::INFO>( 303 std::format("Loading configuration from {}", confFile.string()) 304 .c_str()); 305 file.open(confFile); 306 try 307 { 308 // Enable ignoring `//` or `/* */` comments 309 jsonConf = json::parse(file, nullptr, true, true); 310 } 311 catch (const std::exception& e) 312 { 313 log<level::ERR>( 314 std::format( 315 "Failed to parse JSON config file: {}, error: {}", 316 confFile.string(), e.what()) 317 .c_str()); 318 throw std::runtime_error( 319 std::format( 320 "Failed to parse JSON config file: {}, error: {}", 321 confFile.string(), e.what()) 322 .c_str()); 323 } 324 } 325 else 326 { 327 log<level::ERR>(std::format("Unable to open JSON config file: {}", 328 confFile.string()) 329 .c_str()); 330 throw std::runtime_error( 331 std::format("Unable to open JSON config file: {}", 332 confFile.string()) 333 .c_str()); 334 } 335 336 return jsonConf; 337 } 338 339 /** 340 * @brief Return the compatible values property 341 * 342 * @return const std::vector<std::string>& - The values 343 */ getCompatValues()344 static const std::vector<std::string>& getCompatValues() 345 { 346 return _confCompatValues; 347 } 348 349 private: 350 /* Load function to call for a fan app to load its config file(s). */ 351 std::function<void()> _loadFunc; 352 353 /** 354 * @brief The interfacesAdded match that is used to wait 355 * for the Inventory.Decorator.Compatible interface to show up. 356 */ 357 std::unique_ptr<sdbusplus::bus::match_t> _match; 358 359 /** 360 * @brief List of compatible values from the compatible interface 361 * 362 * Only supports a single instance of the compatible interface on a dbus 363 * object. If more than one dbus object exists with the compatible 364 * interface, the last one found will be the list of compatible values used. 365 */ 366 inline static std::vector<std::string> _confCompatValues; 367 368 /** 369 * @brief The compatible value that is currently used to load configuration 370 * 371 * The value extracted from the achieved property value list that is used 372 * as a sub-folder to append to the configuration location and really 373 * contains the configruation files 374 */ 375 376 inline static std::string _compatibleName; 377 }; 378 379 } // namespace phosphor::fan 380