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 80 boost::asio::io_context io; 81 82 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]"); 83 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]"); 84 85 FoundProbeTypeT findProbeType(const std::string& probe) 86 { 87 boost::container::flat_map<const char*, probe_type_codes, 88 CmpStr>::const_iterator probeType; 89 for (probeType = probeTypes.begin(); probeType != probeTypes.end(); 90 ++probeType) 91 { 92 if (probe.find(probeType->first) != std::string::npos) 93 { 94 return probeType; 95 } 96 } 97 98 return std::nullopt; 99 } 100 101 static std::shared_ptr<sdbusplus::asio::dbus_interface> 102 createInterface(sdbusplus::asio::object_server& objServer, 103 const std::string& path, const std::string& interface, 104 const std::string& parent, bool checkNull = false) 105 { 106 // on first add we have no reason to check for null before add, as there 107 // won't be any. For dynamically added interfaces, we check for null so that 108 // a constant delete/add will not create a memory leak 109 110 auto ptr = objServer.add_interface(path, interface); 111 auto& dataVector = inventory[parent]; 112 if (checkNull) 113 { 114 auto it = std::find_if(dataVector.begin(), dataVector.end(), 115 [](const auto& p) { return p.expired(); }); 116 if (it != dataVector.end()) 117 { 118 *it = ptr; 119 return ptr; 120 } 121 } 122 dataVector.emplace_back(ptr); 123 return ptr; 124 } 125 126 // writes output files to persist data 127 bool writeJsonFiles(const nlohmann::json& systemConfiguration) 128 { 129 std::filesystem::create_directory(configurationOutDir); 130 std::ofstream output(currentConfiguration); 131 if (!output.good()) 132 { 133 return false; 134 } 135 output << systemConfiguration.dump(4); 136 output.close(); 137 return true; 138 } 139 140 template <typename JsonType> 141 bool setJsonFromPointer(const std::string& ptrStr, const JsonType& value, 142 nlohmann::json& systemConfiguration) 143 { 144 try 145 { 146 nlohmann::json::json_pointer ptr(ptrStr); 147 nlohmann::json& ref = systemConfiguration[ptr]; 148 ref = value; 149 return true; 150 } 151 catch (const std::out_of_range&) 152 { 153 return false; 154 } 155 } 156 157 // template function to add array as dbus property 158 template <typename PropertyType> 159 void addArrayToDbus(const std::string& name, const nlohmann::json& array, 160 sdbusplus::asio::dbus_interface* iface, 161 sdbusplus::asio::PropertyPermission permission, 162 nlohmann::json& systemConfiguration, 163 const std::string& jsonPointerString) 164 { 165 std::vector<PropertyType> values; 166 for (const auto& property : array) 167 { 168 auto ptr = property.get_ptr<const PropertyType*>(); 169 if (ptr != nullptr) 170 { 171 values.emplace_back(*ptr); 172 } 173 } 174 175 if (permission == sdbusplus::asio::PropertyPermission::readOnly) 176 { 177 iface->register_property(name, values); 178 } 179 else 180 { 181 iface->register_property( 182 name, values, 183 [&systemConfiguration, 184 jsonPointerString{std::string(jsonPointerString)}]( 185 const std::vector<PropertyType>& newVal, 186 std::vector<PropertyType>& val) { 187 val = newVal; 188 if (!setJsonFromPointer(jsonPointerString, val, 189 systemConfiguration)) 190 { 191 std::cerr << "error setting json field\n"; 192 return -1; 193 } 194 if (!writeJsonFiles(systemConfiguration)) 195 { 196 std::cerr << "error setting json file\n"; 197 return -1; 198 } 199 return 1; 200 }); 201 } 202 } 203 204 template <typename PropertyType> 205 void addProperty(const std::string& name, const PropertyType& value, 206 sdbusplus::asio::dbus_interface* iface, 207 nlohmann::json& systemConfiguration, 208 const std::string& jsonPointerString, 209 sdbusplus::asio::PropertyPermission permission) 210 { 211 if (permission == sdbusplus::asio::PropertyPermission::readOnly) 212 { 213 iface->register_property(name, value); 214 return; 215 } 216 iface->register_property( 217 name, value, 218 [&systemConfiguration, 219 jsonPointerString{std::string(jsonPointerString)}]( 220 const PropertyType& newVal, PropertyType& val) { 221 val = newVal; 222 if (!setJsonFromPointer(jsonPointerString, val, 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("Delete", 244 [&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 boost::asio::post(io, [&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 = nlohmann::json::parse(schemaFile, nullptr, 521 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(), dbusName.end(), 547 illegalDbusMemberRegex, "_"); 548 549 std::shared_ptr<sdbusplus::asio::dbus_interface> interface = 550 createInterface(objServer, path + "/" + dbusName, 551 "xyz.openbmc_project.Configuration." + *type, board, 552 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 else if (itemType == "System") 701 { 702 std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface = 703 createInterface(objServer, ifacePath, 704 "xyz.openbmc_project.Inventory.Item.System", 705 boardKeyOrig); 706 populateInterfaceFromJson(systemConfiguration, jsonPointerPath, 707 systemIface, item, objServer, 708 getPermission(itemType)); 709 } 710 711 populateInterfaceFromJson(systemConfiguration, jsonPointerPath, 712 itemIface, item, objServer, 713 getPermission(itemType)); 714 715 for (const auto& [name, config] : item.items()) 716 { 717 jsonPointerPath = jsonPointerPathBoard; 718 jsonPointerPath.append(std::to_string(exposesIndex)) 719 .append("/") 720 .append(name); 721 if (config.type() == nlohmann::json::value_t::object) 722 { 723 std::string ifaceName = 724 "xyz.openbmc_project.Configuration."; 725 ifaceName.append(itemType).append(".").append(name); 726 727 std::shared_ptr<sdbusplus::asio::dbus_interface> 728 objectIface = createInterface(objServer, ifacePath, 729 ifaceName, boardKeyOrig); 730 731 populateInterfaceFromJson( 732 systemConfiguration, jsonPointerPath, objectIface, 733 config, objServer, getPermission(name)); 734 } 735 else if (config.type() == nlohmann::json::value_t::array) 736 { 737 size_t index = 0; 738 if (config.empty()) 739 { 740 continue; 741 } 742 bool isLegal = true; 743 auto type = config[0].type(); 744 if (type != nlohmann::json::value_t::object) 745 { 746 continue; 747 } 748 749 // verify legal json 750 for (const auto& arrayItem : config) 751 { 752 if (arrayItem.type() != type) 753 { 754 isLegal = false; 755 break; 756 } 757 } 758 if (!isLegal) 759 { 760 std::cerr << "dbus format error" << config << "\n"; 761 break; 762 } 763 764 for (auto& arrayItem : config) 765 { 766 std::string ifaceName = 767 "xyz.openbmc_project.Configuration."; 768 ifaceName.append(itemType).append(".").append(name); 769 ifaceName.append(std::to_string(index)); 770 771 std::shared_ptr<sdbusplus::asio::dbus_interface> 772 objectIface = createInterface( 773 objServer, ifacePath, ifaceName, boardKeyOrig); 774 775 populateInterfaceFromJson( 776 systemConfiguration, 777 jsonPointerPath + "/" + std::to_string(index), 778 objectIface, arrayItem, objServer, 779 getPermission(name)); 780 index++; 781 } 782 } 783 } 784 785 topology.addBoard(boardName, boardType, item); 786 } 787 } 788 789 for (const auto& boardAssoc : topology.getAssocs()) 790 { 791 auto ifacePtr = objServer.add_interface( 792 boardAssoc.first, "xyz.openbmc_project.Association.Definitions"); 793 794 ifacePtr->register_property("Associations", boardAssoc.second); 795 ifacePtr->initialize(); 796 } 797 } 798 799 // reads json files out of the filesystem 800 bool loadConfigurations(std::list<nlohmann::json>& configurations) 801 { 802 // find configuration files 803 std::vector<std::filesystem::path> jsonPaths; 804 if (!findFiles( 805 std::vector<std::filesystem::path>{configurationDirectory, 806 hostConfigurationDirectory}, 807 R"(.*\.json)", jsonPaths)) 808 { 809 std::cerr << "Unable to find any configuration files in " 810 << configurationDirectory << "\n"; 811 return false; 812 } 813 814 std::ifstream schemaStream(std::string(schemaDirectory) + "/" + 815 globalSchema); 816 if (!schemaStream.good()) 817 { 818 std::cerr 819 << "Cannot open schema file, cannot validate JSON, exiting\n\n"; 820 std::exit(EXIT_FAILURE); 821 return false; 822 } 823 nlohmann::json schema = nlohmann::json::parse(schemaStream, nullptr, false); 824 if (schema.is_discarded()) 825 { 826 std::cerr 827 << "Illegal schema file detected, cannot validate JSON, exiting\n"; 828 std::exit(EXIT_FAILURE); 829 return false; 830 } 831 832 for (auto& jsonPath : jsonPaths) 833 { 834 std::ifstream jsonStream(jsonPath.c_str()); 835 if (!jsonStream.good()) 836 { 837 std::cerr << "unable to open " << jsonPath.string() << "\n"; 838 continue; 839 } 840 auto data = nlohmann::json::parse(jsonStream, nullptr, false); 841 if (data.is_discarded()) 842 { 843 std::cerr << "syntax error in " << jsonPath.string() << "\n"; 844 continue; 845 } 846 /* 847 * todo(james): reenable this once less things are in flight 848 * 849 if (!validateJson(schema, data)) 850 { 851 std::cerr << "Error validating " << jsonPath.string() << "\n"; 852 continue; 853 } 854 */ 855 856 if (data.type() == nlohmann::json::value_t::array) 857 { 858 for (auto& d : data) 859 { 860 configurations.emplace_back(d); 861 } 862 } 863 else 864 { 865 configurations.emplace_back(data); 866 } 867 } 868 return true; 869 } 870 871 static bool deviceRequiresPowerOn(const nlohmann::json& entity) 872 { 873 auto powerState = entity.find("PowerState"); 874 if (powerState == entity.end()) 875 { 876 return false; 877 } 878 879 const auto* ptr = powerState->get_ptr<const std::string*>(); 880 if (ptr == nullptr) 881 { 882 return false; 883 } 884 885 return *ptr == "On" || *ptr == "BiosPost"; 886 } 887 888 static void pruneDevice(const nlohmann::json& systemConfiguration, 889 const bool powerOff, const bool scannedPowerOff, 890 const std::string& name, const nlohmann::json& device) 891 { 892 if (systemConfiguration.contains(name)) 893 { 894 return; 895 } 896 897 if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff)) 898 { 899 return; 900 } 901 902 logDeviceRemoved(device); 903 } 904 905 void startRemovedTimer(boost::asio::steady_timer& timer, 906 nlohmann::json& systemConfiguration) 907 { 908 static bool scannedPowerOff = false; 909 static bool scannedPowerOn = false; 910 911 if (systemConfiguration.empty() || lastJson.empty()) 912 { 913 return; // not ready yet 914 } 915 if (scannedPowerOn) 916 { 917 return; 918 } 919 920 if (!isPowerOn() && scannedPowerOff) 921 { 922 return; 923 } 924 925 timer.expires_after(std::chrono::seconds(10)); 926 timer.async_wait( 927 [&systemConfiguration](const boost::system::error_code& ec) { 928 if (ec == boost::asio::error::operation_aborted) 929 { 930 return; 931 } 932 933 bool powerOff = !isPowerOn(); 934 for (const auto& [name, device] : lastJson.items()) 935 { 936 pruneDevice(systemConfiguration, powerOff, scannedPowerOff, name, 937 device); 938 } 939 940 scannedPowerOff = true; 941 if (!powerOff) 942 { 943 scannedPowerOn = true; 944 } 945 }); 946 } 947 948 static std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>& 949 getDeviceInterfaces(const nlohmann::json& device) 950 { 951 return inventory[device["Name"].get<std::string>()]; 952 } 953 954 static void pruneConfiguration(nlohmann::json& systemConfiguration, 955 sdbusplus::asio::object_server& objServer, 956 bool powerOff, const std::string& name, 957 const nlohmann::json& device) 958 { 959 if (powerOff && deviceRequiresPowerOn(device)) 960 { 961 // power not on yet, don't know if it's there or not 962 return; 963 } 964 965 auto& ifaces = getDeviceInterfaces(device); 966 for (auto& iface : ifaces) 967 { 968 auto sharedPtr = iface.lock(); 969 if (!!sharedPtr) 970 { 971 objServer.remove_interface(sharedPtr); 972 } 973 } 974 975 ifaces.clear(); 976 systemConfiguration.erase(name); 977 logDeviceRemoved(device); 978 } 979 980 static void deriveNewConfiguration(const nlohmann::json& oldConfiguration, 981 nlohmann::json& newConfiguration) 982 { 983 for (auto it = newConfiguration.begin(); it != newConfiguration.end();) 984 { 985 auto findKey = oldConfiguration.find(it.key()); 986 if (findKey != oldConfiguration.end()) 987 { 988 it = newConfiguration.erase(it); 989 } 990 else 991 { 992 it++; 993 } 994 } 995 } 996 997 static void publishNewConfiguration( 998 const size_t& instance, const size_t count, 999 boost::asio::steady_timer& timer, nlohmann::json& systemConfiguration, 1000 // Gerrit discussion: 1001 // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6 1002 // 1003 // Discord discussion: 1004 // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854 1005 // 1006 // NOLINTNEXTLINE(performance-unnecessary-value-param) 1007 const nlohmann::json newConfiguration, 1008 sdbusplus::asio::object_server& objServer) 1009 { 1010 loadOverlays(newConfiguration); 1011 1012 boost::asio::post(io, [systemConfiguration]() { 1013 if (!writeJsonFiles(systemConfiguration)) 1014 { 1015 std::cerr << "Error writing json files\n"; 1016 } 1017 }); 1018 1019 boost::asio::post(io, [&instance, count, &timer, newConfiguration, 1020 &systemConfiguration, &objServer]() { 1021 postToDbus(newConfiguration, systemConfiguration, objServer); 1022 if (count == instance) 1023 { 1024 startRemovedTimer(timer, systemConfiguration); 1025 } 1026 }); 1027 } 1028 1029 // main properties changed entry 1030 void propertiesChangedCallback(nlohmann::json& systemConfiguration, 1031 sdbusplus::asio::object_server& objServer) 1032 { 1033 static bool inProgress = false; 1034 static boost::asio::steady_timer timer(io); 1035 static size_t instance = 0; 1036 instance++; 1037 size_t count = instance; 1038 1039 timer.expires_after(std::chrono::seconds(5)); 1040 1041 // setup an async wait as we normally get flooded with new requests 1042 timer.async_wait([&systemConfiguration, &objServer, 1043 count](const boost::system::error_code& ec) { 1044 if (ec == boost::asio::error::operation_aborted) 1045 { 1046 // we were cancelled 1047 return; 1048 } 1049 if (ec) 1050 { 1051 std::cerr << "async wait error " << ec << "\n"; 1052 return; 1053 } 1054 1055 if (inProgress) 1056 { 1057 propertiesChangedCallback(systemConfiguration, objServer); 1058 return; 1059 } 1060 inProgress = true; 1061 1062 nlohmann::json oldConfiguration = systemConfiguration; 1063 auto missingConfigurations = std::make_shared<nlohmann::json>(); 1064 *missingConfigurations = systemConfiguration; 1065 1066 std::list<nlohmann::json> configurations; 1067 if (!loadConfigurations(configurations)) 1068 { 1069 std::cerr << "Could not load configurations\n"; 1070 inProgress = false; 1071 return; 1072 } 1073 1074 auto perfScan = std::make_shared<PerformScan>( 1075 systemConfiguration, *missingConfigurations, configurations, 1076 objServer, 1077 [&systemConfiguration, &objServer, count, oldConfiguration, 1078 missingConfigurations]() { 1079 // this is something that since ac has been applied to the bmc 1080 // we saw, and we no longer see it 1081 bool powerOff = !isPowerOn(); 1082 for (const auto& [name, device] : missingConfigurations->items()) 1083 { 1084 pruneConfiguration(systemConfiguration, objServer, powerOff, 1085 name, device); 1086 } 1087 1088 nlohmann::json newConfiguration = systemConfiguration; 1089 1090 deriveNewConfiguration(oldConfiguration, newConfiguration); 1091 1092 for (const auto& [_, device] : newConfiguration.items()) 1093 { 1094 logDeviceAdded(device); 1095 } 1096 1097 inProgress = false; 1098 1099 boost::asio::post( 1100 io, std::bind_front(publishNewConfiguration, std::ref(instance), 1101 count, std::ref(timer), 1102 std::ref(systemConfiguration), 1103 newConfiguration, std::ref(objServer))); 1104 }); 1105 perfScan->run(); 1106 }); 1107 } 1108 1109 // Extract the D-Bus interfaces to probe from the JSON config files. 1110 static std::set<std::string> getProbeInterfaces() 1111 { 1112 std::set<std::string> interfaces; 1113 std::list<nlohmann::json> configurations; 1114 if (!loadConfigurations(configurations)) 1115 { 1116 return interfaces; 1117 } 1118 1119 for (auto it = configurations.begin(); it != configurations.end();) 1120 { 1121 auto findProbe = it->find("Probe"); 1122 if (findProbe == it->end()) 1123 { 1124 std::cerr << "configuration file missing probe:\n " << *it << "\n"; 1125 it++; 1126 continue; 1127 } 1128 1129 nlohmann::json probeCommand; 1130 if ((*findProbe).type() != nlohmann::json::value_t::array) 1131 { 1132 probeCommand = nlohmann::json::array(); 1133 probeCommand.push_back(*findProbe); 1134 } 1135 else 1136 { 1137 probeCommand = *findProbe; 1138 } 1139 1140 for (const nlohmann::json& probeJson : probeCommand) 1141 { 1142 const std::string* probe = probeJson.get_ptr<const std::string*>(); 1143 if (probe == nullptr) 1144 { 1145 std::cerr << "Probe statement wasn't a string, can't parse"; 1146 continue; 1147 } 1148 // Skip it if the probe cmd doesn't contain an interface. 1149 if (findProbeType(*probe)) 1150 { 1151 continue; 1152 } 1153 1154 // syntax requires probe before first open brace 1155 auto findStart = probe->find('('); 1156 if (findStart != std::string::npos) 1157 { 1158 std::string interface = probe->substr(0, findStart); 1159 interfaces.emplace(interface); 1160 } 1161 } 1162 it++; 1163 } 1164 1165 return interfaces; 1166 } 1167 1168 // Check if InterfacesAdded payload contains an iface that needs probing. 1169 static bool 1170 iaContainsProbeInterface(sdbusplus::message_t& msg, 1171 const std::set<std::string>& probeInterfaces) 1172 { 1173 sdbusplus::message::object_path path; 1174 DBusObject interfaces; 1175 std::set<std::string> interfaceSet; 1176 std::set<std::string> intersect; 1177 1178 msg.read(path, interfaces); 1179 1180 std::for_each(interfaces.begin(), interfaces.end(), 1181 [&interfaceSet](const auto& iface) { 1182 interfaceSet.insert(iface.first); 1183 }); 1184 1185 std::set_intersection(interfaceSet.begin(), interfaceSet.end(), 1186 probeInterfaces.begin(), probeInterfaces.end(), 1187 std::inserter(intersect, intersect.end())); 1188 return !intersect.empty(); 1189 } 1190 1191 // Check if InterfacesRemoved payload contains an iface that needs probing. 1192 static bool 1193 irContainsProbeInterface(sdbusplus::message_t& msg, 1194 const std::set<std::string>& probeInterfaces) 1195 { 1196 sdbusplus::message::object_path path; 1197 std::set<std::string> interfaces; 1198 std::set<std::string> intersect; 1199 1200 msg.read(path, interfaces); 1201 1202 std::set_intersection(interfaces.begin(), interfaces.end(), 1203 probeInterfaces.begin(), probeInterfaces.end(), 1204 std::inserter(intersect, intersect.end())); 1205 return !intersect.empty(); 1206 } 1207 1208 int main() 1209 { 1210 // setup connection to dbus 1211 systemBus = std::make_shared<sdbusplus::asio::connection>(io); 1212 systemBus->request_name("xyz.openbmc_project.EntityManager"); 1213 1214 // The EntityManager object itself doesn't expose any properties. 1215 // No need to set up ObjectManager for the |EntityManager| object. 1216 sdbusplus::asio::object_server objServer(systemBus, /*skipManager=*/true); 1217 1218 // All other objects that EntityManager currently support are under the 1219 // inventory subtree. 1220 // See the discussion at 1221 // https://discord.com/channels/775381525260664832/1018929092009144380 1222 objServer.add_manager("/xyz/openbmc_project/inventory"); 1223 1224 std::shared_ptr<sdbusplus::asio::dbus_interface> entityIface = 1225 objServer.add_interface("/xyz/openbmc_project/EntityManager", 1226 "xyz.openbmc_project.EntityManager"); 1227 1228 // to keep reference to the match / filter objects so they don't get 1229 // destroyed 1230 1231 nlohmann::json systemConfiguration = nlohmann::json::object(); 1232 1233 std::set<std::string> probeInterfaces = getProbeInterfaces(); 1234 1235 // We need a poke from DBus for static providers that create all their 1236 // objects prior to claiming a well-known name, and thus don't emit any 1237 // org.freedesktop.DBus.Properties signals. Similarly if a process exits 1238 // for any reason, expected or otherwise, we'll need a poke to remove 1239 // entities from DBus. 1240 sdbusplus::bus::match_t nameOwnerChangedMatch( 1241 static_cast<sdbusplus::bus_t&>(*systemBus), 1242 sdbusplus::bus::match::rules::nameOwnerChanged(), 1243 [&](sdbusplus::message_t& m) { 1244 auto [name, oldOwner, 1245 newOwner] = m.unpack<std::string, std::string, std::string>(); 1246 1247 if (name.starts_with(':')) 1248 { 1249 // We should do nothing with unique-name connections. 1250 return; 1251 } 1252 1253 propertiesChangedCallback(systemConfiguration, objServer); 1254 }); 1255 // We also need a poke from DBus when new interfaces are created or 1256 // destroyed. 1257 sdbusplus::bus::match_t interfacesAddedMatch( 1258 static_cast<sdbusplus::bus_t&>(*systemBus), 1259 sdbusplus::bus::match::rules::interfacesAdded(), 1260 [&](sdbusplus::message_t& msg) { 1261 if (iaContainsProbeInterface(msg, probeInterfaces)) 1262 { 1263 propertiesChangedCallback(systemConfiguration, objServer); 1264 } 1265 }); 1266 sdbusplus::bus::match_t interfacesRemovedMatch( 1267 static_cast<sdbusplus::bus_t&>(*systemBus), 1268 sdbusplus::bus::match::rules::interfacesRemoved(), 1269 [&](sdbusplus::message_t& msg) { 1270 if (irContainsProbeInterface(msg, probeInterfaces)) 1271 { 1272 propertiesChangedCallback(systemConfiguration, objServer); 1273 } 1274 }); 1275 1276 boost::asio::post(io, [&]() { 1277 propertiesChangedCallback(systemConfiguration, objServer); 1278 }); 1279 1280 entityIface->register_method("ReScan", [&]() { 1281 propertiesChangedCallback(systemConfiguration, objServer); 1282 }); 1283 entityIface->initialize(); 1284 1285 if (fwVersionIsSame()) 1286 { 1287 if (std::filesystem::is_regular_file(currentConfiguration)) 1288 { 1289 // this file could just be deleted, but it's nice for debug 1290 std::filesystem::create_directory(tempConfigDir); 1291 std::filesystem::remove(lastConfiguration); 1292 std::filesystem::copy(currentConfiguration, lastConfiguration); 1293 std::filesystem::remove(currentConfiguration); 1294 1295 std::ifstream jsonStream(lastConfiguration); 1296 if (jsonStream.good()) 1297 { 1298 auto data = nlohmann::json::parse(jsonStream, nullptr, false); 1299 if (data.is_discarded()) 1300 { 1301 std::cerr << "syntax error in " << lastConfiguration 1302 << "\n"; 1303 } 1304 else 1305 { 1306 lastJson = std::move(data); 1307 } 1308 } 1309 else 1310 { 1311 std::cerr << "unable to open " << lastConfiguration << "\n"; 1312 } 1313 } 1314 } 1315 else 1316 { 1317 // not an error, just logging at this level to make it in the journal 1318 std::cerr << "Clearing previous configuration\n"; 1319 std::filesystem::remove(currentConfiguration); 1320 } 1321 1322 // some boards only show up after power is on, we want to not say they are 1323 // removed until the same state happens 1324 setupPowerMatch(systemBus); 1325 1326 io.run(); 1327 1328 return 0; 1329 } 1330