blob: ff8094ed0c640b2021f3d492b1af1246fbc30f25 [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,
Alexander Hansena555acf2025-06-27 11:59:10 +020075 nlohmann::json& systemConfiguration, boost::asio::io_context& io)
Christopher Meis12bea9b2025-04-03 10:14:42 +020076{
77 std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface;
78 iface->register_method(
79 "Delete", [&objServer, &systemConfiguration, interface,
Alexander Hansena555acf2025-06-27 11:59:10 +020080 jsonPointerPath{std::string(jsonPointerPath)}, &io]() {
Christopher Meis12bea9b2025-04-03 10:14:42 +020081 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 Hansen91e360e2025-06-10 12:21:30 +0200125static nlohmann::json::value_t getDBusType(
126 const nlohmann::json& value, nlohmann::json::value_t type,
Alexander Hansen17f8e6a2025-06-10 12:10:09 +0200127 sdbusplus::asio::PropertyPermission permission)
128{
129 const bool array = value.type() == nlohmann::json::value_t::array;
130
131 if (permission == sdbusplus::asio::PropertyPermission::readWrite)
132 {
133 // all setable numbers are doubles as it is difficult to always
134 // create a configuration file with all whole numbers as decimals
135 // i.e. 1.0
136 if (array)
137 {
138 if (value[0].is_number())
139 {
Alexander Hansen91e360e2025-06-10 12:21:30 +0200140 return nlohmann::json::value_t::number_float;
Alexander Hansen17f8e6a2025-06-10 12:10:09 +0200141 }
142 }
143 else if (value.is_number())
144 {
Alexander Hansen91e360e2025-06-10 12:21:30 +0200145 return nlohmann::json::value_t::number_float;
Alexander Hansen17f8e6a2025-06-10 12:10:09 +0200146 }
147 }
148
Alexander Hansen91e360e2025-06-10 12:21:30 +0200149 return type;
150}
151
152static void populateInterfacePropertyFromJson(
153 nlohmann::json& systemConfiguration, const std::string& path,
154 const nlohmann::json& key, const nlohmann::json& value,
155 nlohmann::json::value_t type,
156 std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
157 sdbusplus::asio::PropertyPermission permission)
158{
159 const auto modifiedType = getDBusType(value, type, permission);
160
161 switch (modifiedType)
Alexander Hansen17f8e6a2025-06-10 12:10:09 +0200162 {
163 case (nlohmann::json::value_t::boolean):
164 {
Alexander Hansen0123f8a2025-06-27 12:11:08 +0200165 // todo: array of bool isn't detected correctly by
166 // sdbusplus, change it to numbers
167 addValueToDBus<uint64_t, bool>(key, value, *iface, permission,
168 systemConfiguration, path);
Alexander Hansen17f8e6a2025-06-10 12:10:09 +0200169 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(
Alexander Hansena555acf2025-06-27 11:59:10 +0200206 boost::asio::io_context& io, nlohmann::json& systemConfiguration,
207 const std::string& jsonPointerPath,
Christopher Meis12bea9b2025-04-03 10:14:42 +0200208 std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
209 nlohmann::json& dict, sdbusplus::asio::object_server& objServer,
210 sdbusplus::asio::PropertyPermission permission)
211{
212 for (const auto& [key, value] : dict.items())
213 {
214 auto type = value.type();
Christopher Meis12bea9b2025-04-03 10:14:42 +0200215 if (value.type() == nlohmann::json::value_t::array)
216 {
Christopher Meis12bea9b2025-04-03 10:14:42 +0200217 if (value.empty())
218 {
219 continue;
220 }
221 type = value[0].type();
Alexander Hansend72dc332025-06-10 10:49:24 +0200222 if (!checkArrayElementsSameType(value))
Christopher Meis12bea9b2025-04-03 10:14:42 +0200223 {
224 std::cerr << "dbus format error" << value << "\n";
225 continue;
226 }
227 }
228 if (type == nlohmann::json::value_t::object)
229 {
230 continue; // handled elsewhere
231 }
232
233 std::string path = jsonPointerPath;
234 path.append("/").append(key);
Christopher Meis12bea9b2025-04-03 10:14:42 +0200235
Alexander Hansen17f8e6a2025-06-10 12:10:09 +0200236 populateInterfacePropertyFromJson(systemConfiguration, path, key, value,
237 type, iface, permission);
Christopher Meis12bea9b2025-04-03 10:14:42 +0200238 }
239 if (permission == sdbusplus::asio::PropertyPermission::readWrite)
240 {
241 createDeleteObjectMethod(jsonPointerPath, iface, objServer,
Alexander Hansena555acf2025-06-27 11:59:10 +0200242 systemConfiguration, io);
Christopher Meis12bea9b2025-04-03 10:14:42 +0200243 }
244 tryIfaceInitialize(iface);
245}
246
247void createAddObjectMethod(
Alexander Hansena555acf2025-06-27 11:59:10 +0200248 boost::asio::io_context& io, const std::string& jsonPointerPath,
249 const std::string& path, nlohmann::json& systemConfiguration,
Christopher Meis12bea9b2025-04-03 10:14:42 +0200250 sdbusplus::asio::object_server& objServer, const std::string& board)
251{
252 std::shared_ptr<sdbusplus::asio::dbus_interface> iface = createInterface(
253 objServer, path, "xyz.openbmc_project.AddObject", board);
254
255 iface->register_method(
256 "AddObject",
257 [&systemConfiguration, &objServer,
258 jsonPointerPath{std::string(jsonPointerPath)}, path{std::string(path)},
Alexander Hansena555acf2025-06-27 11:59:10 +0200259 board,
260 &io](const boost::container::flat_map<std::string, JsonVariantType>&
261 data) {
Christopher Meis12bea9b2025-04-03 10:14:42 +0200262 nlohmann::json::json_pointer ptr(jsonPointerPath);
263 nlohmann::json& base = systemConfiguration[ptr];
264 auto findExposes = base.find("Exposes");
265
266 if (findExposes == base.end())
267 {
268 throw std::invalid_argument("Entity must have children.");
269 }
270
271 // this will throw invalid-argument to sdbusplus if invalid json
272 nlohmann::json newData{};
273 for (const auto& item : data)
274 {
275 nlohmann::json& newJson = newData[item.first];
276 std::visit(
277 [&newJson](auto&& val) {
278 newJson = std::forward<decltype(val)>(val);
279 },
280 item.second);
281 }
282
283 auto findName = newData.find("Name");
284 auto findType = newData.find("Type");
285 if (findName == newData.end() || findType == newData.end())
286 {
287 throw std::invalid_argument("AddObject missing Name or Type");
288 }
289 const std::string* type = findType->get_ptr<const std::string*>();
290 const std::string* name = findName->get_ptr<const std::string*>();
291 if (type == nullptr || name == nullptr)
292 {
293 throw std::invalid_argument("Type and Name must be a string.");
294 }
295
296 bool foundNull = false;
297 size_t lastIndex = 0;
298 // we add in the "exposes"
299 for (const auto& expose : *findExposes)
300 {
301 if (expose.is_null())
302 {
303 foundNull = true;
304 continue;
305 }
306
307 if (expose["Name"] == *name && expose["Type"] == *type)
308 {
309 throw std::invalid_argument(
310 "Field already in JSON, not adding");
311 }
312
313 if (foundNull)
314 {
315 continue;
316 }
317
318 lastIndex++;
319 }
320
321 std::ifstream schemaFile(
322 std::string(configuration::schemaDirectory) + "/" +
323 boost::to_lower_copy(*type) + ".json");
324 // todo(james) we might want to also make a list of 'can add'
325 // interfaces but for now I think the assumption if there is a
326 // schema avaliable that it is allowed to update is fine
327 if (!schemaFile.good())
328 {
329 throw std::invalid_argument(
330 "No schema avaliable, cannot validate.");
331 }
332 nlohmann::json schema =
333 nlohmann::json::parse(schemaFile, nullptr, false, true);
334 if (schema.is_discarded())
335 {
336 std::cerr << "Schema not legal" << *type << ".json\n";
337 throw DBusInternalError();
338 }
339 if (!configuration::validateJson(schema, newData))
340 {
341 throw std::invalid_argument("Data does not match schema");
342 }
343 if (foundNull)
344 {
345 findExposes->at(lastIndex) = newData;
346 }
347 else
348 {
349 findExposes->push_back(newData);
350 }
351 if (!configuration::writeJsonFiles(systemConfiguration))
352 {
353 std::cerr << "Error writing json files\n";
354 throw DBusInternalError();
355 }
356 std::string dbusName = *name;
357
358 std::regex_replace(dbusName.begin(), dbusName.begin(),
359 dbusName.end(), illegalDbusMemberRegex, "_");
360
361 std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
362 createInterface(objServer, path + "/" + dbusName,
363 "xyz.openbmc_project.Configuration." + *type,
364 board, true);
365 // permission is read-write, as since we just created it, must be
366 // runtime modifiable
367 populateInterfaceFromJson(
Alexander Hansena555acf2025-06-27 11:59:10 +0200368 io, systemConfiguration,
Christopher Meis12bea9b2025-04-03 10:14:42 +0200369 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
370 interface, newData, objServer,
371 sdbusplus::asio::PropertyPermission::readWrite);
372 });
373 tryIfaceInitialize(iface);
374}
375
376std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
377 getDeviceInterfaces(const nlohmann::json& device)
378{
379 return inventory[device["Name"].get<std::string>()];
380}
381
382} // namespace dbus_interface