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