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 "node.hpp" 18 #include "registries.hpp" 19 #include "registries/base_message_registry.hpp" 20 #include "registries/openbmc_message_registry.hpp" 21 #include "registries/task_event_message_registry.hpp" 22 23 #include <sys/inotify.h> 24 25 #include <boost/asio/io_context.hpp> 26 #include <boost/container/flat_map.hpp> 27 #include <error_messages.hpp> 28 #include <http_client.hpp> 29 #include <random.hpp> 30 #include <server_sent_events.hpp> 31 #include <utils/json_utils.hpp> 32 33 #include <cstdlib> 34 #include <ctime> 35 #include <fstream> 36 #include <memory> 37 #include <variant> 38 39 namespace redfish 40 { 41 42 using ReadingsObjType = 43 std::vector<std::tuple<std::string, std::string, double, int32_t>>; 44 using EventServiceConfig = std::tuple<bool, uint32_t, uint32_t>; 45 46 static constexpr const char* eventFormatType = "Event"; 47 static constexpr const char* metricReportFormatType = "MetricReport"; 48 49 static constexpr const char* eventServiceFile = 50 "/var/lib/bmcweb/eventservice_config.json"; 51 52 namespace message_registries 53 { 54 inline boost::beast::span<const MessageEntry> 55 getRegistryFromPrefix(const std::string& registryName) 56 { 57 if (task_event::header.registryPrefix == registryName) 58 { 59 return boost::beast::span<const MessageEntry>(task_event::registry); 60 } 61 if (openbmc::header.registryPrefix == registryName) 62 { 63 return boost::beast::span<const MessageEntry>(openbmc::registry); 64 } 65 if (base::header.registryPrefix == registryName) 66 { 67 return boost::beast::span<const MessageEntry>(base::registry); 68 } 69 return boost::beast::span<const MessageEntry>(openbmc::registry); 70 } 71 } // namespace message_registries 72 73 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES 74 static std::optional<boost::asio::posix::stream_descriptor> inotifyConn; 75 static constexpr const char* redfishEventLogDir = "/var/log"; 76 static constexpr const char* redfishEventLogFile = "/var/log/redfish"; 77 static constexpr const size_t iEventSize = sizeof(inotify_event); 78 static int inotifyFd = -1; 79 static int dirWatchDesc = -1; 80 static int fileWatchDesc = -1; 81 82 // <ID, timestamp, RedfishLogId, registryPrefix, MessageId, MessageArgs> 83 using EventLogObjectsType = 84 std::tuple<std::string, std::string, std::string, std::string, std::string, 85 std::vector<std::string>>; 86 87 namespace message_registries 88 { 89 static const Message* 90 getMsgFromRegistry(const std::string& messageKey, 91 const boost::beast::span<const MessageEntry>& registry) 92 { 93 boost::beast::span<const MessageEntry>::const_iterator messageIt = 94 std::find_if(registry.cbegin(), registry.cend(), 95 [&messageKey](const MessageEntry& messageEntry) { 96 return !messageKey.compare(messageEntry.first); 97 }); 98 if (messageIt != registry.cend()) 99 { 100 return &messageIt->second; 101 } 102 103 return nullptr; 104 } 105 106 static const Message* formatMessage(const std::string_view& messageID) 107 { 108 // Redfish MessageIds are in the form 109 // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find 110 // the right Message 111 std::vector<std::string> fields; 112 fields.reserve(4); 113 boost::split(fields, messageID, boost::is_any_of(".")); 114 if (fields.size() != 4) 115 { 116 return nullptr; 117 } 118 std::string& registryName = fields[0]; 119 std::string& messageKey = fields[3]; 120 121 // Find the right registry and check it for the MessageKey 122 return getMsgFromRegistry(messageKey, getRegistryFromPrefix(registryName)); 123 } 124 } // namespace message_registries 125 126 namespace event_log 127 { 128 inline bool getUniqueEntryID(const std::string& logEntry, std::string& entryID, 129 const bool firstEntry = true) 130 { 131 static time_t prevTs = 0; 132 static int index = 0; 133 if (firstEntry) 134 { 135 prevTs = 0; 136 } 137 138 // Get the entry timestamp 139 std::time_t curTs = 0; 140 std::tm timeStruct = {}; 141 std::istringstream entryStream(logEntry); 142 if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S")) 143 { 144 curTs = std::mktime(&timeStruct); 145 if (curTs == -1) 146 { 147 return false; 148 } 149 } 150 // If the timestamp isn't unique, increment the index 151 index = (curTs == prevTs) ? index + 1 : 0; 152 153 // Save the timestamp 154 prevTs = curTs; 155 156 entryID = std::to_string(curTs); 157 if (index > 0) 158 { 159 entryID += "_" + std::to_string(index); 160 } 161 return true; 162 } 163 164 inline int getEventLogParams(const std::string& logEntry, 165 std::string& timestamp, std::string& messageID, 166 std::vector<std::string>& messageArgs) 167 { 168 // The redfish log format is "<Timestamp> <MessageId>,<MessageArgs>" 169 // First get the Timestamp 170 size_t space = logEntry.find_first_of(' '); 171 if (space == std::string::npos) 172 { 173 return -EINVAL; 174 } 175 timestamp = logEntry.substr(0, space); 176 // Then get the log contents 177 size_t entryStart = logEntry.find_first_not_of(' ', space); 178 if (entryStart == std::string::npos) 179 { 180 return -EINVAL; 181 } 182 std::string_view entry(logEntry); 183 entry.remove_prefix(entryStart); 184 // Use split to separate the entry into its fields 185 std::vector<std::string> logEntryFields; 186 boost::split(logEntryFields, entry, boost::is_any_of(","), 187 boost::token_compress_on); 188 // We need at least a MessageId to be valid 189 if (logEntryFields.size() < 1) 190 { 191 return -EINVAL; 192 } 193 messageID = logEntryFields[0]; 194 195 // Get the MessageArgs from the log if there are any 196 if (logEntryFields.size() > 1) 197 { 198 std::string& messageArgsStart = logEntryFields[1]; 199 // If the first string is empty, assume there are no MessageArgs 200 if (!messageArgsStart.empty()) 201 { 202 messageArgs.assign(logEntryFields.begin() + 1, 203 logEntryFields.end()); 204 } 205 } 206 207 return 0; 208 } 209 210 inline void getRegistryAndMessageKey(const std::string& messageID, 211 std::string& registryName, 212 std::string& messageKey) 213 { 214 // Redfish MessageIds are in the form 215 // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find 216 // the right Message 217 std::vector<std::string> fields; 218 fields.reserve(4); 219 boost::split(fields, messageID, boost::is_any_of(".")); 220 if (fields.size() == 4) 221 { 222 registryName = fields[0]; 223 messageKey = fields[3]; 224 } 225 } 226 227 inline int formatEventLogEntry(const std::string& logEntryID, 228 const std::string& messageID, 229 const std::vector<std::string>& messageArgs, 230 std::string timestamp, 231 const std::string& customText, 232 nlohmann::json& logEntryJson) 233 { 234 // Get the Message from the MessageRegistry 235 const message_registries::Message* message = 236 message_registries::formatMessage(messageID); 237 238 std::string msg; 239 std::string severity; 240 if (message != nullptr) 241 { 242 msg = message->message; 243 severity = message->severity; 244 } 245 246 // Fill the MessageArgs into the Message 247 int i = 0; 248 for (const std::string& messageArg : messageArgs) 249 { 250 std::string argStr = "%" + std::to_string(++i); 251 size_t argPos = msg.find(argStr); 252 if (argPos != std::string::npos) 253 { 254 msg.replace(argPos, argStr.length(), messageArg); 255 } 256 } 257 258 // Get the Created time from the timestamp. The log timestamp is in 259 // RFC3339 format which matches the Redfish format except for the 260 // fractional seconds between the '.' and the '+', so just remove them. 261 std::size_t dot = timestamp.find_first_of('.'); 262 std::size_t plus = timestamp.find_first_of('+'); 263 if (dot != std::string::npos && plus != std::string::npos) 264 { 265 timestamp.erase(dot, plus - dot); 266 } 267 268 // Fill in the log entry with the gathered data 269 logEntryJson = {{"EventId", logEntryID}, 270 {"EventType", "Event"}, 271 {"Severity", std::move(severity)}, 272 {"Message", std::move(msg)}, 273 {"MessageId", messageID}, 274 {"MessageArgs", messageArgs}, 275 {"EventTimestamp", std::move(timestamp)}, 276 {"Context", customText}}; 277 return 0; 278 } 279 280 } // namespace event_log 281 #endif 282 283 inline bool isFilterQuerySpecialChar(char c) 284 { 285 switch (c) 286 { 287 case '(': 288 case ')': 289 case '\'': 290 return true; 291 default: 292 return false; 293 } 294 } 295 296 inline bool 297 readSSEQueryParams(std::string sseFilter, std::string& formatType, 298 std::vector<std::string>& messageIds, 299 std::vector<std::string>& registryPrefixes, 300 std::vector<std::string>& metricReportDefinitions) 301 { 302 sseFilter.erase(std::remove_if(sseFilter.begin(), sseFilter.end(), 303 isFilterQuerySpecialChar), 304 sseFilter.end()); 305 306 std::vector<std::string> result; 307 boost::split(result, sseFilter, boost::is_any_of(" "), 308 boost::token_compress_on); 309 310 BMCWEB_LOG_DEBUG << "No of tokens in SEE query: " << result.size(); 311 312 constexpr uint8_t divisor = 4; 313 constexpr uint8_t minTokenSize = 3; 314 if (result.size() % divisor != minTokenSize) 315 { 316 BMCWEB_LOG_ERROR << "Invalid SSE filter specified."; 317 return false; 318 } 319 320 for (std::size_t i = 0; i < result.size(); i += divisor) 321 { 322 std::string& key = result[i]; 323 std::string& op = result[i + 1]; 324 std::string& value = result[i + 2]; 325 326 if ((i + minTokenSize) < result.size()) 327 { 328 std::string& separator = result[i + minTokenSize]; 329 // SSE supports only "or" and "and" in query params. 330 if ((separator != "or") && (separator != "and")) 331 { 332 BMCWEB_LOG_ERROR 333 << "Invalid group operator in SSE query parameters"; 334 return false; 335 } 336 } 337 338 // SSE supports only "eq" as per spec. 339 if (op != "eq") 340 { 341 BMCWEB_LOG_ERROR 342 << "Invalid assignment operator in SSE query parameters"; 343 return false; 344 } 345 346 BMCWEB_LOG_DEBUG << key << " : " << value; 347 if (key == "EventFormatType") 348 { 349 formatType = value; 350 } 351 else if (key == "MessageId") 352 { 353 messageIds.push_back(value); 354 } 355 else if (key == "RegistryPrefix") 356 { 357 registryPrefixes.push_back(value); 358 } 359 else if (key == "MetricReportDefinition") 360 { 361 metricReportDefinitions.push_back(value); 362 } 363 else 364 { 365 BMCWEB_LOG_ERROR << "Invalid property(" << key 366 << ")in SSE filter query."; 367 return false; 368 } 369 } 370 return true; 371 } 372 373 class Subscription 374 { 375 public: 376 std::string id; 377 std::string destinationUrl; 378 std::string protocol; 379 std::string retryPolicy; 380 std::string customText; 381 std::string eventFormatType; 382 std::string subscriptionType; 383 std::vector<std::string> registryMsgIds; 384 std::vector<std::string> registryPrefixes; 385 std::vector<std::string> resourceTypes; 386 std::vector<nlohmann::json> httpHeaders; // key-value pair 387 std::vector<std::string> metricReportDefinitions; 388 389 Subscription(const Subscription&) = delete; 390 Subscription& operator=(const Subscription&) = delete; 391 Subscription(Subscription&&) = delete; 392 Subscription& operator=(Subscription&&) = delete; 393 394 Subscription(const std::string& inHost, const std::string& inPort, 395 const std::string& inPath, const std::string& inUriProto) : 396 eventSeqNum(1), 397 host(inHost), port(inPort), path(inPath), uriProto(inUriProto) 398 { 399 conn = std::make_shared<crow::HttpClient>( 400 crow::connections::systemBus->get_io_context(), id, host, port, 401 path); 402 } 403 404 Subscription(const std::shared_ptr<boost::beast::tcp_stream>& adaptor) : 405 eventSeqNum(1) 406 { 407 sseConn = std::make_shared<crow::ServerSentEvents>(adaptor); 408 } 409 410 ~Subscription() = default; 411 412 void sendEvent(const std::string& msg) 413 { 414 if (conn != nullptr) 415 { 416 std::vector<std::pair<std::string, std::string>> reqHeaders; 417 for (const auto& header : httpHeaders) 418 { 419 for (const auto& item : header.items()) 420 { 421 std::string key = item.key(); 422 std::string val = item.value(); 423 reqHeaders.emplace_back(std::pair(key, val)); 424 } 425 } 426 conn->setHeaders(reqHeaders); 427 conn->sendData(msg); 428 this->eventSeqNum++; 429 } 430 431 if (sseConn != nullptr) 432 { 433 sseConn->sendData(eventSeqNum, msg); 434 } 435 } 436 437 void sendTestEventLog() 438 { 439 nlohmann::json logEntryArray; 440 logEntryArray.push_back({}); 441 nlohmann::json& logEntryJson = logEntryArray.back(); 442 443 logEntryJson = {{"EventId", "TestID"}, 444 {"EventType", "Event"}, 445 {"Severity", "OK"}, 446 {"Message", "Generated test event"}, 447 {"MessageId", "OpenBMC.0.1.TestEventLog"}, 448 {"MessageArgs", nlohmann::json::array()}, 449 {"EventTimestamp", crow::utility::dateTimeNow()}, 450 {"Context", customText}}; 451 452 nlohmann::json msg = {{"@odata.type", "#Event.v1_4_0.Event"}, 453 {"Id", std::to_string(eventSeqNum)}, 454 {"Name", "Event Log"}, 455 {"Events", logEntryArray}}; 456 457 this->sendEvent( 458 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace)); 459 } 460 461 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES 462 void filterAndSendEventLogs( 463 const std::vector<EventLogObjectsType>& eventRecords) 464 { 465 nlohmann::json logEntryArray; 466 for (const EventLogObjectsType& logEntry : eventRecords) 467 { 468 const std::string& idStr = std::get<0>(logEntry); 469 const std::string& timestamp = std::get<1>(logEntry); 470 const std::string& messageID = std::get<2>(logEntry); 471 const std::string& registryName = std::get<3>(logEntry); 472 const std::string& messageKey = std::get<4>(logEntry); 473 const std::vector<std::string>& messageArgs = std::get<5>(logEntry); 474 475 // If registryPrefixes list is empty, don't filter events 476 // send everything. 477 if (registryPrefixes.size()) 478 { 479 auto obj = std::find(registryPrefixes.begin(), 480 registryPrefixes.end(), registryName); 481 if (obj == registryPrefixes.end()) 482 { 483 continue; 484 } 485 } 486 487 // If registryMsgIds list is empty, don't filter events 488 // send everything. 489 if (registryMsgIds.size()) 490 { 491 auto obj = std::find(registryMsgIds.begin(), 492 registryMsgIds.end(), messageKey); 493 if (obj == registryMsgIds.end()) 494 { 495 continue; 496 } 497 } 498 499 logEntryArray.push_back({}); 500 nlohmann::json& bmcLogEntry = logEntryArray.back(); 501 if (event_log::formatEventLogEntry(idStr, messageID, messageArgs, 502 timestamp, customText, 503 bmcLogEntry) != 0) 504 { 505 BMCWEB_LOG_DEBUG << "Read eventLog entry failed"; 506 continue; 507 } 508 } 509 510 if (logEntryArray.size() < 1) 511 { 512 BMCWEB_LOG_DEBUG << "No log entries available to be transferred."; 513 return; 514 } 515 516 nlohmann::json msg = {{"@odata.type", "#Event.v1_4_0.Event"}, 517 {"Id", std::to_string(eventSeqNum)}, 518 {"Name", "Event Log"}, 519 {"Events", logEntryArray}}; 520 521 this->sendEvent( 522 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace)); 523 } 524 #endif 525 526 void filterAndSendReports(const std::string& id2, 527 const std::string& readingsTs, 528 const ReadingsObjType& readings) 529 { 530 std::string metricReportDef = 531 "/redfish/v1/TelemetryService/MetricReportDefinitions/" + id2; 532 533 // Empty list means no filter. Send everything. 534 if (metricReportDefinitions.size()) 535 { 536 if (std::find(metricReportDefinitions.begin(), 537 metricReportDefinitions.end(), 538 metricReportDef) == metricReportDefinitions.end()) 539 { 540 return; 541 } 542 } 543 544 nlohmann::json metricValuesArray = nlohmann::json::array(); 545 for (const auto& it : readings) 546 { 547 metricValuesArray.push_back({}); 548 nlohmann::json& entry = metricValuesArray.back(); 549 550 auto& [id, property, value, timestamp] = it; 551 552 entry = {{"MetricId", id}, 553 {"MetricProperty", property}, 554 {"MetricValue", std::to_string(value)}, 555 {"Timestamp", crow::utility::getDateTime(timestamp)}}; 556 } 557 558 nlohmann::json msg = { 559 {"@odata.id", "/redfish/v1/TelemetryService/MetricReports/" + id}, 560 {"@odata.type", "#MetricReport.v1_3_0.MetricReport"}, 561 {"Id", id2}, 562 {"Name", id2}, 563 {"Timestamp", readingsTs}, 564 {"MetricReportDefinition", {{"@odata.id", metricReportDef}}}, 565 {"MetricValues", metricValuesArray}}; 566 567 this->sendEvent( 568 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace)); 569 } 570 571 void updateRetryConfig(const uint32_t retryAttempts, 572 const uint32_t retryTimeoutInterval) 573 { 574 if (conn != nullptr) 575 { 576 conn->setRetryConfig(retryAttempts, retryTimeoutInterval); 577 } 578 } 579 580 void updateRetryPolicy() 581 { 582 if (conn != nullptr) 583 { 584 conn->setRetryPolicy(retryPolicy); 585 } 586 } 587 588 uint64_t getEventSeqNum() 589 { 590 return eventSeqNum; 591 } 592 593 private: 594 uint64_t eventSeqNum; 595 std::string host; 596 std::string port; 597 std::string path; 598 std::string uriProto; 599 std::shared_ptr<crow::HttpClient> conn = nullptr; 600 std::shared_ptr<crow::ServerSentEvents> sseConn = nullptr; 601 }; 602 603 static constexpr const bool defaultEnabledState = true; 604 static constexpr const uint32_t defaultRetryAttempts = 3; 605 static constexpr const uint32_t defaultRetryInterval = 30; 606 static constexpr const char* defaulEventFormatType = "Event"; 607 static constexpr const char* defaulSubscriptionType = "RedfishEvent"; 608 static constexpr const char* defaulRetryPolicy = "TerminateAfterRetries"; 609 610 class EventServiceManager 611 { 612 private: 613 bool serviceEnabled; 614 uint32_t retryAttempts; 615 uint32_t retryTimeoutInterval; 616 617 EventServiceManager() 618 { 619 // Load config from persist store. 620 initConfig(); 621 } 622 623 std::string lastEventTStr; 624 size_t noOfEventLogSubscribers{0}; 625 size_t noOfMetricReportSubscribers{0}; 626 std::shared_ptr<sdbusplus::bus::match::match> matchTelemetryMonitor; 627 boost::container::flat_map<std::string, std::shared_ptr<Subscription>> 628 subscriptionsMap; 629 630 uint64_t eventId{1}; 631 632 public: 633 EventServiceManager(const EventServiceManager&) = delete; 634 EventServiceManager& operator=(const EventServiceManager&) = delete; 635 EventServiceManager(EventServiceManager&&) = delete; 636 EventServiceManager& operator=(EventServiceManager&&) = delete; 637 638 static EventServiceManager& getInstance() 639 { 640 static EventServiceManager handler; 641 return handler; 642 } 643 644 void loadDefaultConfig() 645 { 646 serviceEnabled = defaultEnabledState; 647 retryAttempts = defaultRetryAttempts; 648 retryTimeoutInterval = defaultRetryInterval; 649 } 650 651 void initConfig() 652 { 653 std::ifstream eventConfigFile(eventServiceFile); 654 if (!eventConfigFile.good()) 655 { 656 BMCWEB_LOG_DEBUG << "EventService config not exist"; 657 loadDefaultConfig(); 658 return; 659 } 660 auto jsonData = nlohmann::json::parse(eventConfigFile, nullptr, false); 661 if (jsonData.is_discarded()) 662 { 663 BMCWEB_LOG_ERROR << "EventService config parse error."; 664 loadDefaultConfig(); 665 return; 666 } 667 668 nlohmann::json jsonConfig; 669 if (json_util::getValueFromJsonObject(jsonData, "Configuration", 670 jsonConfig)) 671 { 672 if (!json_util::getValueFromJsonObject(jsonConfig, "ServiceEnabled", 673 serviceEnabled)) 674 { 675 serviceEnabled = defaultEnabledState; 676 } 677 if (!json_util::getValueFromJsonObject( 678 jsonConfig, "DeliveryRetryAttempts", retryAttempts)) 679 { 680 retryAttempts = defaultRetryAttempts; 681 } 682 if (!json_util::getValueFromJsonObject( 683 jsonConfig, "DeliveryRetryIntervalSeconds", 684 retryTimeoutInterval)) 685 { 686 retryTimeoutInterval = defaultRetryInterval; 687 } 688 } 689 else 690 { 691 loadDefaultConfig(); 692 } 693 694 nlohmann::json subscriptionsList; 695 if (!json_util::getValueFromJsonObject(jsonData, "Subscriptions", 696 subscriptionsList)) 697 { 698 BMCWEB_LOG_DEBUG << "EventService: Subscriptions not exist."; 699 return; 700 } 701 702 for (nlohmann::json& jsonObj : subscriptionsList) 703 { 704 std::string protocol; 705 if (!json_util::getValueFromJsonObject(jsonObj, "Protocol", 706 protocol)) 707 { 708 BMCWEB_LOG_DEBUG << "Invalid subscription Protocol exist."; 709 continue; 710 } 711 712 std::string subscriptionType; 713 if (!json_util::getValueFromJsonObject(jsonObj, "SubscriptionType", 714 subscriptionType)) 715 { 716 subscriptionType = defaulSubscriptionType; 717 } 718 // SSE connections are initiated from client 719 // and can't be re-established from server. 720 if (subscriptionType == "SSE") 721 { 722 BMCWEB_LOG_DEBUG 723 << "The subscription type is SSE, so skipping."; 724 continue; 725 } 726 727 std::string destination; 728 if (!json_util::getValueFromJsonObject(jsonObj, "Destination", 729 destination)) 730 { 731 BMCWEB_LOG_DEBUG << "Invalid subscription destination exist."; 732 continue; 733 } 734 std::string host; 735 std::string urlProto; 736 std::string port; 737 std::string path; 738 bool status = 739 validateAndSplitUrl(destination, urlProto, host, port, path); 740 741 if (!status) 742 { 743 BMCWEB_LOG_ERROR 744 << "Failed to validate and split destination url"; 745 continue; 746 } 747 std::shared_ptr<Subscription> subValue = 748 std::make_shared<Subscription>(host, port, path, urlProto); 749 750 subValue->destinationUrl = destination; 751 subValue->protocol = protocol; 752 subValue->subscriptionType = subscriptionType; 753 if (!json_util::getValueFromJsonObject( 754 jsonObj, "DeliveryRetryPolicy", subValue->retryPolicy)) 755 { 756 subValue->retryPolicy = defaulRetryPolicy; 757 } 758 if (!json_util::getValueFromJsonObject(jsonObj, "EventFormatType", 759 subValue->eventFormatType)) 760 { 761 subValue->eventFormatType = defaulEventFormatType; 762 } 763 json_util::getValueFromJsonObject(jsonObj, "Context", 764 subValue->customText); 765 json_util::getValueFromJsonObject(jsonObj, "MessageIds", 766 subValue->registryMsgIds); 767 json_util::getValueFromJsonObject(jsonObj, "RegistryPrefixes", 768 subValue->registryPrefixes); 769 json_util::getValueFromJsonObject(jsonObj, "ResourceTypes", 770 subValue->resourceTypes); 771 json_util::getValueFromJsonObject(jsonObj, "HttpHeaders", 772 subValue->httpHeaders); 773 json_util::getValueFromJsonObject( 774 jsonObj, "MetricReportDefinitions", 775 subValue->metricReportDefinitions); 776 777 std::string id = addSubscription(subValue, false); 778 if (id.empty()) 779 { 780 BMCWEB_LOG_ERROR << "Failed to add subscription"; 781 } 782 } 783 return; 784 } 785 786 void updateSubscriptionData() 787 { 788 // Persist the config and subscription data. 789 nlohmann::json jsonData; 790 791 nlohmann::json& configObj = jsonData["Configuration"]; 792 configObj["ServiceEnabled"] = serviceEnabled; 793 configObj["DeliveryRetryAttempts"] = retryAttempts; 794 configObj["DeliveryRetryIntervalSeconds"] = retryTimeoutInterval; 795 796 nlohmann::json& subListArray = jsonData["Subscriptions"]; 797 subListArray = nlohmann::json::array(); 798 799 for (const auto& it : subscriptionsMap) 800 { 801 std::shared_ptr<Subscription> subValue = it.second; 802 // Don't preserve SSE connections. Its initiated from 803 // client side and can't be re-established from server. 804 if (subValue->subscriptionType == "SSE") 805 { 806 BMCWEB_LOG_DEBUG 807 << "The subscription type is SSE, so skipping."; 808 continue; 809 } 810 811 nlohmann::json entry; 812 entry["Context"] = subValue->customText; 813 entry["DeliveryRetryPolicy"] = subValue->retryPolicy; 814 entry["Destination"] = subValue->destinationUrl; 815 entry["EventFormatType"] = subValue->eventFormatType; 816 entry["HttpHeaders"] = subValue->httpHeaders; 817 entry["MessageIds"] = subValue->registryMsgIds; 818 entry["Protocol"] = subValue->protocol; 819 entry["RegistryPrefixes"] = subValue->registryPrefixes; 820 entry["ResourceTypes"] = subValue->resourceTypes; 821 entry["SubscriptionType"] = subValue->subscriptionType; 822 entry["MetricReportDefinitions"] = 823 subValue->metricReportDefinitions; 824 825 subListArray.push_back(entry); 826 } 827 828 const std::string tmpFile(std::string(eventServiceFile) + "_tmp"); 829 std::ofstream ofs(tmpFile, std::ios::out); 830 const auto& writeData = jsonData.dump( 831 2, ' ', true, nlohmann::json::error_handler_t::replace); 832 ofs << writeData; 833 ofs.close(); 834 835 BMCWEB_LOG_DEBUG << "EventService config updated to file."; 836 if (std::rename(tmpFile.c_str(), eventServiceFile) != 0) 837 { 838 BMCWEB_LOG_ERROR << "Error in renaming temporary file: " 839 << tmpFile.c_str(); 840 } 841 } 842 843 EventServiceConfig getEventServiceConfig() 844 { 845 return {serviceEnabled, retryAttempts, retryTimeoutInterval}; 846 } 847 848 void setEventServiceConfig(const EventServiceConfig& cfg) 849 { 850 bool updateConfig = false; 851 bool updateRetryCfg = false; 852 853 if (serviceEnabled != std::get<0>(cfg)) 854 { 855 serviceEnabled = std::get<0>(cfg); 856 if (serviceEnabled && noOfMetricReportSubscribers) 857 { 858 registerMetricReportSignal(); 859 } 860 else 861 { 862 unregisterMetricReportSignal(); 863 } 864 updateConfig = true; 865 } 866 867 if (retryAttempts != std::get<1>(cfg)) 868 { 869 retryAttempts = std::get<1>(cfg); 870 updateConfig = true; 871 updateRetryCfg = true; 872 } 873 874 if (retryTimeoutInterval != std::get<2>(cfg)) 875 { 876 retryTimeoutInterval = std::get<2>(cfg); 877 updateConfig = true; 878 updateRetryCfg = true; 879 } 880 881 if (updateConfig) 882 { 883 updateSubscriptionData(); 884 } 885 886 if (updateRetryCfg) 887 { 888 // Update the changed retry config to all subscriptions 889 for (const auto& it : 890 EventServiceManager::getInstance().subscriptionsMap) 891 { 892 std::shared_ptr<Subscription> entry = it.second; 893 entry->updateRetryConfig(retryAttempts, retryTimeoutInterval); 894 } 895 } 896 } 897 898 void updateNoOfSubscribersCount() 899 { 900 size_t eventLogSubCount = 0; 901 size_t metricReportSubCount = 0; 902 for (const auto& it : subscriptionsMap) 903 { 904 std::shared_ptr<Subscription> entry = it.second; 905 if (entry->eventFormatType == eventFormatType) 906 { 907 eventLogSubCount++; 908 } 909 else if (entry->eventFormatType == metricReportFormatType) 910 { 911 metricReportSubCount++; 912 } 913 } 914 915 noOfEventLogSubscribers = eventLogSubCount; 916 if (noOfMetricReportSubscribers != metricReportSubCount) 917 { 918 noOfMetricReportSubscribers = metricReportSubCount; 919 if (noOfMetricReportSubscribers) 920 { 921 registerMetricReportSignal(); 922 } 923 else 924 { 925 unregisterMetricReportSignal(); 926 } 927 } 928 } 929 930 std::shared_ptr<Subscription> getSubscription(const std::string& id) 931 { 932 auto obj = subscriptionsMap.find(id); 933 if (obj == subscriptionsMap.end()) 934 { 935 BMCWEB_LOG_ERROR << "No subscription exist with ID:" << id; 936 return nullptr; 937 } 938 std::shared_ptr<Subscription> subValue = obj->second; 939 return subValue; 940 } 941 942 std::string addSubscription(const std::shared_ptr<Subscription>& subValue, 943 const bool updateFile = true) 944 { 945 946 std::uniform_int_distribution<uint32_t> dist(0); 947 bmcweb::OpenSSLGenerator gen; 948 949 std::string id; 950 951 int retry = 3; 952 while (retry) 953 { 954 id = std::to_string(dist(gen)); 955 if (gen.error()) 956 { 957 retry = 0; 958 break; 959 } 960 auto inserted = subscriptionsMap.insert(std::pair(id, subValue)); 961 if (inserted.second) 962 { 963 break; 964 } 965 --retry; 966 } 967 968 if (retry <= 0) 969 { 970 BMCWEB_LOG_ERROR << "Failed to generate random number"; 971 return std::string(""); 972 } 973 974 updateNoOfSubscribersCount(); 975 976 if (updateFile) 977 { 978 updateSubscriptionData(); 979 } 980 981 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES 982 if (lastEventTStr.empty()) 983 { 984 cacheLastEventTimestamp(); 985 } 986 #endif 987 // Update retry configuration. 988 subValue->updateRetryConfig(retryAttempts, retryTimeoutInterval); 989 subValue->updateRetryPolicy(); 990 991 return id; 992 } 993 994 bool isSubscriptionExist(const std::string& id) 995 { 996 auto obj = subscriptionsMap.find(id); 997 if (obj == subscriptionsMap.end()) 998 { 999 return false; 1000 } 1001 return true; 1002 } 1003 1004 void deleteSubscription(const std::string& id) 1005 { 1006 auto obj = subscriptionsMap.find(id); 1007 if (obj != subscriptionsMap.end()) 1008 { 1009 subscriptionsMap.erase(obj); 1010 updateNoOfSubscribersCount(); 1011 updateSubscriptionData(); 1012 } 1013 } 1014 1015 size_t getNumberOfSubscriptions() 1016 { 1017 return subscriptionsMap.size(); 1018 } 1019 1020 std::vector<std::string> getAllIDs() 1021 { 1022 std::vector<std::string> idList; 1023 for (const auto& it : subscriptionsMap) 1024 { 1025 idList.emplace_back(it.first); 1026 } 1027 return idList; 1028 } 1029 1030 bool isDestinationExist(const std::string& destUrl) 1031 { 1032 for (const auto& it : subscriptionsMap) 1033 { 1034 std::shared_ptr<Subscription> entry = it.second; 1035 if (entry->destinationUrl == destUrl) 1036 { 1037 BMCWEB_LOG_ERROR << "Destination exist already" << destUrl; 1038 return true; 1039 } 1040 } 1041 return false; 1042 } 1043 1044 void sendTestEventLog() 1045 { 1046 for (const auto& it : this->subscriptionsMap) 1047 { 1048 std::shared_ptr<Subscription> entry = it.second; 1049 entry->sendTestEventLog(); 1050 } 1051 } 1052 1053 void sendEvent(const nlohmann::json& eventMessageIn, 1054 const std::string& origin, const std::string& resType) 1055 { 1056 nlohmann::json eventRecord = nlohmann::json::array(); 1057 nlohmann::json eventMessage = eventMessageIn; 1058 // MemberId is 0 : since we are sending one event record. 1059 uint64_t memberId = 0; 1060 1061 nlohmann::json event = { 1062 {"EventId", eventId}, 1063 {"MemberId", memberId}, 1064 {"EventTimestamp", crow::utility::dateTimeNow()}, 1065 {"OriginOfCondition", origin}}; 1066 for (nlohmann::json::iterator it = event.begin(); it != event.end(); 1067 ++it) 1068 { 1069 eventMessage[it.key()] = it.value(); 1070 } 1071 eventRecord.push_back(eventMessage); 1072 1073 for (const auto& it : this->subscriptionsMap) 1074 { 1075 std::shared_ptr<Subscription> entry = it.second; 1076 bool isSubscribed = false; 1077 // Search the resourceTypes list for the subscription. 1078 // If resourceTypes list is empty, don't filter events 1079 // send everything. 1080 if (entry->resourceTypes.size()) 1081 { 1082 for (const auto& resource : entry->resourceTypes) 1083 { 1084 if (resType == resource) 1085 { 1086 BMCWEB_LOG_INFO << "ResourceType " << resource 1087 << " found in the subscribed list"; 1088 isSubscribed = true; 1089 break; 1090 } 1091 } 1092 } 1093 else // resourceTypes list is empty. 1094 { 1095 isSubscribed = true; 1096 } 1097 if (isSubscribed) 1098 { 1099 nlohmann::json msgJson = { 1100 {"@odata.type", "#Event.v1_4_0.Event"}, 1101 {"Name", "Event Log"}, 1102 {"Id", eventId}, 1103 {"Events", eventRecord}}; 1104 entry->sendEvent(msgJson.dump( 1105 2, ' ', true, nlohmann::json::error_handler_t::replace)); 1106 eventId++; // increament the eventId 1107 } 1108 else 1109 { 1110 BMCWEB_LOG_INFO << "Not subscribed to this resource"; 1111 } 1112 } 1113 } 1114 void sendBroadcastMsg(const std::string& broadcastMsg) 1115 { 1116 for (const auto& it : this->subscriptionsMap) 1117 { 1118 std::shared_ptr<Subscription> entry = it.second; 1119 nlohmann::json msgJson = { 1120 {"Timestamp", crow::utility::dateTimeNow()}, 1121 {"OriginOfCondition", "/ibm/v1/HMC/BroadcastService"}, 1122 {"Name", "Broadcast Message"}, 1123 {"Message", broadcastMsg}}; 1124 entry->sendEvent(msgJson.dump( 1125 2, ' ', true, nlohmann::json::error_handler_t::replace)); 1126 } 1127 } 1128 1129 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES 1130 void cacheLastEventTimestamp() 1131 { 1132 lastEventTStr.clear(); 1133 std::ifstream logStream(redfishEventLogFile); 1134 if (!logStream.good()) 1135 { 1136 BMCWEB_LOG_ERROR << " Redfish log file open failed \n"; 1137 return; 1138 } 1139 std::string logEntry; 1140 while (std::getline(logStream, logEntry)) 1141 { 1142 size_t space = logEntry.find_first_of(' '); 1143 if (space == std::string::npos) 1144 { 1145 // Shouldn't enter here but lets skip it. 1146 BMCWEB_LOG_DEBUG << "Invalid log entry found."; 1147 continue; 1148 } 1149 lastEventTStr = logEntry.substr(0, space); 1150 } 1151 BMCWEB_LOG_DEBUG << "Last Event time stamp set: " << lastEventTStr; 1152 } 1153 1154 void readEventLogsFromFile() 1155 { 1156 if (!serviceEnabled || !noOfEventLogSubscribers) 1157 { 1158 BMCWEB_LOG_DEBUG << "EventService disabled or no Subscriptions."; 1159 return; 1160 } 1161 std::ifstream logStream(redfishEventLogFile); 1162 if (!logStream.good()) 1163 { 1164 BMCWEB_LOG_ERROR << " Redfish log file open failed"; 1165 return; 1166 } 1167 1168 std::vector<EventLogObjectsType> eventRecords; 1169 1170 bool startLogCollection = false; 1171 bool firstEntry = true; 1172 1173 std::string logEntry; 1174 while (std::getline(logStream, logEntry)) 1175 { 1176 if (!startLogCollection && !lastEventTStr.empty()) 1177 { 1178 if (boost::starts_with(logEntry, lastEventTStr)) 1179 { 1180 startLogCollection = true; 1181 } 1182 continue; 1183 } 1184 1185 std::string idStr; 1186 if (!event_log::getUniqueEntryID(logEntry, idStr, firstEntry)) 1187 { 1188 continue; 1189 } 1190 firstEntry = false; 1191 1192 std::string timestamp; 1193 std::string messageID; 1194 std::vector<std::string> messageArgs; 1195 if (event_log::getEventLogParams(logEntry, timestamp, messageID, 1196 messageArgs) != 0) 1197 { 1198 BMCWEB_LOG_DEBUG << "Read eventLog entry params failed"; 1199 continue; 1200 } 1201 1202 std::string registryName; 1203 std::string messageKey; 1204 event_log::getRegistryAndMessageKey(messageID, registryName, 1205 messageKey); 1206 if (registryName.empty() || messageKey.empty()) 1207 { 1208 continue; 1209 } 1210 1211 lastEventTStr = timestamp; 1212 eventRecords.emplace_back(idStr, timestamp, messageID, registryName, 1213 messageKey, messageArgs); 1214 } 1215 1216 for (const auto& it : this->subscriptionsMap) 1217 { 1218 std::shared_ptr<Subscription> entry = it.second; 1219 if (entry->eventFormatType == "Event") 1220 { 1221 entry->filterAndSendEventLogs(eventRecords); 1222 } 1223 } 1224 } 1225 1226 static void watchRedfishEventLogFile() 1227 { 1228 if (!inotifyConn) 1229 { 1230 return; 1231 } 1232 1233 static std::array<char, 1024> readBuffer; 1234 1235 inotifyConn->async_read_some( 1236 boost::asio::buffer(readBuffer), 1237 [&](const boost::system::error_code& ec, 1238 const std::size_t& bytesTransferred) { 1239 if (ec) 1240 { 1241 BMCWEB_LOG_ERROR << "Callback Error: " << ec.message(); 1242 return; 1243 } 1244 std::size_t index = 0; 1245 while ((index + iEventSize) <= bytesTransferred) 1246 { 1247 struct inotify_event event; 1248 std::memcpy(&event, &readBuffer[index], iEventSize); 1249 if (event.wd == dirWatchDesc) 1250 { 1251 if ((event.len == 0) || 1252 (index + iEventSize + event.len > bytesTransferred)) 1253 { 1254 index += (iEventSize + event.len); 1255 continue; 1256 } 1257 1258 std::string fileName(&readBuffer[index + iEventSize], 1259 event.len); 1260 if (std::strcmp(fileName.c_str(), "redfish") != 0) 1261 { 1262 index += (iEventSize + event.len); 1263 continue; 1264 } 1265 1266 BMCWEB_LOG_DEBUG 1267 << "Redfish log file created/deleted. event.name: " 1268 << fileName; 1269 if (event.mask == IN_CREATE) 1270 { 1271 if (fileWatchDesc != -1) 1272 { 1273 BMCWEB_LOG_DEBUG 1274 << "Remove and Add inotify watcher on " 1275 "redfish event log file"; 1276 // Remove existing inotify watcher and add 1277 // with new redfish event log file. 1278 inotify_rm_watch(inotifyFd, fileWatchDesc); 1279 fileWatchDesc = -1; 1280 } 1281 1282 fileWatchDesc = inotify_add_watch( 1283 inotifyFd, redfishEventLogFile, IN_MODIFY); 1284 if (fileWatchDesc == -1) 1285 { 1286 BMCWEB_LOG_ERROR 1287 << "inotify_add_watch failed for " 1288 "redfish log file."; 1289 return; 1290 } 1291 1292 EventServiceManager::getInstance() 1293 .cacheLastEventTimestamp(); 1294 EventServiceManager::getInstance() 1295 .readEventLogsFromFile(); 1296 } 1297 else if ((event.mask == IN_DELETE) || 1298 (event.mask == IN_MOVED_TO)) 1299 { 1300 if (fileWatchDesc != -1) 1301 { 1302 inotify_rm_watch(inotifyFd, fileWatchDesc); 1303 fileWatchDesc = -1; 1304 } 1305 } 1306 } 1307 else if (event.wd == fileWatchDesc) 1308 { 1309 if (event.mask == IN_MODIFY) 1310 { 1311 EventServiceManager::getInstance() 1312 .readEventLogsFromFile(); 1313 } 1314 } 1315 index += (iEventSize + event.len); 1316 } 1317 1318 watchRedfishEventLogFile(); 1319 }); 1320 } 1321 1322 static int startEventLogMonitor(boost::asio::io_context& ioc) 1323 { 1324 inotifyConn.emplace(ioc); 1325 inotifyFd = inotify_init1(IN_NONBLOCK); 1326 if (inotifyFd == -1) 1327 { 1328 BMCWEB_LOG_ERROR << "inotify_init1 failed."; 1329 return -1; 1330 } 1331 1332 // Add watch on directory to handle redfish event log file 1333 // create/delete. 1334 dirWatchDesc = inotify_add_watch(inotifyFd, redfishEventLogDir, 1335 IN_CREATE | IN_MOVED_TO | IN_DELETE); 1336 if (dirWatchDesc == -1) 1337 { 1338 BMCWEB_LOG_ERROR 1339 << "inotify_add_watch failed for event log directory."; 1340 return -1; 1341 } 1342 1343 // Watch redfish event log file for modifications. 1344 fileWatchDesc = 1345 inotify_add_watch(inotifyFd, redfishEventLogFile, IN_MODIFY); 1346 if (fileWatchDesc == -1) 1347 { 1348 BMCWEB_LOG_ERROR 1349 << "inotify_add_watch failed for redfish log file."; 1350 // Don't return error if file not exist. 1351 // Watch on directory will handle create/delete of file. 1352 } 1353 1354 // monitor redfish event log file 1355 inotifyConn->assign(inotifyFd); 1356 watchRedfishEventLogFile(); 1357 1358 return 0; 1359 } 1360 1361 #endif 1362 1363 void getMetricReading(const std::string& service, 1364 const std::string& objPath, const std::string& intf) 1365 { 1366 std::size_t found = objPath.find_last_of('/'); 1367 if (found == std::string::npos) 1368 { 1369 BMCWEB_LOG_DEBUG << "Invalid objPath received"; 1370 return; 1371 } 1372 1373 std::string idStr = objPath.substr(found + 1); 1374 if (idStr.empty()) 1375 { 1376 BMCWEB_LOG_DEBUG << "Invalid ID in objPath"; 1377 return; 1378 } 1379 1380 crow::connections::systemBus->async_method_call( 1381 [idStr{std::move(idStr)}]( 1382 const boost::system::error_code ec, 1383 boost::container::flat_map< 1384 std::string, std::variant<int32_t, ReadingsObjType>>& 1385 resp) { 1386 if (ec) 1387 { 1388 BMCWEB_LOG_DEBUG 1389 << "D-Bus call failed to GetAll metric readings."; 1390 return; 1391 } 1392 1393 const int32_t* timestampPtr = 1394 std::get_if<int32_t>(&resp["Timestamp"]); 1395 if (!timestampPtr) 1396 { 1397 BMCWEB_LOG_DEBUG << "Failed to Get timestamp."; 1398 return; 1399 } 1400 1401 ReadingsObjType* readingsPtr = 1402 std::get_if<ReadingsObjType>(&resp["Readings"]); 1403 if (!readingsPtr) 1404 { 1405 BMCWEB_LOG_DEBUG << "Failed to Get Readings property."; 1406 return; 1407 } 1408 1409 if (!readingsPtr->size()) 1410 { 1411 BMCWEB_LOG_DEBUG << "No metrics report to be transferred"; 1412 return; 1413 } 1414 1415 for (const auto& it : 1416 EventServiceManager::getInstance().subscriptionsMap) 1417 { 1418 std::shared_ptr<Subscription> entry = it.second; 1419 if (entry->eventFormatType == metricReportFormatType) 1420 { 1421 entry->filterAndSendReports( 1422 idStr, crow::utility::getDateTime(*timestampPtr), 1423 *readingsPtr); 1424 } 1425 } 1426 }, 1427 service, objPath, "org.freedesktop.DBus.Properties", "GetAll", 1428 intf); 1429 } 1430 1431 void unregisterMetricReportSignal() 1432 { 1433 if (matchTelemetryMonitor) 1434 { 1435 BMCWEB_LOG_DEBUG << "Metrics report signal - Unregister"; 1436 matchTelemetryMonitor.reset(); 1437 matchTelemetryMonitor = nullptr; 1438 } 1439 } 1440 1441 void registerMetricReportSignal() 1442 { 1443 if (!serviceEnabled || matchTelemetryMonitor) 1444 { 1445 BMCWEB_LOG_DEBUG << "Not registering metric report signal."; 1446 return; 1447 } 1448 1449 BMCWEB_LOG_DEBUG << "Metrics report signal - Register"; 1450 std::string matchStr( 1451 "type='signal',member='ReportUpdate', " 1452 "interface='xyz.openbmc_project.MonitoringService.Report'"); 1453 1454 matchTelemetryMonitor = std::make_shared<sdbusplus::bus::match::match>( 1455 *crow::connections::systemBus, matchStr, 1456 [this](sdbusplus::message::message& msg) { 1457 if (msg.is_method_error()) 1458 { 1459 BMCWEB_LOG_ERROR << "TelemetryMonitor Signal error"; 1460 return; 1461 } 1462 1463 std::string service = msg.get_sender(); 1464 std::string objPath = msg.get_path(); 1465 std::string intf = msg.get_interface(); 1466 getMetricReading(service, objPath, intf); 1467 }); 1468 } 1469 1470 bool validateAndSplitUrl(const std::string& destUrl, std::string& urlProto, 1471 std::string& host, std::string& port, 1472 std::string& path) 1473 { 1474 // Validate URL using regex expression 1475 // Format: <protocol>://<host>:<port>/<path> 1476 // protocol: http/https 1477 const std::regex urlRegex( 1478 "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/" 1479 "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)"); 1480 std::cmatch match; 1481 if (!std::regex_match(destUrl.c_str(), match, urlRegex)) 1482 { 1483 BMCWEB_LOG_INFO << "Dest. url did not match "; 1484 return false; 1485 } 1486 1487 urlProto = std::string(match[1].first, match[1].second); 1488 if (urlProto == "http") 1489 { 1490 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING 1491 return false; 1492 #endif 1493 } 1494 1495 host = std::string(match[2].first, match[2].second); 1496 port = std::string(match[3].first, match[3].second); 1497 path = std::string(match[4].first, match[4].second); 1498 if (port.empty()) 1499 { 1500 if (urlProto == "http") 1501 { 1502 port = "80"; 1503 } 1504 else 1505 { 1506 port = "443"; 1507 } 1508 } 1509 if (path.empty()) 1510 { 1511 path = "/"; 1512 } 1513 return true; 1514 } 1515 }; 1516 1517 } // namespace redfish 1518