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 confCompatIntf =
40     "xyz.openbmc_project.Configuration.IBMCompatibleSystem";
41 constexpr auto confCompatProp = "Names";
42 
43 class JsonConfig
44 {
45   public:
46     using ConfFileReadyFunc = std::function<void(const std::string&)>;
47 
48     /**
49      * @brief Constructor
50      *
51      * Looks for the JSON config file.  If it can't find one, then it
52      * will watch entity-manager for the IBMCompatibleSystem interface
53      * to show up and then use that data to try again.  If the config
54      * file is initially present, the callback function is executed
55      * with the path to the file.
56      *
57      * @param[in] bus - The dbus bus object
58      * @param[in] appName - The appName portion of the config file path
59      * @param[in] fileName - Application's configuration file's name
60      * @param[in] func - The function to call when the config file
61      *                   is found.
62      */
63     JsonConfig(sdbusplus::bus::bus& bus, const std::string& appName,
64                const std::string& fileName, ConfFileReadyFunc func) :
65         _appName(appName),
66         _fileName(fileName), _readyFunc(func)
67     {
68         _match = std::make_unique<sdbusplus::server::match::match>(
69             bus,
70             sdbusplus::bus::match::rules::interfacesAdded() +
71                 sdbusplus::bus::match::rules::sender(
72                     "xyz.openbmc_project.EntityManager"),
73             std::bind(&JsonConfig::ifacesAddedCallback, this,
74                       std::placeholders::_1));
75         try
76         {
77             _confFile = getConfFile(bus, _appName, _fileName);
78         }
79         catch (const std::runtime_error& e)
80         {
81             // No conf file found, so let the interfacesAdded
82             // match callback handle finding it.
83         }
84 
85         if (!_confFile.empty())
86         {
87             _match.reset();
88             _readyFunc(_confFile);
89         }
90     }
91 
92     /**
93      * @brief The interfacesAdded callback function that looks for
94      *        the IBMCompatibleSystem interface.  If it finds it,
95      *        it uses the Names property in the interface to find
96      *        the JSON config file to use.  If it finds one, it calls
97      *        the _readyFunc function with the config file path.
98      *
99      * @param[in] msg - The D-Bus message contents
100      */
101     void ifacesAddedCallback(sdbusplus::message::message& msg)
102     {
103         sdbusplus::message::object_path path;
104         std::map<std::string,
105                  std::map<std::string, std::variant<std::vector<std::string>>>>
106             interfaces;
107 
108         msg.read(path, interfaces);
109 
110         if (interfaces.find(confCompatIntf) == interfaces.end())
111         {
112             return;
113         }
114 
115         const auto& properties = interfaces.at(confCompatIntf);
116         auto names =
117             std::get<std::vector<std::string>>(properties.at(confCompatProp));
118 
119         auto it =
120             std::find_if(names.begin(), names.end(), [this](auto const& name) {
121                 auto confFile =
122                     fs::path{confBasePath} / _appName / name / _fileName;
123                 if (fs::exists(confFile))
124                 {
125                     _confFile = confFile;
126                     return true;
127                 }
128                 return false;
129             });
130 
131         if (it != names.end())
132         {
133             _readyFunc(_confFile);
134             _match.reset();
135         }
136         else
137         {
138             log<level::ERR>(fmt::format("Could not find fan {} conf file {} "
139                                         "even after {} iface became available",
140                                         _appName, _fileName, confCompatIntf)
141                                 .c_str());
142         }
143     }
144 
145     /**
146      * Get the json configuration file. The first location found to contain
147      * the json config file for the given fan application is used from the
148      * following locations in order.
149      * 1.) From the confOverridePath location
150      * 2.) From config file found using an entry from a list obtained from an
151      * interface's property as a relative path extension on the base path where:
152      *     interface = Interface set in confCompatIntf with the property
153      *     property = Property set in confCompatProp containing a list of
154      *                subdirectories in priority order to find a config
155      * 3.) *DEFAULT* - From the confBasePath location
156      *
157      * @brief Get the configuration file to be used
158      *
159      * @param[in] bus - The dbus bus object
160      * @param[in] appName - The phosphor-fan-presence application name
161      * @param[in] fileName - Application's configuration file's name
162      * @param[in] isOptional - Config file is optional, default to 'false'
163      *
164      * @return filesystem path
165      *     The filesystem path to the configuration file to use
166      */
167     static const fs::path getConfFile(sdbusplus::bus::bus& bus,
168                                       const std::string& appName,
169                                       const std::string& fileName,
170                                       bool isOptional = false)
171     {
172         // Check override location
173         fs::path confFile = fs::path{confOverridePath} / appName / fileName;
174         if (fs::exists(confFile))
175         {
176             return confFile;
177         }
178 
179         // If the default file is there, use it
180         confFile = fs::path{confBasePath} / appName / fileName;
181         if (fs::exists(confFile))
182         {
183             return confFile;
184         }
185         confFile.clear();
186 
187         // Get all objects implementing the compatible interface
188         auto objects =
189             util::SDBusPlus::getSubTreePathsRaw(bus, "/", confCompatIntf, 0);
190         for (auto& path : objects)
191         {
192             try
193             {
194                 // Retrieve json config compatible relative path locations
195                 auto confCompatValue =
196                     util::SDBusPlus::getProperty<std::vector<std::string>>(
197                         bus, path, confCompatIntf, confCompatProp);
198                 // Look for a config file at each entry relative to the base
199                 // path and use the first one found
200                 auto it = std::find_if(
201                     confCompatValue.begin(), confCompatValue.end(),
202                     [&confFile, &appName, &fileName](auto const& entry) {
203                         confFile =
204                             fs::path{confBasePath} / appName / entry / fileName;
205                         return fs::exists(confFile);
206                     });
207                 if (it != confCompatValue.end())
208                 {
209                     // Use the first config file found at a listed location
210                     break;
211                 }
212                 confFile.clear();
213             }
214             catch (const util::DBusError&)
215             {
216                 // Property unavailable on object.
217             }
218         }
219 
220         if (!isOptional && confFile.empty() && !objects.empty())
221         {
222             log<level::ERR>(fmt::format("Could not find fan {} conf file {}",
223                                         appName, fileName)
224                                 .c_str());
225         }
226 
227         if (confFile.empty() && !isOptional)
228         {
229             throw std::runtime_error("No JSON config file found");
230         }
231 
232         return confFile;
233     }
234 
235     /**
236      * @brief Load the JSON config file
237      *
238      * @param[in] confFile - File system path of the configuration file to load
239      *
240      * @return Parsed JSON object
241      *     The parsed JSON configuration file object
242      */
243     static const json load(const fs::path& confFile)
244     {
245         std::ifstream file;
246         json jsonConf;
247 
248         if (!confFile.empty() && fs::exists(confFile))
249         {
250             log<level::INFO>(
251                 fmt::format("Loading configuration from {}", confFile.string())
252                     .c_str());
253             file.open(confFile);
254             try
255             {
256                 jsonConf = json::parse(file);
257             }
258             catch (std::exception& e)
259             {
260                 log<level::ERR>(
261                     fmt::format(
262                         "Failed to parse JSON config file: {}, error: {}",
263                         confFile.string(), e.what())
264                         .c_str());
265                 throw std::runtime_error(
266                     fmt::format(
267                         "Failed to parse JSON config file: {}, error: {}",
268                         confFile.string(), e.what())
269                         .c_str());
270             }
271         }
272         else
273         {
274             log<level::ERR>(fmt::format("Unable to open JSON config file: {}",
275                                         confFile.string())
276                                 .c_str());
277             throw std::runtime_error(
278                 fmt::format("Unable to open JSON config file: {}",
279                             confFile.string())
280                     .c_str());
281         }
282 
283         return jsonConf;
284     }
285 
286   private:
287     /**
288      * @brief The 'appName' portion of the config file path.
289      */
290     const std::string _appName;
291 
292     /**
293      * @brief The config file name.
294      */
295     const std::string _fileName;
296 
297     /**
298      * @brief The function to call when the config file is available.
299      */
300     ConfFileReadyFunc _readyFunc;
301 
302     /**
303      * @brief The JSON config file
304      */
305     fs::path _confFile;
306 
307     /**
308      * @brief The interfacesAdded match that is used to wait
309      *        for the IBMCompatibleSystem interface to show up.
310      */
311     std::unique_ptr<sdbusplus::server::match::match> _match;
312 };
313 
314 } // namespace phosphor::fan
315