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