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