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