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