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