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