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, systemConfiguration)) 241 { 242 std::cerr << "error setting json field\n"; 243 return -1; 244 } 245 if (!writeJsonFiles(systemConfiguration)) 246 { 247 std::cerr << "error setting json file\n"; 248 return -1; 249 } 250 return 1; 251 }); 252 } 253 254 void createDeleteObjectMethod( 255 const std::string& jsonPointerPath, 256 const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface, 257 sdbusplus::asio::object_server& objServer, 258 nlohmann::json& systemConfiguration) 259 { 260 std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface; 261 iface->register_method("Delete", 262 [&objServer, &systemConfiguration, interface, 263 jsonPointerPath{std::string(jsonPointerPath)}]() { 264 std::shared_ptr<sdbusplus::asio::dbus_interface> dbusInterface = 265 interface.lock(); 266 if (!dbusInterface) 267 { 268 // this technically can't happen as the pointer is pointing to 269 // us 270 throw DBusInternalError(); 271 } 272 nlohmann::json::json_pointer ptr(jsonPointerPath); 273 systemConfiguration[ptr] = nullptr; 274 275 // todo(james): dig through sdbusplus to find out why we can't 276 // delete it in a method call 277 boost::asio::post(io, [&objServer, dbusInterface]() mutable { 278 objServer.remove_interface(dbusInterface); 279 }); 280 281 if (!writeJsonFiles(systemConfiguration)) 282 { 283 std::cerr << "error setting json file\n"; 284 throw DBusInternalError(); 285 } 286 }); 287 } 288 289 // adds simple json types to interface's properties 290 void populateInterfaceFromJson( 291 nlohmann::json& systemConfiguration, const std::string& jsonPointerPath, 292 std::shared_ptr<sdbusplus::asio::dbus_interface>& iface, 293 nlohmann::json& dict, sdbusplus::asio::object_server& objServer, 294 sdbusplus::asio::PropertyPermission permission = 295 sdbusplus::asio::PropertyPermission::readOnly) 296 { 297 for (const auto& [key, value] : dict.items()) 298 { 299 auto type = value.type(); 300 bool array = false; 301 if (value.type() == nlohmann::json::value_t::array) 302 { 303 array = true; 304 if (value.empty()) 305 { 306 continue; 307 } 308 type = value[0].type(); 309 bool isLegal = true; 310 for (const auto& arrayItem : value) 311 { 312 if (arrayItem.type() != type) 313 { 314 isLegal = false; 315 break; 316 } 317 } 318 if (!isLegal) 319 { 320 std::cerr << "dbus format error" << value << "\n"; 321 continue; 322 } 323 } 324 if (type == nlohmann::json::value_t::object) 325 { 326 continue; // handled elsewhere 327 } 328 329 std::string path = jsonPointerPath; 330 path.append("/").append(key); 331 if (permission == sdbusplus::asio::PropertyPermission::readWrite) 332 { 333 // all setable numbers are doubles as it is difficult to always 334 // create a configuration file with all whole numbers as decimals 335 // i.e. 1.0 336 if (array) 337 { 338 if (value[0].is_number()) 339 { 340 type = nlohmann::json::value_t::number_float; 341 } 342 } 343 else if (value.is_number()) 344 { 345 type = nlohmann::json::value_t::number_float; 346 } 347 } 348 349 switch (type) 350 { 351 case (nlohmann::json::value_t::boolean): 352 { 353 if (array) 354 { 355 // todo: array of bool isn't detected correctly by 356 // sdbusplus, change it to numbers 357 addArrayToDbus<uint64_t>(key, value, iface.get(), 358 permission, systemConfiguration, 359 path); 360 } 361 362 else 363 { 364 addProperty(key, value.get<bool>(), iface.get(), 365 systemConfiguration, path, permission); 366 } 367 break; 368 } 369 case (nlohmann::json::value_t::number_integer): 370 { 371 if (array) 372 { 373 addArrayToDbus<int64_t>(key, value, iface.get(), permission, 374 systemConfiguration, path); 375 } 376 else 377 { 378 addProperty(key, value.get<int64_t>(), iface.get(), 379 systemConfiguration, path, 380 sdbusplus::asio::PropertyPermission::readOnly); 381 } 382 break; 383 } 384 case (nlohmann::json::value_t::number_unsigned): 385 { 386 if (array) 387 { 388 addArrayToDbus<uint64_t>(key, value, iface.get(), 389 permission, systemConfiguration, 390 path); 391 } 392 else 393 { 394 addProperty(key, value.get<uint64_t>(), iface.get(), 395 systemConfiguration, path, 396 sdbusplus::asio::PropertyPermission::readOnly); 397 } 398 break; 399 } 400 case (nlohmann::json::value_t::number_float): 401 { 402 if (array) 403 { 404 addArrayToDbus<double>(key, value, iface.get(), permission, 405 systemConfiguration, path); 406 } 407 408 else 409 { 410 addProperty(key, value.get<double>(), iface.get(), 411 systemConfiguration, path, permission); 412 } 413 break; 414 } 415 case (nlohmann::json::value_t::string): 416 { 417 if (array) 418 { 419 addArrayToDbus<std::string>(key, value, iface.get(), 420 permission, systemConfiguration, 421 path); 422 } 423 else 424 { 425 addProperty(key, value.get<std::string>(), iface.get(), 426 systemConfiguration, path, permission); 427 } 428 break; 429 } 430 default: 431 { 432 std::cerr << "Unexpected json type in system configuration " 433 << key << ": " << value.type_name() << "\n"; 434 break; 435 } 436 } 437 } 438 if (permission == sdbusplus::asio::PropertyPermission::readWrite) 439 { 440 createDeleteObjectMethod(jsonPointerPath, iface, objServer, 441 systemConfiguration); 442 } 443 tryIfaceInitialize(iface); 444 } 445 446 sdbusplus::asio::PropertyPermission getPermission(const std::string& interface) 447 { 448 return std::find(settableInterfaces.begin(), settableInterfaces.end(), 449 interface) != settableInterfaces.end() 450 ? sdbusplus::asio::PropertyPermission::readWrite 451 : sdbusplus::asio::PropertyPermission::readOnly; 452 } 453 454 void createAddObjectMethod(const std::string& jsonPointerPath, 455 const std::string& path, 456 nlohmann::json& systemConfiguration, 457 sdbusplus::asio::object_server& objServer, 458 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 = nlohmann::json::parse(schemaFile, nullptr, 539 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(), dbusName.end(), 565 illegalDbusMemberRegex, "_"); 566 567 std::shared_ptr<sdbusplus::asio::dbus_interface> interface = 568 createInterface(objServer, path + "/" + dbusName, 569 "xyz.openbmc_project.Configuration." + *type, board, 570 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 std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface = 704 createInterface(objServer, ifacePath, 705 "xyz.openbmc_project.Configuration." + itemType, 706 boardNameOrig); 707 708 if (itemType == "BMC") 709 { 710 std::shared_ptr<sdbusplus::asio::dbus_interface> bmcIface = 711 createInterface(objServer, ifacePath, 712 "xyz.openbmc_project.Inventory.Item.Bmc", 713 boardNameOrig); 714 populateInterfaceFromJson(systemConfiguration, jsonPointerPath, 715 bmcIface, item, objServer, 716 getPermission(itemType)); 717 } 718 else if (itemType == "System") 719 { 720 std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface = 721 createInterface(objServer, ifacePath, 722 "xyz.openbmc_project.Inventory.Item.System", 723 boardNameOrig); 724 populateInterfaceFromJson(systemConfiguration, jsonPointerPath, 725 systemIface, item, objServer, 726 getPermission(itemType)); 727 } 728 729 populateInterfaceFromJson(systemConfiguration, jsonPointerPath, 730 itemIface, item, objServer, 731 getPermission(itemType)); 732 733 for (const auto& [name, config] : item.items()) 734 { 735 jsonPointerPath = jsonPointerPathBoard; 736 jsonPointerPath.append(std::to_string(exposesIndex)) 737 .append("/") 738 .append(name); 739 if (config.type() == nlohmann::json::value_t::object) 740 { 741 std::string ifaceName = 742 "xyz.openbmc_project.Configuration."; 743 ifaceName.append(itemType).append(".").append(name); 744 745 std::shared_ptr<sdbusplus::asio::dbus_interface> 746 objectIface = createInterface(objServer, ifacePath, 747 ifaceName, boardNameOrig); 748 749 populateInterfaceFromJson( 750 systemConfiguration, jsonPointerPath, objectIface, 751 config, objServer, getPermission(name)); 752 } 753 else if (config.type() == nlohmann::json::value_t::array) 754 { 755 size_t index = 0; 756 if (config.empty()) 757 { 758 continue; 759 } 760 bool isLegal = true; 761 auto type = config[0].type(); 762 if (type != nlohmann::json::value_t::object) 763 { 764 continue; 765 } 766 767 // verify legal json 768 for (const auto& arrayItem : config) 769 { 770 if (arrayItem.type() != type) 771 { 772 isLegal = false; 773 break; 774 } 775 } 776 if (!isLegal) 777 { 778 std::cerr << "dbus format error" << config << "\n"; 779 break; 780 } 781 782 for (auto& arrayItem : config) 783 { 784 std::string ifaceName = 785 "xyz.openbmc_project.Configuration."; 786 ifaceName.append(itemType).append(".").append(name); 787 ifaceName.append(std::to_string(index)); 788 789 std::shared_ptr<sdbusplus::asio::dbus_interface> 790 objectIface = createInterface( 791 objServer, ifacePath, ifaceName, boardNameOrig); 792 793 populateInterfaceFromJson( 794 systemConfiguration, 795 jsonPointerPath + "/" + std::to_string(index), 796 objectIface, arrayItem, objServer, 797 getPermission(name)); 798 index++; 799 } 800 } 801 } 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(std::string(schemaDirectory) + "/" + 843 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 = nlohmann::json::parse(schemaStream, nullptr, false, 852 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, name, 966 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::seconds(5)); 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] : missingConfigurations->items()) 1113 { 1114 pruneConfiguration(systemConfiguration, objServer, powerOff, 1115 name, device); 1116 } 1117 1118 nlohmann::json newConfiguration = systemConfiguration; 1119 1120 deriveNewConfiguration(oldConfiguration, newConfiguration); 1121 1122 for (const auto& [_, device] : newConfiguration.items()) 1123 { 1124 logDeviceAdded(device); 1125 } 1126 1127 inProgress = false; 1128 1129 boost::asio::post( 1130 io, std::bind_front(publishNewConfiguration, std::ref(instance), 1131 count, std::ref(timer), 1132 std::ref(systemConfiguration), 1133 newConfiguration, std::ref(objServer))); 1134 }); 1135 perfScan->run(); 1136 }); 1137 } 1138 1139 // Extract the D-Bus interfaces to probe from the JSON config files. 1140 static std::set<std::string> getProbeInterfaces() 1141 { 1142 std::set<std::string> interfaces; 1143 std::list<nlohmann::json> configurations; 1144 if (!loadConfigurations(configurations)) 1145 { 1146 return interfaces; 1147 } 1148 1149 for (auto it = configurations.begin(); it != configurations.end();) 1150 { 1151 auto findProbe = it->find("Probe"); 1152 if (findProbe == it->end()) 1153 { 1154 std::cerr << "configuration file missing probe:\n " << *it << "\n"; 1155 it++; 1156 continue; 1157 } 1158 1159 nlohmann::json probeCommand; 1160 if ((*findProbe).type() != nlohmann::json::value_t::array) 1161 { 1162 probeCommand = nlohmann::json::array(); 1163 probeCommand.push_back(*findProbe); 1164 } 1165 else 1166 { 1167 probeCommand = *findProbe; 1168 } 1169 1170 for (const nlohmann::json& probeJson : probeCommand) 1171 { 1172 const std::string* probe = probeJson.get_ptr<const std::string*>(); 1173 if (probe == nullptr) 1174 { 1175 std::cerr << "Probe statement wasn't a string, can't parse"; 1176 continue; 1177 } 1178 // Skip it if the probe cmd doesn't contain an interface. 1179 if (findProbeType(*probe)) 1180 { 1181 continue; 1182 } 1183 1184 // syntax requires probe before first open brace 1185 auto findStart = probe->find('('); 1186 if (findStart != std::string::npos) 1187 { 1188 std::string interface = probe->substr(0, findStart); 1189 interfaces.emplace(interface); 1190 } 1191 } 1192 it++; 1193 } 1194 1195 return interfaces; 1196 } 1197 1198 // Check if InterfacesAdded payload contains an iface that needs probing. 1199 static bool 1200 iaContainsProbeInterface(sdbusplus::message_t& msg, 1201 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 1223 irContainsProbeInterface(sdbusplus::message_t& msg, 1224 const std::set<std::string>& probeInterfaces) 1225 { 1226 sdbusplus::message::object_path path; 1227 std::set<std::string> interfaces; 1228 std::set<std::string> intersect; 1229 1230 msg.read(path, interfaces); 1231 1232 std::set_intersection(interfaces.begin(), interfaces.end(), 1233 probeInterfaces.begin(), probeInterfaces.end(), 1234 std::inserter(intersect, intersect.end())); 1235 return !intersect.empty(); 1236 } 1237 1238 int main() 1239 { 1240 // setup connection to dbus 1241 systemBus = std::make_shared<sdbusplus::asio::connection>(io); 1242 systemBus->request_name("xyz.openbmc_project.EntityManager"); 1243 1244 // The EntityManager object itself doesn't expose any properties. 1245 // No need to set up ObjectManager for the |EntityManager| object. 1246 sdbusplus::asio::object_server objServer(systemBus, /*skipManager=*/true); 1247 1248 // All other objects that EntityManager currently support are under the 1249 // inventory subtree. 1250 // See the discussion at 1251 // https://discord.com/channels/775381525260664832/1018929092009144380 1252 objServer.add_manager("/xyz/openbmc_project/inventory"); 1253 1254 std::shared_ptr<sdbusplus::asio::dbus_interface> entityIface = 1255 objServer.add_interface("/xyz/openbmc_project/EntityManager", 1256 "xyz.openbmc_project.EntityManager"); 1257 1258 // to keep reference to the match / filter objects so they don't get 1259 // destroyed 1260 1261 nlohmann::json systemConfiguration = nlohmann::json::object(); 1262 1263 std::set<std::string> probeInterfaces = getProbeInterfaces(); 1264 1265 // We need a poke from DBus for static providers that create all their 1266 // objects prior to claiming a well-known name, and thus don't emit any 1267 // org.freedesktop.DBus.Properties signals. Similarly if a process exits 1268 // for any reason, expected or otherwise, we'll need a poke to remove 1269 // entities from DBus. 1270 sdbusplus::bus::match_t nameOwnerChangedMatch( 1271 static_cast<sdbusplus::bus_t&>(*systemBus), 1272 sdbusplus::bus::match::rules::nameOwnerChanged(), 1273 [&](sdbusplus::message_t& m) { 1274 auto [name, oldOwner, 1275 newOwner] = m.unpack<std::string, std::string, std::string>(); 1276 1277 if (name.starts_with(':')) 1278 { 1279 // We should do nothing with unique-name connections. 1280 return; 1281 } 1282 1283 propertiesChangedCallback(systemConfiguration, objServer); 1284 }); 1285 // We also need a poke from DBus when new interfaces are created or 1286 // destroyed. 1287 sdbusplus::bus::match_t interfacesAddedMatch( 1288 static_cast<sdbusplus::bus_t&>(*systemBus), 1289 sdbusplus::bus::match::rules::interfacesAdded(), 1290 [&](sdbusplus::message_t& msg) { 1291 if (iaContainsProbeInterface(msg, probeInterfaces)) 1292 { 1293 propertiesChangedCallback(systemConfiguration, objServer); 1294 } 1295 }); 1296 sdbusplus::bus::match_t interfacesRemovedMatch( 1297 static_cast<sdbusplus::bus_t&>(*systemBus), 1298 sdbusplus::bus::match::rules::interfacesRemoved(), 1299 [&](sdbusplus::message_t& msg) { 1300 if (irContainsProbeInterface(msg, probeInterfaces)) 1301 { 1302 propertiesChangedCallback(systemConfiguration, objServer); 1303 } 1304 }); 1305 1306 boost::asio::post(io, [&]() { 1307 propertiesChangedCallback(systemConfiguration, objServer); 1308 }); 1309 1310 entityIface->register_method("ReScan", [&]() { 1311 propertiesChangedCallback(systemConfiguration, objServer); 1312 }); 1313 tryIfaceInitialize(entityIface); 1314 1315 if (fwVersionIsSame()) 1316 { 1317 if (std::filesystem::is_regular_file(currentConfiguration)) 1318 { 1319 // this file could just be deleted, but it's nice for debug 1320 std::filesystem::create_directory(tempConfigDir); 1321 std::filesystem::remove(lastConfiguration); 1322 std::filesystem::copy(currentConfiguration, lastConfiguration); 1323 std::filesystem::remove(currentConfiguration); 1324 1325 std::ifstream jsonStream(lastConfiguration); 1326 if (jsonStream.good()) 1327 { 1328 auto data = nlohmann::json::parse(jsonStream, nullptr, false); 1329 if (data.is_discarded()) 1330 { 1331 std::cerr << "syntax error in " << lastConfiguration 1332 << "\n"; 1333 } 1334 else 1335 { 1336 lastJson = std::move(data); 1337 } 1338 } 1339 else 1340 { 1341 std::cerr << "unable to open " << lastConfiguration << "\n"; 1342 } 1343 } 1344 } 1345 else 1346 { 1347 // not an error, just logging at this level to make it in the journal 1348 std::cerr << "Clearing previous configuration\n"; 1349 std::filesystem::remove(currentConfiguration); 1350 } 1351 1352 // some boards only show up after power is on, we want to not say they are 1353 // removed until the same state happens 1354 setupPowerMatch(systemBus); 1355 1356 io.run(); 1357 1358 return 0; 1359 } 1360