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