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