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