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