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