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_t& 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] appName - The phosphor-fan-presence application name
213      * @param[in] fileName - Application's configuration file's name
214      * @param[in] isOptional - Config file is optional, default to 'false'
215      *
216      * @return filesystem path
217      *     The filesystem path to the configuration file to use
218      */
219     static const fs::path getConfFile(const std::string& appName,
220                                       const std::string& fileName,
221                                       bool isOptional = false)
222     {
223         // Check override location
224         fs::path confFile = fs::path{confOverridePath} / appName / fileName;
225         if (fs::exists(confFile))
226         {
227             return confFile;
228         }
229 
230         // If the default file is there, use it
231         confFile = fs::path{confBasePath} / appName / fileName;
232         if (fs::exists(confFile))
233         {
234             return confFile;
235         }
236 
237         // Look for a config file at each entry relative to the base
238         // path and use the first one found
239         auto it =
240             std::find_if(_confCompatValues.begin(), _confCompatValues.end(),
241                          [&confFile, &appName, &fileName](const auto& value) {
242             confFile = fs::path{confBasePath} / appName / value / fileName;
243             return fs::exists(confFile);
244             });
245         if (it == _confCompatValues.end())
246         {
247             confFile.clear();
248         }
249 
250         if (confFile.empty() && !isOptional)
251         {
252             throw NoConfigFound(appName, fileName);
253         }
254 
255         return confFile;
256     }
257 
258     /**
259      * @brief Load the JSON config file
260      *
261      * @param[in] confFile - File system path of the configuration file to load
262      *
263      * @return Parsed JSON object
264      *     The parsed JSON configuration file object
265      */
266     static const json load(const fs::path& confFile)
267     {
268         std::ifstream file;
269         json jsonConf;
270 
271         if (!confFile.empty() && fs::exists(confFile))
272         {
273             log<level::INFO>(
274                 fmt::format("Loading configuration from {}", confFile.string())
275                     .c_str());
276             file.open(confFile);
277             try
278             {
279                 // Enable ignoring `//` or `/* */` comments
280                 jsonConf = json::parse(file, nullptr, true, true);
281             }
282             catch (const std::exception& e)
283             {
284                 log<level::ERR>(
285                     fmt::format(
286                         "Failed to parse JSON config file: {}, error: {}",
287                         confFile.string(), e.what())
288                         .c_str());
289                 throw std::runtime_error(
290                     fmt::format(
291                         "Failed to parse JSON config file: {}, error: {}",
292                         confFile.string(), e.what())
293                         .c_str());
294             }
295         }
296         else
297         {
298             log<level::ERR>(fmt::format("Unable to open JSON config file: {}",
299                                         confFile.string())
300                                 .c_str());
301             throw std::runtime_error(
302                 fmt::format("Unable to open JSON config file: {}",
303                             confFile.string())
304                     .c_str());
305         }
306 
307         return jsonConf;
308     }
309 
310     /**
311      * @brief Return the compatible values property
312      *
313      * @return const std::vector<std::string>& - The values
314      */
315     static const std::vector<std::string>& getCompatValues()
316     {
317         return _confCompatValues;
318     }
319 
320   private:
321     /* Load function to call for a fan app to load its config file(s). */
322     std::function<void()> _loadFunc;
323 
324     /**
325      * @brief The interfacesAdded match that is used to wait
326      *        for the IBMCompatibleSystem interface to show up.
327      */
328     std::unique_ptr<sdbusplus::bus::match_t> _match;
329 
330     /**
331      * @brief List of compatible values from the compatible interface
332      *
333      * Only supports a single instance of the compatible interface on a dbus
334      * object. If more than one dbus object exists with the compatible
335      * interface, the last one found will be the list of compatible values used.
336      */
337     inline static std::vector<std::string> _confCompatValues;
338 };
339 
340 } // namespace phosphor::fan
341