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