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 nlohmann::json& 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 auto findName = newData.find("Name"); 295 auto findType = newData.find("Type"); 296 if (findName == newData.end() || findType == newData.end()) 297 { 298 throw std::invalid_argument("AddObject missing Name or Type"); 299 } 300 const std::string* type = findType->get_ptr<const std::string*>(); 301 const std::string* name = findName->get_ptr<const std::string*>(); 302 if (type == nullptr || name == nullptr) 303 { 304 throw std::invalid_argument("Type and Name must be a string."); 305 } 306 307 bool foundNull = false; 308 size_t lastIndex = 0; 309 // we add in the "exposes" 310 for (const auto& expose : *findExposes) 311 { 312 if (expose.is_null()) 313 { 314 foundNull = true; 315 continue; 316 } 317 318 if (expose["Name"] == *name && expose["Type"] == *type) 319 { 320 throw std::invalid_argument("Field already in JSON, not adding"); 321 } 322 323 if (foundNull) 324 { 325 continue; 326 } 327 328 lastIndex++; 329 } 330 331 addObjectRuntimeValidateJson(newData, type); 332 333 if (foundNull) 334 { 335 findExposes->at(lastIndex) = newData; 336 } 337 else 338 { 339 findExposes->push_back(newData); 340 } 341 if (!writeJsonFiles(systemConfiguration)) 342 { 343 lg2::error("Error writing json files"); 344 } 345 std::string dbusName = *name; 346 347 std::regex_replace(dbusName.begin(), dbusName.begin(), dbusName.end(), 348 illegalDbusMemberRegex, "_"); 349 350 std::shared_ptr<sdbusplus::asio::dbus_interface> interface = 351 createInterface(path + "/" + dbusName, 352 "xyz.openbmc_project.Configuration." + *type, board, 353 true); 354 // permission is read-write, as since we just created it, must be 355 // runtime modifiable 356 populateInterfaceFromJson( 357 systemConfiguration, 358 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex), interface, 359 newData, sdbusplus::asio::PropertyPermission::readWrite); 360 } 361 362 void EMDBusInterface::createAddObjectMethod( 363 const std::string& jsonPointerPath, const std::string& path, 364 nlohmann::json& systemConfiguration, const std::string& board) 365 { 366 std::shared_ptr<sdbusplus::asio::dbus_interface> iface = 367 createInterface(path, "xyz.openbmc_project.AddObject", board); 368 369 iface->register_method( 370 "AddObject", 371 [&systemConfiguration, jsonPointerPath{std::string(jsonPointerPath)}, 372 path{std::string(path)}, board{std::string(board)}, 373 this](const std::flat_map<std::string, JsonVariantType, std::less<>>& 374 data) { 375 addObject(data, systemConfiguration, jsonPointerPath, path, board); 376 }); 377 tryIfaceInitialize(iface); 378 } 379 380 std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>& 381 EMDBusInterface::getDeviceInterfaces(const nlohmann::json& device) 382 { 383 return inventory[device["Name"].get<std::string>()]; 384 } 385 386 } // namespace dbus_interface 387