1 /* 2 // Copyright (c) 2020 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 #pragma once 17 #include "dbus_utility.hpp" 18 #include "error_messages.hpp" 19 #include "event_service_store.hpp" 20 #include "filter_expr_executor.hpp" 21 #include "http_client.hpp" 22 #include "metric_report.hpp" 23 #include "ossl_random.hpp" 24 #include "persistent_data.hpp" 25 #include "registries.hpp" 26 #include "registries_selector.hpp" 27 #include "str_utility.hpp" 28 #include "utility.hpp" 29 #include "utils/json_utils.hpp" 30 #include "utils/time_utils.hpp" 31 32 #include <sys/inotify.h> 33 34 #include <boost/asio/io_context.hpp> 35 #include <boost/circular_buffer.hpp> 36 #include <boost/container/flat_map.hpp> 37 #include <boost/url/format.hpp> 38 #include <boost/url/url_view_base.hpp> 39 #include <sdbusplus/bus/match.hpp> 40 41 #include <algorithm> 42 #include <cstdlib> 43 #include <ctime> 44 #include <fstream> 45 #include <memory> 46 #include <ranges> 47 #include <span> 48 49 namespace redfish 50 { 51 52 static constexpr const char* eventFormatType = "Event"; 53 static constexpr const char* metricReportFormatType = "MetricReport"; 54 55 static constexpr const char* subscriptionTypeSSE = "SSE"; 56 static constexpr const char* eventServiceFile = 57 "/var/lib/bmcweb/eventservice_config.json"; 58 59 static constexpr const uint8_t maxNoOfSubscriptions = 20; 60 static constexpr const uint8_t maxNoOfSSESubscriptions = 10; 61 62 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 63 static std::optional<boost::asio::posix::stream_descriptor> inotifyConn; 64 static constexpr const char* redfishEventLogDir = "/var/log"; 65 static constexpr const char* redfishEventLogFile = "/var/log/redfish"; 66 static constexpr const size_t iEventSize = sizeof(inotify_event); 67 68 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 69 static int inotifyFd = -1; 70 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 71 static int dirWatchDesc = -1; 72 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 73 static int fileWatchDesc = -1; 74 struct EventLogObjectsType 75 { 76 std::string id; 77 std::string timestamp; 78 std::string messageId; 79 std::vector<std::string> messageArgs; 80 }; 81 82 namespace registries 83 { 84 static const Message* 85 getMsgFromRegistry(const std::string& messageKey, 86 const std::span<const MessageEntry>& registry) 87 { 88 std::span<const MessageEntry>::iterator messageIt = std::ranges::find_if( 89 registry, [&messageKey](const MessageEntry& messageEntry) { 90 return messageKey == messageEntry.first; 91 }); 92 if (messageIt != registry.end()) 93 { 94 return &messageIt->second; 95 } 96 97 return nullptr; 98 } 99 100 static const Message* formatMessage(std::string_view messageID) 101 { 102 // Redfish MessageIds are in the form 103 // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find 104 // the right Message 105 std::vector<std::string> fields; 106 fields.reserve(4); 107 108 bmcweb::split(fields, messageID, '.'); 109 if (fields.size() != 4) 110 { 111 return nullptr; 112 } 113 const std::string& registryName = fields[0]; 114 const std::string& messageKey = fields[3]; 115 116 // Find the right registry and check it for the MessageKey 117 return getMsgFromRegistry(messageKey, getRegistryFromPrefix(registryName)); 118 } 119 } // namespace registries 120 121 namespace event_log 122 { 123 inline bool getUniqueEntryID(const std::string& logEntry, std::string& entryID) 124 { 125 static time_t prevTs = 0; 126 static int index = 0; 127 128 // Get the entry timestamp 129 std::time_t curTs = 0; 130 std::tm timeStruct = {}; 131 std::istringstream entryStream(logEntry); 132 if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S")) 133 { 134 curTs = std::mktime(&timeStruct); 135 if (curTs == -1) 136 { 137 return false; 138 } 139 } 140 // If the timestamp isn't unique, increment the index 141 index = (curTs == prevTs) ? index + 1 : 0; 142 143 // Save the timestamp 144 prevTs = curTs; 145 146 entryID = std::to_string(curTs); 147 if (index > 0) 148 { 149 entryID += "_" + std::to_string(index); 150 } 151 return true; 152 } 153 154 inline int getEventLogParams(const std::string& logEntry, 155 std::string& timestamp, std::string& messageID, 156 std::vector<std::string>& messageArgs) 157 { 158 // The redfish log format is "<Timestamp> <MessageId>,<MessageArgs>" 159 // First get the Timestamp 160 size_t space = logEntry.find_first_of(' '); 161 if (space == std::string::npos) 162 { 163 return -EINVAL; 164 } 165 timestamp = logEntry.substr(0, space); 166 // Then get the log contents 167 size_t entryStart = logEntry.find_first_not_of(' ', space); 168 if (entryStart == std::string::npos) 169 { 170 return -EINVAL; 171 } 172 std::string_view entry(logEntry); 173 entry.remove_prefix(entryStart); 174 // Use split to separate the entry into its fields 175 std::vector<std::string> logEntryFields; 176 bmcweb::split(logEntryFields, entry, ','); 177 // We need at least a MessageId to be valid 178 if (logEntryFields.empty()) 179 { 180 return -EINVAL; 181 } 182 messageID = logEntryFields[0]; 183 184 // Get the MessageArgs from the log if there are any 185 if (logEntryFields.size() > 1) 186 { 187 const std::string& messageArgsStart = logEntryFields[1]; 188 // If the first string is empty, assume there are no MessageArgs 189 if (!messageArgsStart.empty()) 190 { 191 messageArgs.assign(logEntryFields.begin() + 1, 192 logEntryFields.end()); 193 } 194 } 195 196 return 0; 197 } 198 199 inline void getRegistryAndMessageKey(const std::string& messageID, 200 std::string& registryName, 201 std::string& messageKey) 202 { 203 // Redfish MessageIds are in the form 204 // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find 205 // the right Message 206 std::vector<std::string> fields; 207 fields.reserve(4); 208 bmcweb::split(fields, messageID, '.'); 209 if (fields.size() == 4) 210 { 211 registryName = fields[0]; 212 messageKey = fields[3]; 213 } 214 } 215 216 inline int formatEventLogEntry(const std::string& logEntryID, 217 const std::string& messageID, 218 const std::span<std::string_view> messageArgs, 219 std::string timestamp, 220 const std::string& customText, 221 nlohmann::json::object_t& logEntryJson) 222 { 223 // Get the Message from the MessageRegistry 224 const registries::Message* message = registries::formatMessage(messageID); 225 226 if (message == nullptr) 227 { 228 return -1; 229 } 230 231 std::string msg = redfish::registries::fillMessageArgs(messageArgs, 232 message->message); 233 if (msg.empty()) 234 { 235 return -1; 236 } 237 238 // Get the Created time from the timestamp. The log timestamp is in 239 // RFC3339 format which matches the Redfish format except for the 240 // fractional seconds between the '.' and the '+', so just remove them. 241 std::size_t dot = timestamp.find_first_of('.'); 242 std::size_t plus = timestamp.find_first_of('+', dot); 243 if (dot != std::string::npos && plus != std::string::npos) 244 { 245 timestamp.erase(dot, plus - dot); 246 } 247 248 // Fill in the log entry with the gathered data 249 logEntryJson["EventId"] = logEntryID; 250 logEntryJson["EventType"] = "Event"; 251 logEntryJson["Severity"] = message->messageSeverity; 252 logEntryJson["Message"] = std::move(msg); 253 logEntryJson["MessageId"] = messageID; 254 logEntryJson["MessageArgs"] = messageArgs; 255 logEntryJson["EventTimestamp"] = std::move(timestamp); 256 logEntryJson["Context"] = customText; 257 return 0; 258 } 259 260 } // namespace event_log 261 262 class Subscription : public persistent_data::UserSubscription 263 { 264 public: 265 Subscription(const Subscription&) = delete; 266 Subscription& operator=(const Subscription&) = delete; 267 Subscription(Subscription&&) = delete; 268 Subscription& operator=(Subscription&&) = delete; 269 270 Subscription(const boost::urls::url_view_base& url, 271 boost::asio::io_context& ioc) : 272 policy(std::make_shared<crow::ConnectionPolicy>()) 273 { 274 destinationUrl = url; 275 client.emplace(ioc, policy); 276 // Subscription constructor 277 policy->invalidResp = retryRespHandler; 278 } 279 280 explicit Subscription(crow::sse_socket::Connection& connIn) : 281 sseConn(&connIn) 282 {} 283 284 ~Subscription() = default; 285 286 bool sendEvent(std::string&& msg) 287 { 288 persistent_data::EventServiceConfig eventServiceConfig = 289 persistent_data::EventServiceStore::getInstance() 290 .getEventServiceConfig(); 291 if (!eventServiceConfig.enabled) 292 { 293 return false; 294 } 295 296 if (client) 297 { 298 client->sendData( 299 std::move(msg), destinationUrl, 300 static_cast<ensuressl::VerifyCertificate>(verifyCertificate), 301 httpHeaders, boost::beast::http::verb::post); 302 return true; 303 } 304 305 if (sseConn != nullptr) 306 { 307 eventSeqNum++; 308 sseConn->sendEvent(std::to_string(eventSeqNum), msg); 309 } 310 return true; 311 } 312 313 bool eventMatchesFilter(const nlohmann::json::object_t& eventMessage, 314 std::string_view resType) 315 { 316 // If resourceTypes list is empty, assume all 317 if (!resourceTypes.empty()) 318 { 319 // Search the resourceTypes list for the subscription. 320 auto resourceTypeIndex = std::ranges::find_if( 321 resourceTypes, [resType](const std::string& rtEntry) { 322 return rtEntry == resType; 323 }); 324 if (resourceTypeIndex == resourceTypes.end()) 325 { 326 BMCWEB_LOG_DEBUG("Not subscribed to this resource"); 327 return false; 328 } 329 BMCWEB_LOG_DEBUG("ResourceType {} found in the subscribed list", 330 resType); 331 } 332 333 // If registryMsgIds list is empty, assume all 334 if (!registryMsgIds.empty()) 335 { 336 auto eventJson = eventMessage.find("MessageId"); 337 if (eventJson == eventMessage.end()) 338 { 339 return false; 340 } 341 342 const std::string* messageId = 343 eventJson->second.get_ptr<const std::string*>(); 344 if (messageId == nullptr) 345 { 346 BMCWEB_LOG_ERROR("EventType wasn't a string???"); 347 return false; 348 } 349 350 std::string registry; 351 std::string messageKey; 352 event_log::getRegistryAndMessageKey(*messageId, registry, 353 messageKey); 354 355 auto obj = std::ranges::find(registryMsgIds, registry); 356 if (obj == registryMsgIds.end()) 357 { 358 return false; 359 } 360 } 361 362 if (filter) 363 { 364 if (!memberMatches(eventMessage, *filter)) 365 { 366 BMCWEB_LOG_DEBUG("Filter didn't match"); 367 return false; 368 } 369 } 370 371 return true; 372 } 373 374 bool sendTestEventLog() 375 { 376 nlohmann::json::array_t logEntryArray; 377 nlohmann::json& logEntryJson = logEntryArray.emplace_back(); 378 379 logEntryJson["EventId"] = "TestID"; 380 logEntryJson["EventType"] = "Event"; 381 logEntryJson["Severity"] = "OK"; 382 logEntryJson["Message"] = "Generated test event"; 383 logEntryJson["MessageId"] = "OpenBMC.0.2.TestEventLog"; 384 logEntryJson["MessageArgs"] = nlohmann::json::array(); 385 logEntryJson["EventTimestamp"] = 386 redfish::time_utils::getDateTimeOffsetNow().first; 387 logEntryJson["Context"] = customText; 388 389 nlohmann::json msg; 390 msg["@odata.type"] = "#Event.v1_4_0.Event"; 391 msg["Id"] = std::to_string(eventSeqNum); 392 msg["Name"] = "Event Log"; 393 msg["Events"] = logEntryArray; 394 395 std::string strMsg = msg.dump(2, ' ', true, 396 nlohmann::json::error_handler_t::replace); 397 return sendEvent(std::move(strMsg)); 398 } 399 400 void filterAndSendEventLogs( 401 const std::vector<EventLogObjectsType>& eventRecords) 402 { 403 nlohmann::json::array_t logEntryArray; 404 for (const EventLogObjectsType& logEntry : eventRecords) 405 { 406 std::vector<std::string_view> messageArgsView( 407 logEntry.messageArgs.begin(), logEntry.messageArgs.end()); 408 409 nlohmann::json::object_t bmcLogEntry; 410 if (event_log::formatEventLogEntry( 411 logEntry.id, logEntry.messageId, messageArgsView, 412 logEntry.timestamp, customText, bmcLogEntry) != 0) 413 { 414 BMCWEB_LOG_DEBUG("Read eventLog entry failed"); 415 continue; 416 } 417 418 if (!eventMatchesFilter(bmcLogEntry, "")) 419 { 420 continue; 421 } 422 423 logEntryArray.emplace_back(std::move(bmcLogEntry)); 424 } 425 426 if (logEntryArray.empty()) 427 { 428 BMCWEB_LOG_DEBUG("No log entries available to be transferred."); 429 return; 430 } 431 432 nlohmann::json msg; 433 msg["@odata.type"] = "#Event.v1_4_0.Event"; 434 msg["Id"] = std::to_string(eventSeqNum); 435 msg["Name"] = "Event Log"; 436 msg["Events"] = std::move(logEntryArray); 437 std::string strMsg = msg.dump(2, ' ', true, 438 nlohmann::json::error_handler_t::replace); 439 sendEvent(std::move(strMsg)); 440 eventSeqNum++; 441 } 442 443 void filterAndSendReports(const std::string& reportId, 444 const telemetry::TimestampReadings& var) 445 { 446 boost::urls::url mrdUri = boost::urls::format( 447 "/redfish/v1/TelemetryService/MetricReportDefinitions/{}", 448 reportId); 449 450 // Empty list means no filter. Send everything. 451 if (!metricReportDefinitions.empty()) 452 { 453 if (std::ranges::find(metricReportDefinitions, mrdUri.buffer()) == 454 metricReportDefinitions.end()) 455 { 456 return; 457 } 458 } 459 460 nlohmann::json msg; 461 if (!telemetry::fillReport(msg, reportId, var)) 462 { 463 BMCWEB_LOG_ERROR("Failed to fill the MetricReport for DBus " 464 "Report with id {}", 465 reportId); 466 return; 467 } 468 469 // Context is set by user during Event subscription and it must be 470 // set for MetricReport response. 471 if (!customText.empty()) 472 { 473 msg["Context"] = customText; 474 } 475 476 std::string strMsg = msg.dump(2, ' ', true, 477 nlohmann::json::error_handler_t::replace); 478 sendEvent(std::move(strMsg)); 479 } 480 481 void updateRetryConfig(uint32_t retryAttempts, 482 uint32_t retryTimeoutInterval) 483 { 484 if (policy == nullptr) 485 { 486 BMCWEB_LOG_DEBUG("Retry policy was nullptr, ignoring set"); 487 return; 488 } 489 policy->maxRetryAttempts = retryAttempts; 490 policy->retryIntervalSecs = std::chrono::seconds(retryTimeoutInterval); 491 } 492 493 uint64_t getEventSeqNum() const 494 { 495 return eventSeqNum; 496 } 497 498 void setSubscriptionId(const std::string& id2) 499 { 500 BMCWEB_LOG_DEBUG("Subscription ID: {}", id2); 501 subId = id2; 502 } 503 504 std::string getSubscriptionId() 505 { 506 return subId; 507 } 508 509 bool matchSseId(const crow::sse_socket::Connection& thisConn) 510 { 511 return &thisConn == sseConn; 512 } 513 514 // Check used to indicate what response codes are valid as part of our retry 515 // policy. 2XX is considered acceptable 516 static boost::system::error_code retryRespHandler(unsigned int respCode) 517 { 518 BMCWEB_LOG_DEBUG( 519 "Checking response code validity for SubscriptionEvent"); 520 if ((respCode < 200) || (respCode >= 300)) 521 { 522 return boost::system::errc::make_error_code( 523 boost::system::errc::result_out_of_range); 524 } 525 526 // Return 0 if the response code is valid 527 return boost::system::errc::make_error_code( 528 boost::system::errc::success); 529 } 530 531 private: 532 std::string subId; 533 uint64_t eventSeqNum = 1; 534 boost::urls::url host; 535 std::shared_ptr<crow::ConnectionPolicy> policy; 536 crow::sse_socket::Connection* sseConn = nullptr; 537 538 std::optional<crow::HttpClient> client; 539 540 public: 541 std::optional<filter_ast::LogicalAnd> filter; 542 }; 543 544 class EventServiceManager 545 { 546 private: 547 bool serviceEnabled = false; 548 uint32_t retryAttempts = 0; 549 uint32_t retryTimeoutInterval = 0; 550 551 std::streampos redfishLogFilePosition{0}; 552 size_t noOfEventLogSubscribers{0}; 553 size_t noOfMetricReportSubscribers{0}; 554 std::shared_ptr<sdbusplus::bus::match_t> matchTelemetryMonitor; 555 boost::container::flat_map<std::string, std::shared_ptr<Subscription>> 556 subscriptionsMap; 557 558 uint64_t eventId{1}; 559 560 struct Event 561 { 562 std::string id; 563 nlohmann::json message; 564 }; 565 566 constexpr static size_t maxMessages = 200; 567 boost::circular_buffer<Event> messages{maxMessages}; 568 569 boost::asio::io_context& ioc; 570 571 public: 572 EventServiceManager(const EventServiceManager&) = delete; 573 EventServiceManager& operator=(const EventServiceManager&) = delete; 574 EventServiceManager(EventServiceManager&&) = delete; 575 EventServiceManager& operator=(EventServiceManager&&) = delete; 576 ~EventServiceManager() = default; 577 578 explicit EventServiceManager(boost::asio::io_context& iocIn) : ioc(iocIn) 579 { 580 // Load config from persist store. 581 initConfig(); 582 } 583 584 static EventServiceManager& 585 getInstance(boost::asio::io_context* ioc = nullptr) 586 { 587 static EventServiceManager handler(*ioc); 588 return handler; 589 } 590 591 void initConfig() 592 { 593 loadOldBehavior(); 594 595 persistent_data::EventServiceConfig eventServiceConfig = 596 persistent_data::EventServiceStore::getInstance() 597 .getEventServiceConfig(); 598 599 serviceEnabled = eventServiceConfig.enabled; 600 retryAttempts = eventServiceConfig.retryAttempts; 601 retryTimeoutInterval = eventServiceConfig.retryTimeoutInterval; 602 603 for (const auto& it : persistent_data::EventServiceStore::getInstance() 604 .subscriptionsConfigMap) 605 { 606 std::shared_ptr<persistent_data::UserSubscription> newSub = 607 it.second; 608 609 boost::system::result<boost::urls::url> url = 610 boost::urls::parse_absolute_uri(newSub->destinationUrl); 611 612 if (!url) 613 { 614 BMCWEB_LOG_ERROR( 615 "Failed to validate and split destination url"); 616 continue; 617 } 618 std::shared_ptr<Subscription> subValue = 619 std::make_shared<Subscription>(*url, ioc); 620 621 subValue->id = newSub->id; 622 subValue->destinationUrl = newSub->destinationUrl; 623 subValue->protocol = newSub->protocol; 624 subValue->verifyCertificate = newSub->verifyCertificate; 625 subValue->retryPolicy = newSub->retryPolicy; 626 subValue->customText = newSub->customText; 627 subValue->eventFormatType = newSub->eventFormatType; 628 subValue->subscriptionType = newSub->subscriptionType; 629 subValue->registryMsgIds = newSub->registryMsgIds; 630 subValue->registryPrefixes = newSub->registryPrefixes; 631 subValue->resourceTypes = newSub->resourceTypes; 632 subValue->httpHeaders = newSub->httpHeaders; 633 subValue->metricReportDefinitions = newSub->metricReportDefinitions; 634 635 if (subValue->id.empty()) 636 { 637 BMCWEB_LOG_ERROR("Failed to add subscription"); 638 } 639 subscriptionsMap.insert(std::pair(subValue->id, subValue)); 640 641 updateNoOfSubscribersCount(); 642 643 if constexpr (!BMCWEB_REDFISH_DBUS_LOG) 644 { 645 cacheRedfishLogFile(); 646 } 647 648 // Update retry configuration. 649 subValue->updateRetryConfig(retryAttempts, retryTimeoutInterval); 650 } 651 } 652 653 static void loadOldBehavior() 654 { 655 std::ifstream eventConfigFile(eventServiceFile); 656 if (!eventConfigFile.good()) 657 { 658 BMCWEB_LOG_DEBUG("Old eventService config not exist"); 659 return; 660 } 661 auto jsonData = nlohmann::json::parse(eventConfigFile, nullptr, false); 662 if (jsonData.is_discarded()) 663 { 664 BMCWEB_LOG_ERROR("Old eventService config parse error."); 665 return; 666 } 667 668 const nlohmann::json::object_t* obj = 669 jsonData.get_ptr<const nlohmann::json::object_t*>(); 670 for (const auto& item : *obj) 671 { 672 if (item.first == "Configuration") 673 { 674 persistent_data::EventServiceStore::getInstance() 675 .getEventServiceConfig() 676 .fromJson(item.second); 677 } 678 else if (item.first == "Subscriptions") 679 { 680 for (const auto& elem : item.second) 681 { 682 std::shared_ptr<persistent_data::UserSubscription> 683 newSubscription = 684 persistent_data::UserSubscription::fromJson(elem, 685 true); 686 if (newSubscription == nullptr) 687 { 688 BMCWEB_LOG_ERROR("Problem reading subscription " 689 "from old persistent store"); 690 continue; 691 } 692 693 std::uniform_int_distribution<uint32_t> dist(0); 694 bmcweb::OpenSSLGenerator gen; 695 696 std::string id; 697 698 int retry = 3; 699 while (retry != 0) 700 { 701 id = std::to_string(dist(gen)); 702 if (gen.error()) 703 { 704 retry = 0; 705 break; 706 } 707 newSubscription->id = id; 708 auto inserted = 709 persistent_data::EventServiceStore::getInstance() 710 .subscriptionsConfigMap.insert( 711 std::pair(id, newSubscription)); 712 if (inserted.second) 713 { 714 break; 715 } 716 --retry; 717 } 718 719 if (retry <= 0) 720 { 721 BMCWEB_LOG_ERROR( 722 "Failed to generate random number from old " 723 "persistent store"); 724 continue; 725 } 726 } 727 } 728 729 persistent_data::getConfig().writeData(); 730 std::error_code ec; 731 std::filesystem::remove(eventServiceFile, ec); 732 if (ec) 733 { 734 BMCWEB_LOG_DEBUG( 735 "Failed to remove old event service file. Ignoring"); 736 } 737 else 738 { 739 BMCWEB_LOG_DEBUG("Remove old eventservice config"); 740 } 741 } 742 } 743 744 void updateSubscriptionData() const 745 { 746 persistent_data::EventServiceStore::getInstance() 747 .eventServiceConfig.enabled = serviceEnabled; 748 persistent_data::EventServiceStore::getInstance() 749 .eventServiceConfig.retryAttempts = retryAttempts; 750 persistent_data::EventServiceStore::getInstance() 751 .eventServiceConfig.retryTimeoutInterval = retryTimeoutInterval; 752 753 persistent_data::getConfig().writeData(); 754 } 755 756 void setEventServiceConfig(const persistent_data::EventServiceConfig& cfg) 757 { 758 bool updateConfig = false; 759 bool updateRetryCfg = false; 760 761 if (serviceEnabled != cfg.enabled) 762 { 763 serviceEnabled = cfg.enabled; 764 if (serviceEnabled && noOfMetricReportSubscribers != 0U) 765 { 766 registerMetricReportSignal(); 767 } 768 else 769 { 770 unregisterMetricReportSignal(); 771 } 772 updateConfig = true; 773 } 774 775 if (retryAttempts != cfg.retryAttempts) 776 { 777 retryAttempts = cfg.retryAttempts; 778 updateConfig = true; 779 updateRetryCfg = true; 780 } 781 782 if (retryTimeoutInterval != cfg.retryTimeoutInterval) 783 { 784 retryTimeoutInterval = cfg.retryTimeoutInterval; 785 updateConfig = true; 786 updateRetryCfg = true; 787 } 788 789 if (updateConfig) 790 { 791 updateSubscriptionData(); 792 } 793 794 if (updateRetryCfg) 795 { 796 // Update the changed retry config to all subscriptions 797 for (const auto& it : 798 EventServiceManager::getInstance().subscriptionsMap) 799 { 800 Subscription& entry = *it.second; 801 entry.updateRetryConfig(retryAttempts, retryTimeoutInterval); 802 } 803 } 804 } 805 806 void updateNoOfSubscribersCount() 807 { 808 size_t eventLogSubCount = 0; 809 size_t metricReportSubCount = 0; 810 for (const auto& it : subscriptionsMap) 811 { 812 std::shared_ptr<Subscription> entry = it.second; 813 if (entry->eventFormatType == eventFormatType) 814 { 815 eventLogSubCount++; 816 } 817 else if (entry->eventFormatType == metricReportFormatType) 818 { 819 metricReportSubCount++; 820 } 821 } 822 823 noOfEventLogSubscribers = eventLogSubCount; 824 if (noOfMetricReportSubscribers != metricReportSubCount) 825 { 826 noOfMetricReportSubscribers = metricReportSubCount; 827 if (noOfMetricReportSubscribers != 0U) 828 { 829 registerMetricReportSignal(); 830 } 831 else 832 { 833 unregisterMetricReportSignal(); 834 } 835 } 836 } 837 838 std::shared_ptr<Subscription> getSubscription(const std::string& id) 839 { 840 auto obj = subscriptionsMap.find(id); 841 if (obj == subscriptionsMap.end()) 842 { 843 BMCWEB_LOG_ERROR("No subscription exist with ID:{}", id); 844 return nullptr; 845 } 846 std::shared_ptr<Subscription> subValue = obj->second; 847 return subValue; 848 } 849 850 std::string 851 addSubscriptionInternal(const std::shared_ptr<Subscription>& subValue) 852 { 853 std::uniform_int_distribution<uint32_t> dist(0); 854 bmcweb::OpenSSLGenerator gen; 855 856 std::string id; 857 858 int retry = 3; 859 while (retry != 0) 860 { 861 id = std::to_string(dist(gen)); 862 if (gen.error()) 863 { 864 retry = 0; 865 break; 866 } 867 auto inserted = subscriptionsMap.insert(std::pair(id, subValue)); 868 if (inserted.second) 869 { 870 break; 871 } 872 --retry; 873 } 874 875 if (retry <= 0) 876 { 877 BMCWEB_LOG_ERROR("Failed to generate random number"); 878 return ""; 879 } 880 881 std::shared_ptr<persistent_data::UserSubscription> newSub = 882 std::make_shared<persistent_data::UserSubscription>(); 883 newSub->id = id; 884 newSub->destinationUrl = subValue->destinationUrl; 885 newSub->protocol = subValue->protocol; 886 newSub->retryPolicy = subValue->retryPolicy; 887 newSub->customText = subValue->customText; 888 newSub->eventFormatType = subValue->eventFormatType; 889 newSub->subscriptionType = subValue->subscriptionType; 890 newSub->registryMsgIds = subValue->registryMsgIds; 891 newSub->registryPrefixes = subValue->registryPrefixes; 892 newSub->resourceTypes = subValue->resourceTypes; 893 newSub->httpHeaders = subValue->httpHeaders; 894 newSub->metricReportDefinitions = subValue->metricReportDefinitions; 895 persistent_data::EventServiceStore::getInstance() 896 .subscriptionsConfigMap.emplace(newSub->id, newSub); 897 898 updateNoOfSubscribersCount(); 899 900 if constexpr (!BMCWEB_REDFISH_DBUS_LOG) 901 { 902 if (redfishLogFilePosition != 0) 903 { 904 cacheRedfishLogFile(); 905 } 906 } 907 // Update retry configuration. 908 subValue->updateRetryConfig(retryAttempts, retryTimeoutInterval); 909 910 // Set Subscription ID for back trace 911 subValue->setSubscriptionId(id); 912 913 return id; 914 } 915 916 std::string 917 addSSESubscription(const std::shared_ptr<Subscription>& subValue, 918 std::string_view lastEventId) 919 { 920 std::string id = addSubscriptionInternal(subValue); 921 922 if (!lastEventId.empty()) 923 { 924 BMCWEB_LOG_INFO("Attempting to find message for last id {}", 925 lastEventId); 926 boost::circular_buffer<Event>::iterator lastEvent = 927 std::find_if(messages.begin(), messages.end(), 928 [&lastEventId](const Event& event) { 929 return event.id == lastEventId; 930 }); 931 // Can't find a matching ID 932 if (lastEvent == messages.end()) 933 { 934 nlohmann::json msg = messages::eventBufferExceeded(); 935 // If the buffer overloaded, send all messages. 936 subValue->sendEvent(msg); 937 lastEvent = messages.begin(); 938 } 939 else 940 { 941 // Skip the last event the user already has 942 lastEvent++; 943 } 944 945 for (boost::circular_buffer<Event>::const_iterator event = 946 lastEvent; 947 lastEvent != messages.end(); lastEvent++) 948 { 949 subValue->sendEvent(event->message); 950 } 951 } 952 return id; 953 } 954 955 std::string 956 addPushSubscription(const std::shared_ptr<Subscription>& subValue) 957 { 958 std::string id = addSubscriptionInternal(subValue); 959 960 updateSubscriptionData(); 961 return id; 962 } 963 964 bool isSubscriptionExist(const std::string& id) 965 { 966 auto obj = subscriptionsMap.find(id); 967 return obj != subscriptionsMap.end(); 968 } 969 970 void deleteSubscription(const std::string& id) 971 { 972 auto obj = subscriptionsMap.find(id); 973 if (obj != subscriptionsMap.end()) 974 { 975 subscriptionsMap.erase(obj); 976 auto obj2 = persistent_data::EventServiceStore::getInstance() 977 .subscriptionsConfigMap.find(id); 978 persistent_data::EventServiceStore::getInstance() 979 .subscriptionsConfigMap.erase(obj2); 980 updateNoOfSubscribersCount(); 981 updateSubscriptionData(); 982 } 983 } 984 985 void deleteSseSubscription(const crow::sse_socket::Connection& thisConn) 986 { 987 for (auto it = subscriptionsMap.begin(); it != subscriptionsMap.end();) 988 { 989 std::shared_ptr<Subscription> entry = it->second; 990 bool entryIsThisConn = entry->matchSseId(thisConn); 991 if (entryIsThisConn) 992 { 993 persistent_data::EventServiceStore::getInstance() 994 .subscriptionsConfigMap.erase( 995 it->second->getSubscriptionId()); 996 it = subscriptionsMap.erase(it); 997 return; 998 } 999 it++; 1000 } 1001 } 1002 1003 size_t getNumberOfSubscriptions() const 1004 { 1005 return subscriptionsMap.size(); 1006 } 1007 1008 size_t getNumberOfSSESubscriptions() const 1009 { 1010 auto size = std::ranges::count_if( 1011 subscriptionsMap, 1012 [](const std::pair<std::string, std::shared_ptr<Subscription>>& 1013 entry) { 1014 return (entry.second->subscriptionType == subscriptionTypeSSE); 1015 }); 1016 return static_cast<size_t>(size); 1017 } 1018 1019 std::vector<std::string> getAllIDs() 1020 { 1021 std::vector<std::string> idList; 1022 for (const auto& it : subscriptionsMap) 1023 { 1024 idList.emplace_back(it.first); 1025 } 1026 return idList; 1027 } 1028 1029 bool sendTestEventLog() 1030 { 1031 for (const auto& it : subscriptionsMap) 1032 { 1033 std::shared_ptr<Subscription> entry = it.second; 1034 if (!entry->sendTestEventLog()) 1035 { 1036 return false; 1037 } 1038 } 1039 return true; 1040 } 1041 1042 void sendEvent(nlohmann::json::object_t eventMessage, 1043 std::string_view origin, std::string_view resourceType) 1044 { 1045 eventMessage["EventId"] = eventId; 1046 1047 eventMessage["EventTimestamp"] = 1048 redfish::time_utils::getDateTimeOffsetNow().first; 1049 eventMessage["OriginOfCondition"] = origin; 1050 1051 // MemberId is 0 : since we are sending one event record. 1052 eventMessage["MemberId"] = 0; 1053 1054 messages.push_back(Event(std::to_string(eventId), eventMessage)); 1055 1056 for (auto& it : subscriptionsMap) 1057 { 1058 std::shared_ptr<Subscription>& entry = it.second; 1059 if (!entry->eventMatchesFilter(eventMessage, resourceType)) 1060 { 1061 BMCWEB_LOG_DEBUG("Filter didn't match"); 1062 continue; 1063 } 1064 1065 nlohmann::json::array_t eventRecord; 1066 eventRecord.emplace_back(eventMessage); 1067 1068 nlohmann::json msgJson; 1069 1070 msgJson["@odata.type"] = "#Event.v1_4_0.Event"; 1071 msgJson["Name"] = "Event Log"; 1072 msgJson["Id"] = eventId; 1073 msgJson["Events"] = std::move(eventRecord); 1074 1075 std::string strMsg = msgJson.dump( 1076 2, ' ', true, nlohmann::json::error_handler_t::replace); 1077 entry->sendEvent(std::move(strMsg)); 1078 eventId++; // increment the eventId 1079 } 1080 } 1081 1082 void resetRedfishFilePosition() 1083 { 1084 // Control would be here when Redfish file is created. 1085 // Reset File Position as new file is created 1086 redfishLogFilePosition = 0; 1087 } 1088 1089 void cacheRedfishLogFile() 1090 { 1091 // Open the redfish file and read till the last record. 1092 1093 std::ifstream logStream(redfishEventLogFile); 1094 if (!logStream.good()) 1095 { 1096 BMCWEB_LOG_ERROR(" Redfish log file open failed "); 1097 return; 1098 } 1099 std::string logEntry; 1100 while (std::getline(logStream, logEntry)) 1101 { 1102 redfishLogFilePosition = logStream.tellg(); 1103 } 1104 } 1105 1106 void readEventLogsFromFile() 1107 { 1108 std::ifstream logStream(redfishEventLogFile); 1109 if (!logStream.good()) 1110 { 1111 BMCWEB_LOG_ERROR(" Redfish log file open failed"); 1112 return; 1113 } 1114 1115 std::vector<EventLogObjectsType> eventRecords; 1116 1117 std::string logEntry; 1118 1119 // Get the read pointer to the next log to be read. 1120 logStream.seekg(redfishLogFilePosition); 1121 1122 while (std::getline(logStream, logEntry)) 1123 { 1124 // Update Pointer position 1125 redfishLogFilePosition = logStream.tellg(); 1126 1127 std::string idStr; 1128 if (!event_log::getUniqueEntryID(logEntry, idStr)) 1129 { 1130 continue; 1131 } 1132 1133 if (!serviceEnabled || noOfEventLogSubscribers == 0) 1134 { 1135 // If Service is not enabled, no need to compute 1136 // the remaining items below. 1137 // But, Loop must continue to keep track of Timestamp 1138 continue; 1139 } 1140 1141 std::string timestamp; 1142 std::string messageID; 1143 std::vector<std::string> messageArgs; 1144 if (event_log::getEventLogParams(logEntry, timestamp, messageID, 1145 messageArgs) != 0) 1146 { 1147 BMCWEB_LOG_DEBUG("Read eventLog entry params failed"); 1148 continue; 1149 } 1150 1151 eventRecords.emplace_back(idStr, timestamp, messageID, messageArgs); 1152 } 1153 1154 if (!serviceEnabled || noOfEventLogSubscribers == 0) 1155 { 1156 BMCWEB_LOG_DEBUG("EventService disabled or no Subscriptions."); 1157 return; 1158 } 1159 1160 if (eventRecords.empty()) 1161 { 1162 // No Records to send 1163 BMCWEB_LOG_DEBUG("No log entries available to be transferred."); 1164 return; 1165 } 1166 1167 for (const auto& it : subscriptionsMap) 1168 { 1169 std::shared_ptr<Subscription> entry = it.second; 1170 if (entry->eventFormatType == "Event") 1171 { 1172 entry->filterAndSendEventLogs(eventRecords); 1173 } 1174 } 1175 } 1176 1177 static void watchRedfishEventLogFile() 1178 { 1179 if (!inotifyConn) 1180 { 1181 return; 1182 } 1183 1184 static std::array<char, 1024> readBuffer; 1185 1186 inotifyConn->async_read_some(boost::asio::buffer(readBuffer), 1187 [&](const boost::system::error_code& ec, 1188 const std::size_t& bytesTransferred) { 1189 if (ec == boost::asio::error::operation_aborted) 1190 { 1191 BMCWEB_LOG_DEBUG("Inotify was canceled (shutdown?)"); 1192 return; 1193 } 1194 if (ec) 1195 { 1196 BMCWEB_LOG_ERROR("Callback Error: {}", ec.message()); 1197 return; 1198 } 1199 std::size_t index = 0; 1200 while ((index + iEventSize) <= bytesTransferred) 1201 { 1202 struct inotify_event event 1203 {}; 1204 std::memcpy(&event, &readBuffer[index], iEventSize); 1205 if (event.wd == dirWatchDesc) 1206 { 1207 if ((event.len == 0) || 1208 (index + iEventSize + event.len > bytesTransferred)) 1209 { 1210 index += (iEventSize + event.len); 1211 continue; 1212 } 1213 1214 std::string fileName(&readBuffer[index + iEventSize]); 1215 if (fileName != "redfish") 1216 { 1217 index += (iEventSize + event.len); 1218 continue; 1219 } 1220 1221 BMCWEB_LOG_DEBUG( 1222 "Redfish log file created/deleted. event.name: {}", 1223 fileName); 1224 if (event.mask == IN_CREATE) 1225 { 1226 if (fileWatchDesc != -1) 1227 { 1228 BMCWEB_LOG_DEBUG( 1229 "Remove and Add inotify watcher on " 1230 "redfish event log file"); 1231 // Remove existing inotify watcher and add 1232 // with new redfish event log file. 1233 inotify_rm_watch(inotifyFd, fileWatchDesc); 1234 fileWatchDesc = -1; 1235 } 1236 1237 fileWatchDesc = inotify_add_watch( 1238 inotifyFd, redfishEventLogFile, IN_MODIFY); 1239 if (fileWatchDesc == -1) 1240 { 1241 BMCWEB_LOG_ERROR("inotify_add_watch failed for " 1242 "redfish log file."); 1243 return; 1244 } 1245 1246 EventServiceManager::getInstance() 1247 .resetRedfishFilePosition(); 1248 EventServiceManager::getInstance() 1249 .readEventLogsFromFile(); 1250 } 1251 else if ((event.mask == IN_DELETE) || 1252 (event.mask == IN_MOVED_TO)) 1253 { 1254 if (fileWatchDesc != -1) 1255 { 1256 inotify_rm_watch(inotifyFd, fileWatchDesc); 1257 fileWatchDesc = -1; 1258 } 1259 } 1260 } 1261 else if (event.wd == fileWatchDesc) 1262 { 1263 if (event.mask == IN_MODIFY) 1264 { 1265 EventServiceManager::getInstance() 1266 .readEventLogsFromFile(); 1267 } 1268 } 1269 index += (iEventSize + event.len); 1270 } 1271 1272 watchRedfishEventLogFile(); 1273 }); 1274 } 1275 1276 static int startEventLogMonitor(boost::asio::io_context& ioc) 1277 { 1278 inotifyConn.emplace(ioc); 1279 inotifyFd = inotify_init1(IN_NONBLOCK); 1280 if (inotifyFd == -1) 1281 { 1282 BMCWEB_LOG_ERROR("inotify_init1 failed."); 1283 return -1; 1284 } 1285 1286 // Add watch on directory to handle redfish event log file 1287 // create/delete. 1288 dirWatchDesc = inotify_add_watch(inotifyFd, redfishEventLogDir, 1289 IN_CREATE | IN_MOVED_TO | IN_DELETE); 1290 if (dirWatchDesc == -1) 1291 { 1292 BMCWEB_LOG_ERROR( 1293 "inotify_add_watch failed for event log directory."); 1294 return -1; 1295 } 1296 1297 // Watch redfish event log file for modifications. 1298 fileWatchDesc = inotify_add_watch(inotifyFd, redfishEventLogFile, 1299 IN_MODIFY); 1300 if (fileWatchDesc == -1) 1301 { 1302 BMCWEB_LOG_ERROR("inotify_add_watch failed for redfish log file."); 1303 // Don't return error if file not exist. 1304 // Watch on directory will handle create/delete of file. 1305 } 1306 1307 // monitor redfish event log file 1308 inotifyConn->assign(inotifyFd); 1309 watchRedfishEventLogFile(); 1310 1311 return 0; 1312 } 1313 1314 static void stopEventLogMonitor() 1315 { 1316 inotifyConn.reset(); 1317 } 1318 1319 static void getReadingsForReport(sdbusplus::message_t& msg) 1320 { 1321 if (msg.is_method_error()) 1322 { 1323 BMCWEB_LOG_ERROR("TelemetryMonitor Signal error"); 1324 return; 1325 } 1326 1327 sdbusplus::message::object_path path(msg.get_path()); 1328 std::string id = path.filename(); 1329 if (id.empty()) 1330 { 1331 BMCWEB_LOG_ERROR("Failed to get Id from path"); 1332 return; 1333 } 1334 1335 std::string interface; 1336 dbus::utility::DBusPropertiesMap props; 1337 std::vector<std::string> invalidProps; 1338 msg.read(interface, props, invalidProps); 1339 1340 auto found = std::ranges::find_if( 1341 props, [](const auto& x) { return x.first == "Readings"; }); 1342 if (found == props.end()) 1343 { 1344 BMCWEB_LOG_INFO("Failed to get Readings from Report properties"); 1345 return; 1346 } 1347 1348 const telemetry::TimestampReadings* readings = 1349 std::get_if<telemetry::TimestampReadings>(&found->second); 1350 if (readings == nullptr) 1351 { 1352 BMCWEB_LOG_INFO("Failed to get Readings from Report properties"); 1353 return; 1354 } 1355 1356 for (const auto& it : 1357 EventServiceManager::getInstance().subscriptionsMap) 1358 { 1359 Subscription& entry = *it.second; 1360 if (entry.eventFormatType == metricReportFormatType) 1361 { 1362 entry.filterAndSendReports(id, *readings); 1363 } 1364 } 1365 } 1366 1367 void unregisterMetricReportSignal() 1368 { 1369 if (matchTelemetryMonitor) 1370 { 1371 BMCWEB_LOG_DEBUG("Metrics report signal - Unregister"); 1372 matchTelemetryMonitor.reset(); 1373 matchTelemetryMonitor = nullptr; 1374 } 1375 } 1376 1377 void registerMetricReportSignal() 1378 { 1379 if (!serviceEnabled || matchTelemetryMonitor) 1380 { 1381 BMCWEB_LOG_DEBUG("Not registering metric report signal."); 1382 return; 1383 } 1384 1385 BMCWEB_LOG_DEBUG("Metrics report signal - Register"); 1386 std::string matchStr = "type='signal',member='PropertiesChanged'," 1387 "interface='org.freedesktop.DBus.Properties'," 1388 "arg0=xyz.openbmc_project.Telemetry.Report"; 1389 1390 matchTelemetryMonitor = std::make_shared<sdbusplus::bus::match_t>( 1391 *crow::connections::systemBus, matchStr, getReadingsForReport); 1392 } 1393 }; 1394 1395 } // namespace redfish 1396