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