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         }
149         else
150         {
151             // Check if required config(s) are found not needing the
152             // compatible interface, otherwise this is intended to catch the
153             // exception thrown by the getConfFile function when the
154             // required config file was not found. This would then result in
155             // waiting for the compatible interfacesAdded signal
156             try
157             {
158                 _loadFunc();
159             }
160             catch (const NoConfigFound&)
161             {
162                 // Wait for compatible interfacesAdded signal
163             }
164         }
165     }
166 
167     /**
168      * @brief InterfacesAdded callback function for the compatible interface.
169      *
170      * @param[in] msg - The D-Bus message contents
171      *
172      * If the compatible interface is found, it uses the compatible property on
173      * the interface to set the list of compatible values to be used when
174      * attempting to get a configuration file. Once the list of compatible
175      * values has been updated, it calls the load function.
176      */
177     void compatIntfAdded(sdbusplus::message::message& msg)
178     {
179         sdbusplus::message::object_path op;
180         std::map<std::string,
181                  std::map<std::string, std::variant<std::vector<std::string>>>>
182             intfProps;
183 
184         msg.read(op, intfProps);
185 
186         if (intfProps.find(confCompatIntf) == intfProps.end())
187         {
188             return;
189         }
190 
191         const auto& props = intfProps.at(confCompatIntf);
192         // Only one dbus object with the compatible interface is used at a time
193         _confCompatValues =
194             std::get<std::vector<std::string>>(props.at(confCompatProp));
195         _loadFunc();
196     }
197 
198     /**
199      * Get the json configuration file. The first location found to contain
200      * the json config file for the given fan application is used from the
201      * following locations in order.
202      * 1.) From the confOverridePath location
203      * 2.) From the default confBasePath location
204      * 3.) From config file found using an entry from a list obtained from an
205      * interface's property as a relative path extension on the base path where:
206      *     interface = Interface set in confCompatIntf with the property
207      *     property = Property set in confCompatProp containing a list of
208      *                subdirectories in priority order to find a config
209      *
210      * @brief Get the configuration file to be used
211      *
212      * @param[in] bus - The dbus bus object
213      * @param[in] appName - The phosphor-fan-presence application name
214      * @param[in] fileName - Application's configuration file's name
215      * @param[in] isOptional - Config file is optional, default to 'false'
216      *
217      * @return filesystem path
218      *     The filesystem path to the configuration file to use
219      */
220     static const fs::path getConfFile(sdbusplus::bus::bus& bus,
221                                       const std::string& appName,
222                                       const std::string& fileName,
223                                       bool isOptional = false)
224     {
225         // Check override location
226         fs::path confFile = fs::path{confOverridePath} / appName / fileName;
227         if (fs::exists(confFile))
228         {
229             return confFile;
230         }
231 
232         // If the default file is there, use it
233         confFile = fs::path{confBasePath} / appName / fileName;
234         if (fs::exists(confFile))
235         {
236             return confFile;
237         }
238 
239         // Look for a config file at each entry relative to the base
240         // path and use the first one found
241         auto it = std::find_if(
242             _confCompatValues.begin(), _confCompatValues.end(),
243             [&confFile, &appName, &fileName](const auto& value) {
244                 confFile = fs::path{confBasePath} / appName / value / fileName;
245                 return fs::exists(confFile);
246             });
247         if (it == _confCompatValues.end())
248         {
249             confFile.clear();
250         }
251 
252         if (confFile.empty() && !isOptional)
253         {
254             throw NoConfigFound(appName, fileName);
255         }
256 
257         return confFile;
258     }
259 
260     /**
261      * @brief Load the JSON config file
262      *
263      * @param[in] confFile - File system path of the configuration file to load
264      *
265      * @return Parsed JSON object
266      *     The parsed JSON configuration file object
267      */
268     static const json load(const fs::path& confFile)
269     {
270         std::ifstream file;
271         json jsonConf;
272 
273         if (!confFile.empty() && fs::exists(confFile))
274         {
275             log<level::INFO>(
276                 fmt::format("Loading configuration from {}", confFile.string())
277                     .c_str());
278             file.open(confFile);
279             try
280             {
281                 // Enable ignoring `//` or `/* */` comments
282                 jsonConf = json::parse(file, nullptr, true, true);
283             }
284             catch (const std::exception& e)
285             {
286                 log<level::ERR>(
287                     fmt::format(
288                         "Failed to parse JSON config file: {}, error: {}",
289                         confFile.string(), e.what())
290                         .c_str());
291                 throw std::runtime_error(
292                     fmt::format(
293                         "Failed to parse JSON config file: {}, error: {}",
294                         confFile.string(), e.what())
295                         .c_str());
296             }
297         }
298         else
299         {
300             log<level::ERR>(fmt::format("Unable to open JSON config file: {}",
301                                         confFile.string())
302                                 .c_str());
303             throw std::runtime_error(
304                 fmt::format("Unable to open JSON config file: {}",
305                             confFile.string())
306                     .c_str());
307         }
308 
309         return jsonConf;
310     }
311 
312     /**
313      * @brief Return the compatible values property
314      *
315      * @return const std::vector<std::string>& - The values
316      */
317     static const std::vector<std::string>& getCompatValues()
318     {
319         return _confCompatValues;
320     }
321 
322   private:
323     /* Load function to call for a fan app to load its config file(s). */
324     std::function<void()> _loadFunc;
325 
326     /**
327      * @brief The interfacesAdded match that is used to wait
328      *        for the IBMCompatibleSystem interface to show up.
329      */
330     std::unique_ptr<sdbusplus::bus::match_t> _match;
331 
332     /**
333      * @brief List of compatible values from the compatible interface
334      *
335      * Only supports a single instance of the compatible interface on a dbus
336      * object. If more than one dbus object exists with the compatible
337      * interface, the last one found will be the list of compatible values used.
338      */
339     inline static std::vector<std::string> _confCompatValues;
340 };
341 
342 } // namespace phosphor::fan
343