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