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