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     }
137 
138     /**
139      * Get the json configuration file. The first location found to contain
140      * the json config file for the given fan application is used from the
141      * following locations in order.
142      * 1.) From the confOverridePath location
143      * 2.) From config file found using an entry from a list obtained from an
144      * interface's property as a relative path extension on the base path where:
145      *     interface = Interface set in confCompatIntf with the property
146      *     property = Property set in confCompatProp containing a list of
147      *                subdirectories in priority order to find a config
148      * 3.) *DEFAULT* - From the confBasePath location
149      *
150      * @brief Get the configuration file to be used
151      *
152      * @param[in] bus - The dbus bus object
153      * @param[in] appName - The phosphor-fan-presence application name
154      * @param[in] fileName - Application's configuration file's name
155      * @param[in] isOptional - Config file is optional, default to 'false'
156      *
157      * @return filesystem path
158      *     The filesystem path to the configuration file to use
159      */
160     static const fs::path getConfFile(sdbusplus::bus::bus& bus,
161                                       const std::string& appName,
162                                       const std::string& fileName,
163                                       bool isOptional = false)
164     {
165         // Check override location
166         fs::path confFile = fs::path{confOverridePath} / appName / fileName;
167         if (fs::exists(confFile))
168         {
169             return confFile;
170         }
171 
172         // If the default file is there, use it
173         confFile = fs::path{confBasePath} / appName / fileName;
174         if (fs::exists(confFile))
175         {
176             return confFile;
177         }
178         confFile.clear();
179 
180         // Get all objects implementing the compatible interface
181         auto objects =
182             util::SDBusPlus::getSubTreePathsRaw(bus, "/", confCompatIntf, 0);
183         for (auto& path : objects)
184         {
185             try
186             {
187                 // Retrieve json config compatible relative path locations
188                 auto confCompatValue =
189                     util::SDBusPlus::getProperty<std::vector<std::string>>(
190                         bus, path, confCompatIntf, confCompatProp);
191                 // Look for a config file at each entry relative to the base
192                 // path and use the first one found
193                 auto it = std::find_if(
194                     confCompatValue.begin(), confCompatValue.end(),
195                     [&confFile, &appName, &fileName](auto const& entry) {
196                         confFile =
197                             fs::path{confBasePath} / appName / entry / fileName;
198                         return fs::exists(confFile);
199                     });
200                 if (it != confCompatValue.end())
201                 {
202                     // Use the first config file found at a listed location
203                     break;
204                 }
205                 confFile.clear();
206             }
207             catch (const util::DBusError&)
208             {
209                 // Property unavailable on object.
210             }
211         }
212 
213         if (confFile.empty() && !isOptional)
214         {
215             throw std::runtime_error("No JSON config file found");
216         }
217 
218         return confFile;
219     }
220 
221     /**
222      * @brief Load the JSON config file
223      *
224      * @param[in] confFile - File system path of the configuration file to load
225      *
226      * @return Parsed JSON object
227      *     The parsed JSON configuration file object
228      */
229     static const json load(const fs::path& confFile)
230     {
231         std::ifstream file;
232         json jsonConf;
233 
234         if (!confFile.empty() && fs::exists(confFile))
235         {
236             log<level::INFO>(
237                 fmt::format("Loading configuration from {}", confFile.string())
238                     .c_str());
239             file.open(confFile);
240             try
241             {
242                 jsonConf = json::parse(file);
243             }
244             catch (std::exception& e)
245             {
246                 log<level::ERR>(
247                     fmt::format(
248                         "Failed to parse JSON config file: {}, error: {}",
249                         confFile.string(), e.what())
250                         .c_str());
251                 throw std::runtime_error(
252                     fmt::format(
253                         "Failed to parse JSON config file: {}, error: {}",
254                         confFile.string(), e.what())
255                         .c_str());
256             }
257         }
258         else
259         {
260             log<level::ERR>(fmt::format("Unable to open JSON config file: {}",
261                                         confFile.string())
262                                 .c_str());
263             throw std::runtime_error(
264                 fmt::format("Unable to open JSON config file: {}",
265                             confFile.string())
266                     .c_str());
267         }
268 
269         return jsonConf;
270     }
271 
272   private:
273     /**
274      * @brief The 'appName' portion of the config file path.
275      */
276     const std::string _appName;
277 
278     /**
279      * @brief The config file name.
280      */
281     const std::string _fileName;
282 
283     /**
284      * @brief The function to call when the config file is available.
285      */
286     ConfFileReadyFunc _readyFunc;
287 
288     /**
289      * @brief The JSON config file
290      */
291     fs::path _confFile;
292 
293     /**
294      * @brief The interfacesAdded match that is used to wait
295      *        for the IBMCompatibleSystem interface to show up.
296      */
297     std::unique_ptr<sdbusplus::server::match::match> _match;
298 };
299 
300 } // namespace phosphor::fan
301