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