xref: /openbmc/entity-manager/src/entity_manager/dbus_interface.cpp (revision 8feb04544ae69154a47c4323f5ada2e6da34f50e)
1 #include "dbus_interface.hpp"
2 
3 #include "perform_probe.hpp"
4 #include "utils.hpp"
5 
6 #include <boost/algorithm/string/case_conv.hpp>
7 #include <boost/container/flat_map.hpp>
8 #include <phosphor-logging/lg2.hpp>
9 
10 #include <fstream>
11 #include <regex>
12 #include <string>
13 #include <vector>
14 
15 using JsonVariantType =
16     std::variant<std::vector<std::string>, std::vector<double>, std::string,
17                  int64_t, uint64_t, double, int32_t, uint32_t, int16_t,
18                  uint16_t, uint8_t, bool>;
19 
20 namespace dbus_interface
21 {
22 
23 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
24 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
25 
26 EMDBusInterface::EMDBusInterface(boost::asio::io_context& io,
27                                  sdbusplus::asio::object_server& objServer) :
28     io(io), objServer(objServer)
29 {}
30 
31 void tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface>& iface)
32 {
33     try
34     {
35         iface->initialize();
36     }
37     catch (std::exception& e)
38     {
39         lg2::error(
40             "Unable to initialize dbus interface : {ERR} object Path : {PATH} interface name : {INTF}",
41             "ERR", e, "PATH", iface->get_object_path(), "INTF",
42             iface->get_interface_name());
43     }
44 }
45 
46 std::shared_ptr<sdbusplus::asio::dbus_interface>
47     EMDBusInterface::createInterface(const std::string& path,
48                                      const std::string& interface,
49                                      const std::string& parent, bool checkNull)
50 {
51     // on first add we have no reason to check for null before add, as there
52     // won't be any. For dynamically added interfaces, we check for null so that
53     // a constant delete/add will not create a memory leak
54 
55     auto ptr = objServer.add_interface(path, interface);
56     auto& dataVector = inventory[parent];
57     if (checkNull)
58     {
59         auto it = std::find_if(dataVector.begin(), dataVector.end(),
60                                [](const auto& p) { return p.expired(); });
61         if (it != dataVector.end())
62         {
63             *it = ptr;
64             return ptr;
65         }
66     }
67     dataVector.emplace_back(ptr);
68     return ptr;
69 }
70 
71 void EMDBusInterface::createDeleteObjectMethod(
72     const std::string& jsonPointerPath,
73     const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
74     nlohmann::json& systemConfiguration)
75 {
76     std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface;
77     iface->register_method(
78         "Delete", [this, &systemConfiguration, interface,
79                    jsonPointerPath{std::string(jsonPointerPath)}]() {
80             std::shared_ptr<sdbusplus::asio::dbus_interface> dbusInterface =
81                 interface.lock();
82             if (!dbusInterface)
83             {
84                 // this technically can't happen as the pointer is pointing to
85                 // us
86                 throw DBusInternalError();
87             }
88             nlohmann::json::json_pointer ptr(jsonPointerPath);
89             systemConfiguration[ptr] = nullptr;
90 
91             // todo(james): dig through sdbusplus to find out why we can't
92             // delete it in a method call
93             boost::asio::post(io, [dbusInterface, this]() mutable {
94                 objServer.remove_interface(dbusInterface);
95             });
96 
97             if (!writeJsonFiles(systemConfiguration))
98             {
99                 lg2::error("error setting json file");
100                 throw DBusInternalError();
101             }
102         });
103 }
104 
105 static bool checkArrayElementsSameType(nlohmann::json& value)
106 {
107     nlohmann::json::array_t* arr = value.get_ptr<nlohmann::json::array_t*>();
108     if (arr == nullptr)
109     {
110         return false;
111     }
112 
113     if (arr->empty())
114     {
115         return true;
116     }
117 
118     nlohmann::json::value_t firstType = value[0].type();
119     return std::ranges::all_of(value, [firstType](const nlohmann::json& el) {
120         return el.type() == firstType;
121     });
122 }
123 
124 static nlohmann::json::value_t getDBusType(
125     const nlohmann::json& value, nlohmann::json::value_t type,
126     sdbusplus::asio::PropertyPermission permission)
127 {
128     const bool array = value.type() == nlohmann::json::value_t::array;
129 
130     if (permission == sdbusplus::asio::PropertyPermission::readWrite)
131     {
132         // all setable numbers are doubles as it is difficult to always
133         // create a configuration file with all whole numbers as decimals
134         // i.e. 1.0
135         if (array)
136         {
137             if (value[0].is_number())
138             {
139                 return nlohmann::json::value_t::number_float;
140             }
141         }
142         else if (value.is_number())
143         {
144             return nlohmann::json::value_t::number_float;
145         }
146     }
147 
148     return type;
149 }
150 
151 static void populateInterfacePropertyFromJson(
152     nlohmann::json& systemConfiguration, const std::string& path,
153     const nlohmann::json& key, const nlohmann::json& value,
154     nlohmann::json::value_t type,
155     std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
156     sdbusplus::asio::PropertyPermission permission)
157 {
158     const auto modifiedType = getDBusType(value, type, permission);
159 
160     switch (modifiedType)
161     {
162         case (nlohmann::json::value_t::boolean):
163         {
164             addValueToDBus<bool>(key, value, *iface, permission,
165                                  systemConfiguration, path);
166             break;
167         }
168         case (nlohmann::json::value_t::number_integer):
169         {
170             addValueToDBus<int64_t>(key, value, *iface, permission,
171                                     systemConfiguration, path);
172             break;
173         }
174         case (nlohmann::json::value_t::number_unsigned):
175         {
176             addValueToDBus<uint64_t>(key, value, *iface, permission,
177                                      systemConfiguration, path);
178             break;
179         }
180         case (nlohmann::json::value_t::number_float):
181         {
182             addValueToDBus<double>(key, value, *iface, permission,
183                                    systemConfiguration, path);
184             break;
185         }
186         case (nlohmann::json::value_t::string):
187         {
188             addValueToDBus<std::string>(key, value, *iface, permission,
189                                         systemConfiguration, path);
190             break;
191         }
192         default:
193         {
194             lg2::error(
195                 "Unexpected json type in system configuration {KEY}: {VALUE}",
196                 "KEY", key, "VALUE", value.type_name());
197             break;
198         }
199     }
200 }
201 
202 // adds simple json types to interface's properties
203 void EMDBusInterface::populateInterfaceFromJson(
204     nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
205     std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
206     nlohmann::json& dict, sdbusplus::asio::PropertyPermission permission)
207 {
208     for (const auto& [key, value] : dict.items())
209     {
210         auto type = value.type();
211         if (value.type() == nlohmann::json::value_t::array)
212         {
213             if (value.empty())
214             {
215                 continue;
216             }
217             type = value[0].type();
218             if (!checkArrayElementsSameType(value))
219             {
220                 lg2::error("dbus format error {VALUE}", "VALUE", value);
221                 continue;
222             }
223         }
224         if (type == nlohmann::json::value_t::object)
225         {
226             continue; // handled elsewhere
227         }
228 
229         std::string path = jsonPointerPath;
230         path.append("/").append(key);
231 
232         populateInterfacePropertyFromJson(systemConfiguration, path, key, value,
233                                           type, iface, permission);
234     }
235     if (permission == sdbusplus::asio::PropertyPermission::readWrite)
236     {
237         createDeleteObjectMethod(jsonPointerPath, iface, systemConfiguration);
238     }
239     tryIfaceInitialize(iface);
240 }
241 
242 void EMDBusInterface::createAddObjectMethod(
243     const std::string& jsonPointerPath, const std::string& path,
244     nlohmann::json& systemConfiguration, const std::string& board)
245 {
246     std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
247         createInterface(path, "xyz.openbmc_project.AddObject", board);
248 
249     iface->register_method(
250         "AddObject",
251         [&systemConfiguration, jsonPointerPath{std::string(jsonPointerPath)},
252          path{std::string(path)}, board,
253          this](const boost::container::flat_map<std::string, JsonVariantType>&
254                    data) {
255             nlohmann::json::json_pointer ptr(jsonPointerPath);
256             nlohmann::json& base = systemConfiguration[ptr];
257             auto findExposes = base.find("Exposes");
258 
259             if (findExposes == base.end())
260             {
261                 throw std::invalid_argument("Entity must have children.");
262             }
263 
264             // this will throw invalid-argument to sdbusplus if invalid json
265             nlohmann::json newData{};
266             for (const auto& item : data)
267             {
268                 nlohmann::json& newJson = newData[item.first];
269                 std::visit(
270                     [&newJson](auto&& val) {
271                         newJson = std::forward<decltype(val)>(val);
272                     },
273                     item.second);
274             }
275 
276             auto findName = newData.find("Name");
277             auto findType = newData.find("Type");
278             if (findName == newData.end() || findType == newData.end())
279             {
280                 throw std::invalid_argument("AddObject missing Name or Type");
281             }
282             const std::string* type = findType->get_ptr<const std::string*>();
283             const std::string* name = findName->get_ptr<const std::string*>();
284             if (type == nullptr || name == nullptr)
285             {
286                 throw std::invalid_argument("Type and Name must be a string.");
287             }
288 
289             bool foundNull = false;
290             size_t lastIndex = 0;
291             // we add in the "exposes"
292             for (const auto& expose : *findExposes)
293             {
294                 if (expose.is_null())
295                 {
296                     foundNull = true;
297                     continue;
298                 }
299 
300                 if (expose["Name"] == *name && expose["Type"] == *type)
301                 {
302                     throw std::invalid_argument(
303                         "Field already in JSON, not adding");
304                 }
305 
306                 if (foundNull)
307                 {
308                     continue;
309                 }
310 
311                 lastIndex++;
312             }
313 
314             if constexpr (ENABLE_RUNTIME_VALIDATE_JSON)
315             {
316                 const std::filesystem::path schemaPath =
317                     std::filesystem::path(schemaDirectory) /
318                     "exposes_record.json";
319 
320                 std::ifstream schemaFile{schemaPath};
321 
322                 if (!schemaFile.good())
323                 {
324                     throw std::invalid_argument(
325                         "No schema avaliable, cannot validate.");
326                 }
327                 nlohmann::json schema =
328                     nlohmann::json::parse(schemaFile, nullptr, false, true);
329                 if (schema.is_discarded())
330                 {
331                     lg2::error("Schema not legal: {TYPE}.json", "TYPE", *type);
332                     throw DBusInternalError();
333                 }
334 
335                 if (!validateJson(schema, newData))
336                 {
337                     throw std::invalid_argument("Data does not match schema");
338                 }
339             }
340 
341             if (foundNull)
342             {
343                 findExposes->at(lastIndex) = newData;
344             }
345             else
346             {
347                 findExposes->push_back(newData);
348             }
349             if (!writeJsonFiles(systemConfiguration))
350             {
351                 lg2::error("Error writing json files");
352             }
353             std::string dbusName = *name;
354 
355             std::regex_replace(dbusName.begin(), dbusName.begin(),
356                                dbusName.end(), illegalDbusMemberRegex, "_");
357 
358             std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
359                 createInterface(path + "/" + dbusName,
360                                 "xyz.openbmc_project.Configuration." + *type,
361                                 board, true);
362             // permission is read-write, as since we just created it, must be
363             // runtime modifiable
364             populateInterfaceFromJson(
365                 systemConfiguration,
366                 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
367                 interface, newData,
368                 sdbusplus::asio::PropertyPermission::readWrite);
369         });
370     tryIfaceInitialize(iface);
371 }
372 
373 std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
374     EMDBusInterface::getDeviceInterfaces(const nlohmann::json& device)
375 {
376     return inventory[device["Name"].get<std::string>()];
377 }
378 
379 } // namespace dbus_interface
380