111547c9bSMatthew Barth /**
211547c9bSMatthew Barth  * Copyright © 2020 IBM Corporation
311547c9bSMatthew Barth  *
411547c9bSMatthew Barth  * Licensed under the Apache License, Version 2.0 (the "License");
511547c9bSMatthew Barth  * you may not use this file except in compliance with the License.
611547c9bSMatthew Barth  * You may obtain a copy of the License at
711547c9bSMatthew Barth  *
811547c9bSMatthew Barth  *     http://www.apache.org/licenses/LICENSE-2.0
911547c9bSMatthew Barth  *
1011547c9bSMatthew Barth  * Unless required by applicable law or agreed to in writing, software
1111547c9bSMatthew Barth  * distributed under the License is distributed on an "AS IS" BASIS,
1211547c9bSMatthew Barth  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1311547c9bSMatthew Barth  * See the License for the specific language governing permissions and
1411547c9bSMatthew Barth  * limitations under the License.
1511547c9bSMatthew Barth  */
1611547c9bSMatthew Barth #pragma once
1711547c9bSMatthew Barth 
1811547c9bSMatthew Barth #include "sdbusplus.hpp"
1911547c9bSMatthew Barth 
2011547c9bSMatthew Barth #include <nlohmann/json.hpp>
2111547c9bSMatthew Barth #include <phosphor-logging/log.hpp>
2211547c9bSMatthew Barth #include <sdbusplus/bus.hpp>
2311547c9bSMatthew Barth #include <sdeventplus/source/signal.hpp>
2411547c9bSMatthew Barth 
2511547c9bSMatthew Barth #include <filesystem>
26fbf4703fSPatrick Williams #include <format>
2711547c9bSMatthew Barth #include <fstream>
2811547c9bSMatthew Barth 
2911547c9bSMatthew Barth namespace phosphor::fan
3011547c9bSMatthew Barth {
3111547c9bSMatthew Barth 
3211547c9bSMatthew Barth namespace fs = std::filesystem;
3311547c9bSMatthew Barth using json = nlohmann::json;
3411547c9bSMatthew Barth using namespace phosphor::logging;
3511547c9bSMatthew Barth 
3611547c9bSMatthew Barth constexpr auto confOverridePath = "/etc/phosphor-fan-presence";
3711547c9bSMatthew Barth constexpr auto confBasePath = "/usr/share/phosphor-fan-presence";
38b370ab3bSMatthew Barth constexpr auto confCompatServ = "xyz.openbmc_project.EntityManager";
39fcbdc0e4SMatthew Barth constexpr auto confCompatIntf =
40*b99ce0edSChau Ly     "xyz.openbmc_project.Inventory.Decorator.Compatible";
41fcbdc0e4SMatthew Barth constexpr auto confCompatProp = "Names";
4211547c9bSMatthew Barth 
436eb603e8SMatthew Barth /**
446eb603e8SMatthew Barth  * @class NoConfigFound - A no JSON configuration found exception
456eb603e8SMatthew Barth  *
466eb603e8SMatthew Barth  * A no JSON configuration found exception that is used to denote that a JSON
476eb603e8SMatthew Barth  * configuration has not been found yet.
486eb603e8SMatthew Barth  */
496eb603e8SMatthew Barth class NoConfigFound : public std::runtime_error
506eb603e8SMatthew Barth {
516eb603e8SMatthew Barth   public:
526eb603e8SMatthew Barth     NoConfigFound() = delete;
536eb603e8SMatthew Barth     NoConfigFound(const NoConfigFound&) = delete;
546eb603e8SMatthew Barth     NoConfigFound(NoConfigFound&&) = delete;
556eb603e8SMatthew Barth     NoConfigFound& operator=(const NoConfigFound&) = delete;
566eb603e8SMatthew Barth     NoConfigFound& operator=(NoConfigFound&&) = delete;
576eb603e8SMatthew Barth     ~NoConfigFound() = default;
586eb603e8SMatthew Barth 
596eb603e8SMatthew Barth     /**
606eb603e8SMatthew Barth      * @brief No JSON configuration found exception object
616eb603e8SMatthew Barth      *
626eb603e8SMatthew Barth      * When attempting to find the JSON configuration file(s), a NoConfigFound
636eb603e8SMatthew Barth      * exception can be thrown to denote that at that time finding/loading the
646eb603e8SMatthew Barth      * JSON configuration file(s) for a fan application failed. Details on what
656eb603e8SMatthew Barth      * application and JSON configuration file that failed to be found will be
666eb603e8SMatthew Barth      * logged resulting in the application being terminated.
676eb603e8SMatthew Barth      *
686eb603e8SMatthew Barth      * @param[in] details - Additional details
696eb603e8SMatthew Barth      */
NoConfigFound(const std::string & appName,const std::string & fileName)706eb603e8SMatthew Barth     NoConfigFound(const std::string& appName, const std::string& fileName) :
71fbf4703fSPatrick Williams         std::runtime_error(std::format("JSON configuration not found [Could "
726eb603e8SMatthew Barth                                        "not find fan {} conf file {}]",
736eb603e8SMatthew Barth                                        appName, fileName)
746eb603e8SMatthew Barth                                .c_str())
756eb603e8SMatthew Barth     {}
766eb603e8SMatthew Barth };
776eb603e8SMatthew Barth 
7811547c9bSMatthew Barth class JsonConfig
7911547c9bSMatthew Barth {
8011547c9bSMatthew Barth   public:
8159850df3SMatt Spinler     /**
82d80c8753SMatthew Barth      * @brief Get the object paths with the compatible interface
83d80c8753SMatthew Barth      *
84d80c8753SMatthew Barth      * Retrieve all the object paths implementing the compatible interface for
85d80c8753SMatthew Barth      * configuration file loading.
86d80c8753SMatthew Barth      */
getCompatObjPaths()87*b99ce0edSChau Ly     std::vector<std::string>& getCompatObjPaths()
88d80c8753SMatthew Barth     {
89*b99ce0edSChau Ly         using SubTreeMap =
90*b99ce0edSChau Ly             std::map<std::string,
91*b99ce0edSChau Ly                      std::map<std::string, std::vector<std::string>>>;
92*b99ce0edSChau Ly         SubTreeMap subTreeObjs = util::SDBusPlus::getSubTreeRaw(
93d80c8753SMatthew Barth             util::SDBusPlus::getBus(), "/", confCompatIntf, 0);
94*b99ce0edSChau Ly 
95*b99ce0edSChau Ly         static std::vector<std::string> paths;
96*b99ce0edSChau Ly         for (auto& [path, serviceMap] : subTreeObjs)
97*b99ce0edSChau Ly         {
98*b99ce0edSChau Ly             // Only save objects under confCompatServ
99*b99ce0edSChau Ly             if (serviceMap.find(confCompatServ) != serviceMap.end())
100*b99ce0edSChau Ly             {
101*b99ce0edSChau Ly                 paths.emplace_back(path);
102*b99ce0edSChau Ly             }
103*b99ce0edSChau Ly         }
104d80c8753SMatthew Barth         return paths;
105d80c8753SMatthew Barth     }
106d80c8753SMatthew Barth 
107d80c8753SMatthew Barth     /**
10859850df3SMatt Spinler      * @brief Constructor
10959850df3SMatt Spinler      *
110b370ab3bSMatthew Barth      * Attempts to set the list of compatible values from the compatible
111b370ab3bSMatthew Barth      * interface and call the fan app's function to load its config file(s). If
112b370ab3bSMatthew Barth      * the compatible interface is not found, it subscribes to the
113b370ab3bSMatthew Barth      * interfacesAdded signal for that interface on the compatible service
114b370ab3bSMatthew Barth      * defined above.
115b370ab3bSMatthew Barth      *
116b370ab3bSMatthew Barth      * @param[in] func - Fan app function to call to load its config file(s)
117b370ab3bSMatthew Barth      */
JsonConfig(std::function<void ()> func)118b370ab3bSMatthew Barth     JsonConfig(std::function<void()> func) : _loadFunc(func)
119b370ab3bSMatthew Barth     {
1208d1193cbSMatthew Barth         std::vector<std::string> compatObjPaths;
1218d1193cbSMatthew Barth 
1223ea9ec2bSPatrick Williams         _match = std::make_unique<sdbusplus::bus::match_t>(
123b370ab3bSMatthew Barth             util::SDBusPlus::getBus(),
124b370ab3bSMatthew Barth             sdbusplus::bus::match::rules::interfacesAdded() +
125b370ab3bSMatthew Barth                 sdbusplus::bus::match::rules::sender(confCompatServ),
126b370ab3bSMatthew Barth             std::bind(&JsonConfig::compatIntfAdded, this,
127b370ab3bSMatthew Barth                       std::placeholders::_1));
1281689cb6cSMatthew Barth 
1298d1193cbSMatthew Barth         try
1308d1193cbSMatthew Barth         {
1318d1193cbSMatthew Barth             compatObjPaths = getCompatObjPaths();
1328d1193cbSMatthew Barth         }
1338d1193cbSMatthew Barth         catch (const util::DBusMethodError&)
1348d1193cbSMatthew Barth         {
1358d1193cbSMatthew Barth             // Compatible interface does not exist on any dbus object yet
1368d1193cbSMatthew Barth         }
1378d1193cbSMatthew Barth 
138b370ab3bSMatthew Barth         if (!compatObjPaths.empty())
139b370ab3bSMatthew Barth         {
140b370ab3bSMatthew Barth             for (auto& path : compatObjPaths)
141b370ab3bSMatthew Barth             {
142b370ab3bSMatthew Barth                 try
143b370ab3bSMatthew Barth                 {
144b370ab3bSMatthew Barth                     // Retrieve json config compatible relative path
145b370ab3bSMatthew Barth                     // locations (last one found will be what's used if more
146*b99ce0edSChau Ly                     // than one dbus object implementing the compatible
147b370ab3bSMatthew Barth                     // interface exists).
1481689cb6cSMatthew Barth                     _confCompatValues =
1491689cb6cSMatthew Barth                         util::SDBusPlus::getProperty<std::vector<std::string>>(
1501689cb6cSMatthew Barth                             util::SDBusPlus::getBus(), path, confCompatIntf,
151b370ab3bSMatthew Barth                             confCompatProp);
152b370ab3bSMatthew Barth                 }
153b370ab3bSMatthew Barth                 catch (const util::DBusError&)
154b370ab3bSMatthew Barth                 {
155b370ab3bSMatthew Barth                     // Compatible property unavailable on this dbus object
156b370ab3bSMatthew Barth                     // path's compatible interface, ignore
157b370ab3bSMatthew Barth                 }
158b370ab3bSMatthew Barth             }
159*b99ce0edSChau Ly             try
160*b99ce0edSChau Ly             {
161b370ab3bSMatthew Barth                 _loadFunc();
162b370ab3bSMatthew Barth             }
163*b99ce0edSChau Ly             catch (const NoConfigFound&)
164*b99ce0edSChau Ly             {
165*b99ce0edSChau Ly                 // The Decorator.Compatible interface is not unique to one
166*b99ce0edSChau Ly                 // single object on DBus so this should not be treated as a
167*b99ce0edSChau Ly                 // failure, wait for interfacesAdded signal.
168*b99ce0edSChau Ly             }
169*b99ce0edSChau Ly         }
170b370ab3bSMatthew Barth         else
171b370ab3bSMatthew Barth         {
172b370ab3bSMatthew Barth             // Check if required config(s) are found not needing the
173b370ab3bSMatthew Barth             // compatible interface, otherwise this is intended to catch the
174b370ab3bSMatthew Barth             // exception thrown by the getConfFile function when the
175b370ab3bSMatthew Barth             // required config file was not found. This would then result in
176b370ab3bSMatthew Barth             // waiting for the compatible interfacesAdded signal
177b370ab3bSMatthew Barth             try
178b370ab3bSMatthew Barth             {
179b370ab3bSMatthew Barth                 _loadFunc();
180b370ab3bSMatthew Barth             }
1816eb603e8SMatthew Barth             catch (const NoConfigFound&)
182b370ab3bSMatthew Barth             {
183b370ab3bSMatthew Barth                 // Wait for compatible interfacesAdded signal
184b370ab3bSMatthew Barth             }
185b370ab3bSMatthew Barth         }
186b370ab3bSMatthew Barth     }
187b370ab3bSMatthew Barth 
188b370ab3bSMatthew Barth     /**
189b370ab3bSMatthew Barth      * @brief InterfacesAdded callback function for the compatible interface.
190b370ab3bSMatthew Barth      *
191b370ab3bSMatthew Barth      * @param[in] msg - The D-Bus message contents
192b370ab3bSMatthew Barth      *
193b370ab3bSMatthew Barth      * If the compatible interface is found, it uses the compatible property on
194b370ab3bSMatthew Barth      * the interface to set the list of compatible values to be used when
195b370ab3bSMatthew Barth      * attempting to get a configuration file. Once the list of compatible
196b370ab3bSMatthew Barth      * values has been updated, it calls the load function.
197b370ab3bSMatthew Barth      */
compatIntfAdded(sdbusplus::message_t & msg)198cb356d48SPatrick Williams     void compatIntfAdded(sdbusplus::message_t& msg)
199b370ab3bSMatthew Barth     {
200*b99ce0edSChau Ly         if (!_compatibleName.empty())
201*b99ce0edSChau Ly         {
202*b99ce0edSChau Ly             // Do not process the interfaceAdded signal if one compatible name
203*b99ce0edSChau Ly             // has been successfully used to get config files
204*b99ce0edSChau Ly             return;
205*b99ce0edSChau Ly         }
206b370ab3bSMatthew Barth         sdbusplus::message::object_path op;
207b370ab3bSMatthew Barth         std::map<std::string,
208b370ab3bSMatthew Barth                  std::map<std::string, std::variant<std::vector<std::string>>>>
209b370ab3bSMatthew Barth             intfProps;
210b370ab3bSMatthew Barth 
211b370ab3bSMatthew Barth         msg.read(op, intfProps);
212b370ab3bSMatthew Barth 
213b370ab3bSMatthew Barth         if (intfProps.find(confCompatIntf) == intfProps.end())
214b370ab3bSMatthew Barth         {
215b370ab3bSMatthew Barth             return;
216b370ab3bSMatthew Barth         }
217b370ab3bSMatthew Barth 
218b370ab3bSMatthew Barth         const auto& props = intfProps.at(confCompatIntf);
219b370ab3bSMatthew Barth         // Only one dbus object with the compatible interface is used at a time
220b370ab3bSMatthew Barth         _confCompatValues =
221b370ab3bSMatthew Barth             std::get<std::vector<std::string>>(props.at(confCompatProp));
222b370ab3bSMatthew Barth         _loadFunc();
223b370ab3bSMatthew Barth     }
224b370ab3bSMatthew Barth 
225b370ab3bSMatthew Barth     /**
22611547c9bSMatthew Barth      * Get the json configuration file. The first location found to contain
22711547c9bSMatthew Barth      * the json config file for the given fan application is used from the
22811547c9bSMatthew Barth      * following locations in order.
22911547c9bSMatthew Barth      * 1.) From the confOverridePath location
230b67089bfSMatthew Barth      * 2.) From the default confBasePath location
231b67089bfSMatthew Barth      * 3.) From config file found using an entry from a list obtained from an
232fcbdc0e4SMatthew Barth      * interface's property as a relative path extension on the base path where:
233fcbdc0e4SMatthew Barth      *     interface = Interface set in confCompatIntf with the property
234fcbdc0e4SMatthew Barth      *     property = Property set in confCompatProp containing a list of
235fcbdc0e4SMatthew Barth      *                subdirectories in priority order to find a config
23611547c9bSMatthew Barth      *
23711547c9bSMatthew Barth      * @brief Get the configuration file to be used
23811547c9bSMatthew Barth      *
23911547c9bSMatthew Barth      * @param[in] appName - The phosphor-fan-presence application name
24011547c9bSMatthew Barth      * @param[in] fileName - Application's configuration file's name
241b599102dSMatthew Barth      * @param[in] isOptional - Config file is optional, default to 'false'
24211547c9bSMatthew Barth      *
24311547c9bSMatthew Barth      * @return filesystem path
24411547c9bSMatthew Barth      *     The filesystem path to the configuration file to use
24511547c9bSMatthew Barth      */
getConfFile(const std::string & appName,const std::string & fileName,bool isOptional=false)246808d7fe8SMike Capps     static const fs::path getConfFile(const std::string& appName,
247b599102dSMatthew Barth                                       const std::string& fileName,
248b599102dSMatthew Barth                                       bool isOptional = false)
24911547c9bSMatthew Barth     {
25011547c9bSMatthew Barth         // Check override location
25111547c9bSMatthew Barth         fs::path confFile = fs::path{confOverridePath} / appName / fileName;
25211547c9bSMatthew Barth         if (fs::exists(confFile))
25311547c9bSMatthew Barth         {
25411547c9bSMatthew Barth             return confFile;
25511547c9bSMatthew Barth         }
25611547c9bSMatthew Barth 
25759850df3SMatt Spinler         // If the default file is there, use it
258fcbdc0e4SMatthew Barth         confFile = fs::path{confBasePath} / appName / fileName;
25959850df3SMatt Spinler         if (fs::exists(confFile))
26059850df3SMatt Spinler         {
26159850df3SMatt Spinler             return confFile;
26259850df3SMatt Spinler         }
263fcbdc0e4SMatthew Barth 
264b67089bfSMatthew Barth         // Look for a config file at each entry relative to the base
265b67089bfSMatthew Barth         // path and use the first one found
26661b73296SPatrick Williams         auto it =
26761b73296SPatrick Williams             std::find_if(_confCompatValues.begin(), _confCompatValues.end(),
268b67089bfSMatthew Barth                          [&confFile, &appName, &fileName](const auto& value) {
269b67089bfSMatthew Barth             confFile = fs::path{confBasePath} / appName / value / fileName;
270*b99ce0edSChau Ly             _compatibleName = value;
271b67089bfSMatthew Barth             return fs::exists(confFile);
272b67089bfSMatthew Barth         });
273b67089bfSMatthew Barth         if (it == _confCompatValues.end())
274b67089bfSMatthew Barth         {
275b67089bfSMatthew Barth             confFile.clear();
276*b99ce0edSChau Ly             _compatibleName.clear();
277b67089bfSMatthew Barth         }
278b67089bfSMatthew Barth 
27959850df3SMatt Spinler         if (confFile.empty() && !isOptional)
280a916f039SMatthew Barth         {
2816eb603e8SMatthew Barth             throw NoConfigFound(appName, fileName);
282a916f039SMatthew Barth         }
28311547c9bSMatthew Barth 
28411547c9bSMatthew Barth         return confFile;
28511547c9bSMatthew Barth     }
28611547c9bSMatthew Barth 
28711547c9bSMatthew Barth     /**
28811547c9bSMatthew Barth      * @brief Load the JSON config file
28911547c9bSMatthew Barth      *
29011547c9bSMatthew Barth      * @param[in] confFile - File system path of the configuration file to load
29111547c9bSMatthew Barth      *
29211547c9bSMatthew Barth      * @return Parsed JSON object
29311547c9bSMatthew Barth      *     The parsed JSON configuration file object
29411547c9bSMatthew Barth      */
load(const fs::path & confFile)29511547c9bSMatthew Barth     static const json load(const fs::path& confFile)
29611547c9bSMatthew Barth     {
29711547c9bSMatthew Barth         std::ifstream file;
29811547c9bSMatthew Barth         json jsonConf;
29911547c9bSMatthew Barth 
300b599102dSMatthew Barth         if (!confFile.empty() && fs::exists(confFile))
30111547c9bSMatthew Barth         {
3021826c730SMatthew Barth             log<level::INFO>(
303fbf4703fSPatrick Williams                 std::format("Loading configuration from {}", confFile.string())
3041826c730SMatthew Barth                     .c_str());
30511547c9bSMatthew Barth             file.open(confFile);
30611547c9bSMatthew Barth             try
30711547c9bSMatthew Barth             {
308de72d5d1SMatthew Barth                 // Enable ignoring `//` or `/* */` comments
309de72d5d1SMatthew Barth                 jsonConf = json::parse(file, nullptr, true, true);
31011547c9bSMatthew Barth             }
311ddb773b2SPatrick Williams             catch (const std::exception& e)
31211547c9bSMatthew Barth             {
3131826c730SMatthew Barth                 log<level::ERR>(
314fbf4703fSPatrick Williams                     std::format(
3151826c730SMatthew Barth                         "Failed to parse JSON config file: {}, error: {}",
3161826c730SMatthew Barth                         confFile.string(), e.what())
3171826c730SMatthew Barth                         .c_str());
3181826c730SMatthew Barth                 throw std::runtime_error(
319fbf4703fSPatrick Williams                     std::format(
3201826c730SMatthew Barth                         "Failed to parse JSON config file: {}, error: {}",
3211826c730SMatthew Barth                         confFile.string(), e.what())
3221826c730SMatthew Barth                         .c_str());
32311547c9bSMatthew Barth             }
32411547c9bSMatthew Barth         }
32511547c9bSMatthew Barth         else
32611547c9bSMatthew Barth         {
327fbf4703fSPatrick Williams             log<level::ERR>(std::format("Unable to open JSON config file: {}",
3281826c730SMatthew Barth                                         confFile.string())
3291826c730SMatthew Barth                                 .c_str());
3301826c730SMatthew Barth             throw std::runtime_error(
331fbf4703fSPatrick Williams                 std::format("Unable to open JSON config file: {}",
3321826c730SMatthew Barth                             confFile.string())
3331826c730SMatthew Barth                     .c_str());
33411547c9bSMatthew Barth         }
33511547c9bSMatthew Barth 
33611547c9bSMatthew Barth         return jsonConf;
33711547c9bSMatthew Barth     }
33859850df3SMatt Spinler 
339d4c7fb7eSMatt Spinler     /**
340d4c7fb7eSMatt Spinler      * @brief Return the compatible values property
341d4c7fb7eSMatt Spinler      *
342d4c7fb7eSMatt Spinler      * @return const std::vector<std::string>& - The values
343d4c7fb7eSMatt Spinler      */
getCompatValues()344d4c7fb7eSMatt Spinler     static const std::vector<std::string>& getCompatValues()
345d4c7fb7eSMatt Spinler     {
346d4c7fb7eSMatt Spinler         return _confCompatValues;
347d4c7fb7eSMatt Spinler     }
348d4c7fb7eSMatt Spinler 
34959850df3SMatt Spinler   private:
350b370ab3bSMatthew Barth     /* Load function to call for a fan app to load its config file(s). */
351b370ab3bSMatthew Barth     std::function<void()> _loadFunc;
352b370ab3bSMatthew Barth 
35359850df3SMatt Spinler     /**
35459850df3SMatt Spinler      * @brief The interfacesAdded match that is used to wait
355*b99ce0edSChau Ly      *        for the Inventory.Decorator.Compatible interface to show up.
35659850df3SMatt Spinler      */
3573ea9ec2bSPatrick Williams     std::unique_ptr<sdbusplus::bus::match_t> _match;
358b67089bfSMatthew Barth 
359b67089bfSMatthew Barth     /**
360b67089bfSMatthew Barth      * @brief List of compatible values from the compatible interface
361b67089bfSMatthew Barth      *
362b67089bfSMatthew Barth      * Only supports a single instance of the compatible interface on a dbus
363b67089bfSMatthew Barth      * object. If more than one dbus object exists with the compatible
364b67089bfSMatthew Barth      * interface, the last one found will be the list of compatible values used.
365b67089bfSMatthew Barth      */
366b67089bfSMatthew Barth     inline static std::vector<std::string> _confCompatValues;
367*b99ce0edSChau Ly 
368*b99ce0edSChau Ly     /**
369*b99ce0edSChau Ly      * @brief The compatible value that is currently used to load configuration
370*b99ce0edSChau Ly      *
371*b99ce0edSChau Ly      * The value extracted from the achieved property value list that is used
372*b99ce0edSChau Ly      * as a sub-folder to append to the configuration location and really
373*b99ce0edSChau Ly      * contains the configruation files
374*b99ce0edSChau Ly      */
375*b99ce0edSChau Ly 
376*b99ce0edSChau Ly     inline static std::string _compatibleName;
37711547c9bSMatthew Barth };
37811547c9bSMatthew Barth 
37911547c9bSMatthew Barth } // namespace phosphor::fan
380