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