blob: 45e4f6d00b422cf9c1a393b91a71d5ab3f54a841 [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 {
Alexander Hansend7908692025-06-10 11:14:20 +0200197 addValueToDBus<int64_t>(key, value, *iface, permission,
198 systemConfiguration, path);
Christopher Meis12bea9b2025-04-03 10:14:42 +0200199 break;
200 }
201 case (nlohmann::json::value_t::number_unsigned):
202 {
Alexander Hansend7908692025-06-10 11:14:20 +0200203 addValueToDBus<uint64_t>(key, value, *iface, permission,
204 systemConfiguration, path);
Christopher Meis12bea9b2025-04-03 10:14:42 +0200205 break;
206 }
207 case (nlohmann::json::value_t::number_float):
208 {
Alexander Hansend7908692025-06-10 11:14:20 +0200209 addValueToDBus<double>(key, value, *iface, permission,
210 systemConfiguration, path);
Christopher Meis12bea9b2025-04-03 10:14:42 +0200211 break;
212 }
213 case (nlohmann::json::value_t::string):
214 {
Alexander Hansend7908692025-06-10 11:14:20 +0200215 addValueToDBus<std::string>(key, value, *iface, permission,
216 systemConfiguration, path);
Christopher Meis12bea9b2025-04-03 10:14:42 +0200217 break;
218 }
219 default:
220 {
221 std::cerr << "Unexpected json type in system configuration "
222 << key << ": " << value.type_name() << "\n";
223 break;
224 }
225 }
226 }
227 if (permission == sdbusplus::asio::PropertyPermission::readWrite)
228 {
229 createDeleteObjectMethod(jsonPointerPath, iface, objServer,
230 systemConfiguration);
231 }
232 tryIfaceInitialize(iface);
233}
234
235void createAddObjectMethod(
236 const std::string& jsonPointerPath, const std::string& path,
237 nlohmann::json& systemConfiguration,
238 sdbusplus::asio::object_server& objServer, const std::string& board)
239{
240 std::shared_ptr<sdbusplus::asio::dbus_interface> iface = createInterface(
241 objServer, path, "xyz.openbmc_project.AddObject", board);
242
243 iface->register_method(
244 "AddObject",
245 [&systemConfiguration, &objServer,
246 jsonPointerPath{std::string(jsonPointerPath)}, path{std::string(path)},
247 board](const boost::container::flat_map<std::string, JsonVariantType>&
248 data) {
249 nlohmann::json::json_pointer ptr(jsonPointerPath);
250 nlohmann::json& base = systemConfiguration[ptr];
251 auto findExposes = base.find("Exposes");
252
253 if (findExposes == base.end())
254 {
255 throw std::invalid_argument("Entity must have children.");
256 }
257
258 // this will throw invalid-argument to sdbusplus if invalid json
259 nlohmann::json newData{};
260 for (const auto& item : data)
261 {
262 nlohmann::json& newJson = newData[item.first];
263 std::visit(
264 [&newJson](auto&& val) {
265 newJson = std::forward<decltype(val)>(val);
266 },
267 item.second);
268 }
269
270 auto findName = newData.find("Name");
271 auto findType = newData.find("Type");
272 if (findName == newData.end() || findType == newData.end())
273 {
274 throw std::invalid_argument("AddObject missing Name or Type");
275 }
276 const std::string* type = findType->get_ptr<const std::string*>();
277 const std::string* name = findName->get_ptr<const std::string*>();
278 if (type == nullptr || name == nullptr)
279 {
280 throw std::invalid_argument("Type and Name must be a string.");
281 }
282
283 bool foundNull = false;
284 size_t lastIndex = 0;
285 // we add in the "exposes"
286 for (const auto& expose : *findExposes)
287 {
288 if (expose.is_null())
289 {
290 foundNull = true;
291 continue;
292 }
293
294 if (expose["Name"] == *name && expose["Type"] == *type)
295 {
296 throw std::invalid_argument(
297 "Field already in JSON, not adding");
298 }
299
300 if (foundNull)
301 {
302 continue;
303 }
304
305 lastIndex++;
306 }
307
308 std::ifstream schemaFile(
309 std::string(configuration::schemaDirectory) + "/" +
310 boost::to_lower_copy(*type) + ".json");
311 // todo(james) we might want to also make a list of 'can add'
312 // interfaces but for now I think the assumption if there is a
313 // schema avaliable that it is allowed to update is fine
314 if (!schemaFile.good())
315 {
316 throw std::invalid_argument(
317 "No schema avaliable, cannot validate.");
318 }
319 nlohmann::json schema =
320 nlohmann::json::parse(schemaFile, nullptr, false, true);
321 if (schema.is_discarded())
322 {
323 std::cerr << "Schema not legal" << *type << ".json\n";
324 throw DBusInternalError();
325 }
326 if (!configuration::validateJson(schema, newData))
327 {
328 throw std::invalid_argument("Data does not match schema");
329 }
330 if (foundNull)
331 {
332 findExposes->at(lastIndex) = newData;
333 }
334 else
335 {
336 findExposes->push_back(newData);
337 }
338 if (!configuration::writeJsonFiles(systemConfiguration))
339 {
340 std::cerr << "Error writing json files\n";
341 throw DBusInternalError();
342 }
343 std::string dbusName = *name;
344
345 std::regex_replace(dbusName.begin(), dbusName.begin(),
346 dbusName.end(), illegalDbusMemberRegex, "_");
347
348 std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
349 createInterface(objServer, path + "/" + dbusName,
350 "xyz.openbmc_project.Configuration." + *type,
351 board, true);
352 // permission is read-write, as since we just created it, must be
353 // runtime modifiable
354 populateInterfaceFromJson(
355 systemConfiguration,
356 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
357 interface, newData, objServer,
358 sdbusplus::asio::PropertyPermission::readWrite);
359 });
360 tryIfaceInitialize(iface);
361}
362
363std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
364 getDeviceInterfaces(const nlohmann::json& device)
365{
366 return inventory[device["Name"].get<std::string>()];
367}
368
369} // namespace dbus_interface