xref: /openbmc/entity-manager/src/entity_manager/dbus_interface.cpp (revision a182cb7c338c70e65bf6f8a828cb7176ffde152d)
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 
20 EMDBusInterface::EMDBusInterface(boost::asio::io_context& io,
21                                  sdbusplus::asio::object_server& objServer) :
22     io(io), objServer(objServer)
23 {}
24 
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>
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 
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 
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 
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 
145 static void populateInterfacePropertyFromJson(
146     nlohmann::json& systemConfiguration, const std::string& path,
147     const nlohmann::json& 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
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 void EMDBusInterface::addObject(
237     const std::flat_map<std::string, JsonVariantType, std::less<>>& data,
238     nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
239     const std::string& path, const std::string& board)
240 {
241     nlohmann::json::json_pointer ptr(jsonPointerPath);
242     nlohmann::json& base = systemConfiguration[ptr];
243     auto findExposes = base.find("Exposes");
244 
245     if (findExposes == base.end())
246     {
247         throw std::invalid_argument("Entity must have children.");
248     }
249 
250     // this will throw invalid-argument to sdbusplus if invalid json
251     nlohmann::json newData{};
252     for (const auto& item : data)
253     {
254         nlohmann::json& newJson = newData[item.first];
255         std::visit(
256             [&newJson](auto&& val) {
257                 newJson = std::forward<decltype(val)>(val);
258             },
259             item.second);
260     }
261 
262     auto findName = newData.find("Name");
263     auto findType = newData.find("Type");
264     if (findName == newData.end() || findType == newData.end())
265     {
266         throw std::invalid_argument("AddObject missing Name or Type");
267     }
268     const std::string* type = findType->get_ptr<const std::string*>();
269     const std::string* name = findName->get_ptr<const std::string*>();
270     if (type == nullptr || name == nullptr)
271     {
272         throw std::invalid_argument("Type and Name must be a string.");
273     }
274 
275     bool foundNull = false;
276     size_t lastIndex = 0;
277     // we add in the "exposes"
278     for (const auto& expose : *findExposes)
279     {
280         if (expose.is_null())
281         {
282             foundNull = true;
283             continue;
284         }
285 
286         if (expose["Name"] == *name && expose["Type"] == *type)
287         {
288             throw std::invalid_argument("Field already in JSON, not adding");
289         }
290 
291         if (foundNull)
292         {
293             continue;
294         }
295 
296         lastIndex++;
297     }
298 
299     if constexpr (ENABLE_RUNTIME_VALIDATE_JSON)
300     {
301         const std::filesystem::path schemaPath =
302             std::filesystem::path(schemaDirectory) / "exposes_record.json";
303 
304         std::ifstream schemaFile{schemaPath};
305 
306         if (!schemaFile.good())
307         {
308             throw std::invalid_argument(
309                 "No schema avaliable, cannot validate.");
310         }
311         nlohmann::json schema =
312             nlohmann::json::parse(schemaFile, nullptr, false, true);
313         if (schema.is_discarded())
314         {
315             lg2::error("Schema not legal: {TYPE}.json", "TYPE", *type);
316             throw DBusInternalError();
317         }
318 
319         if (!validateJson(schema, newData))
320         {
321             throw std::invalid_argument("Data does not match schema");
322         }
323     }
324 
325     if (foundNull)
326     {
327         findExposes->at(lastIndex) = newData;
328     }
329     else
330     {
331         findExposes->push_back(newData);
332     }
333     if (!writeJsonFiles(systemConfiguration))
334     {
335         lg2::error("Error writing json files");
336     }
337     std::string dbusName = *name;
338 
339     std::regex_replace(dbusName.begin(), dbusName.begin(), dbusName.end(),
340                        illegalDbusMemberRegex, "_");
341 
342     std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
343         createInterface(path + "/" + dbusName,
344                         "xyz.openbmc_project.Configuration." + *type, board,
345                         true);
346     // permission is read-write, as since we just created it, must be
347     // runtime modifiable
348     populateInterfaceFromJson(
349         systemConfiguration,
350         jsonPointerPath + "/Exposes/" + std::to_string(lastIndex), interface,
351         newData, sdbusplus::asio::PropertyPermission::readWrite);
352 }
353 
354 void EMDBusInterface::createAddObjectMethod(
355     const std::string& jsonPointerPath, const std::string& path,
356     nlohmann::json& systemConfiguration, const std::string& board)
357 {
358     std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
359         createInterface(path, "xyz.openbmc_project.AddObject", board);
360 
361     iface->register_method(
362         "AddObject",
363         [&systemConfiguration, jsonPointerPath{std::string(jsonPointerPath)},
364          path{std::string(path)}, board{std::string(board)},
365          this](const std::flat_map<std::string, JsonVariantType, std::less<>>&
366                    data) {
367             addObject(data, systemConfiguration, jsonPointerPath, path, board);
368         });
369     tryIfaceInitialize(iface);
370 }
371 
372 std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
373     EMDBusInterface::getDeviceInterfaces(const nlohmann::json& device)
374 {
375     return inventory[device["Name"].get<std::string>()];
376 }
377 
378 } // namespace dbus_interface
379