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