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