1 /* 2 // Copyright (c) 2018 Intel Corporation 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 */ 16 /// \file entity_manager.cpp 17 18 #include "entity_manager.hpp" 19 20 #include "overlay.hpp" 21 #include "topology.hpp" 22 #include "utils.hpp" 23 #include "variant_visitors.hpp" 24 25 #include <boost/algorithm/string/case_conv.hpp> 26 #include <boost/algorithm/string/classification.hpp> 27 #include <boost/algorithm/string/predicate.hpp> 28 #include <boost/algorithm/string/replace.hpp> 29 #include <boost/algorithm/string/split.hpp> 30 #include <boost/asio/io_context.hpp> 31 #include <boost/asio/post.hpp> 32 #include <boost/asio/steady_timer.hpp> 33 #include <boost/container/flat_map.hpp> 34 #include <boost/container/flat_set.hpp> 35 #include <boost/range/iterator_range.hpp> 36 #include <nlohmann/json.hpp> 37 #include <sdbusplus/asio/connection.hpp> 38 #include <sdbusplus/asio/object_server.hpp> 39 40 #include <charconv> 41 #include <filesystem> 42 #include <fstream> 43 #include <functional> 44 #include <iostream> 45 #include <map> 46 #include <regex> 47 #include <variant> 48 constexpr const char* hostConfigurationDirectory = SYSCONF_DIR "configurations"; 49 constexpr const char* configurationDirectory = PACKAGE_DIR "configurations"; 50 constexpr const char* schemaDirectory = PACKAGE_DIR "configurations/schemas"; 51 constexpr const char* tempConfigDir = "/tmp/configuration/"; 52 constexpr const char* lastConfiguration = "/tmp/configuration/last.json"; 53 constexpr const char* currentConfiguration = "/var/configuration/system.json"; 54 constexpr const char* globalSchema = "global.json"; 55 56 const boost::container::flat_map<const char*, probe_type_codes, CmpStr> 57 probeTypes{{{"FALSE", probe_type_codes::FALSE_T}, 58 {"TRUE", probe_type_codes::TRUE_T}, 59 {"AND", probe_type_codes::AND}, 60 {"OR", probe_type_codes::OR}, 61 {"FOUND", probe_type_codes::FOUND}, 62 {"MATCH_ONE", probe_type_codes::MATCH_ONE}}}; 63 64 static constexpr std::array<const char*, 6> settableInterfaces = { 65 "FanProfile", "Pid", "Pid.Zone", "Stepwise", "Thresholds", "Polling"}; 66 using JsonVariantType = 67 std::variant<std::vector<std::string>, std::vector<double>, std::string, 68 int64_t, uint64_t, double, int32_t, uint32_t, int16_t, 69 uint16_t, uint8_t, bool>; 70 71 // store reference to all interfaces so we can destroy them later 72 boost::container::flat_map< 73 std::string, std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>> 74 inventory; 75 76 // todo: pass this through nicer 77 std::shared_ptr<sdbusplus::asio::connection> systemBus; 78 nlohmann::json lastJson; 79 Topology topology; 80 81 boost::asio::io_context io; 82 83 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]"); 84 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]"); 85 86 void tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface>& iface) 87 { 88 try 89 { 90 iface->initialize(); 91 } 92 catch (std::exception& e) 93 { 94 std::cerr << "Unable to initialize dbus interface : " << e.what() 95 << "\n" 96 << "object Path : " << iface->get_object_path() << "\n" 97 << "interface name : " << iface->get_interface_name() << "\n"; 98 } 99 } 100 101 FoundProbeTypeT findProbeType(const std::string& probe) 102 { 103 boost::container::flat_map<const char*, probe_type_codes, 104 CmpStr>::const_iterator probeType; 105 for (probeType = probeTypes.begin(); probeType != probeTypes.end(); 106 ++probeType) 107 { 108 if (probe.find(probeType->first) != std::string::npos) 109 { 110 return probeType; 111 } 112 } 113 114 return std::nullopt; 115 } 116 117 static std::shared_ptr<sdbusplus::asio::dbus_interface> 118 createInterface(sdbusplus::asio::object_server& objServer, 119 const std::string& path, const std::string& interface, 120 const std::string& parent, bool checkNull = false) 121 { 122 // on first add we have no reason to check for null before add, as there 123 // won't be any. For dynamically added interfaces, we check for null so that 124 // a constant delete/add will not create a memory leak 125 126 auto ptr = objServer.add_interface(path, interface); 127 auto& dataVector = inventory[parent]; 128 if (checkNull) 129 { 130 auto it = std::find_if(dataVector.begin(), dataVector.end(), 131 [](const auto& p) { return p.expired(); }); 132 if (it != dataVector.end()) 133 { 134 *it = ptr; 135 return ptr; 136 } 137 } 138 dataVector.emplace_back(ptr); 139 return ptr; 140 } 141 142 // writes output files to persist data 143 bool writeJsonFiles(const nlohmann::json& systemConfiguration) 144 { 145 std::filesystem::create_directory(configurationOutDir); 146 std::ofstream output(currentConfiguration); 147 if (!output.good()) 148 { 149 return false; 150 } 151 output << systemConfiguration.dump(4); 152 output.close(); 153 return true; 154 } 155 156 template <typename JsonType> 157 bool setJsonFromPointer(const std::string& ptrStr, const JsonType& value, 158 nlohmann::json& systemConfiguration) 159 { 160 try 161 { 162 nlohmann::json::json_pointer ptr(ptrStr); 163 nlohmann::json& ref = systemConfiguration[ptr]; 164 ref = value; 165 return true; 166 } 167 catch (const std::out_of_range&) 168 { 169 return false; 170 } 171 } 172 173 // template function to add array as dbus property 174 template <typename PropertyType> 175 void addArrayToDbus(const std::string& name, const nlohmann::json& array, 176 sdbusplus::asio::dbus_interface* iface, 177 sdbusplus::asio::PropertyPermission permission, 178 nlohmann::json& systemConfiguration, 179 const std::string& jsonPointerString) 180 { 181 std::vector<PropertyType> values; 182 for (const auto& property : array) 183 { 184 auto ptr = property.get_ptr<const PropertyType*>(); 185 if (ptr != nullptr) 186 { 187 values.emplace_back(*ptr); 188 } 189 } 190 191 if (permission == sdbusplus::asio::PropertyPermission::readOnly) 192 { 193 iface->register_property(name, values); 194 } 195 else 196 { 197 iface->register_property( 198 name, values, 199 [&systemConfiguration, 200 jsonPointerString{std::string(jsonPointerString)}]( 201 const std::vector<PropertyType>& newVal, 202 std::vector<PropertyType>& val) { 203 val = newVal; 204 if (!setJsonFromPointer(jsonPointerString, val, 205 systemConfiguration)) 206 { 207 std::cerr << "error setting json field\n"; 208 return -1; 209 } 210 if (!writeJsonFiles(systemConfiguration)) 211 { 212 std::cerr << "error setting json file\n"; 213 return -1; 214 } 215 return 1; 216 }); 217 } 218 } 219 220 template <typename PropertyType> 221 void addProperty(const std::string& name, const PropertyType& value, 222 sdbusplus::asio::dbus_interface* iface, 223 nlohmann::json& systemConfiguration, 224 const std::string& jsonPointerString, 225 sdbusplus::asio::PropertyPermission permission) 226 { 227 if (permission == sdbusplus::asio::PropertyPermission::readOnly) 228 { 229 iface->register_property(name, value); 230 return; 231 } 232 iface->register_property( 233 name, value, 234 [&systemConfiguration, 235 jsonPointerString{std::string(jsonPointerString)}]( 236 const PropertyType& newVal, PropertyType& val) { 237 val = newVal; 238 if (!setJsonFromPointer(jsonPointerString, val, systemConfiguration)) 239 { 240 std::cerr << "error setting json field\n"; 241 return -1; 242 } 243 if (!writeJsonFiles(systemConfiguration)) 244 { 245 std::cerr << "error setting json file\n"; 246 return -1; 247 } 248 return 1; 249 }); 250 } 251 252 void createDeleteObjectMethod( 253 const std::string& jsonPointerPath, 254 const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface, 255 sdbusplus::asio::object_server& objServer, 256 nlohmann::json& systemConfiguration) 257 { 258 std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface; 259 iface->register_method("Delete", 260 [&objServer, &systemConfiguration, interface, 261 jsonPointerPath{std::string(jsonPointerPath)}]() { 262 std::shared_ptr<sdbusplus::asio::dbus_interface> dbusInterface = 263 interface.lock(); 264 if (!dbusInterface) 265 { 266 // this technically can't happen as the pointer is pointing to 267 // us 268 throw DBusInternalError(); 269 } 270 nlohmann::json::json_pointer ptr(jsonPointerPath); 271 systemConfiguration[ptr] = nullptr; 272 273 // todo(james): dig through sdbusplus to find out why we can't 274 // delete it in a method call 275 boost::asio::post(io, [&objServer, dbusInterface]() mutable { 276 objServer.remove_interface(dbusInterface); 277 }); 278 279 if (!writeJsonFiles(systemConfiguration)) 280 { 281 std::cerr << "error setting json file\n"; 282 throw DBusInternalError(); 283 } 284 }); 285 } 286 287 // adds simple json types to interface's properties 288 void populateInterfaceFromJson( 289 nlohmann::json& systemConfiguration, const std::string& jsonPointerPath, 290 std::shared_ptr<sdbusplus::asio::dbus_interface>& iface, 291 nlohmann::json& dict, sdbusplus::asio::object_server& objServer, 292 sdbusplus::asio::PropertyPermission permission = 293 sdbusplus::asio::PropertyPermission::readOnly) 294 { 295 for (const auto& [key, value] : dict.items()) 296 { 297 auto type = value.type(); 298 bool array = false; 299 if (value.type() == nlohmann::json::value_t::array) 300 { 301 array = true; 302 if (value.empty()) 303 { 304 continue; 305 } 306 type = value[0].type(); 307 bool isLegal = true; 308 for (const auto& arrayItem : value) 309 { 310 if (arrayItem.type() != type) 311 { 312 isLegal = false; 313 break; 314 } 315 } 316 if (!isLegal) 317 { 318 std::cerr << "dbus format error" << value << "\n"; 319 continue; 320 } 321 } 322 if (type == nlohmann::json::value_t::object) 323 { 324 continue; // handled elsewhere 325 } 326 327 std::string path = jsonPointerPath; 328 path.append("/").append(key); 329 if (permission == sdbusplus::asio::PropertyPermission::readWrite) 330 { 331 // all setable numbers are doubles as it is difficult to always 332 // create a configuration file with all whole numbers as decimals 333 // i.e. 1.0 334 if (array) 335 { 336 if (value[0].is_number()) 337 { 338 type = nlohmann::json::value_t::number_float; 339 } 340 } 341 else if (value.is_number()) 342 { 343 type = nlohmann::json::value_t::number_float; 344 } 345 } 346 347 switch (type) 348 { 349 case (nlohmann::json::value_t::boolean): 350 { 351 if (array) 352 { 353 // todo: array of bool isn't detected correctly by 354 // sdbusplus, change it to numbers 355 addArrayToDbus<uint64_t>(key, value, iface.get(), 356 permission, systemConfiguration, 357 path); 358 } 359 360 else 361 { 362 addProperty(key, value.get<bool>(), iface.get(), 363 systemConfiguration, path, permission); 364 } 365 break; 366 } 367 case (nlohmann::json::value_t::number_integer): 368 { 369 if (array) 370 { 371 addArrayToDbus<int64_t>(key, value, iface.get(), permission, 372 systemConfiguration, path); 373 } 374 else 375 { 376 addProperty(key, value.get<int64_t>(), iface.get(), 377 systemConfiguration, path, 378 sdbusplus::asio::PropertyPermission::readOnly); 379 } 380 break; 381 } 382 case (nlohmann::json::value_t::number_unsigned): 383 { 384 if (array) 385 { 386 addArrayToDbus<uint64_t>(key, value, iface.get(), 387 permission, systemConfiguration, 388 path); 389 } 390 else 391 { 392 addProperty(key, value.get<uint64_t>(), iface.get(), 393 systemConfiguration, path, 394 sdbusplus::asio::PropertyPermission::readOnly); 395 } 396 break; 397 } 398 case (nlohmann::json::value_t::number_float): 399 { 400 if (array) 401 { 402 addArrayToDbus<double>(key, value, iface.get(), permission, 403 systemConfiguration, path); 404 } 405 406 else 407 { 408 addProperty(key, value.get<double>(), iface.get(), 409 systemConfiguration, path, permission); 410 } 411 break; 412 } 413 case (nlohmann::json::value_t::string): 414 { 415 if (array) 416 { 417 addArrayToDbus<std::string>(key, value, iface.get(), 418 permission, systemConfiguration, 419 path); 420 } 421 else 422 { 423 addProperty(key, value.get<std::string>(), iface.get(), 424 systemConfiguration, path, permission); 425 } 426 break; 427 } 428 default: 429 { 430 std::cerr << "Unexpected json type in system configuration " 431 << key << ": " << value.type_name() << "\n"; 432 break; 433 } 434 } 435 } 436 if (permission == sdbusplus::asio::PropertyPermission::readWrite) 437 { 438 createDeleteObjectMethod(jsonPointerPath, iface, objServer, 439 systemConfiguration); 440 } 441 tryIfaceInitialize(iface); 442 } 443 444 sdbusplus::asio::PropertyPermission getPermission(const std::string& interface) 445 { 446 return std::find(settableInterfaces.begin(), settableInterfaces.end(), 447 interface) != settableInterfaces.end() 448 ? sdbusplus::asio::PropertyPermission::readWrite 449 : sdbusplus::asio::PropertyPermission::readOnly; 450 } 451 452 void createAddObjectMethod(const std::string& jsonPointerPath, 453 const std::string& path, 454 nlohmann::json& systemConfiguration, 455 sdbusplus::asio::object_server& objServer, 456 const std::string& board) 457 { 458 std::shared_ptr<sdbusplus::asio::dbus_interface> iface = createInterface( 459 objServer, path, "xyz.openbmc_project.AddObject", board); 460 461 iface->register_method( 462 "AddObject", 463 [&systemConfiguration, &objServer, 464 jsonPointerPath{std::string(jsonPointerPath)}, path{std::string(path)}, 465 board](const boost::container::flat_map<std::string, JsonVariantType>& 466 data) { 467 nlohmann::json::json_pointer ptr(jsonPointerPath); 468 nlohmann::json& base = systemConfiguration[ptr]; 469 auto findExposes = base.find("Exposes"); 470 471 if (findExposes == base.end()) 472 { 473 throw std::invalid_argument("Entity must have children."); 474 } 475 476 // this will throw invalid-argument to sdbusplus if invalid json 477 nlohmann::json newData{}; 478 for (const auto& item : data) 479 { 480 nlohmann::json& newJson = newData[item.first]; 481 std::visit( 482 [&newJson](auto&& val) { 483 newJson = std::forward<decltype(val)>(val); 484 }, 485 item.second); 486 } 487 488 auto findName = newData.find("Name"); 489 auto findType = newData.find("Type"); 490 if (findName == newData.end() || findType == newData.end()) 491 { 492 throw std::invalid_argument("AddObject missing Name or Type"); 493 } 494 const std::string* type = findType->get_ptr<const std::string*>(); 495 const std::string* name = findName->get_ptr<const std::string*>(); 496 if (type == nullptr || name == nullptr) 497 { 498 throw std::invalid_argument("Type and Name must be a string."); 499 } 500 501 bool foundNull = false; 502 size_t lastIndex = 0; 503 // we add in the "exposes" 504 for (const auto& expose : *findExposes) 505 { 506 if (expose.is_null()) 507 { 508 foundNull = true; 509 continue; 510 } 511 512 if (expose["Name"] == *name && expose["Type"] == *type) 513 { 514 throw std::invalid_argument( 515 "Field already in JSON, not adding"); 516 } 517 518 if (foundNull) 519 { 520 continue; 521 } 522 523 lastIndex++; 524 } 525 526 std::ifstream schemaFile(std::string(schemaDirectory) + "/" + 527 boost::to_lower_copy(*type) + ".json"); 528 // todo(james) we might want to also make a list of 'can add' 529 // interfaces but for now I think the assumption if there is a 530 // schema avaliable that it is allowed to update is fine 531 if (!schemaFile.good()) 532 { 533 throw std::invalid_argument( 534 "No schema avaliable, cannot validate."); 535 } 536 nlohmann::json schema = nlohmann::json::parse(schemaFile, nullptr, 537 false); 538 if (schema.is_discarded()) 539 { 540 std::cerr << "Schema not legal" << *type << ".json\n"; 541 throw DBusInternalError(); 542 } 543 if (!validateJson(schema, newData)) 544 { 545 throw std::invalid_argument("Data does not match schema"); 546 } 547 if (foundNull) 548 { 549 findExposes->at(lastIndex) = newData; 550 } 551 else 552 { 553 findExposes->push_back(newData); 554 } 555 if (!writeJsonFiles(systemConfiguration)) 556 { 557 std::cerr << "Error writing json files\n"; 558 throw DBusInternalError(); 559 } 560 std::string dbusName = *name; 561 562 std::regex_replace(dbusName.begin(), dbusName.begin(), dbusName.end(), 563 illegalDbusMemberRegex, "_"); 564 565 std::shared_ptr<sdbusplus::asio::dbus_interface> interface = 566 createInterface(objServer, path + "/" + dbusName, 567 "xyz.openbmc_project.Configuration." + *type, board, 568 true); 569 // permission is read-write, as since we just created it, must be 570 // runtime modifiable 571 populateInterfaceFromJson( 572 systemConfiguration, 573 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex), 574 interface, newData, objServer, 575 sdbusplus::asio::PropertyPermission::readWrite); 576 }); 577 tryIfaceInitialize(iface); 578 } 579 580 void postToDbus(const nlohmann::json& newConfiguration, 581 nlohmann::json& systemConfiguration, 582 sdbusplus::asio::object_server& objServer) 583 584 { 585 std::map<std::string, std::string> newBoards; // path -> name 586 587 // iterate through boards 588 for (const auto& [boardId, boardConfig] : newConfiguration.items()) 589 { 590 std::string boardName = boardConfig["Name"]; 591 std::string boardNameOrig = boardConfig["Name"]; 592 std::string jsonPointerPath = "/" + boardId; 593 // loop through newConfiguration, but use values from system 594 // configuration to be able to modify via dbus later 595 auto boardValues = systemConfiguration[boardId]; 596 auto findBoardType = boardValues.find("Type"); 597 std::string boardType; 598 if (findBoardType != boardValues.end() && 599 findBoardType->type() == nlohmann::json::value_t::string) 600 { 601 boardType = findBoardType->get<std::string>(); 602 std::regex_replace(boardType.begin(), boardType.begin(), 603 boardType.end(), illegalDbusMemberRegex, "_"); 604 } 605 else 606 { 607 std::cerr << "Unable to find type for " << boardName 608 << " reverting to Chassis.\n"; 609 boardType = "Chassis"; 610 } 611 std::string boardtypeLower = boost::algorithm::to_lower_copy(boardType); 612 613 std::regex_replace(boardName.begin(), boardName.begin(), 614 boardName.end(), illegalDbusMemberRegex, "_"); 615 std::string boardPath = "/xyz/openbmc_project/inventory/system/"; 616 boardPath += boardtypeLower; 617 boardPath += "/"; 618 boardPath += boardName; 619 620 std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface = 621 createInterface(objServer, boardPath, 622 "xyz.openbmc_project.Inventory.Item", boardName); 623 624 std::shared_ptr<sdbusplus::asio::dbus_interface> boardIface = 625 createInterface(objServer, boardPath, 626 "xyz.openbmc_project.Inventory.Item." + boardType, 627 boardNameOrig); 628 629 createAddObjectMethod(jsonPointerPath, boardPath, systemConfiguration, 630 objServer, boardNameOrig); 631 632 populateInterfaceFromJson(systemConfiguration, jsonPointerPath, 633 boardIface, boardValues, objServer); 634 jsonPointerPath += "/"; 635 // iterate through board properties 636 for (const auto& [propName, propValue] : boardValues.items()) 637 { 638 if (propValue.type() == nlohmann::json::value_t::object) 639 { 640 std::shared_ptr<sdbusplus::asio::dbus_interface> iface = 641 createInterface(objServer, boardPath, propName, 642 boardNameOrig); 643 644 populateInterfaceFromJson(systemConfiguration, 645 jsonPointerPath + propName, iface, 646 propValue, objServer); 647 } 648 } 649 650 auto exposes = boardValues.find("Exposes"); 651 if (exposes == boardValues.end()) 652 { 653 continue; 654 } 655 // iterate through exposes 656 jsonPointerPath += "Exposes/"; 657 658 // store the board level pointer so we can modify it on the way down 659 std::string jsonPointerPathBoard = jsonPointerPath; 660 size_t exposesIndex = -1; 661 for (auto& item : *exposes) 662 { 663 exposesIndex++; 664 jsonPointerPath = jsonPointerPathBoard; 665 jsonPointerPath += std::to_string(exposesIndex); 666 667 auto findName = item.find("Name"); 668 if (findName == item.end()) 669 { 670 std::cerr << "cannot find name in field " << item << "\n"; 671 continue; 672 } 673 auto findStatus = item.find("Status"); 674 // if status is not found it is assumed to be status = 'okay' 675 if (findStatus != item.end()) 676 { 677 if (*findStatus == "disabled") 678 { 679 continue; 680 } 681 } 682 auto findType = item.find("Type"); 683 std::string itemType; 684 if (findType != item.end()) 685 { 686 itemType = findType->get<std::string>(); 687 std::regex_replace(itemType.begin(), itemType.begin(), 688 itemType.end(), illegalDbusPathRegex, "_"); 689 } 690 else 691 { 692 itemType = "unknown"; 693 } 694 std::string itemName = findName->get<std::string>(); 695 std::regex_replace(itemName.begin(), itemName.begin(), 696 itemName.end(), illegalDbusMemberRegex, "_"); 697 std::string ifacePath = boardPath; 698 ifacePath += "/"; 699 ifacePath += itemName; 700 701 std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface = 702 createInterface(objServer, ifacePath, 703 "xyz.openbmc_project.Configuration." + itemType, 704 boardNameOrig); 705 706 if (itemType == "BMC") 707 { 708 std::shared_ptr<sdbusplus::asio::dbus_interface> bmcIface = 709 createInterface(objServer, ifacePath, 710 "xyz.openbmc_project.Inventory.Item.Bmc", 711 boardNameOrig); 712 populateInterfaceFromJson(systemConfiguration, jsonPointerPath, 713 bmcIface, item, objServer, 714 getPermission(itemType)); 715 } 716 else if (itemType == "System") 717 { 718 std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface = 719 createInterface(objServer, ifacePath, 720 "xyz.openbmc_project.Inventory.Item.System", 721 boardNameOrig); 722 populateInterfaceFromJson(systemConfiguration, jsonPointerPath, 723 systemIface, item, objServer, 724 getPermission(itemType)); 725 } 726 727 populateInterfaceFromJson(systemConfiguration, jsonPointerPath, 728 itemIface, item, objServer, 729 getPermission(itemType)); 730 731 for (const auto& [name, config] : item.items()) 732 { 733 jsonPointerPath = jsonPointerPathBoard; 734 jsonPointerPath.append(std::to_string(exposesIndex)) 735 .append("/") 736 .append(name); 737 if (config.type() == nlohmann::json::value_t::object) 738 { 739 std::string ifaceName = 740 "xyz.openbmc_project.Configuration."; 741 ifaceName.append(itemType).append(".").append(name); 742 743 std::shared_ptr<sdbusplus::asio::dbus_interface> 744 objectIface = createInterface(objServer, ifacePath, 745 ifaceName, boardNameOrig); 746 747 populateInterfaceFromJson( 748 systemConfiguration, jsonPointerPath, objectIface, 749 config, objServer, getPermission(name)); 750 } 751 else if (config.type() == nlohmann::json::value_t::array) 752 { 753 size_t index = 0; 754 if (config.empty()) 755 { 756 continue; 757 } 758 bool isLegal = true; 759 auto type = config[0].type(); 760 if (type != nlohmann::json::value_t::object) 761 { 762 continue; 763 } 764 765 // verify legal json 766 for (const auto& arrayItem : config) 767 { 768 if (arrayItem.type() != type) 769 { 770 isLegal = false; 771 break; 772 } 773 } 774 if (!isLegal) 775 { 776 std::cerr << "dbus format error" << config << "\n"; 777 break; 778 } 779 780 for (auto& arrayItem : config) 781 { 782 std::string ifaceName = 783 "xyz.openbmc_project.Configuration."; 784 ifaceName.append(itemType).append(".").append(name); 785 ifaceName.append(std::to_string(index)); 786 787 std::shared_ptr<sdbusplus::asio::dbus_interface> 788 objectIface = createInterface( 789 objServer, ifacePath, ifaceName, boardNameOrig); 790 791 populateInterfaceFromJson( 792 systemConfiguration, 793 jsonPointerPath + "/" + std::to_string(index), 794 objectIface, arrayItem, objServer, 795 getPermission(name)); 796 index++; 797 } 798 } 799 } 800 801 topology.addBoard(boardPath, boardType, boardNameOrig, item); 802 } 803 804 newBoards.emplace(boardPath, boardNameOrig); 805 } 806 807 for (const auto& [assocPath, assocPropValue] : 808 topology.getAssocs(newBoards)) 809 { 810 auto findBoard = newBoards.find(assocPath); 811 if (findBoard == newBoards.end()) 812 { 813 continue; 814 } 815 816 auto ifacePtr = createInterface( 817 objServer, assocPath, "xyz.openbmc_project.Association.Definitions", 818 findBoard->second); 819 820 ifacePtr->register_property("Associations", assocPropValue); 821 tryIfaceInitialize(ifacePtr); 822 } 823 } 824 825 // reads json files out of the filesystem 826 bool loadConfigurations(std::list<nlohmann::json>& configurations) 827 { 828 // find configuration files 829 std::vector<std::filesystem::path> jsonPaths; 830 if (!findFiles( 831 std::vector<std::filesystem::path>{configurationDirectory, 832 hostConfigurationDirectory}, 833 R"(.*\.json)", jsonPaths)) 834 { 835 std::cerr << "Unable to find any configuration files in " 836 << configurationDirectory << "\n"; 837 return false; 838 } 839 840 std::ifstream schemaStream(std::string(schemaDirectory) + "/" + 841 globalSchema); 842 if (!schemaStream.good()) 843 { 844 std::cerr 845 << "Cannot open schema file, cannot validate JSON, exiting\n\n"; 846 std::exit(EXIT_FAILURE); 847 return false; 848 } 849 nlohmann::json schema = nlohmann::json::parse(schemaStream, nullptr, false); 850 if (schema.is_discarded()) 851 { 852 std::cerr 853 << "Illegal schema file detected, cannot validate JSON, exiting\n"; 854 std::exit(EXIT_FAILURE); 855 return false; 856 } 857 858 for (auto& jsonPath : jsonPaths) 859 { 860 std::ifstream jsonStream(jsonPath.c_str()); 861 if (!jsonStream.good()) 862 { 863 std::cerr << "unable to open " << jsonPath.string() << "\n"; 864 continue; 865 } 866 auto data = nlohmann::json::parse(jsonStream, nullptr, false); 867 if (data.is_discarded()) 868 { 869 std::cerr << "syntax error in " << jsonPath.string() << "\n"; 870 continue; 871 } 872 /* 873 * todo(james): reenable this once less things are in flight 874 * 875 if (!validateJson(schema, data)) 876 { 877 std::cerr << "Error validating " << jsonPath.string() << "\n"; 878 continue; 879 } 880 */ 881 882 if (data.type() == nlohmann::json::value_t::array) 883 { 884 for (auto& d : data) 885 { 886 configurations.emplace_back(d); 887 } 888 } 889 else 890 { 891 configurations.emplace_back(data); 892 } 893 } 894 return true; 895 } 896 897 static bool deviceRequiresPowerOn(const nlohmann::json& entity) 898 { 899 auto powerState = entity.find("PowerState"); 900 if (powerState == entity.end()) 901 { 902 return false; 903 } 904 905 const auto* ptr = powerState->get_ptr<const std::string*>(); 906 if (ptr == nullptr) 907 { 908 return false; 909 } 910 911 return *ptr == "On" || *ptr == "BiosPost"; 912 } 913 914 static void pruneDevice(const nlohmann::json& systemConfiguration, 915 const bool powerOff, const bool scannedPowerOff, 916 const std::string& name, const nlohmann::json& device) 917 { 918 if (systemConfiguration.contains(name)) 919 { 920 return; 921 } 922 923 if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff)) 924 { 925 return; 926 } 927 928 logDeviceRemoved(device); 929 } 930 931 void startRemovedTimer(boost::asio::steady_timer& timer, 932 nlohmann::json& systemConfiguration) 933 { 934 static bool scannedPowerOff = false; 935 static bool scannedPowerOn = false; 936 937 if (systemConfiguration.empty() || lastJson.empty()) 938 { 939 return; // not ready yet 940 } 941 if (scannedPowerOn) 942 { 943 return; 944 } 945 946 if (!isPowerOn() && scannedPowerOff) 947 { 948 return; 949 } 950 951 timer.expires_after(std::chrono::seconds(10)); 952 timer.async_wait( 953 [&systemConfiguration](const boost::system::error_code& ec) { 954 if (ec == boost::asio::error::operation_aborted) 955 { 956 return; 957 } 958 959 bool powerOff = !isPowerOn(); 960 for (const auto& [name, device] : lastJson.items()) 961 { 962 pruneDevice(systemConfiguration, powerOff, scannedPowerOff, name, 963 device); 964 } 965 966 scannedPowerOff = true; 967 if (!powerOff) 968 { 969 scannedPowerOn = true; 970 } 971 }); 972 } 973 974 static std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>& 975 getDeviceInterfaces(const nlohmann::json& device) 976 { 977 return inventory[device["Name"].get<std::string>()]; 978 } 979 980 static void pruneConfiguration(nlohmann::json& systemConfiguration, 981 sdbusplus::asio::object_server& objServer, 982 bool powerOff, const std::string& name, 983 const nlohmann::json& device) 984 { 985 if (powerOff && deviceRequiresPowerOn(device)) 986 { 987 // power not on yet, don't know if it's there or not 988 return; 989 } 990 991 auto& ifaces = getDeviceInterfaces(device); 992 for (auto& iface : ifaces) 993 { 994 auto sharedPtr = iface.lock(); 995 if (!!sharedPtr) 996 { 997 objServer.remove_interface(sharedPtr); 998 } 999 } 1000 1001 ifaces.clear(); 1002 systemConfiguration.erase(name); 1003 topology.remove(device["Name"].get<std::string>()); 1004 logDeviceRemoved(device); 1005 } 1006 1007 static void deriveNewConfiguration(const nlohmann::json& oldConfiguration, 1008 nlohmann::json& newConfiguration) 1009 { 1010 for (auto it = newConfiguration.begin(); it != newConfiguration.end();) 1011 { 1012 auto findKey = oldConfiguration.find(it.key()); 1013 if (findKey != oldConfiguration.end()) 1014 { 1015 it = newConfiguration.erase(it); 1016 } 1017 else 1018 { 1019 it++; 1020 } 1021 } 1022 } 1023 1024 static void publishNewConfiguration( 1025 const size_t& instance, const size_t count, 1026 boost::asio::steady_timer& timer, nlohmann::json& systemConfiguration, 1027 // Gerrit discussion: 1028 // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6 1029 // 1030 // Discord discussion: 1031 // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854 1032 // 1033 // NOLINTNEXTLINE(performance-unnecessary-value-param) 1034 const nlohmann::json newConfiguration, 1035 sdbusplus::asio::object_server& objServer) 1036 { 1037 loadOverlays(newConfiguration); 1038 1039 boost::asio::post(io, [systemConfiguration]() { 1040 if (!writeJsonFiles(systemConfiguration)) 1041 { 1042 std::cerr << "Error writing json files\n"; 1043 } 1044 }); 1045 1046 boost::asio::post(io, [&instance, count, &timer, newConfiguration, 1047 &systemConfiguration, &objServer]() { 1048 postToDbus(newConfiguration, systemConfiguration, objServer); 1049 if (count == instance) 1050 { 1051 startRemovedTimer(timer, systemConfiguration); 1052 } 1053 }); 1054 } 1055 1056 // main properties changed entry 1057 void propertiesChangedCallback(nlohmann::json& systemConfiguration, 1058 sdbusplus::asio::object_server& objServer) 1059 { 1060 static bool inProgress = false; 1061 static boost::asio::steady_timer timer(io); 1062 static size_t instance = 0; 1063 instance++; 1064 size_t count = instance; 1065 1066 timer.expires_after(std::chrono::seconds(5)); 1067 1068 // setup an async wait as we normally get flooded with new requests 1069 timer.async_wait([&systemConfiguration, &objServer, 1070 count](const boost::system::error_code& ec) { 1071 if (ec == boost::asio::error::operation_aborted) 1072 { 1073 // we were cancelled 1074 return; 1075 } 1076 if (ec) 1077 { 1078 std::cerr << "async wait error " << ec << "\n"; 1079 return; 1080 } 1081 1082 if (inProgress) 1083 { 1084 propertiesChangedCallback(systemConfiguration, objServer); 1085 return; 1086 } 1087 inProgress = true; 1088 1089 nlohmann::json oldConfiguration = systemConfiguration; 1090 auto missingConfigurations = std::make_shared<nlohmann::json>(); 1091 *missingConfigurations = systemConfiguration; 1092 1093 std::list<nlohmann::json> configurations; 1094 if (!loadConfigurations(configurations)) 1095 { 1096 std::cerr << "Could not load configurations\n"; 1097 inProgress = false; 1098 return; 1099 } 1100 1101 auto perfScan = std::make_shared<PerformScan>( 1102 systemConfiguration, *missingConfigurations, configurations, 1103 objServer, 1104 [&systemConfiguration, &objServer, count, oldConfiguration, 1105 missingConfigurations]() { 1106 // this is something that since ac has been applied to the bmc 1107 // we saw, and we no longer see it 1108 bool powerOff = !isPowerOn(); 1109 for (const auto& [name, device] : missingConfigurations->items()) 1110 { 1111 pruneConfiguration(systemConfiguration, objServer, powerOff, 1112 name, device); 1113 } 1114 1115 nlohmann::json newConfiguration = systemConfiguration; 1116 1117 deriveNewConfiguration(oldConfiguration, newConfiguration); 1118 1119 for (const auto& [_, device] : newConfiguration.items()) 1120 { 1121 logDeviceAdded(device); 1122 } 1123 1124 inProgress = false; 1125 1126 boost::asio::post( 1127 io, std::bind_front(publishNewConfiguration, std::ref(instance), 1128 count, std::ref(timer), 1129 std::ref(systemConfiguration), 1130 newConfiguration, std::ref(objServer))); 1131 }); 1132 perfScan->run(); 1133 }); 1134 } 1135 1136 // Extract the D-Bus interfaces to probe from the JSON config files. 1137 static std::set<std::string> getProbeInterfaces() 1138 { 1139 std::set<std::string> interfaces; 1140 std::list<nlohmann::json> configurations; 1141 if (!loadConfigurations(configurations)) 1142 { 1143 return interfaces; 1144 } 1145 1146 for (auto it = configurations.begin(); it != configurations.end();) 1147 { 1148 auto findProbe = it->find("Probe"); 1149 if (findProbe == it->end()) 1150 { 1151 std::cerr << "configuration file missing probe:\n " << *it << "\n"; 1152 it++; 1153 continue; 1154 } 1155 1156 nlohmann::json probeCommand; 1157 if ((*findProbe).type() != nlohmann::json::value_t::array) 1158 { 1159 probeCommand = nlohmann::json::array(); 1160 probeCommand.push_back(*findProbe); 1161 } 1162 else 1163 { 1164 probeCommand = *findProbe; 1165 } 1166 1167 for (const nlohmann::json& probeJson : probeCommand) 1168 { 1169 const std::string* probe = probeJson.get_ptr<const std::string*>(); 1170 if (probe == nullptr) 1171 { 1172 std::cerr << "Probe statement wasn't a string, can't parse"; 1173 continue; 1174 } 1175 // Skip it if the probe cmd doesn't contain an interface. 1176 if (findProbeType(*probe)) 1177 { 1178 continue; 1179 } 1180 1181 // syntax requires probe before first open brace 1182 auto findStart = probe->find('('); 1183 if (findStart != std::string::npos) 1184 { 1185 std::string interface = probe->substr(0, findStart); 1186 interfaces.emplace(interface); 1187 } 1188 } 1189 it++; 1190 } 1191 1192 return interfaces; 1193 } 1194 1195 // Check if InterfacesAdded payload contains an iface that needs probing. 1196 static bool 1197 iaContainsProbeInterface(sdbusplus::message_t& msg, 1198 const std::set<std::string>& probeInterfaces) 1199 { 1200 sdbusplus::message::object_path path; 1201 DBusObject interfaces; 1202 std::set<std::string> interfaceSet; 1203 std::set<std::string> intersect; 1204 1205 msg.read(path, interfaces); 1206 1207 std::for_each(interfaces.begin(), interfaces.end(), 1208 [&interfaceSet](const auto& iface) { 1209 interfaceSet.insert(iface.first); 1210 }); 1211 1212 std::set_intersection(interfaceSet.begin(), interfaceSet.end(), 1213 probeInterfaces.begin(), probeInterfaces.end(), 1214 std::inserter(intersect, intersect.end())); 1215 return !intersect.empty(); 1216 } 1217 1218 // Check if InterfacesRemoved payload contains an iface that needs probing. 1219 static bool 1220 irContainsProbeInterface(sdbusplus::message_t& msg, 1221 const std::set<std::string>& probeInterfaces) 1222 { 1223 sdbusplus::message::object_path path; 1224 std::set<std::string> interfaces; 1225 std::set<std::string> intersect; 1226 1227 msg.read(path, interfaces); 1228 1229 std::set_intersection(interfaces.begin(), interfaces.end(), 1230 probeInterfaces.begin(), probeInterfaces.end(), 1231 std::inserter(intersect, intersect.end())); 1232 return !intersect.empty(); 1233 } 1234 1235 int main() 1236 { 1237 // setup connection to dbus 1238 systemBus = std::make_shared<sdbusplus::asio::connection>(io); 1239 systemBus->request_name("xyz.openbmc_project.EntityManager"); 1240 1241 // The EntityManager object itself doesn't expose any properties. 1242 // No need to set up ObjectManager for the |EntityManager| object. 1243 sdbusplus::asio::object_server objServer(systemBus, /*skipManager=*/true); 1244 1245 // All other objects that EntityManager currently support are under the 1246 // inventory subtree. 1247 // See the discussion at 1248 // https://discord.com/channels/775381525260664832/1018929092009144380 1249 objServer.add_manager("/xyz/openbmc_project/inventory"); 1250 1251 std::shared_ptr<sdbusplus::asio::dbus_interface> entityIface = 1252 objServer.add_interface("/xyz/openbmc_project/EntityManager", 1253 "xyz.openbmc_project.EntityManager"); 1254 1255 // to keep reference to the match / filter objects so they don't get 1256 // destroyed 1257 1258 nlohmann::json systemConfiguration = nlohmann::json::object(); 1259 1260 std::set<std::string> probeInterfaces = getProbeInterfaces(); 1261 1262 // We need a poke from DBus for static providers that create all their 1263 // objects prior to claiming a well-known name, and thus don't emit any 1264 // org.freedesktop.DBus.Properties signals. Similarly if a process exits 1265 // for any reason, expected or otherwise, we'll need a poke to remove 1266 // entities from DBus. 1267 sdbusplus::bus::match_t nameOwnerChangedMatch( 1268 static_cast<sdbusplus::bus_t&>(*systemBus), 1269 sdbusplus::bus::match::rules::nameOwnerChanged(), 1270 [&](sdbusplus::message_t& m) { 1271 auto [name, oldOwner, 1272 newOwner] = m.unpack<std::string, std::string, std::string>(); 1273 1274 if (name.starts_with(':')) 1275 { 1276 // We should do nothing with unique-name connections. 1277 return; 1278 } 1279 1280 propertiesChangedCallback(systemConfiguration, objServer); 1281 }); 1282 // We also need a poke from DBus when new interfaces are created or 1283 // destroyed. 1284 sdbusplus::bus::match_t interfacesAddedMatch( 1285 static_cast<sdbusplus::bus_t&>(*systemBus), 1286 sdbusplus::bus::match::rules::interfacesAdded(), 1287 [&](sdbusplus::message_t& msg) { 1288 if (iaContainsProbeInterface(msg, probeInterfaces)) 1289 { 1290 propertiesChangedCallback(systemConfiguration, objServer); 1291 } 1292 }); 1293 sdbusplus::bus::match_t interfacesRemovedMatch( 1294 static_cast<sdbusplus::bus_t&>(*systemBus), 1295 sdbusplus::bus::match::rules::interfacesRemoved(), 1296 [&](sdbusplus::message_t& msg) { 1297 if (irContainsProbeInterface(msg, probeInterfaces)) 1298 { 1299 propertiesChangedCallback(systemConfiguration, objServer); 1300 } 1301 }); 1302 1303 boost::asio::post(io, [&]() { 1304 propertiesChangedCallback(systemConfiguration, objServer); 1305 }); 1306 1307 entityIface->register_method("ReScan", [&]() { 1308 propertiesChangedCallback(systemConfiguration, objServer); 1309 }); 1310 tryIfaceInitialize(entityIface); 1311 1312 if (fwVersionIsSame()) 1313 { 1314 if (std::filesystem::is_regular_file(currentConfiguration)) 1315 { 1316 // this file could just be deleted, but it's nice for debug 1317 std::filesystem::create_directory(tempConfigDir); 1318 std::filesystem::remove(lastConfiguration); 1319 std::filesystem::copy(currentConfiguration, lastConfiguration); 1320 std::filesystem::remove(currentConfiguration); 1321 1322 std::ifstream jsonStream(lastConfiguration); 1323 if (jsonStream.good()) 1324 { 1325 auto data = nlohmann::json::parse(jsonStream, nullptr, false); 1326 if (data.is_discarded()) 1327 { 1328 std::cerr << "syntax error in " << lastConfiguration 1329 << "\n"; 1330 } 1331 else 1332 { 1333 lastJson = std::move(data); 1334 } 1335 } 1336 else 1337 { 1338 std::cerr << "unable to open " << lastConfiguration << "\n"; 1339 } 1340 } 1341 } 1342 else 1343 { 1344 // not an error, just logging at this level to make it in the journal 1345 std::cerr << "Clearing previous configuration\n"; 1346 std::filesystem::remove(currentConfiguration); 1347 } 1348 1349 // some boards only show up after power is on, we want to not say they are 1350 // removed until the same state happens 1351 setupPowerMatch(systemBus); 1352 1353 io.run(); 1354 1355 return 0; 1356 } 1357