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