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