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 confCompatIntf = 40 "xyz.openbmc_project.Configuration.IBMCompatibleSystem"; 41 constexpr auto confCompatProp = "Names"; 42 43 class JsonConfig 44 { 45 public: 46 using ConfFileReadyFunc = std::function<void(const std::string&)>; 47 48 /** 49 * @brief Constructor 50 * 51 * Looks for the JSON config file. If it can't find one, then it 52 * will watch entity-manager for the IBMCompatibleSystem interface 53 * to show up and then use that data to try again. If the config 54 * file is initially present, the callback function is executed 55 * with the path to the file. 56 * 57 * @param[in] bus - The dbus bus object 58 * @param[in] appName - The appName portion of the config file path 59 * @param[in] fileName - Application's configuration file's name 60 * @param[in] func - The function to call when the config file 61 * is found. 62 */ 63 JsonConfig(sdbusplus::bus::bus& bus, const std::string& appName, 64 const std::string& fileName, ConfFileReadyFunc func) : 65 _appName(appName), 66 _fileName(fileName), _readyFunc(func) 67 { 68 _match = std::make_unique<sdbusplus::server::match::match>( 69 bus, 70 sdbusplus::bus::match::rules::interfacesAdded() + 71 sdbusplus::bus::match::rules::sender( 72 "xyz.openbmc_project.EntityManager"), 73 std::bind(&JsonConfig::ifacesAddedCallback, this, 74 std::placeholders::_1)); 75 try 76 { 77 _confFile = getConfFile(bus, _appName, _fileName); 78 } 79 catch (const std::runtime_error& e) 80 { 81 // No conf file found, so let the interfacesAdded 82 // match callback handle finding it. 83 } 84 85 if (!_confFile.empty()) 86 { 87 _match.reset(); 88 _readyFunc(_confFile); 89 } 90 } 91 92 /** 93 * @brief The interfacesAdded callback function that looks for 94 * the IBMCompatibleSystem interface. If it finds it, 95 * it uses the Names property in the interface to find 96 * the JSON config file to use. If it finds one, it calls 97 * the _readyFunc function with the config file path. 98 * 99 * @param[in] msg - The D-Bus message contents 100 */ 101 void ifacesAddedCallback(sdbusplus::message::message& msg) 102 { 103 sdbusplus::message::object_path path; 104 std::map<std::string, 105 std::map<std::string, std::variant<std::vector<std::string>>>> 106 interfaces; 107 108 msg.read(path, interfaces); 109 110 if (interfaces.find(confCompatIntf) == interfaces.end()) 111 { 112 return; 113 } 114 115 const auto& properties = interfaces.at(confCompatIntf); 116 auto names = 117 std::get<std::vector<std::string>>(properties.at(confCompatProp)); 118 119 auto it = 120 std::find_if(names.begin(), names.end(), [this](auto const& name) { 121 auto confFile = 122 fs::path{confBasePath} / _appName / name / _fileName; 123 if (fs::exists(confFile)) 124 { 125 _confFile = confFile; 126 return true; 127 } 128 return false; 129 }); 130 131 if (it != names.end()) 132 { 133 _readyFunc(_confFile); 134 _match.reset(); 135 } 136 else 137 { 138 log<level::ERR>(fmt::format("Could not find fan {} conf file {} " 139 "even after {} iface became available", 140 _appName, _fileName, confCompatIntf) 141 .c_str()); 142 } 143 } 144 145 /** 146 * Get the json configuration file. The first location found to contain 147 * the json config file for the given fan application is used from the 148 * following locations in order. 149 * 1.) From the confOverridePath location 150 * 2.) From config file found using an entry from a list obtained from an 151 * interface's property as a relative path extension on the base path where: 152 * interface = Interface set in confCompatIntf with the property 153 * property = Property set in confCompatProp containing a list of 154 * subdirectories in priority order to find a config 155 * 3.) *DEFAULT* - From the confBasePath location 156 * 157 * @brief Get the configuration file to be used 158 * 159 * @param[in] bus - The dbus bus object 160 * @param[in] appName - The phosphor-fan-presence application name 161 * @param[in] fileName - Application's configuration file's name 162 * @param[in] isOptional - Config file is optional, default to 'false' 163 * 164 * @return filesystem path 165 * The filesystem path to the configuration file to use 166 */ 167 static const fs::path getConfFile(sdbusplus::bus::bus& bus, 168 const std::string& appName, 169 const std::string& fileName, 170 bool isOptional = false) 171 { 172 // Check override location 173 fs::path confFile = fs::path{confOverridePath} / appName / fileName; 174 if (fs::exists(confFile)) 175 { 176 return confFile; 177 } 178 179 // If the default file is there, use it 180 confFile = fs::path{confBasePath} / appName / fileName; 181 if (fs::exists(confFile)) 182 { 183 return confFile; 184 } 185 confFile.clear(); 186 187 // Get all objects implementing the compatible interface 188 auto objects = 189 util::SDBusPlus::getSubTreePathsRaw(bus, "/", confCompatIntf, 0); 190 for (auto& path : objects) 191 { 192 try 193 { 194 // Retrieve json config compatible relative path locations 195 auto confCompatValue = 196 util::SDBusPlus::getProperty<std::vector<std::string>>( 197 bus, path, confCompatIntf, confCompatProp); 198 // Look for a config file at each entry relative to the base 199 // path and use the first one found 200 auto it = std::find_if( 201 confCompatValue.begin(), confCompatValue.end(), 202 [&confFile, &appName, &fileName](auto const& entry) { 203 confFile = 204 fs::path{confBasePath} / appName / entry / fileName; 205 return fs::exists(confFile); 206 }); 207 if (it != confCompatValue.end()) 208 { 209 // Use the first config file found at a listed location 210 break; 211 } 212 confFile.clear(); 213 } 214 catch (const util::DBusError&) 215 { 216 // Property unavailable on object. 217 } 218 } 219 220 if (!isOptional && confFile.empty() && !objects.empty()) 221 { 222 log<level::ERR>(fmt::format("Could not find fan {} conf file {}", 223 appName, fileName) 224 .c_str()); 225 } 226 227 if (confFile.empty() && !isOptional) 228 { 229 throw std::runtime_error("No JSON config file found"); 230 } 231 232 return confFile; 233 } 234 235 /** 236 * @brief Load the JSON config file 237 * 238 * @param[in] confFile - File system path of the configuration file to load 239 * 240 * @return Parsed JSON object 241 * The parsed JSON configuration file object 242 */ 243 static const json load(const fs::path& confFile) 244 { 245 std::ifstream file; 246 json jsonConf; 247 248 if (!confFile.empty() && fs::exists(confFile)) 249 { 250 log<level::INFO>( 251 fmt::format("Loading configuration from {}", confFile.string()) 252 .c_str()); 253 file.open(confFile); 254 try 255 { 256 jsonConf = json::parse(file); 257 } 258 catch (std::exception& e) 259 { 260 log<level::ERR>( 261 fmt::format( 262 "Failed to parse JSON config file: {}, error: {}", 263 confFile.string(), e.what()) 264 .c_str()); 265 throw std::runtime_error( 266 fmt::format( 267 "Failed to parse JSON config file: {}, error: {}", 268 confFile.string(), e.what()) 269 .c_str()); 270 } 271 } 272 else 273 { 274 log<level::ERR>(fmt::format("Unable to open JSON config file: {}", 275 confFile.string()) 276 .c_str()); 277 throw std::runtime_error( 278 fmt::format("Unable to open JSON config file: {}", 279 confFile.string()) 280 .c_str()); 281 } 282 283 return jsonConf; 284 } 285 286 private: 287 /** 288 * @brief The 'appName' portion of the config file path. 289 */ 290 const std::string _appName; 291 292 /** 293 * @brief The config file name. 294 */ 295 const std::string _fileName; 296 297 /** 298 * @brief The function to call when the config file is available. 299 */ 300 ConfFileReadyFunc _readyFunc; 301 302 /** 303 * @brief The JSON config file 304 */ 305 fs::path _confFile; 306 307 /** 308 * @brief The interfacesAdded match that is used to wait 309 * for the IBMCompatibleSystem interface to show up. 310 */ 311 std::unique_ptr<sdbusplus::server::match::match> _match; 312 }; 313 314 } // namespace phosphor::fan 315