xref: /openbmc/entity-manager/src/entity_manager/dbus_interface.cpp (revision 4e1142d6f418f48ea260132ebb5a4995b2310c90)
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 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         std::cerr << "Unable to initialize dbus interface : " << e.what()
34                   << "\n"
35                   << "object Path : " << iface->get_object_path() << "\n"
36                   << "interface name : " << iface->get_interface_name() << "\n";
37     }
38 }
39 
40 std::shared_ptr<sdbusplus::asio::dbus_interface>
41     EMDBusInterface::createInterface(
42         sdbusplus::asio::object_server& objServer, const std::string& path,
43         const std::string& interface, 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 createDeleteObjectMethod(
66     const std::string& jsonPointerPath,
67     const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
68     sdbusplus::asio::object_server& objServer,
69     nlohmann::json& systemConfiguration, boost::asio::io_context& io)
70 {
71     std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface;
72     iface->register_method(
73         "Delete", [&objServer, &systemConfiguration, interface,
74                    jsonPointerPath{std::string(jsonPointerPath)}, &io]() {
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, [&objServer, dbusInterface]() mutable {
89                 objServer.remove_interface(dbusInterface);
90             });
91 
92             if (!writeJsonFiles(systemConfiguration))
93             {
94                 std::cerr << "error setting json file\n";
95                 throw DBusInternalError();
96             }
97         });
98 }
99 
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 
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 
146 static void populateInterfacePropertyFromJson(
147     nlohmann::json& systemConfiguration, const std::string& path,
148     const nlohmann::json& 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             // todo: array of bool isn't detected correctly by
160             // sdbusplus, change it to numbers
161             addValueToDBus<uint64_t, bool>(key, value, *iface, permission,
162                                            systemConfiguration, path);
163             break;
164         }
165         case (nlohmann::json::value_t::number_integer):
166         {
167             addValueToDBus<int64_t>(key, value, *iface, permission,
168                                     systemConfiguration, path);
169             break;
170         }
171         case (nlohmann::json::value_t::number_unsigned):
172         {
173             addValueToDBus<uint64_t>(key, value, *iface, permission,
174                                      systemConfiguration, path);
175             break;
176         }
177         case (nlohmann::json::value_t::number_float):
178         {
179             addValueToDBus<double>(key, value, *iface, permission,
180                                    systemConfiguration, path);
181             break;
182         }
183         case (nlohmann::json::value_t::string):
184         {
185             addValueToDBus<std::string>(key, value, *iface, permission,
186                                         systemConfiguration, path);
187             break;
188         }
189         default:
190         {
191             std::cerr << "Unexpected json type in system configuration " << key
192                       << ": " << value.type_name() << "\n";
193             break;
194         }
195     }
196 }
197 
198 // adds simple json types to interface's properties
199 void populateInterfaceFromJson(
200     boost::asio::io_context& io, nlohmann::json& systemConfiguration,
201     const std::string& jsonPointerPath,
202     std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
203     nlohmann::json& dict, sdbusplus::asio::object_server& objServer,
204     sdbusplus::asio::PropertyPermission permission)
205 {
206     for (const auto& [key, value] : dict.items())
207     {
208         auto type = value.type();
209         if (value.type() == nlohmann::json::value_t::array)
210         {
211             if (value.empty())
212             {
213                 continue;
214             }
215             type = value[0].type();
216             if (!checkArrayElementsSameType(value))
217             {
218                 std::cerr << "dbus format error" << value << "\n";
219                 continue;
220             }
221         }
222         if (type == nlohmann::json::value_t::object)
223         {
224             continue; // handled elsewhere
225         }
226 
227         std::string path = jsonPointerPath;
228         path.append("/").append(key);
229 
230         populateInterfacePropertyFromJson(systemConfiguration, path, key, value,
231                                           type, iface, permission);
232     }
233     if (permission == sdbusplus::asio::PropertyPermission::readWrite)
234     {
235         createDeleteObjectMethod(jsonPointerPath, iface, objServer,
236                                  systemConfiguration, io);
237     }
238     tryIfaceInitialize(iface);
239 }
240 
241 void EMDBusInterface::createAddObjectMethod(
242     boost::asio::io_context& io, const std::string& jsonPointerPath,
243     const std::string& path, nlohmann::json& systemConfiguration,
244     sdbusplus::asio::object_server& objServer, const std::string& board)
245 {
246     std::shared_ptr<sdbusplus::asio::dbus_interface> iface = createInterface(
247         objServer, path, "xyz.openbmc_project.AddObject", board);
248 
249     iface->register_method(
250         "AddObject",
251         [&systemConfiguration, &objServer,
252          jsonPointerPath{std::string(jsonPointerPath)}, path{std::string(path)},
253          board, &io,
254          this](const boost::container::flat_map<std::string, JsonVariantType>&
255                    data) {
256             nlohmann::json::json_pointer ptr(jsonPointerPath);
257             nlohmann::json& base = systemConfiguration[ptr];
258             auto findExposes = base.find("Exposes");
259 
260             if (findExposes == base.end())
261             {
262                 throw std::invalid_argument("Entity must have children.");
263             }
264 
265             // this will throw invalid-argument to sdbusplus if invalid json
266             nlohmann::json newData{};
267             for (const auto& item : data)
268             {
269                 nlohmann::json& newJson = newData[item.first];
270                 std::visit(
271                     [&newJson](auto&& val) {
272                         newJson = std::forward<decltype(val)>(val);
273                     },
274                     item.second);
275             }
276 
277             auto findName = newData.find("Name");
278             auto findType = newData.find("Type");
279             if (findName == newData.end() || findType == newData.end())
280             {
281                 throw std::invalid_argument("AddObject missing Name or Type");
282             }
283             const std::string* type = findType->get_ptr<const std::string*>();
284             const std::string* name = findName->get_ptr<const std::string*>();
285             if (type == nullptr || name == nullptr)
286             {
287                 throw std::invalid_argument("Type and Name must be a string.");
288             }
289 
290             bool foundNull = false;
291             size_t lastIndex = 0;
292             // we add in the "exposes"
293             for (const auto& expose : *findExposes)
294             {
295                 if (expose.is_null())
296                 {
297                     foundNull = true;
298                     continue;
299                 }
300 
301                 if (expose["Name"] == *name && expose["Type"] == *type)
302                 {
303                     throw std::invalid_argument(
304                         "Field already in JSON, not adding");
305                 }
306 
307                 if (foundNull)
308                 {
309                     continue;
310                 }
311 
312                 lastIndex++;
313             }
314 
315             std::ifstream schemaFile(std::string(schemaDirectory) + "/" +
316                                      boost::to_lower_copy(*type) + ".json");
317             // todo(james) we might want to also make a list of 'can add'
318             // interfaces but for now I think the assumption if there is a
319             // schema avaliable that it is allowed to update is fine
320             if (!schemaFile.good())
321             {
322                 throw std::invalid_argument(
323                     "No schema avaliable, cannot validate.");
324             }
325             nlohmann::json schema =
326                 nlohmann::json::parse(schemaFile, nullptr, false, true);
327             if (schema.is_discarded())
328             {
329                 std::cerr << "Schema not legal" << *type << ".json\n";
330                 throw DBusInternalError();
331             }
332             if (!validateJson(schema, newData))
333             {
334                 throw std::invalid_argument("Data does not match schema");
335             }
336             if (foundNull)
337             {
338                 findExposes->at(lastIndex) = newData;
339             }
340             else
341             {
342                 findExposes->push_back(newData);
343             }
344             if (!writeJsonFiles(systemConfiguration))
345             {
346                 std::cerr << "Error writing json files\n";
347                 throw DBusInternalError();
348             }
349             std::string dbusName = *name;
350 
351             std::regex_replace(dbusName.begin(), dbusName.begin(),
352                                dbusName.end(), illegalDbusMemberRegex, "_");
353 
354             std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
355                 createInterface(objServer, path + "/" + dbusName,
356                                 "xyz.openbmc_project.Configuration." + *type,
357                                 board, true);
358             // permission is read-write, as since we just created it, must be
359             // runtime modifiable
360             populateInterfaceFromJson(
361                 io, systemConfiguration,
362                 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
363                 interface, newData, objServer,
364                 sdbusplus::asio::PropertyPermission::readWrite);
365         });
366     tryIfaceInitialize(iface);
367 }
368 
369 std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
370     EMDBusInterface::getDeviceInterfaces(const nlohmann::json& device)
371 {
372     return inventory[device["Name"].get<std::string>()];
373 }
374 
375 } // namespace dbus_interface
376