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