xref: /openbmc/entity-manager/src/entity_manager/dbus_interface.cpp (revision 5531eeaccad761e9f7eee03b64da4bcd2eb1011d)
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 
EMDBusInterface(boost::asio::io_context & io,sdbusplus::asio::object_server & objServer)25 EMDBusInterface::EMDBusInterface(boost::asio::io_context& io,
26                                  sdbusplus::asio::object_server& objServer) :
27     io(io), objServer(objServer)
28 {}
29 
tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface> & iface)30 void tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface>& iface)
31 {
32     try
33     {
34         iface->initialize();
35     }
36     catch (std::exception& e)
37     {
38         std::cerr << "Unable to initialize dbus interface : " << e.what()
39                   << "\n"
40                   << "object Path : " << iface->get_object_path() << "\n"
41                   << "interface name : " << iface->get_interface_name() << "\n";
42     }
43 }
44 
45 std::shared_ptr<sdbusplus::asio::dbus_interface>
createInterface(const std::string & path,const std::string & interface,const std::string & parent,bool checkNull)46     EMDBusInterface::createInterface(const std::string& path,
47                                      const std::string& interface,
48                                      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 
createDeleteObjectMethod(const std::string & jsonPointerPath,const std::shared_ptr<sdbusplus::asio::dbus_interface> & iface,nlohmann::json & systemConfiguration)70 void EMDBusInterface::createDeleteObjectMethod(
71     const std::string& jsonPointerPath,
72     const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
73     nlohmann::json& systemConfiguration)
74 {
75     std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface;
76     iface->register_method(
77         "Delete", [this, &systemConfiguration, interface,
78                    jsonPointerPath{std::string(jsonPointerPath)}]() {
79             std::shared_ptr<sdbusplus::asio::dbus_interface> dbusInterface =
80                 interface.lock();
81             if (!dbusInterface)
82             {
83                 // this technically can't happen as the pointer is pointing to
84                 // us
85                 throw DBusInternalError();
86             }
87             nlohmann::json::json_pointer ptr(jsonPointerPath);
88             systemConfiguration[ptr] = nullptr;
89 
90             // todo(james): dig through sdbusplus to find out why we can't
91             // delete it in a method call
92             boost::asio::post(io, [dbusInterface, this]() mutable {
93                 objServer.remove_interface(dbusInterface);
94             });
95 
96             if (!writeJsonFiles(systemConfiguration))
97             {
98                 std::cerr << "error setting json file\n";
99                 throw DBusInternalError();
100             }
101         });
102 }
103 
checkArrayElementsSameType(nlohmann::json & value)104 static bool checkArrayElementsSameType(nlohmann::json& value)
105 {
106     nlohmann::json::array_t* arr = value.get_ptr<nlohmann::json::array_t*>();
107     if (arr == nullptr)
108     {
109         return false;
110     }
111 
112     if (arr->empty())
113     {
114         return true;
115     }
116 
117     nlohmann::json::value_t firstType = value[0].type();
118     return std::ranges::all_of(value, [firstType](const nlohmann::json& el) {
119         return el.type() == firstType;
120     });
121 }
122 
getDBusType(const nlohmann::json & value,nlohmann::json::value_t type,sdbusplus::asio::PropertyPermission permission)123 static nlohmann::json::value_t getDBusType(
124     const nlohmann::json& value, nlohmann::json::value_t type,
125     sdbusplus::asio::PropertyPermission permission)
126 {
127     const bool array = value.type() == nlohmann::json::value_t::array;
128 
129     if (permission == sdbusplus::asio::PropertyPermission::readWrite)
130     {
131         // all setable numbers are doubles as it is difficult to always
132         // create a configuration file with all whole numbers as decimals
133         // i.e. 1.0
134         if (array)
135         {
136             if (value[0].is_number())
137             {
138                 return nlohmann::json::value_t::number_float;
139             }
140         }
141         else if (value.is_number())
142         {
143             return nlohmann::json::value_t::number_float;
144         }
145     }
146 
147     return type;
148 }
149 
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)150 static void populateInterfacePropertyFromJson(
151     nlohmann::json& systemConfiguration, const std::string& path,
152     const nlohmann::json& key, const nlohmann::json& value,
153     nlohmann::json::value_t type,
154     std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
155     sdbusplus::asio::PropertyPermission permission)
156 {
157     const auto modifiedType = getDBusType(value, type, permission);
158 
159     switch (modifiedType)
160     {
161         case (nlohmann::json::value_t::boolean):
162         {
163             addValueToDBus<bool>(key, value, *iface, permission,
164                                  systemConfiguration, path);
165             break;
166         }
167         case (nlohmann::json::value_t::number_integer):
168         {
169             addValueToDBus<int64_t>(key, value, *iface, permission,
170                                     systemConfiguration, path);
171             break;
172         }
173         case (nlohmann::json::value_t::number_unsigned):
174         {
175             addValueToDBus<uint64_t>(key, value, *iface, permission,
176                                      systemConfiguration, path);
177             break;
178         }
179         case (nlohmann::json::value_t::number_float):
180         {
181             addValueToDBus<double>(key, value, *iface, permission,
182                                    systemConfiguration, path);
183             break;
184         }
185         case (nlohmann::json::value_t::string):
186         {
187             addValueToDBus<std::string>(key, value, *iface, permission,
188                                         systemConfiguration, path);
189             break;
190         }
191         default:
192         {
193             std::cerr << "Unexpected json type in system configuration " << key
194                       << ": " << value.type_name() << "\n";
195             break;
196         }
197     }
198 }
199 
200 // 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)201 void EMDBusInterface::populateInterfaceFromJson(
202     nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
203     std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
204     nlohmann::json& dict, 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, systemConfiguration);
236     }
237     tryIfaceInitialize(iface);
238 }
239 
createAddObjectMethod(const std::string & jsonPointerPath,const std::string & path,nlohmann::json & systemConfiguration,const std::string & board)240 void EMDBusInterface::createAddObjectMethod(
241     const std::string& jsonPointerPath, const std::string& path,
242     nlohmann::json& systemConfiguration, const std::string& board)
243 {
244     std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
245         createInterface(path, "xyz.openbmc_project.AddObject", board);
246 
247     iface->register_method(
248         "AddObject",
249         [&systemConfiguration, jsonPointerPath{std::string(jsonPointerPath)},
250          path{std::string(path)}, board,
251          this](const boost::container::flat_map<std::string, JsonVariantType>&
252                    data) {
253             nlohmann::json::json_pointer ptr(jsonPointerPath);
254             nlohmann::json& base = systemConfiguration[ptr];
255             auto findExposes = base.find("Exposes");
256 
257             if (findExposes == base.end())
258             {
259                 throw std::invalid_argument("Entity must have children.");
260             }
261 
262             // this will throw invalid-argument to sdbusplus if invalid json
263             nlohmann::json newData{};
264             for (const auto& item : data)
265             {
266                 nlohmann::json& newJson = newData[item.first];
267                 std::visit(
268                     [&newJson](auto&& val) {
269                         newJson = std::forward<decltype(val)>(val);
270                     },
271                     item.second);
272             }
273 
274             auto findName = newData.find("Name");
275             auto findType = newData.find("Type");
276             if (findName == newData.end() || findType == newData.end())
277             {
278                 throw std::invalid_argument("AddObject missing Name or Type");
279             }
280             const std::string* type = findType->get_ptr<const std::string*>();
281             const std::string* name = findName->get_ptr<const std::string*>();
282             if (type == nullptr || name == nullptr)
283             {
284                 throw std::invalid_argument("Type and Name must be a string.");
285             }
286 
287             bool foundNull = false;
288             size_t lastIndex = 0;
289             // we add in the "exposes"
290             for (const auto& expose : *findExposes)
291             {
292                 if (expose.is_null())
293                 {
294                     foundNull = true;
295                     continue;
296                 }
297 
298                 if (expose["Name"] == *name && expose["Type"] == *type)
299                 {
300                     throw std::invalid_argument(
301                         "Field already in JSON, not adding");
302                 }
303 
304                 if (foundNull)
305                 {
306                     continue;
307                 }
308 
309                 lastIndex++;
310             }
311 
312             if constexpr (ENABLE_RUNTIME_VALIDATE_JSON)
313             {
314                 const std::filesystem::path schemaPath =
315                     std::filesystem::path(schemaDirectory) /
316                     "exposes_record.json";
317 
318                 std::ifstream schemaFile{schemaPath};
319 
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 
333                 if (!validateJson(schema, newData))
334                 {
335                     throw std::invalid_argument("Data does not match schema");
336                 }
337             }
338 
339             if (foundNull)
340             {
341                 findExposes->at(lastIndex) = newData;
342             }
343             else
344             {
345                 findExposes->push_back(newData);
346             }
347             if (!writeJsonFiles(systemConfiguration))
348             {
349                 std::cerr << "Error writing json files\n";
350             }
351             std::string dbusName = *name;
352 
353             std::regex_replace(dbusName.begin(), dbusName.begin(),
354                                dbusName.end(), illegalDbusMemberRegex, "_");
355 
356             std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
357                 createInterface(path + "/" + dbusName,
358                                 "xyz.openbmc_project.Configuration." + *type,
359                                 board, true);
360             // permission is read-write, as since we just created it, must be
361             // runtime modifiable
362             populateInterfaceFromJson(
363                 systemConfiguration,
364                 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
365                 interface, newData,
366                 sdbusplus::asio::PropertyPermission::readWrite);
367         });
368     tryIfaceInitialize(iface);
369 }
370 
371 std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
getDeviceInterfaces(const nlohmann::json & device)372     EMDBusInterface::getDeviceInterfaces(const nlohmann::json& device)
373 {
374     return inventory[device["Name"].get<std::string>()];
375 }
376 
377 } // namespace dbus_interface
378