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