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& propertyName, 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(propertyName, value); 212 return; 213 } 214 iface->register_property( 215 propertyName, 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 (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.size()) 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([&newJson](auto&& val) { newJson = std::move(val); }, 465 item.second); 466 } 467 468 auto findName = newData.find("Name"); 469 auto findType = newData.find("Type"); 470 if (findName == newData.end() || findType == newData.end()) 471 { 472 throw std::invalid_argument("AddObject missing Name or Type"); 473 } 474 const std::string* type = findType->get_ptr<const std::string*>(); 475 const std::string* name = findName->get_ptr<const std::string*>(); 476 if (type == nullptr || name == nullptr) 477 { 478 throw std::invalid_argument("Type and Name must be a string."); 479 } 480 481 bool foundNull = false; 482 size_t lastIndex = 0; 483 // we add in the "exposes" 484 for (const auto& expose : *findExposes) 485 { 486 if (expose.is_null()) 487 { 488 foundNull = true; 489 continue; 490 } 491 492 if (expose["Name"] == *name && expose["Type"] == *type) 493 { 494 throw std::invalid_argument( 495 "Field already in JSON, not adding"); 496 } 497 498 if (foundNull) 499 { 500 continue; 501 } 502 503 lastIndex++; 504 } 505 506 std::ifstream schemaFile(std::string(schemaDirectory) + "/" + 507 *type + ".json"); 508 // todo(james) we might want to also make a list of 'can add' 509 // interfaces but for now I think the assumption if there is a 510 // schema avaliable that it is allowed to update is fine 511 if (!schemaFile.good()) 512 { 513 throw std::invalid_argument( 514 "No schema avaliable, cannot validate."); 515 } 516 nlohmann::json schema = 517 nlohmann::json::parse(schemaFile, nullptr, false); 518 if (schema.is_discarded()) 519 { 520 std::cerr << "Schema not legal" << *type << ".json\n"; 521 throw DBusInternalError(); 522 } 523 if (!validateJson(schema, newData)) 524 { 525 throw std::invalid_argument("Data does not match schema"); 526 } 527 if (foundNull) 528 { 529 findExposes->at(lastIndex) = newData; 530 } 531 else 532 { 533 findExposes->push_back(newData); 534 } 535 if (!writeJsonFiles(systemConfiguration)) 536 { 537 std::cerr << "Error writing json files\n"; 538 throw DBusInternalError(); 539 } 540 std::string dbusName = *name; 541 542 std::regex_replace(dbusName.begin(), dbusName.begin(), 543 dbusName.end(), illegalDbusMemberRegex, "_"); 544 545 std::shared_ptr<sdbusplus::asio::dbus_interface> interface = 546 createInterface(objServer, path + "/" + dbusName, 547 "xyz.openbmc_project.Configuration." + *type, 548 board, true); 549 // permission is read-write, as since we just created it, must be 550 // runtime modifiable 551 populateInterfaceFromJson( 552 systemConfiguration, 553 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex), 554 interface, newData, objServer, 555 sdbusplus::asio::PropertyPermission::readWrite); 556 }); 557 iface->initialize(); 558 } 559 560 void postToDbus(const nlohmann::json& newConfiguration, 561 nlohmann::json& systemConfiguration, 562 sdbusplus::asio::object_server& objServer) 563 564 { 565 // iterate through boards 566 for (auto& [boardId, boardConfig] : newConfiguration.items()) 567 { 568 std::string boardKey = boardConfig["Name"]; 569 std::string boardKeyOrig = boardConfig["Name"]; 570 std::string jsonPointerPath = "/" + boardId; 571 // loop through newConfiguration, but use values from system 572 // configuration to be able to modify via dbus later 573 auto boardValues = systemConfiguration[boardId]; 574 auto findBoardType = boardValues.find("Type"); 575 std::string boardType; 576 if (findBoardType != boardValues.end() && 577 findBoardType->type() == nlohmann::json::value_t::string) 578 { 579 boardType = findBoardType->get<std::string>(); 580 std::regex_replace(boardType.begin(), boardType.begin(), 581 boardType.end(), illegalDbusMemberRegex, "_"); 582 } 583 else 584 { 585 std::cerr << "Unable to find type for " << boardKey 586 << " reverting to Chassis.\n"; 587 boardType = "Chassis"; 588 } 589 std::string boardtypeLower = boost::algorithm::to_lower_copy(boardType); 590 591 std::regex_replace(boardKey.begin(), boardKey.begin(), boardKey.end(), 592 illegalDbusMemberRegex, "_"); 593 std::string boardName = "/xyz/openbmc_project/inventory/system/"; 594 boardName += boardtypeLower; 595 boardName += "/"; 596 boardName += boardKey; 597 598 std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface = 599 createInterface(objServer, boardName, 600 "xyz.openbmc_project.Inventory.Item", boardKey); 601 602 std::shared_ptr<sdbusplus::asio::dbus_interface> boardIface = 603 createInterface(objServer, boardName, 604 "xyz.openbmc_project.Inventory.Item." + boardType, 605 boardKeyOrig); 606 607 createAddObjectMethod(jsonPointerPath, boardName, systemConfiguration, 608 objServer, boardKeyOrig); 609 610 populateInterfaceFromJson(systemConfiguration, jsonPointerPath, 611 boardIface, boardValues, objServer); 612 jsonPointerPath += "/"; 613 // iterate through board properties 614 for (auto& [propName, propValue] : boardValues.items()) 615 { 616 if (propValue.type() == nlohmann::json::value_t::object) 617 { 618 std::shared_ptr<sdbusplus::asio::dbus_interface> iface = 619 createInterface(objServer, boardName, propName, 620 boardKeyOrig); 621 622 populateInterfaceFromJson(systemConfiguration, 623 jsonPointerPath + propName, iface, 624 propValue, objServer); 625 } 626 } 627 628 auto exposes = boardValues.find("Exposes"); 629 if (exposes == boardValues.end()) 630 { 631 continue; 632 } 633 // iterate through exposes 634 jsonPointerPath += "Exposes/"; 635 636 // store the board level pointer so we can modify it on the way down 637 std::string jsonPointerPathBoard = jsonPointerPath; 638 size_t exposesIndex = -1; 639 for (auto& item : *exposes) 640 { 641 exposesIndex++; 642 jsonPointerPath = jsonPointerPathBoard; 643 jsonPointerPath += std::to_string(exposesIndex); 644 645 auto findName = item.find("Name"); 646 if (findName == item.end()) 647 { 648 std::cerr << "cannot find name in field " << item << "\n"; 649 continue; 650 } 651 auto findStatus = item.find("Status"); 652 // if status is not found it is assumed to be status = 'okay' 653 if (findStatus != item.end()) 654 { 655 if (*findStatus == "disabled") 656 { 657 continue; 658 } 659 } 660 auto findType = item.find("Type"); 661 std::string itemType; 662 if (findType != item.end()) 663 { 664 itemType = findType->get<std::string>(); 665 std::regex_replace(itemType.begin(), itemType.begin(), 666 itemType.end(), illegalDbusPathRegex, "_"); 667 } 668 else 669 { 670 itemType = "unknown"; 671 } 672 std::string itemName = findName->get<std::string>(); 673 std::regex_replace(itemName.begin(), itemName.begin(), 674 itemName.end(), illegalDbusMemberRegex, "_"); 675 std::string ifacePath = boardName; 676 ifacePath += "/"; 677 ifacePath += itemName; 678 679 std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface = 680 createInterface(objServer, ifacePath, 681 "xyz.openbmc_project.Configuration." + itemType, 682 boardKeyOrig); 683 684 populateInterfaceFromJson(systemConfiguration, jsonPointerPath, 685 itemIface, item, objServer, 686 getPermission(itemType)); 687 688 for (auto& [name, config] : item.items()) 689 { 690 jsonPointerPath = jsonPointerPathBoard; 691 jsonPointerPath.append(std::to_string(exposesIndex)) 692 .append("/") 693 .append(name); 694 if (config.type() == nlohmann::json::value_t::object) 695 { 696 std::string ifaceName = 697 "xyz.openbmc_project.Configuration."; 698 ifaceName.append(itemType).append(".").append(name); 699 700 std::shared_ptr<sdbusplus::asio::dbus_interface> 701 objectIface = createInterface(objServer, ifacePath, 702 ifaceName, boardKeyOrig); 703 704 populateInterfaceFromJson( 705 systemConfiguration, jsonPointerPath, objectIface, 706 config, objServer, getPermission(name)); 707 } 708 else if (config.type() == nlohmann::json::value_t::array) 709 { 710 size_t index = 0; 711 if (!config.size()) 712 { 713 continue; 714 } 715 bool isLegal = true; 716 auto type = config[0].type(); 717 if (type != nlohmann::json::value_t::object) 718 { 719 continue; 720 } 721 722 // verify legal json 723 for (const auto& arrayItem : config) 724 { 725 if (arrayItem.type() != type) 726 { 727 isLegal = false; 728 break; 729 } 730 } 731 if (!isLegal) 732 { 733 std::cerr << "dbus format error" << config << "\n"; 734 break; 735 } 736 737 for (auto& arrayItem : config) 738 { 739 std::string ifaceName = 740 "xyz.openbmc_project.Configuration."; 741 ifaceName.append(itemType).append(".").append(name); 742 ifaceName.append(std::to_string(index)); 743 744 std::shared_ptr<sdbusplus::asio::dbus_interface> 745 objectIface = createInterface( 746 objServer, ifacePath, ifaceName, boardKeyOrig); 747 748 populateInterfaceFromJson( 749 systemConfiguration, 750 jsonPointerPath + "/" + std::to_string(index), 751 objectIface, arrayItem, objServer, 752 getPermission(name)); 753 index++; 754 } 755 } 756 } 757 } 758 } 759 } 760 761 // reads json files out of the filesystem 762 bool loadConfigurations(std::list<nlohmann::json>& configurations) 763 { 764 // find configuration files 765 std::vector<std::filesystem::path> jsonPaths; 766 if (!findFiles( 767 std::vector<std::filesystem::path>{configurationDirectory, 768 hostConfigurationDirectory}, 769 R"(.*\.json)", jsonPaths)) 770 { 771 std::cerr << "Unable to find any configuration files in " 772 << configurationDirectory << "\n"; 773 return false; 774 } 775 776 std::ifstream schemaStream(std::string(schemaDirectory) + "/" + 777 globalSchema); 778 if (!schemaStream.good()) 779 { 780 std::cerr 781 << "Cannot open schema file, cannot validate JSON, exiting\n\n"; 782 std::exit(EXIT_FAILURE); 783 return false; 784 } 785 nlohmann::json schema = nlohmann::json::parse(schemaStream, nullptr, false); 786 if (schema.is_discarded()) 787 { 788 std::cerr 789 << "Illegal schema file detected, cannot validate JSON, exiting\n"; 790 std::exit(EXIT_FAILURE); 791 return false; 792 } 793 794 for (auto& jsonPath : jsonPaths) 795 { 796 std::ifstream jsonStream(jsonPath.c_str()); 797 if (!jsonStream.good()) 798 { 799 std::cerr << "unable to open " << jsonPath.string() << "\n"; 800 continue; 801 } 802 auto data = nlohmann::json::parse(jsonStream, nullptr, false); 803 if (data.is_discarded()) 804 { 805 std::cerr << "syntax error in " << jsonPath.string() << "\n"; 806 continue; 807 } 808 /* 809 * todo(james): reenable this once less things are in flight 810 * 811 if (!validateJson(schema, data)) 812 { 813 std::cerr << "Error validating " << jsonPath.string() << "\n"; 814 continue; 815 } 816 */ 817 818 if (data.type() == nlohmann::json::value_t::array) 819 { 820 for (auto& d : data) 821 { 822 configurations.emplace_back(d); 823 } 824 } 825 else 826 { 827 configurations.emplace_back(data); 828 } 829 } 830 return true; 831 } 832 833 static bool deviceRequiresPowerOn(const nlohmann::json& entity) 834 { 835 auto powerState = entity.find("PowerState"); 836 if (powerState == entity.end()) 837 { 838 return false; 839 } 840 841 auto ptr = powerState->get_ptr<const std::string*>(); 842 if (!ptr) 843 { 844 return false; 845 } 846 847 return *ptr == "On" || *ptr == "BiosPost"; 848 } 849 850 static void pruneDevice(const nlohmann::json& systemConfiguration, 851 const bool powerOff, const bool scannedPowerOff, 852 const std::string& name, const nlohmann::json& device) 853 { 854 if (systemConfiguration.contains(name)) 855 { 856 return; 857 } 858 859 if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff)) 860 { 861 return; 862 } 863 864 logDeviceRemoved(device); 865 } 866 867 void startRemovedTimer(boost::asio::steady_timer& timer, 868 nlohmann::json& systemConfiguration) 869 { 870 static bool scannedPowerOff = false; 871 static bool scannedPowerOn = false; 872 873 if (systemConfiguration.empty() || lastJson.empty()) 874 { 875 return; // not ready yet 876 } 877 if (scannedPowerOn) 878 { 879 return; 880 } 881 882 if (!isPowerOn() && scannedPowerOff) 883 { 884 return; 885 } 886 887 timer.expires_after(std::chrono::seconds(10)); 888 timer.async_wait( 889 [&systemConfiguration](const boost::system::error_code& ec) { 890 if (ec == boost::asio::error::operation_aborted) 891 { 892 return; 893 } 894 895 bool powerOff = !isPowerOn(); 896 for (const auto& [name, device] : lastJson.items()) 897 { 898 pruneDevice(systemConfiguration, powerOff, scannedPowerOff, 899 name, device); 900 } 901 902 scannedPowerOff = true; 903 if (!powerOff) 904 { 905 scannedPowerOn = true; 906 } 907 }); 908 } 909 910 static std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>& 911 getDeviceInterfaces(const nlohmann::json& device) 912 { 913 return inventory[device["Name"].get<std::string>()]; 914 } 915 916 static void pruneConfiguration(nlohmann::json& systemConfiguration, 917 sdbusplus::asio::object_server& objServer, 918 bool powerOff, const std::string& name, 919 const nlohmann::json& device) 920 { 921 if (powerOff && deviceRequiresPowerOn(device)) 922 { 923 // power not on yet, don't know if it's there or not 924 return; 925 } 926 927 auto& ifaces = getDeviceInterfaces(device); 928 for (auto& iface : ifaces) 929 { 930 auto sharedPtr = iface.lock(); 931 if (!!sharedPtr) 932 { 933 objServer.remove_interface(sharedPtr); 934 } 935 } 936 937 ifaces.clear(); 938 systemConfiguration.erase(name); 939 logDeviceRemoved(device); 940 } 941 942 static void deriveNewConfiguration(const nlohmann::json& oldConfiguration, 943 nlohmann::json& newConfiguration) 944 { 945 for (auto it = newConfiguration.begin(); it != newConfiguration.end();) 946 { 947 auto findKey = oldConfiguration.find(it.key()); 948 if (findKey != oldConfiguration.end()) 949 { 950 it = newConfiguration.erase(it); 951 } 952 else 953 { 954 it++; 955 } 956 } 957 } 958 959 static void publishNewConfiguration( 960 const size_t& instance, const size_t count, 961 boost::asio::steady_timer& timer, nlohmann::json& systemConfiguration, 962 // Gerrit discussion: 963 // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6 964 // 965 // Discord discussion: 966 // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854 967 // 968 // NOLINTNEXTLINE(performance-unnecessary-value-param) 969 const nlohmann::json newConfiguration, 970 sdbusplus::asio::object_server& objServer) 971 { 972 loadOverlays(newConfiguration); 973 974 io.post([systemConfiguration]() { 975 if (!writeJsonFiles(systemConfiguration)) 976 { 977 std::cerr << "Error writing json files\n"; 978 } 979 }); 980 981 io.post([&instance, count, &timer, newConfiguration, &systemConfiguration, 982 &objServer]() { 983 postToDbus(newConfiguration, systemConfiguration, objServer); 984 if (count == instance) 985 { 986 startRemovedTimer(timer, systemConfiguration); 987 } 988 }); 989 } 990 991 // main properties changed entry 992 void propertiesChangedCallback(nlohmann::json& systemConfiguration, 993 sdbusplus::asio::object_server& objServer) 994 { 995 static bool inProgress = false; 996 static boost::asio::steady_timer timer(io); 997 static size_t instance = 0; 998 instance++; 999 size_t count = instance; 1000 1001 timer.expires_after(std::chrono::seconds(5)); 1002 1003 // setup an async wait as we normally get flooded with new requests 1004 timer.async_wait([&systemConfiguration, &objServer, 1005 count](const boost::system::error_code& ec) { 1006 if (ec == boost::asio::error::operation_aborted) 1007 { 1008 // we were cancelled 1009 return; 1010 } 1011 if (ec) 1012 { 1013 std::cerr << "async wait error " << ec << "\n"; 1014 return; 1015 } 1016 1017 if (inProgress) 1018 { 1019 propertiesChangedCallback(systemConfiguration, objServer); 1020 return; 1021 } 1022 inProgress = true; 1023 1024 nlohmann::json oldConfiguration = systemConfiguration; 1025 auto missingConfigurations = std::make_shared<nlohmann::json>(); 1026 *missingConfigurations = systemConfiguration; 1027 1028 std::list<nlohmann::json> configurations; 1029 if (!loadConfigurations(configurations)) 1030 { 1031 std::cerr << "Could not load configurations\n"; 1032 inProgress = false; 1033 return; 1034 } 1035 1036 auto perfScan = std::make_shared<PerformScan>( 1037 systemConfiguration, *missingConfigurations, configurations, 1038 objServer, 1039 [&systemConfiguration, &objServer, count, oldConfiguration, 1040 missingConfigurations]() { 1041 // this is something that since ac has been applied to the bmc 1042 // we saw, and we no longer see it 1043 bool powerOff = !isPowerOn(); 1044 for (const auto& [name, device] : 1045 missingConfigurations->items()) 1046 { 1047 pruneConfiguration(systemConfiguration, objServer, powerOff, 1048 name, device); 1049 } 1050 1051 nlohmann::json newConfiguration = systemConfiguration; 1052 1053 deriveNewConfiguration(oldConfiguration, newConfiguration); 1054 1055 for (const auto& [_, device] : newConfiguration.items()) 1056 { 1057 logDeviceAdded(device); 1058 } 1059 1060 inProgress = false; 1061 1062 io.post(std::bind_front( 1063 publishNewConfiguration, std::ref(instance), count, 1064 std::ref(timer), std::ref(systemConfiguration), 1065 newConfiguration, std::ref(objServer))); 1066 }); 1067 perfScan->run(); 1068 }); 1069 } 1070 1071 int main() 1072 { 1073 // setup connection to dbus 1074 systemBus = std::make_shared<sdbusplus::asio::connection>(io); 1075 systemBus->request_name("xyz.openbmc_project.EntityManager"); 1076 1077 sdbusplus::asio::object_server objServer(systemBus); 1078 1079 std::shared_ptr<sdbusplus::asio::dbus_interface> entityIface = 1080 objServer.add_interface("/xyz/openbmc_project/EntityManager", 1081 "xyz.openbmc_project.EntityManager"); 1082 1083 // to keep reference to the match / filter objects so they don't get 1084 // destroyed 1085 1086 nlohmann::json systemConfiguration = nlohmann::json::object(); 1087 1088 // We need a poke from DBus for static providers that create all their 1089 // objects prior to claiming a well-known name, and thus don't emit any 1090 // org.freedesktop.DBus.Properties signals. Similarly if a process exits 1091 // for any reason, expected or otherwise, we'll need a poke to remove 1092 // entities from DBus. 1093 sdbusplus::bus::match::match nameOwnerChangedMatch( 1094 static_cast<sdbusplus::bus::bus&>(*systemBus), 1095 sdbusplus::bus::match::rules::nameOwnerChanged(), 1096 [&](sdbusplus::message::message&) { 1097 propertiesChangedCallback(systemConfiguration, objServer); 1098 }); 1099 // We also need a poke from DBus when new interfaces are created or 1100 // destroyed. 1101 sdbusplus::bus::match::match interfacesAddedMatch( 1102 static_cast<sdbusplus::bus::bus&>(*systemBus), 1103 sdbusplus::bus::match::rules::interfacesAdded(), 1104 [&](sdbusplus::message::message&) { 1105 propertiesChangedCallback(systemConfiguration, objServer); 1106 }); 1107 sdbusplus::bus::match::match interfacesRemovedMatch( 1108 static_cast<sdbusplus::bus::bus&>(*systemBus), 1109 sdbusplus::bus::match::rules::interfacesRemoved(), 1110 [&](sdbusplus::message::message&) { 1111 propertiesChangedCallback(systemConfiguration, objServer); 1112 }); 1113 1114 io.post( 1115 [&]() { propertiesChangedCallback(systemConfiguration, objServer); }); 1116 1117 entityIface->register_method("ReScan", [&]() { 1118 propertiesChangedCallback(systemConfiguration, objServer); 1119 }); 1120 entityIface->initialize(); 1121 1122 if (fwVersionIsSame()) 1123 { 1124 if (std::filesystem::is_regular_file(currentConfiguration)) 1125 { 1126 // this file could just be deleted, but it's nice for debug 1127 std::filesystem::create_directory(tempConfigDir); 1128 std::filesystem::remove(lastConfiguration); 1129 std::filesystem::copy(currentConfiguration, lastConfiguration); 1130 std::filesystem::remove(currentConfiguration); 1131 1132 std::ifstream jsonStream(lastConfiguration); 1133 if (jsonStream.good()) 1134 { 1135 auto data = nlohmann::json::parse(jsonStream, nullptr, false); 1136 if (data.is_discarded()) 1137 { 1138 std::cerr << "syntax error in " << lastConfiguration 1139 << "\n"; 1140 } 1141 else 1142 { 1143 lastJson = std::move(data); 1144 } 1145 } 1146 else 1147 { 1148 std::cerr << "unable to open " << lastConfiguration << "\n"; 1149 } 1150 } 1151 } 1152 else 1153 { 1154 // not an error, just logging at this level to make it in the journal 1155 std::cerr << "Clearing previous configuration\n"; 1156 std::filesystem::remove(currentConfiguration); 1157 } 1158 1159 // some boards only show up after power is on, we want to not say they are 1160 // removed until the same state happens 1161 setupPowerMatch(systemBus); 1162 1163 io.run(); 1164 1165 return 0; 1166 } 1167