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