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