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