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