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