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     using ConfFileReadyFunc = std::function<void(const std::string&)>;
48 
49     /**
50      * @brief Get the object paths with the compatible interface
51      *
52      * Retrieve all the object paths implementing the compatible interface for
53      * configuration file loading.
54      */
55     static auto& getCompatObjPaths() __attribute__((pure))
56     {
57         static auto paths = util::SDBusPlus::getSubTreePathsRaw(
58             util::SDBusPlus::getBus(), "/", confCompatIntf, 0);
59         return paths;
60     }
61 
62     /**
63      * @brief Constructor
64      *
65      * Looks for the JSON config file.  If it can't find one, then it
66      * will watch entity-manager for the IBMCompatibleSystem interface
67      * to show up and then use that data to try again.  If the config
68      * file is initially present, the callback function is executed
69      * with the path to the file.
70      *
71      * @param[in] bus - The dbus bus object
72      * @param[in] appName - The appName portion of the config file path
73      * @param[in] fileName - Application's configuration file's name
74      * @param[in] func - The function to call when the config file
75      *                   is found.
76      */
77     JsonConfig(sdbusplus::bus::bus& bus, const std::string& appName,
78                const std::string& fileName, ConfFileReadyFunc func) :
79         _appName(appName),
80         _fileName(fileName), _readyFunc(func)
81     {
82         _match = std::make_unique<sdbusplus::server::match::match>(
83             bus,
84             sdbusplus::bus::match::rules::interfacesAdded() +
85                 sdbusplus::bus::match::rules::sender(
86                     "xyz.openbmc_project.EntityManager"),
87             std::bind(&JsonConfig::ifacesAddedCallback, this,
88                       std::placeholders::_1));
89         try
90         {
91             auto compatObjPaths = getCompatObjPaths();
92             if (!compatObjPaths.empty())
93             {
94                 for (auto& path : compatObjPaths)
95                 {
96                     try
97                     {
98                         // Retrieve json config compatible relative path
99                         // locations (last one found will be what's used if more
100                         // than one dbus object implementing the comptaible
101                         // interface exists).
102                         _confCompatValues = util::SDBusPlus::getProperty<
103                             std::vector<std::string>>(bus, path, confCompatIntf,
104                                                       confCompatProp);
105                     }
106                     catch (const util::DBusError&)
107                     {
108                         // Property unavailable on this dbus object path.
109                     }
110                 }
111             }
112             _confFile = getConfFile(bus, _appName, _fileName);
113         }
114         catch (const std::runtime_error& e)
115         {
116             // No conf file found, so let the interfacesAdded
117             // match callback handle finding it.
118         }
119 
120         if (!_confFile.empty())
121         {
122             _match.reset();
123             _readyFunc(_confFile);
124         }
125     }
126 
127     /**
128      * @brief Constructor
129      *
130      * Attempts to set the list of compatible values from the compatible
131      * interface and call the fan app's function to load its config file(s). If
132      * the compatible interface is not found, it subscribes to the
133      * interfacesAdded signal for that interface on the compatible service
134      * defined above.
135      *
136      * @param[in] func - Fan app function to call to load its config file(s)
137      */
138     JsonConfig(std::function<void()> func) : _loadFunc(func)
139     {
140         _match = std::make_unique<sdbusplus::server::match::match>(
141             util::SDBusPlus::getBus(),
142             sdbusplus::bus::match::rules::interfacesAdded() +
143                 sdbusplus::bus::match::rules::sender(confCompatServ),
144             std::bind(&JsonConfig::compatIntfAdded, this,
145                       std::placeholders::_1));
146         try
147         {
148             auto compatObjPaths = getCompatObjPaths();
149             if (!compatObjPaths.empty())
150             {
151                 for (auto& path : compatObjPaths)
152                 {
153                     try
154                     {
155                         // Retrieve json config compatible relative path
156                         // locations (last one found will be what's used if more
157                         // than one dbus object implementing the comptaible
158                         // interface exists).
159                         _confCompatValues = util::SDBusPlus::getProperty<
160                             std::vector<std::string>>(util::SDBusPlus::getBus(),
161                                                       path, confCompatIntf,
162                                                       confCompatProp);
163                     }
164                     catch (const util::DBusError&)
165                     {
166                         // Compatible property unavailable on this dbus object
167                         // path's compatible interface, ignore
168                     }
169                 }
170                 _loadFunc();
171                 _match.reset();
172             }
173             else
174             {
175                 // Check if required config(s) are found not needing the
176                 // compatible interface, otherwise this is intended to catch the
177                 // exception thrown by the getConfFile function when the
178                 // required config file was not found. This would then result in
179                 // waiting for the compatible interfacesAdded signal
180                 try
181                 {
182                     _loadFunc();
183                     _match.reset();
184                 }
185                 catch (const std::runtime_error&)
186                 {
187                     // Wait for compatible interfacesAdded signal
188                 }
189             }
190         }
191         catch (const std::runtime_error&)
192         {
193             // Wait for compatible interfacesAdded signal
194         }
195     }
196 
197     /**
198      * @brief The interfacesAdded callback function that looks for
199      *        the IBMCompatibleSystem interface.  If it finds it,
200      *        it uses the Names property in the interface to find
201      *        the JSON config file to use.  If it finds one, it calls
202      *        the _readyFunc function with the config file path.
203      *
204      * @param[in] msg - The D-Bus message contents
205      */
206     void ifacesAddedCallback(sdbusplus::message::message& msg)
207     {
208         sdbusplus::message::object_path path;
209         std::map<std::string,
210                  std::map<std::string, std::variant<std::vector<std::string>>>>
211             interfaces;
212 
213         msg.read(path, interfaces);
214 
215         if (interfaces.find(confCompatIntf) == interfaces.end())
216         {
217             return;
218         }
219 
220         const auto& properties = interfaces.at(confCompatIntf);
221         auto names =
222             std::get<std::vector<std::string>>(properties.at(confCompatProp));
223 
224         auto it =
225             std::find_if(names.begin(), names.end(), [this](auto const& name) {
226                 auto confFile =
227                     fs::path{confBasePath} / _appName / name / _fileName;
228                 if (fs::exists(confFile))
229                 {
230                     _confFile = confFile;
231                     return true;
232                 }
233                 return false;
234             });
235 
236         if (it != names.end())
237         {
238             _readyFunc(_confFile);
239             _match.reset();
240         }
241         else
242         {
243             log<level::ERR>(fmt::format("Could not find fan {} conf file {} "
244                                         "even after {} iface became available",
245                                         _appName, _fileName, confCompatIntf)
246                                 .c_str());
247         }
248     }
249 
250     /**
251      * @brief InterfacesAdded callback function for the compatible interface.
252      *
253      * @param[in] msg - The D-Bus message contents
254      *
255      * If the compatible interface is found, it uses the compatible property on
256      * the interface to set the list of compatible values to be used when
257      * attempting to get a configuration file. Once the list of compatible
258      * values has been updated, it calls the load function.
259      */
260     void compatIntfAdded(sdbusplus::message::message& msg)
261     {
262         sdbusplus::message::object_path op;
263         std::map<std::string,
264                  std::map<std::string, std::variant<std::vector<std::string>>>>
265             intfProps;
266 
267         msg.read(op, intfProps);
268 
269         if (intfProps.find(confCompatIntf) == intfProps.end())
270         {
271             return;
272         }
273 
274         const auto& props = intfProps.at(confCompatIntf);
275         // Only one dbus object with the compatible interface is used at a time
276         _confCompatValues =
277             std::get<std::vector<std::string>>(props.at(confCompatProp));
278         _loadFunc();
279     }
280 
281     /**
282      * Get the json configuration file. The first location found to contain
283      * the json config file for the given fan application is used from the
284      * following locations in order.
285      * 1.) From the confOverridePath location
286      * 2.) From the default confBasePath location
287      * 3.) From config file found using an entry from a list obtained from an
288      * interface's property as a relative path extension on the base path where:
289      *     interface = Interface set in confCompatIntf with the property
290      *     property = Property set in confCompatProp containing a list of
291      *                subdirectories in priority order to find a config
292      *
293      * @brief Get the configuration file to be used
294      *
295      * @param[in] bus - The dbus bus object
296      * @param[in] appName - The phosphor-fan-presence application name
297      * @param[in] fileName - Application's configuration file's name
298      * @param[in] isOptional - Config file is optional, default to 'false'
299      *
300      * @return filesystem path
301      *     The filesystem path to the configuration file to use
302      */
303     static const fs::path getConfFile(sdbusplus::bus::bus& bus,
304                                       const std::string& appName,
305                                       const std::string& fileName,
306                                       bool isOptional = false)
307     {
308         // Check override location
309         fs::path confFile = fs::path{confOverridePath} / appName / fileName;
310         if (fs::exists(confFile))
311         {
312             return confFile;
313         }
314 
315         // If the default file is there, use it
316         confFile = fs::path{confBasePath} / appName / fileName;
317         if (fs::exists(confFile))
318         {
319             return confFile;
320         }
321 
322         // Look for a config file at each entry relative to the base
323         // path and use the first one found
324         auto it = std::find_if(
325             _confCompatValues.begin(), _confCompatValues.end(),
326             [&confFile, &appName, &fileName](const auto& value) {
327                 confFile = fs::path{confBasePath} / appName / value / fileName;
328                 return fs::exists(confFile);
329             });
330         if (it == _confCompatValues.end())
331         {
332             confFile.clear();
333         }
334 
335         if (!isOptional && confFile.empty() && !_confCompatValues.empty())
336         {
337             log<level::ERR>(fmt::format("Could not find fan {} conf file {}",
338                                         appName, fileName)
339                                 .c_str());
340         }
341 
342         if (confFile.empty() && !isOptional)
343         {
344             throw std::runtime_error("No JSON config file found");
345         }
346 
347         return confFile;
348     }
349 
350     /**
351      * @brief Load the JSON config file
352      *
353      * @param[in] confFile - File system path of the configuration file to load
354      *
355      * @return Parsed JSON object
356      *     The parsed JSON configuration file object
357      */
358     static const json load(const fs::path& confFile)
359     {
360         std::ifstream file;
361         json jsonConf;
362 
363         if (!confFile.empty() && fs::exists(confFile))
364         {
365             log<level::INFO>(
366                 fmt::format("Loading configuration from {}", confFile.string())
367                     .c_str());
368             file.open(confFile);
369             try
370             {
371                 jsonConf = json::parse(file);
372             }
373             catch (std::exception& e)
374             {
375                 log<level::ERR>(
376                     fmt::format(
377                         "Failed to parse JSON config file: {}, error: {}",
378                         confFile.string(), e.what())
379                         .c_str());
380                 throw std::runtime_error(
381                     fmt::format(
382                         "Failed to parse JSON config file: {}, error: {}",
383                         confFile.string(), e.what())
384                         .c_str());
385             }
386         }
387         else
388         {
389             log<level::ERR>(fmt::format("Unable to open JSON config file: {}",
390                                         confFile.string())
391                                 .c_str());
392             throw std::runtime_error(
393                 fmt::format("Unable to open JSON config file: {}",
394                             confFile.string())
395                     .c_str());
396         }
397 
398         return jsonConf;
399     }
400 
401   private:
402     /**
403      * @brief The 'appName' portion of the config file path.
404      */
405     const std::string _appName;
406 
407     /**
408      * @brief The config file name.
409      */
410     const std::string _fileName;
411 
412     /**
413      * @brief The function to call when the config file is available.
414      */
415     ConfFileReadyFunc _readyFunc;
416 
417     /* Load function to call for a fan app to load its config file(s). */
418     std::function<void()> _loadFunc;
419 
420     /**
421      * @brief The JSON config file
422      */
423     fs::path _confFile;
424 
425     /**
426      * @brief The interfacesAdded match that is used to wait
427      *        for the IBMCompatibleSystem interface to show up.
428      */
429     std::unique_ptr<sdbusplus::server::match::match> _match;
430 
431     /**
432      * @brief List of compatible values from the compatible interface
433      *
434      * Only supports a single instance of the compatible interface on a dbus
435      * object. If more than one dbus object exists with the compatible
436      * interface, the last one found will be the list of compatible values used.
437      */
438     inline static std::vector<std::string> _confCompatValues;
439 };
440 
441 } // namespace phosphor::fan
442