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