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