blob: 5c1788be968123c9fae892bf6fd4a7c3cf8b1ace [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
106// adds simple json types to interface's properties
107void populateInterfaceFromJson(
108 nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
109 std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
110 nlohmann::json& dict, sdbusplus::asio::object_server& objServer,
111 sdbusplus::asio::PropertyPermission permission)
112{
113 for (const auto& [key, value] : dict.items())
114 {
115 auto type = value.type();
116 bool array = false;
117 if (value.type() == nlohmann::json::value_t::array)
118 {
119 array = true;
120 if (value.empty())
121 {
122 continue;
123 }
124 type = value[0].type();
125 bool isLegal = true;
126 for (const auto& arrayItem : value)
127 {
128 if (arrayItem.type() != type)
129 {
130 isLegal = false;
131 break;
132 }
133 }
134 if (!isLegal)
135 {
136 std::cerr << "dbus format error" << value << "\n";
137 continue;
138 }
139 }
140 if (type == nlohmann::json::value_t::object)
141 {
142 continue; // handled elsewhere
143 }
144
145 std::string path = jsonPointerPath;
146 path.append("/").append(key);
147 if (permission == sdbusplus::asio::PropertyPermission::readWrite)
148 {
149 // all setable numbers are doubles as it is difficult to always
150 // create a configuration file with all whole numbers as decimals
151 // i.e. 1.0
152 if (array)
153 {
154 if (value[0].is_number())
155 {
156 type = nlohmann::json::value_t::number_float;
157 }
158 }
159 else if (value.is_number())
160 {
161 type = nlohmann::json::value_t::number_float;
162 }
163 }
164
165 switch (type)
166 {
167 case (nlohmann::json::value_t::boolean):
168 {
169 if (array)
170 {
171 // todo: array of bool isn't detected correctly by
172 // sdbusplus, change it to numbers
173 addArrayToDbus<uint64_t>(key, value, iface.get(),
174 permission, systemConfiguration,
175 path);
176 }
177
178 else
179 {
180 addProperty(key, value.get<bool>(), iface.get(),
181 systemConfiguration, path, permission);
182 }
183 break;
184 }
185 case (nlohmann::json::value_t::number_integer):
186 {
187 if (array)
188 {
189 addArrayToDbus<int64_t>(key, value, iface.get(), permission,
190 systemConfiguration, path);
191 }
192 else
193 {
194 addProperty(key, value.get<int64_t>(), iface.get(),
195 systemConfiguration, path,
196 sdbusplus::asio::PropertyPermission::readOnly);
197 }
198 break;
199 }
200 case (nlohmann::json::value_t::number_unsigned):
201 {
202 if (array)
203 {
204 addArrayToDbus<uint64_t>(key, value, iface.get(),
205 permission, systemConfiguration,
206 path);
207 }
208 else
209 {
210 addProperty(key, value.get<uint64_t>(), iface.get(),
211 systemConfiguration, path,
212 sdbusplus::asio::PropertyPermission::readOnly);
213 }
214 break;
215 }
216 case (nlohmann::json::value_t::number_float):
217 {
218 if (array)
219 {
220 addArrayToDbus<double>(key, value, iface.get(), permission,
221 systemConfiguration, path);
222 }
223
224 else
225 {
226 addProperty(key, value.get<double>(), iface.get(),
227 systemConfiguration, path, permission);
228 }
229 break;
230 }
231 case (nlohmann::json::value_t::string):
232 {
233 if (array)
234 {
235 addArrayToDbus<std::string>(key, value, iface.get(),
236 permission, systemConfiguration,
237 path);
238 }
239 else
240 {
241 addProperty(key, value.get<std::string>(), iface.get(),
242 systemConfiguration, path, permission);
243 }
244 break;
245 }
246 default:
247 {
248 std::cerr << "Unexpected json type in system configuration "
249 << key << ": " << value.type_name() << "\n";
250 break;
251 }
252 }
253 }
254 if (permission == sdbusplus::asio::PropertyPermission::readWrite)
255 {
256 createDeleteObjectMethod(jsonPointerPath, iface, objServer,
257 systemConfiguration);
258 }
259 tryIfaceInitialize(iface);
260}
261
262void createAddObjectMethod(
263 const std::string& jsonPointerPath, const std::string& path,
264 nlohmann::json& systemConfiguration,
265 sdbusplus::asio::object_server& objServer, const std::string& board)
266{
267 std::shared_ptr<sdbusplus::asio::dbus_interface> iface = createInterface(
268 objServer, path, "xyz.openbmc_project.AddObject", board);
269
270 iface->register_method(
271 "AddObject",
272 [&systemConfiguration, &objServer,
273 jsonPointerPath{std::string(jsonPointerPath)}, path{std::string(path)},
274 board](const boost::container::flat_map<std::string, JsonVariantType>&
275 data) {
276 nlohmann::json::json_pointer ptr(jsonPointerPath);
277 nlohmann::json& base = systemConfiguration[ptr];
278 auto findExposes = base.find("Exposes");
279
280 if (findExposes == base.end())
281 {
282 throw std::invalid_argument("Entity must have children.");
283 }
284
285 // this will throw invalid-argument to sdbusplus if invalid json
286 nlohmann::json newData{};
287 for (const auto& item : data)
288 {
289 nlohmann::json& newJson = newData[item.first];
290 std::visit(
291 [&newJson](auto&& val) {
292 newJson = std::forward<decltype(val)>(val);
293 },
294 item.second);
295 }
296
297 auto findName = newData.find("Name");
298 auto findType = newData.find("Type");
299 if (findName == newData.end() || findType == newData.end())
300 {
301 throw std::invalid_argument("AddObject missing Name or Type");
302 }
303 const std::string* type = findType->get_ptr<const std::string*>();
304 const std::string* name = findName->get_ptr<const std::string*>();
305 if (type == nullptr || name == nullptr)
306 {
307 throw std::invalid_argument("Type and Name must be a string.");
308 }
309
310 bool foundNull = false;
311 size_t lastIndex = 0;
312 // we add in the "exposes"
313 for (const auto& expose : *findExposes)
314 {
315 if (expose.is_null())
316 {
317 foundNull = true;
318 continue;
319 }
320
321 if (expose["Name"] == *name && expose["Type"] == *type)
322 {
323 throw std::invalid_argument(
324 "Field already in JSON, not adding");
325 }
326
327 if (foundNull)
328 {
329 continue;
330 }
331
332 lastIndex++;
333 }
334
335 std::ifstream schemaFile(
336 std::string(configuration::schemaDirectory) + "/" +
337 boost::to_lower_copy(*type) + ".json");
338 // todo(james) we might want to also make a list of 'can add'
339 // interfaces but for now I think the assumption if there is a
340 // schema avaliable that it is allowed to update is fine
341 if (!schemaFile.good())
342 {
343 throw std::invalid_argument(
344 "No schema avaliable, cannot validate.");
345 }
346 nlohmann::json schema =
347 nlohmann::json::parse(schemaFile, nullptr, false, true);
348 if (schema.is_discarded())
349 {
350 std::cerr << "Schema not legal" << *type << ".json\n";
351 throw DBusInternalError();
352 }
353 if (!configuration::validateJson(schema, newData))
354 {
355 throw std::invalid_argument("Data does not match schema");
356 }
357 if (foundNull)
358 {
359 findExposes->at(lastIndex) = newData;
360 }
361 else
362 {
363 findExposes->push_back(newData);
364 }
365 if (!configuration::writeJsonFiles(systemConfiguration))
366 {
367 std::cerr << "Error writing json files\n";
368 throw DBusInternalError();
369 }
370 std::string dbusName = *name;
371
372 std::regex_replace(dbusName.begin(), dbusName.begin(),
373 dbusName.end(), illegalDbusMemberRegex, "_");
374
375 std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
376 createInterface(objServer, path + "/" + dbusName,
377 "xyz.openbmc_project.Configuration." + *type,
378 board, true);
379 // permission is read-write, as since we just created it, must be
380 // runtime modifiable
381 populateInterfaceFromJson(
382 systemConfiguration,
383 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
384 interface, newData, objServer,
385 sdbusplus::asio::PropertyPermission::readWrite);
386 });
387 tryIfaceInitialize(iface);
388}
389
390std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
391 getDeviceInterfaces(const nlohmann::json& device)
392{
393 return inventory[device["Name"].get<std::string>()];
394}
395
396} // namespace dbus_interface