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