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