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