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 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 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> 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 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 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 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 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 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 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 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 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 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>>& 394 EMDBusInterface::getDeviceInterfaces(const nlohmann::json& device) 395 { 396 return inventory[device["Name"].get<std::string>()]; 397 } 398 399 } // namespace dbus_interface 400