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