1 #include "dbus_interface.hpp"
2
3 #include "perform_probe.hpp"
4 #include "utils.hpp"
5
6 #include <phosphor-logging/lg2.hpp>
7
8 #include <flat_map>
9 #include <fstream>
10 #include <regex>
11 #include <string>
12 #include <vector>
13
14 namespace dbus_interface
15 {
16
17 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
18 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
19
EMDBusInterface(boost::asio::io_context & io,sdbusplus::asio::object_server & objServer)20 EMDBusInterface::EMDBusInterface(boost::asio::io_context& io,
21 sdbusplus::asio::object_server& objServer) :
22 io(io), objServer(objServer)
23 {}
24
tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface> & iface)25 void tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface>& iface)
26 {
27 try
28 {
29 iface->initialize();
30 }
31 catch (std::exception& e)
32 {
33 lg2::error(
34 "Unable to initialize dbus interface : {ERR} object Path : {PATH} interface name : {INTF}",
35 "ERR", e, "PATH", iface->get_object_path(), "INTF",
36 iface->get_interface_name());
37 }
38 }
39
40 std::shared_ptr<sdbusplus::asio::dbus_interface>
createInterface(const std::string & path,const std::string & interface,const std::string & parent,bool checkNull)41 EMDBusInterface::createInterface(const std::string& path,
42 const std::string& interface,
43 const std::string& parent, bool checkNull)
44 {
45 // on first add we have no reason to check for null before add, as there
46 // won't be any. For dynamically added interfaces, we check for null so that
47 // a constant delete/add will not create a memory leak
48
49 auto ptr = objServer.add_interface(path, interface);
50 auto& dataVector = inventory[parent];
51 if (checkNull)
52 {
53 auto it = std::find_if(dataVector.begin(), dataVector.end(),
54 [](const auto& p) { return p.expired(); });
55 if (it != dataVector.end())
56 {
57 *it = ptr;
58 return ptr;
59 }
60 }
61 dataVector.emplace_back(ptr);
62 return ptr;
63 }
64
createDeleteObjectMethod(const std::string & jsonPointerPath,const std::shared_ptr<sdbusplus::asio::dbus_interface> & iface,nlohmann::json & systemConfiguration)65 void EMDBusInterface::createDeleteObjectMethod(
66 const std::string& jsonPointerPath,
67 const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
68 nlohmann::json& systemConfiguration)
69 {
70 std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface;
71 iface->register_method(
72 "Delete", [this, &systemConfiguration, interface,
73 jsonPointerPath{std::string(jsonPointerPath)}]() {
74 std::shared_ptr<sdbusplus::asio::dbus_interface> dbusInterface =
75 interface.lock();
76 if (!dbusInterface)
77 {
78 // this technically can't happen as the pointer is pointing to
79 // us
80 throw DBusInternalError();
81 }
82 nlohmann::json::json_pointer ptr(jsonPointerPath);
83 systemConfiguration[ptr] = nullptr;
84
85 // todo(james): dig through sdbusplus to find out why we can't
86 // delete it in a method call
87 boost::asio::post(io, [dbusInterface, this]() mutable {
88 objServer.remove_interface(dbusInterface);
89 });
90
91 if (!writeJsonFiles(systemConfiguration))
92 {
93 lg2::error("error setting json file");
94 throw DBusInternalError();
95 }
96 });
97 }
98
checkArrayElementsSameType(nlohmann::json & value)99 static bool checkArrayElementsSameType(nlohmann::json& value)
100 {
101 nlohmann::json::array_t* arr = value.get_ptr<nlohmann::json::array_t*>();
102 if (arr == nullptr)
103 {
104 return false;
105 }
106
107 if (arr->empty())
108 {
109 return true;
110 }
111
112 nlohmann::json::value_t firstType = value[0].type();
113 return std::ranges::all_of(value, [firstType](const nlohmann::json& el) {
114 return el.type() == firstType;
115 });
116 }
117
getDBusType(const nlohmann::json & value,nlohmann::json::value_t type,sdbusplus::asio::PropertyPermission permission)118 static nlohmann::json::value_t getDBusType(
119 const nlohmann::json& value, nlohmann::json::value_t type,
120 sdbusplus::asio::PropertyPermission permission)
121 {
122 const bool array = value.type() == nlohmann::json::value_t::array;
123
124 if (permission == sdbusplus::asio::PropertyPermission::readWrite)
125 {
126 // all setable numbers are doubles as it is difficult to always
127 // create a configuration file with all whole numbers as decimals
128 // i.e. 1.0
129 if (array)
130 {
131 if (value[0].is_number())
132 {
133 return nlohmann::json::value_t::number_float;
134 }
135 }
136 else if (value.is_number())
137 {
138 return nlohmann::json::value_t::number_float;
139 }
140 }
141
142 return type;
143 }
144
populateInterfacePropertyFromJson(nlohmann::json & systemConfiguration,const std::string & path,const std::string & key,const nlohmann::json & value,nlohmann::json::value_t type,std::shared_ptr<sdbusplus::asio::dbus_interface> & iface,sdbusplus::asio::PropertyPermission permission)145 static void populateInterfacePropertyFromJson(
146 nlohmann::json& systemConfiguration, const std::string& path,
147 const std::string& key, const nlohmann::json& value,
148 nlohmann::json::value_t type,
149 std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
150 sdbusplus::asio::PropertyPermission permission)
151 {
152 const auto modifiedType = getDBusType(value, type, permission);
153
154 switch (modifiedType)
155 {
156 case (nlohmann::json::value_t::boolean):
157 {
158 addValueToDBus<bool>(key, value, *iface, permission,
159 systemConfiguration, path);
160 break;
161 }
162 case (nlohmann::json::value_t::number_integer):
163 {
164 addValueToDBus<int64_t>(key, value, *iface, permission,
165 systemConfiguration, path);
166 break;
167 }
168 case (nlohmann::json::value_t::number_unsigned):
169 {
170 addValueToDBus<uint64_t>(key, value, *iface, permission,
171 systemConfiguration, path);
172 break;
173 }
174 case (nlohmann::json::value_t::number_float):
175 {
176 addValueToDBus<double>(key, value, *iface, permission,
177 systemConfiguration, path);
178 break;
179 }
180 case (nlohmann::json::value_t::string):
181 {
182 addValueToDBus<std::string>(key, value, *iface, permission,
183 systemConfiguration, path);
184 break;
185 }
186 default:
187 {
188 lg2::error(
189 "Unexpected json type in system configuration {KEY}: {VALUE}",
190 "KEY", key, "VALUE", value.type_name());
191 break;
192 }
193 }
194 }
195
196 // 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::PropertyPermission permission)197 void EMDBusInterface::populateInterfaceFromJson(
198 nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
199 std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
200 nlohmann::json& dict, sdbusplus::asio::PropertyPermission permission)
201 {
202 for (const auto& [key, value] : dict.items())
203 {
204 auto type = value.type();
205 if (value.type() == nlohmann::json::value_t::array)
206 {
207 if (value.empty())
208 {
209 continue;
210 }
211 type = value[0].type();
212 if (!checkArrayElementsSameType(value))
213 {
214 lg2::error("dbus format error {VALUE}", "VALUE", value);
215 continue;
216 }
217 }
218 if (type == nlohmann::json::value_t::object)
219 {
220 continue; // handled elsewhere
221 }
222
223 std::string path = jsonPointerPath;
224 path.append("/").append(key);
225
226 populateInterfacePropertyFromJson(systemConfiguration, path, key, value,
227 type, iface, permission);
228 }
229 if (permission == sdbusplus::asio::PropertyPermission::readWrite)
230 {
231 createDeleteObjectMethod(jsonPointerPath, iface, systemConfiguration);
232 }
233 tryIfaceInitialize(iface);
234 }
235
236 // @brief: throws on error
addObjectRuntimeValidateJson(const nlohmann::json & newData,const std::string * type)237 static void addObjectRuntimeValidateJson(const nlohmann::json& newData,
238 const std::string* type)
239 {
240 if constexpr (!ENABLE_RUNTIME_VALIDATE_JSON)
241 {
242 return;
243 }
244
245 const std::filesystem::path schemaPath =
246 std::filesystem::path(schemaDirectory) / "exposes_record.json";
247
248 std::ifstream schemaFile{schemaPath};
249
250 if (!schemaFile.good())
251 {
252 throw std::invalid_argument("No schema avaliable, cannot validate.");
253 }
254 nlohmann::json schema =
255 nlohmann::json::parse(schemaFile, nullptr, false, true);
256 if (schema.is_discarded())
257 {
258 lg2::error("Schema not legal: {TYPE}.json", "TYPE", *type);
259 throw DBusInternalError();
260 }
261
262 if (!validateJson(schema, newData))
263 {
264 throw std::invalid_argument("Data does not match schema");
265 }
266 }
267
addObject(const std::flat_map<std::string,JsonVariantType,std::less<>> & data,nlohmann::json & systemConfiguration,const std::string & jsonPointerPath,const std::string & path,const std::string & board)268 void EMDBusInterface::addObject(
269 const std::flat_map<std::string, JsonVariantType, std::less<>>& data,
270 nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
271 const std::string& path, const std::string& board)
272 {
273 nlohmann::json::json_pointer ptr(jsonPointerPath);
274 nlohmann::json& base = systemConfiguration[ptr];
275 auto findExposes = base.find("Exposes");
276
277 if (findExposes == base.end())
278 {
279 throw std::invalid_argument("Entity must have children.");
280 }
281
282 // this will throw invalid-argument to sdbusplus if invalid json
283 nlohmann::json newData{};
284 for (const auto& item : data)
285 {
286 nlohmann::json& newJson = newData[item.first];
287 std::visit(
288 [&newJson](auto&& val) {
289 newJson = std::forward<decltype(val)>(val);
290 },
291 item.second);
292 }
293
294 addObjectJson(newData, systemConfiguration, jsonPointerPath, path, board);
295 }
296
addObjectJson(nlohmann::json & newData,nlohmann::json & systemConfiguration,const std::string & jsonPointerPath,const std::string & path,const std::string & board)297 void EMDBusInterface::addObjectJson(
298 nlohmann::json& newData, nlohmann::json& systemConfiguration,
299 const std::string& jsonPointerPath, const std::string& path,
300 const std::string& board)
301 {
302 nlohmann::json::json_pointer ptr(jsonPointerPath);
303 nlohmann::json& base = systemConfiguration[ptr];
304 auto findExposes = base.find("Exposes");
305 auto findName = newData.find("Name");
306 auto findType = newData.find("Type");
307 if (findName == newData.end() || findType == newData.end())
308 {
309 throw std::invalid_argument("AddObject missing Name or Type");
310 }
311 const std::string* type = findType->get_ptr<const std::string*>();
312 const std::string* name = findName->get_ptr<const std::string*>();
313 if (type == nullptr || name == nullptr)
314 {
315 throw std::invalid_argument("Type and Name must be a string.");
316 }
317
318 bool foundNull = false;
319 size_t lastIndex = 0;
320 // we add in the "exposes"
321 for (const auto& expose : *findExposes)
322 {
323 if (expose.is_null())
324 {
325 foundNull = true;
326 continue;
327 }
328
329 if (expose["Name"] == *name && expose["Type"] == *type)
330 {
331 throw std::invalid_argument("Field already in JSON, not adding");
332 }
333
334 if (foundNull)
335 {
336 continue;
337 }
338
339 lastIndex++;
340 }
341
342 addObjectRuntimeValidateJson(newData, type);
343
344 if (foundNull)
345 {
346 findExposes->at(lastIndex) = newData;
347 }
348 else
349 {
350 findExposes->push_back(newData);
351 }
352 if (!writeJsonFiles(systemConfiguration))
353 {
354 lg2::error("Error writing json files");
355 }
356 std::string dbusName = *name;
357
358 std::regex_replace(dbusName.begin(), dbusName.begin(), dbusName.end(),
359 illegalDbusMemberRegex, "_");
360
361 std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
362 createInterface(path + "/" + dbusName,
363 "xyz.openbmc_project.Configuration." + *type, board,
364 true);
365 // permission is read-write, as since we just created it, must be
366 // runtime modifiable
367 populateInterfaceFromJson(
368 systemConfiguration,
369 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex), interface,
370 newData, sdbusplus::asio::PropertyPermission::readWrite);
371 }
372
createAddObjectMethod(const std::string & jsonPointerPath,const std::string & path,nlohmann::json & systemConfiguration,const std::string & board)373 void EMDBusInterface::createAddObjectMethod(
374 const std::string& jsonPointerPath, const std::string& path,
375 nlohmann::json& systemConfiguration, const std::string& board)
376 {
377 std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
378 createInterface(path, "xyz.openbmc_project.AddObject", board);
379
380 iface->register_method(
381 "AddObject",
382 [&systemConfiguration, jsonPointerPath{std::string(jsonPointerPath)},
383 path{std::string(path)}, board{std::string(board)},
384 this](const std::flat_map<std::string, JsonVariantType, std::less<>>&
385 data) {
386 addObject(data, systemConfiguration, jsonPointerPath, path, board);
387 });
388 tryIfaceInitialize(iface);
389 }
390
391 std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
getDeviceInterfaces(const nlohmann::json & device)392 EMDBusInterface::getDeviceInterfaces(const nlohmann::json& device)
393 {
394 return inventory[device["Name"].get<std::string>()];
395 }
396
397 } // namespace dbus_interface
398