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     /**
47      * Get the json configuration file. The first location found to contain
48      * the json config file for the given fan application is used from the
49      * following locations in order.
50      * 1.) From the confOverridePath location
51      * 2.) From config file found using an entry from a list obtained from an
52      * interface's property as a relative path extension on the base path where:
53      *     interface = Interface set in confCompatIntf with the property
54      *     property = Property set in confCompatProp containing a list of
55      *                subdirectories in priority order to find a config
56      * 3.) *DEFAULT* - From the confBasePath location
57      *
58      * @brief Get the configuration file to be used
59      *
60      * @param[in] bus - The dbus bus object
61      * @param[in] appName - The phosphor-fan-presence application name
62      * @param[in] fileName - Application's configuration file's name
63      * @param[in] isOptional - Config file is optional, default to 'false'
64      *
65      * @return filesystem path
66      *     The filesystem path to the configuration file to use
67      */
68     static const fs::path getConfFile(sdbusplus::bus::bus& bus,
69                                       const std::string& appName,
70                                       const std::string& fileName,
71                                       bool isOptional = false)
72     {
73         // Check override location
74         fs::path confFile = fs::path{confOverridePath} / appName / fileName;
75         if (fs::exists(confFile))
76         {
77             return confFile;
78         }
79 
80         // Default base path used if no config file found at any locations
81         // provided on dbus objects with the compatible interface
82         confFile = fs::path{confBasePath} / appName / fileName;
83 
84         // Get all objects implementing the compatible interface
85         auto objects =
86             util::SDBusPlus::getSubTreePathsRaw(bus, "/", confCompatIntf, 0);
87         for (auto& path : objects)
88         {
89             try
90             {
91                 // Retrieve json config compatible relative path locations
92                 auto confCompatValue =
93                     util::SDBusPlus::getProperty<std::vector<std::string>>(
94                         bus, path, confCompatIntf, confCompatProp);
95                 // Look for a config file at each entry relative to the base
96                 // path and use the first one found
97                 auto it = std::find_if(
98                     confCompatValue.begin(), confCompatValue.end(),
99                     [&confFile, &appName, &fileName](auto const& entry) {
100                         confFile =
101                             fs::path{confBasePath} / appName / entry / fileName;
102                         return fs::exists(confFile);
103                     });
104                 if (it != confCompatValue.end())
105                 {
106                     // Use the first config file found at a listed location
107                     break;
108                 }
109             }
110             catch (const util::DBusError&)
111             {
112                 // Property unavailable on object.
113                 // Set to default base path and continue to check next object
114             }
115             confFile = fs::path{confBasePath} / appName / fileName;
116         }
117 
118         if (!fs::exists(confFile))
119         {
120             if (!isOptional)
121             {
122                 log<level::ERR>(
123                     fmt::format("No JSON config file found. Default file: {}",
124                                 confFile.string())
125                         .c_str());
126                 throw std::runtime_error(
127                     fmt::format("No JSON config file found. Default file: {}",
128                                 confFile.string())
129                         .c_str());
130             }
131             else
132             {
133                 confFile.clear();
134             }
135         }
136 
137         return confFile;
138     }
139 
140     /**
141      * @brief Load the JSON config file
142      *
143      * @param[in] confFile - File system path of the configuration file to load
144      *
145      * @return Parsed JSON object
146      *     The parsed JSON configuration file object
147      */
148     static const json load(const fs::path& confFile)
149     {
150         std::ifstream file;
151         json jsonConf;
152 
153         if (!confFile.empty() && fs::exists(confFile))
154         {
155             log<level::INFO>(
156                 fmt::format("Loading configuration from {}", confFile.string())
157                     .c_str());
158             file.open(confFile);
159             try
160             {
161                 jsonConf = json::parse(file);
162             }
163             catch (std::exception& e)
164             {
165                 log<level::ERR>(
166                     fmt::format(
167                         "Failed to parse JSON config file: {}, error: {}",
168                         confFile.string(), e.what())
169                         .c_str());
170                 throw std::runtime_error(
171                     fmt::format(
172                         "Failed to parse JSON config file: {}, error: {}",
173                         confFile.string(), e.what())
174                         .c_str());
175             }
176         }
177         else
178         {
179             log<level::ERR>(fmt::format("Unable to open JSON config file: {}",
180                                         confFile.string())
181                                 .c_str());
182             throw std::runtime_error(
183                 fmt::format("Unable to open JSON config file: {}",
184                             confFile.string())
185                     .c_str());
186         }
187 
188         return jsonConf;
189     }
190 };
191 
192 } // namespace phosphor::fan
193