xref: /openbmc/entity-manager/src/entity_manager/dbus_interface.cpp (revision cfc7f4f423c8163c394fbd777afcaf10a835206f)
1 #include "dbus_interface.hpp"
2 
3 #include "../utils.hpp"
4 #include "perform_probe.hpp"
5 
6 #include <boost/algorithm/string/case_conv.hpp>
7 #include <boost/container/flat_map.hpp>
8 
9 #include <regex>
10 #include <string>
11 #include <vector>
12 
13 using JsonVariantType =
14     std::variant<std::vector<std::string>, std::vector<double>, std::string,
15                  int64_t, uint64_t, double, int32_t, uint32_t, int16_t,
16                  uint16_t, uint8_t, bool>;
17 
18 namespace dbus_interface
19 {
20 
21 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
22 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
23 
24 // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
25 // store reference to all interfaces so we can destroy them later
26 boost::container::flat_map<
27     std::string, std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>>
28     inventory;
29 // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
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         std::cerr << "Unable to initialize dbus interface : " << e.what()
40                   << "\n"
41                   << "object Path : " << iface->get_object_path() << "\n"
42                   << "interface name : " << iface->get_interface_name() << "\n";
43     }
44 }
45 
46 std::shared_ptr<sdbusplus::asio::dbus_interface> createInterface(
47     sdbusplus::asio::object_server& objServer, const std::string& path,
48     const std::string& interface, const std::string& parent, bool checkNull)
49 {
50     // on first add we have no reason to check for null before add, as there
51     // won't be any. For dynamically added interfaces, we check for null so that
52     // a constant delete/add will not create a memory leak
53 
54     auto ptr = objServer.add_interface(path, interface);
55     auto& dataVector = inventory[parent];
56     if (checkNull)
57     {
58         auto it = std::find_if(dataVector.begin(), dataVector.end(),
59                                [](const auto& p) { return p.expired(); });
60         if (it != dataVector.end())
61         {
62             *it = ptr;
63             return ptr;
64         }
65     }
66     dataVector.emplace_back(ptr);
67     return ptr;
68 }
69 
70 void createDeleteObjectMethod(
71     const std::string& jsonPointerPath,
72     const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
73     sdbusplus::asio::object_server& objServer,
74     nlohmann::json& systemConfiguration)
75 {
76     std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface;
77     iface->register_method(
78         "Delete", [&objServer, &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, [&objServer, dbusInterface]() mutable {
94                 objServer.remove_interface(dbusInterface);
95             });
96 
97             if (!configuration::writeJsonFiles(systemConfiguration))
98             {
99                 std::cerr << "error setting json file\n";
100                 throw DBusInternalError();
101             }
102         });
103 }
104 
105 // adds simple json types to interface's properties
106 void populateInterfaceFromJson(
107     nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
108     std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
109     nlohmann::json& dict, sdbusplus::asio::object_server& objServer,
110     sdbusplus::asio::PropertyPermission permission)
111 {
112     for (const auto& [key, value] : dict.items())
113     {
114         auto type = value.type();
115         bool array = false;
116         if (value.type() == nlohmann::json::value_t::array)
117         {
118             array = true;
119             if (value.empty())
120             {
121                 continue;
122             }
123             type = value[0].type();
124             bool isLegal = true;
125             for (const auto& arrayItem : value)
126             {
127                 if (arrayItem.type() != type)
128                 {
129                     isLegal = false;
130                     break;
131                 }
132             }
133             if (!isLegal)
134             {
135                 std::cerr << "dbus format error" << value << "\n";
136                 continue;
137             }
138         }
139         if (type == nlohmann::json::value_t::object)
140         {
141             continue; // handled elsewhere
142         }
143 
144         std::string path = jsonPointerPath;
145         path.append("/").append(key);
146         if (permission == sdbusplus::asio::PropertyPermission::readWrite)
147         {
148             // all setable numbers are doubles as it is difficult to always
149             // create a configuration file with all whole numbers as decimals
150             // i.e. 1.0
151             if (array)
152             {
153                 if (value[0].is_number())
154                 {
155                     type = nlohmann::json::value_t::number_float;
156                 }
157             }
158             else if (value.is_number())
159             {
160                 type = nlohmann::json::value_t::number_float;
161             }
162         }
163 
164         switch (type)
165         {
166             case (nlohmann::json::value_t::boolean):
167             {
168                 if (array)
169                 {
170                     // todo: array of bool isn't detected correctly by
171                     // sdbusplus, change it to numbers
172                     addArrayToDbus<uint64_t>(key, value, iface.get(),
173                                              permission, systemConfiguration,
174                                              path);
175                 }
176 
177                 else
178                 {
179                     addProperty(key, value.get<bool>(), iface.get(),
180                                 systemConfiguration, path, permission);
181                 }
182                 break;
183             }
184             case (nlohmann::json::value_t::number_integer):
185             {
186                 if (array)
187                 {
188                     addArrayToDbus<int64_t>(key, value, iface.get(), permission,
189                                             systemConfiguration, path);
190                 }
191                 else
192                 {
193                     addProperty(key, value.get<int64_t>(), iface.get(),
194                                 systemConfiguration, path,
195                                 sdbusplus::asio::PropertyPermission::readOnly);
196                 }
197                 break;
198             }
199             case (nlohmann::json::value_t::number_unsigned):
200             {
201                 if (array)
202                 {
203                     addArrayToDbus<uint64_t>(key, value, iface.get(),
204                                              permission, systemConfiguration,
205                                              path);
206                 }
207                 else
208                 {
209                     addProperty(key, value.get<uint64_t>(), iface.get(),
210                                 systemConfiguration, path,
211                                 sdbusplus::asio::PropertyPermission::readOnly);
212                 }
213                 break;
214             }
215             case (nlohmann::json::value_t::number_float):
216             {
217                 if (array)
218                 {
219                     addArrayToDbus<double>(key, value, iface.get(), permission,
220                                            systemConfiguration, path);
221                 }
222 
223                 else
224                 {
225                     addProperty(key, value.get<double>(), iface.get(),
226                                 systemConfiguration, path, permission);
227                 }
228                 break;
229             }
230             case (nlohmann::json::value_t::string):
231             {
232                 if (array)
233                 {
234                     addArrayToDbus<std::string>(key, value, iface.get(),
235                                                 permission, systemConfiguration,
236                                                 path);
237                 }
238                 else
239                 {
240                     addProperty(key, value.get<std::string>(), iface.get(),
241                                 systemConfiguration, path, permission);
242                 }
243                 break;
244             }
245             default:
246             {
247                 std::cerr << "Unexpected json type in system configuration "
248                           << key << ": " << value.type_name() << "\n";
249                 break;
250             }
251         }
252     }
253     if (permission == sdbusplus::asio::PropertyPermission::readWrite)
254     {
255         createDeleteObjectMethod(jsonPointerPath, iface, objServer,
256                                  systemConfiguration);
257     }
258     tryIfaceInitialize(iface);
259 }
260 
261 void createAddObjectMethod(
262     const std::string& jsonPointerPath, const std::string& path,
263     nlohmann::json& systemConfiguration,
264     sdbusplus::asio::object_server& objServer, const std::string& board)
265 {
266     std::shared_ptr<sdbusplus::asio::dbus_interface> iface = createInterface(
267         objServer, path, "xyz.openbmc_project.AddObject", board);
268 
269     iface->register_method(
270         "AddObject",
271         [&systemConfiguration, &objServer,
272          jsonPointerPath{std::string(jsonPointerPath)}, path{std::string(path)},
273          board](const boost::container::flat_map<std::string, JsonVariantType>&
274                     data) {
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             auto findName = newData.find("Name");
297             auto findType = newData.find("Type");
298             if (findName == newData.end() || findType == newData.end())
299             {
300                 throw std::invalid_argument("AddObject missing Name or Type");
301             }
302             const std::string* type = findType->get_ptr<const std::string*>();
303             const std::string* name = findName->get_ptr<const std::string*>();
304             if (type == nullptr || name == nullptr)
305             {
306                 throw std::invalid_argument("Type and Name must be a string.");
307             }
308 
309             bool foundNull = false;
310             size_t lastIndex = 0;
311             // we add in the "exposes"
312             for (const auto& expose : *findExposes)
313             {
314                 if (expose.is_null())
315                 {
316                     foundNull = true;
317                     continue;
318                 }
319 
320                 if (expose["Name"] == *name && expose["Type"] == *type)
321                 {
322                     throw std::invalid_argument(
323                         "Field already in JSON, not adding");
324                 }
325 
326                 if (foundNull)
327                 {
328                     continue;
329                 }
330 
331                 lastIndex++;
332             }
333 
334             std::ifstream schemaFile(
335                 std::string(configuration::schemaDirectory) + "/" +
336                 boost::to_lower_copy(*type) + ".json");
337             // todo(james) we might want to also make a list of 'can add'
338             // interfaces but for now I think the assumption if there is a
339             // schema avaliable that it is allowed to update is fine
340             if (!schemaFile.good())
341             {
342                 throw std::invalid_argument(
343                     "No schema avaliable, cannot validate.");
344             }
345             nlohmann::json schema =
346                 nlohmann::json::parse(schemaFile, nullptr, false, true);
347             if (schema.is_discarded())
348             {
349                 std::cerr << "Schema not legal" << *type << ".json\n";
350                 throw DBusInternalError();
351             }
352             if (!configuration::validateJson(schema, newData))
353             {
354                 throw std::invalid_argument("Data does not match schema");
355             }
356             if (foundNull)
357             {
358                 findExposes->at(lastIndex) = newData;
359             }
360             else
361             {
362                 findExposes->push_back(newData);
363             }
364             if (!configuration::writeJsonFiles(systemConfiguration))
365             {
366                 std::cerr << "Error writing json files\n";
367                 throw DBusInternalError();
368             }
369             std::string dbusName = *name;
370 
371             std::regex_replace(dbusName.begin(), dbusName.begin(),
372                                dbusName.end(), illegalDbusMemberRegex, "_");
373 
374             std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
375                 createInterface(objServer, path + "/" + dbusName,
376                                 "xyz.openbmc_project.Configuration." + *type,
377                                 board, true);
378             // permission is read-write, as since we just created it, must be
379             // runtime modifiable
380             populateInterfaceFromJson(
381                 systemConfiguration,
382                 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
383                 interface, newData, objServer,
384                 sdbusplus::asio::PropertyPermission::readWrite);
385         });
386     tryIfaceInitialize(iface);
387 }
388 
389 std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
390     getDeviceInterfaces(const nlohmann::json& device)
391 {
392     return inventory[device["Name"].get<std::string>()];
393 }
394 
395 } // namespace dbus_interface
396