blob: 8c975bbd47637830601276d6db5c5c60fb8559f9 [file] [log] [blame]
Christopher Meis12bea9b2025-04-03 10:14:42 +02001#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 <regex>
10#include <string>
11#include <vector>
12
13using 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
18namespace dbus_interface
19{
20
21const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
22const 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
26boost::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
31void 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
46std::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
70void 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
106void 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
261void 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
389std::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