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, true); 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 true); 851 if (schema.is_discarded()) 852 { 853 std::cerr 854 << "Illegal schema file detected, cannot validate JSON, exiting\n"; 855 std::exit(EXIT_FAILURE); 856 return false; 857 } 858 859 for (auto& jsonPath : jsonPaths) 860 { 861 std::ifstream jsonStream(jsonPath.c_str()); 862 if (!jsonStream.good()) 863 { 864 std::cerr << "unable to open " << jsonPath.string() << "\n"; 865 continue; 866 } 867 auto data = nlohmann::json::parse(jsonStream, nullptr, false, true); 868 if (data.is_discarded()) 869 { 870 std::cerr << "syntax error in " << jsonPath.string() << "\n"; 871 continue; 872 } 873 /* 874 * todo(james): reenable this once less things are in flight 875 * 876 if (!validateJson(schema, data)) 877 { 878 std::cerr << "Error validating " << jsonPath.string() << "\n"; 879 continue; 880 } 881 */ 882 883 if (data.type() == nlohmann::json::value_t::array) 884 { 885 for (auto& d : data) 886 { 887 configurations.emplace_back(d); 888 } 889 } 890 else 891 { 892 configurations.emplace_back(data); 893 } 894 } 895 return true; 896 } 897 898 static bool deviceRequiresPowerOn(const nlohmann::json& entity) 899 { 900 auto powerState = entity.find("PowerState"); 901 if (powerState == entity.end()) 902 { 903 return false; 904 } 905 906 const auto* ptr = powerState->get_ptr<const std::string*>(); 907 if (ptr == nullptr) 908 { 909 return false; 910 } 911 912 return *ptr == "On" || *ptr == "BiosPost"; 913 } 914 915 static void pruneDevice(const nlohmann::json& systemConfiguration, 916 const bool powerOff, const bool scannedPowerOff, 917 const std::string& name, const nlohmann::json& device) 918 { 919 if (systemConfiguration.contains(name)) 920 { 921 return; 922 } 923 924 if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff)) 925 { 926 return; 927 } 928 929 logDeviceRemoved(device); 930 } 931 932 void startRemovedTimer(boost::asio::steady_timer& timer, 933 nlohmann::json& systemConfiguration) 934 { 935 static bool scannedPowerOff = false; 936 static bool scannedPowerOn = false; 937 938 if (systemConfiguration.empty() || lastJson.empty()) 939 { 940 return; // not ready yet 941 } 942 if (scannedPowerOn) 943 { 944 return; 945 } 946 947 if (!isPowerOn() && scannedPowerOff) 948 { 949 return; 950 } 951 952 timer.expires_after(std::chrono::seconds(10)); 953 timer.async_wait( 954 [&systemConfiguration](const boost::system::error_code& ec) { 955 if (ec == boost::asio::error::operation_aborted) 956 { 957 return; 958 } 959 960 bool powerOff = !isPowerOn(); 961 for (const auto& [name, device] : lastJson.items()) 962 { 963 pruneDevice(systemConfiguration, powerOff, scannedPowerOff, name, 964 device); 965 } 966 967 scannedPowerOff = true; 968 if (!powerOff) 969 { 970 scannedPowerOn = true; 971 } 972 }); 973 } 974 975 static std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>& 976 getDeviceInterfaces(const nlohmann::json& device) 977 { 978 return inventory[device["Name"].get<std::string>()]; 979 } 980 981 static void pruneConfiguration(nlohmann::json& systemConfiguration, 982 sdbusplus::asio::object_server& objServer, 983 bool powerOff, const std::string& name, 984 const nlohmann::json& device) 985 { 986 if (powerOff && deviceRequiresPowerOn(device)) 987 { 988 // power not on yet, don't know if it's there or not 989 return; 990 } 991 992 auto& ifaces = getDeviceInterfaces(device); 993 for (auto& iface : ifaces) 994 { 995 auto sharedPtr = iface.lock(); 996 if (!!sharedPtr) 997 { 998 objServer.remove_interface(sharedPtr); 999 } 1000 } 1001 1002 ifaces.clear(); 1003 systemConfiguration.erase(name); 1004 topology.remove(device["Name"].get<std::string>()); 1005 logDeviceRemoved(device); 1006 } 1007 1008 static void deriveNewConfiguration(const nlohmann::json& oldConfiguration, 1009 nlohmann::json& newConfiguration) 1010 { 1011 for (auto it = newConfiguration.begin(); it != newConfiguration.end();) 1012 { 1013 auto findKey = oldConfiguration.find(it.key()); 1014 if (findKey != oldConfiguration.end()) 1015 { 1016 it = newConfiguration.erase(it); 1017 } 1018 else 1019 { 1020 it++; 1021 } 1022 } 1023 } 1024 1025 static void publishNewConfiguration( 1026 const size_t& instance, const size_t count, 1027 boost::asio::steady_timer& timer, nlohmann::json& systemConfiguration, 1028 // Gerrit discussion: 1029 // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6 1030 // 1031 // Discord discussion: 1032 // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854 1033 // 1034 // NOLINTNEXTLINE(performance-unnecessary-value-param) 1035 const nlohmann::json newConfiguration, 1036 sdbusplus::asio::object_server& objServer) 1037 { 1038 loadOverlays(newConfiguration); 1039 1040 boost::asio::post(io, [systemConfiguration]() { 1041 if (!writeJsonFiles(systemConfiguration)) 1042 { 1043 std::cerr << "Error writing json files\n"; 1044 } 1045 }); 1046 1047 boost::asio::post(io, [&instance, count, &timer, newConfiguration, 1048 &systemConfiguration, &objServer]() { 1049 postToDbus(newConfiguration, systemConfiguration, objServer); 1050 if (count == instance) 1051 { 1052 startRemovedTimer(timer, systemConfiguration); 1053 } 1054 }); 1055 } 1056 1057 // main properties changed entry 1058 void propertiesChangedCallback(nlohmann::json& systemConfiguration, 1059 sdbusplus::asio::object_server& objServer) 1060 { 1061 static bool inProgress = false; 1062 static boost::asio::steady_timer timer(io); 1063 static size_t instance = 0; 1064 instance++; 1065 size_t count = instance; 1066 1067 timer.expires_after(std::chrono::seconds(5)); 1068 1069 // setup an async wait as we normally get flooded with new requests 1070 timer.async_wait([&systemConfiguration, &objServer, 1071 count](const boost::system::error_code& ec) { 1072 if (ec == boost::asio::error::operation_aborted) 1073 { 1074 // we were cancelled 1075 return; 1076 } 1077 if (ec) 1078 { 1079 std::cerr << "async wait error " << ec << "\n"; 1080 return; 1081 } 1082 1083 if (inProgress) 1084 { 1085 propertiesChangedCallback(systemConfiguration, objServer); 1086 return; 1087 } 1088 inProgress = true; 1089 1090 nlohmann::json oldConfiguration = systemConfiguration; 1091 auto missingConfigurations = std::make_shared<nlohmann::json>(); 1092 *missingConfigurations = systemConfiguration; 1093 1094 std::list<nlohmann::json> configurations; 1095 if (!loadConfigurations(configurations)) 1096 { 1097 std::cerr << "Could not load configurations\n"; 1098 inProgress = false; 1099 return; 1100 } 1101 1102 auto perfScan = std::make_shared<PerformScan>( 1103 systemConfiguration, *missingConfigurations, configurations, 1104 objServer, 1105 [&systemConfiguration, &objServer, count, oldConfiguration, 1106 missingConfigurations]() { 1107 // this is something that since ac has been applied to the bmc 1108 // we saw, and we no longer see it 1109 bool powerOff = !isPowerOn(); 1110 for (const auto& [name, device] : missingConfigurations->items()) 1111 { 1112 pruneConfiguration(systemConfiguration, objServer, powerOff, 1113 name, device); 1114 } 1115 1116 nlohmann::json newConfiguration = systemConfiguration; 1117 1118 deriveNewConfiguration(oldConfiguration, newConfiguration); 1119 1120 for (const auto& [_, device] : newConfiguration.items()) 1121 { 1122 logDeviceAdded(device); 1123 } 1124 1125 inProgress = false; 1126 1127 boost::asio::post( 1128 io, std::bind_front(publishNewConfiguration, std::ref(instance), 1129 count, std::ref(timer), 1130 std::ref(systemConfiguration), 1131 newConfiguration, std::ref(objServer))); 1132 }); 1133 perfScan->run(); 1134 }); 1135 } 1136 1137 // Extract the D-Bus interfaces to probe from the JSON config files. 1138 static std::set<std::string> getProbeInterfaces() 1139 { 1140 std::set<std::string> interfaces; 1141 std::list<nlohmann::json> configurations; 1142 if (!loadConfigurations(configurations)) 1143 { 1144 return interfaces; 1145 } 1146 1147 for (auto it = configurations.begin(); it != configurations.end();) 1148 { 1149 auto findProbe = it->find("Probe"); 1150 if (findProbe == it->end()) 1151 { 1152 std::cerr << "configuration file missing probe:\n " << *it << "\n"; 1153 it++; 1154 continue; 1155 } 1156 1157 nlohmann::json probeCommand; 1158 if ((*findProbe).type() != nlohmann::json::value_t::array) 1159 { 1160 probeCommand = nlohmann::json::array(); 1161 probeCommand.push_back(*findProbe); 1162 } 1163 else 1164 { 1165 probeCommand = *findProbe; 1166 } 1167 1168 for (const nlohmann::json& probeJson : probeCommand) 1169 { 1170 const std::string* probe = probeJson.get_ptr<const std::string*>(); 1171 if (probe == nullptr) 1172 { 1173 std::cerr << "Probe statement wasn't a string, can't parse"; 1174 continue; 1175 } 1176 // Skip it if the probe cmd doesn't contain an interface. 1177 if (findProbeType(*probe)) 1178 { 1179 continue; 1180 } 1181 1182 // syntax requires probe before first open brace 1183 auto findStart = probe->find('('); 1184 if (findStart != std::string::npos) 1185 { 1186 std::string interface = probe->substr(0, findStart); 1187 interfaces.emplace(interface); 1188 } 1189 } 1190 it++; 1191 } 1192 1193 return interfaces; 1194 } 1195 1196 // Check if InterfacesAdded payload contains an iface that needs probing. 1197 static bool 1198 iaContainsProbeInterface(sdbusplus::message_t& msg, 1199 const std::set<std::string>& probeInterfaces) 1200 { 1201 sdbusplus::message::object_path path; 1202 DBusObject interfaces; 1203 std::set<std::string> interfaceSet; 1204 std::set<std::string> intersect; 1205 1206 msg.read(path, interfaces); 1207 1208 std::for_each(interfaces.begin(), interfaces.end(), 1209 [&interfaceSet](const auto& iface) { 1210 interfaceSet.insert(iface.first); 1211 }); 1212 1213 std::set_intersection(interfaceSet.begin(), interfaceSet.end(), 1214 probeInterfaces.begin(), probeInterfaces.end(), 1215 std::inserter(intersect, intersect.end())); 1216 return !intersect.empty(); 1217 } 1218 1219 // Check if InterfacesRemoved payload contains an iface that needs probing. 1220 static bool 1221 irContainsProbeInterface(sdbusplus::message_t& msg, 1222 const std::set<std::string>& probeInterfaces) 1223 { 1224 sdbusplus::message::object_path path; 1225 std::set<std::string> interfaces; 1226 std::set<std::string> intersect; 1227 1228 msg.read(path, interfaces); 1229 1230 std::set_intersection(interfaces.begin(), interfaces.end(), 1231 probeInterfaces.begin(), probeInterfaces.end(), 1232 std::inserter(intersect, intersect.end())); 1233 return !intersect.empty(); 1234 } 1235 1236 int main() 1237 { 1238 // setup connection to dbus 1239 systemBus = std::make_shared<sdbusplus::asio::connection>(io); 1240 systemBus->request_name("xyz.openbmc_project.EntityManager"); 1241 1242 // The EntityManager object itself doesn't expose any properties. 1243 // No need to set up ObjectManager for the |EntityManager| object. 1244 sdbusplus::asio::object_server objServer(systemBus, /*skipManager=*/true); 1245 1246 // All other objects that EntityManager currently support are under the 1247 // inventory subtree. 1248 // See the discussion at 1249 // https://discord.com/channels/775381525260664832/1018929092009144380 1250 objServer.add_manager("/xyz/openbmc_project/inventory"); 1251 1252 std::shared_ptr<sdbusplus::asio::dbus_interface> entityIface = 1253 objServer.add_interface("/xyz/openbmc_project/EntityManager", 1254 "xyz.openbmc_project.EntityManager"); 1255 1256 // to keep reference to the match / filter objects so they don't get 1257 // destroyed 1258 1259 nlohmann::json systemConfiguration = nlohmann::json::object(); 1260 1261 std::set<std::string> probeInterfaces = getProbeInterfaces(); 1262 1263 // We need a poke from DBus for static providers that create all their 1264 // objects prior to claiming a well-known name, and thus don't emit any 1265 // org.freedesktop.DBus.Properties signals. Similarly if a process exits 1266 // for any reason, expected or otherwise, we'll need a poke to remove 1267 // entities from DBus. 1268 sdbusplus::bus::match_t nameOwnerChangedMatch( 1269 static_cast<sdbusplus::bus_t&>(*systemBus), 1270 sdbusplus::bus::match::rules::nameOwnerChanged(), 1271 [&](sdbusplus::message_t& m) { 1272 auto [name, oldOwner, 1273 newOwner] = m.unpack<std::string, std::string, std::string>(); 1274 1275 if (name.starts_with(':')) 1276 { 1277 // We should do nothing with unique-name connections. 1278 return; 1279 } 1280 1281 propertiesChangedCallback(systemConfiguration, objServer); 1282 }); 1283 // We also need a poke from DBus when new interfaces are created or 1284 // destroyed. 1285 sdbusplus::bus::match_t interfacesAddedMatch( 1286 static_cast<sdbusplus::bus_t&>(*systemBus), 1287 sdbusplus::bus::match::rules::interfacesAdded(), 1288 [&](sdbusplus::message_t& msg) { 1289 if (iaContainsProbeInterface(msg, probeInterfaces)) 1290 { 1291 propertiesChangedCallback(systemConfiguration, objServer); 1292 } 1293 }); 1294 sdbusplus::bus::match_t interfacesRemovedMatch( 1295 static_cast<sdbusplus::bus_t&>(*systemBus), 1296 sdbusplus::bus::match::rules::interfacesRemoved(), 1297 [&](sdbusplus::message_t& msg) { 1298 if (irContainsProbeInterface(msg, probeInterfaces)) 1299 { 1300 propertiesChangedCallback(systemConfiguration, objServer); 1301 } 1302 }); 1303 1304 boost::asio::post(io, [&]() { 1305 propertiesChangedCallback(systemConfiguration, objServer); 1306 }); 1307 1308 entityIface->register_method("ReScan", [&]() { 1309 propertiesChangedCallback(systemConfiguration, objServer); 1310 }); 1311 tryIfaceInitialize(entityIface); 1312 1313 if (fwVersionIsSame()) 1314 { 1315 if (std::filesystem::is_regular_file(currentConfiguration)) 1316 { 1317 // this file could just be deleted, but it's nice for debug 1318 std::filesystem::create_directory(tempConfigDir); 1319 std::filesystem::remove(lastConfiguration); 1320 std::filesystem::copy(currentConfiguration, lastConfiguration); 1321 std::filesystem::remove(currentConfiguration); 1322 1323 std::ifstream jsonStream(lastConfiguration); 1324 if (jsonStream.good()) 1325 { 1326 auto data = nlohmann::json::parse(jsonStream, nullptr, false); 1327 if (data.is_discarded()) 1328 { 1329 std::cerr << "syntax error in " << lastConfiguration 1330 << "\n"; 1331 } 1332 else 1333 { 1334 lastJson = std::move(data); 1335 } 1336 } 1337 else 1338 { 1339 std::cerr << "unable to open " << lastConfiguration << "\n"; 1340 } 1341 } 1342 } 1343 else 1344 { 1345 // not an error, just logging at this level to make it in the journal 1346 std::cerr << "Clearing previous configuration\n"; 1347 std::filesystem::remove(currentConfiguration); 1348 } 1349 1350 // some boards only show up after power is on, we want to not say they are 1351 // removed until the same state happens 1352 setupPowerMatch(systemBus); 1353 1354 io.run(); 1355 1356 return 0; 1357 } 1358