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( 447 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace)); 448 } 449 450 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES 451 void filterAndSendEventLogs( 452 const std::vector<EventLogObjectsType>& eventRecords) 453 { 454 nlohmann::json logEntryArray; 455 for (const EventLogObjectsType& logEntry : eventRecords) 456 { 457 const std::string& idStr = std::get<0>(logEntry); 458 const std::string& timestamp = std::get<1>(logEntry); 459 const std::string& messageID = std::get<2>(logEntry); 460 const std::string& registryName = std::get<3>(logEntry); 461 const std::string& messageKey = std::get<4>(logEntry); 462 const std::vector<std::string>& messageArgs = std::get<5>(logEntry); 463 464 // If registryPrefixes list is empty, don't filter events 465 // send everything. 466 if (registryPrefixes.size()) 467 { 468 auto obj = std::find(registryPrefixes.begin(), 469 registryPrefixes.end(), registryName); 470 if (obj == registryPrefixes.end()) 471 { 472 continue; 473 } 474 } 475 476 // If registryMsgIds list is empty, don't filter events 477 // send everything. 478 if (registryMsgIds.size()) 479 { 480 auto obj = std::find(registryMsgIds.begin(), 481 registryMsgIds.end(), messageKey); 482 if (obj == registryMsgIds.end()) 483 { 484 continue; 485 } 486 } 487 488 logEntryArray.push_back({}); 489 nlohmann::json& bmcLogEntry = logEntryArray.back(); 490 if (event_log::formatEventLogEntry(idStr, messageID, messageArgs, 491 timestamp, customText, 492 bmcLogEntry) != 0) 493 { 494 BMCWEB_LOG_DEBUG << "Read eventLog entry failed"; 495 continue; 496 } 497 } 498 499 if (logEntryArray.size() < 1) 500 { 501 BMCWEB_LOG_DEBUG << "No log entries available to be transferred."; 502 return; 503 } 504 505 nlohmann::json msg = {{"@odata.type", "#Event.v1_4_0.Event"}, 506 {"Id", std::to_string(eventSeqNum)}, 507 {"Name", "Event Log"}, 508 {"Events", logEntryArray}}; 509 510 this->sendEvent( 511 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace)); 512 } 513 #endif 514 515 void filterAndSendReports(const std::string& id2, 516 const std::string& readingsTs, 517 const ReadingsObjType& readings) 518 { 519 std::string metricReportDef = 520 "/redfish/v1/TelemetryService/MetricReportDefinitions/" + id2; 521 522 // Empty list means no filter. Send everything. 523 if (metricReportDefinitions.size()) 524 { 525 if (std::find(metricReportDefinitions.begin(), 526 metricReportDefinitions.end(), 527 metricReportDef) == metricReportDefinitions.end()) 528 { 529 return; 530 } 531 } 532 533 nlohmann::json metricValuesArray = nlohmann::json::array(); 534 for (const auto& it : readings) 535 { 536 metricValuesArray.push_back({}); 537 nlohmann::json& entry = metricValuesArray.back(); 538 539 auto& [id, property, value, timestamp] = it; 540 541 entry = {{"MetricId", id}, 542 {"MetricProperty", property}, 543 {"MetricValue", std::to_string(value)}, 544 {"Timestamp", crow::utility::getDateTime(timestamp)}}; 545 } 546 547 nlohmann::json msg = { 548 {"@odata.id", "/redfish/v1/TelemetryService/MetricReports/" + id}, 549 {"@odata.type", "#MetricReport.v1_3_0.MetricReport"}, 550 {"Id", id2}, 551 {"Name", id2}, 552 {"Timestamp", readingsTs}, 553 {"MetricReportDefinition", {{"@odata.id", metricReportDef}}}, 554 {"MetricValues", metricValuesArray}}; 555 556 this->sendEvent( 557 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace)); 558 } 559 560 void updateRetryConfig(const uint32_t retryAttempts, 561 const uint32_t retryTimeoutInterval) 562 { 563 if (conn != nullptr) 564 { 565 conn->setRetryConfig(retryAttempts, retryTimeoutInterval); 566 } 567 } 568 569 void updateRetryPolicy() 570 { 571 if (conn != nullptr) 572 { 573 conn->setRetryPolicy(retryPolicy); 574 } 575 } 576 577 uint64_t getEventSeqNum() 578 { 579 return eventSeqNum; 580 } 581 582 private: 583 uint64_t eventSeqNum; 584 std::string host; 585 std::string port; 586 std::string path; 587 std::string uriProto; 588 std::shared_ptr<crow::HttpClient> conn = nullptr; 589 std::shared_ptr<crow::ServerSentEvents> sseConn = nullptr; 590 }; 591 592 static constexpr const bool defaultEnabledState = true; 593 static constexpr const uint32_t defaultRetryAttempts = 3; 594 static constexpr const uint32_t defaultRetryInterval = 30; 595 static constexpr const char* defaulEventFormatType = "Event"; 596 static constexpr const char* defaulSubscriptionType = "RedfishEvent"; 597 static constexpr const char* defaulRetryPolicy = "TerminateAfterRetries"; 598 599 class EventServiceManager 600 { 601 private: 602 bool serviceEnabled; 603 uint32_t retryAttempts; 604 uint32_t retryTimeoutInterval; 605 606 EventServiceManager() 607 { 608 // Load config from persist store. 609 initConfig(); 610 } 611 612 std::string lastEventTStr; 613 size_t noOfEventLogSubscribers{0}; 614 size_t noOfMetricReportSubscribers{0}; 615 std::shared_ptr<sdbusplus::bus::match::match> matchTelemetryMonitor; 616 boost::container::flat_map<std::string, std::shared_ptr<Subscription>> 617 subscriptionsMap; 618 619 uint64_t eventId{1}; 620 621 public: 622 EventServiceManager(const EventServiceManager&) = delete; 623 EventServiceManager& operator=(const EventServiceManager&) = delete; 624 EventServiceManager(EventServiceManager&&) = delete; 625 EventServiceManager& operator=(EventServiceManager&&) = delete; 626 627 static EventServiceManager& getInstance() 628 { 629 static EventServiceManager handler; 630 return handler; 631 } 632 633 void loadDefaultConfig() 634 { 635 serviceEnabled = defaultEnabledState; 636 retryAttempts = defaultRetryAttempts; 637 retryTimeoutInterval = defaultRetryInterval; 638 } 639 640 void initConfig() 641 { 642 std::ifstream eventConfigFile(eventServiceFile); 643 if (!eventConfigFile.good()) 644 { 645 BMCWEB_LOG_DEBUG << "EventService config not exist"; 646 loadDefaultConfig(); 647 return; 648 } 649 auto jsonData = nlohmann::json::parse(eventConfigFile, nullptr, false); 650 if (jsonData.is_discarded()) 651 { 652 BMCWEB_LOG_ERROR << "EventService config parse error."; 653 loadDefaultConfig(); 654 return; 655 } 656 657 nlohmann::json jsonConfig; 658 if (json_util::getValueFromJsonObject(jsonData, "Configuration", 659 jsonConfig)) 660 { 661 if (!json_util::getValueFromJsonObject(jsonConfig, "ServiceEnabled", 662 serviceEnabled)) 663 { 664 serviceEnabled = defaultEnabledState; 665 } 666 if (!json_util::getValueFromJsonObject( 667 jsonConfig, "DeliveryRetryAttempts", retryAttempts)) 668 { 669 retryAttempts = defaultRetryAttempts; 670 } 671 if (!json_util::getValueFromJsonObject( 672 jsonConfig, "DeliveryRetryIntervalSeconds", 673 retryTimeoutInterval)) 674 { 675 retryTimeoutInterval = defaultRetryInterval; 676 } 677 } 678 else 679 { 680 loadDefaultConfig(); 681 } 682 683 nlohmann::json subscriptionsList; 684 if (!json_util::getValueFromJsonObject(jsonData, "Subscriptions", 685 subscriptionsList)) 686 { 687 BMCWEB_LOG_DEBUG << "EventService: Subscriptions not exist."; 688 return; 689 } 690 691 for (nlohmann::json& jsonObj : subscriptionsList) 692 { 693 std::string protocol; 694 if (!json_util::getValueFromJsonObject(jsonObj, "Protocol", 695 protocol)) 696 { 697 BMCWEB_LOG_DEBUG << "Invalid subscription Protocol exist."; 698 continue; 699 } 700 701 std::string subscriptionType; 702 if (!json_util::getValueFromJsonObject(jsonObj, "SubscriptionType", 703 subscriptionType)) 704 { 705 subscriptionType = defaulSubscriptionType; 706 } 707 // SSE connections are initiated from client 708 // and can't be re-established from server. 709 if (subscriptionType == "SSE") 710 { 711 BMCWEB_LOG_DEBUG 712 << "The subscription type is SSE, so skipping."; 713 continue; 714 } 715 716 std::string destination; 717 if (!json_util::getValueFromJsonObject(jsonObj, "Destination", 718 destination)) 719 { 720 BMCWEB_LOG_DEBUG << "Invalid subscription destination exist."; 721 continue; 722 } 723 std::string host; 724 std::string urlProto; 725 std::string port; 726 std::string path; 727 bool status = 728 validateAndSplitUrl(destination, urlProto, host, port, path); 729 730 if (!status) 731 { 732 BMCWEB_LOG_ERROR 733 << "Failed to validate and split destination url"; 734 continue; 735 } 736 std::shared_ptr<Subscription> subValue = 737 std::make_shared<Subscription>(host, port, path, urlProto); 738 739 subValue->destinationUrl = destination; 740 subValue->protocol = protocol; 741 subValue->subscriptionType = subscriptionType; 742 if (!json_util::getValueFromJsonObject( 743 jsonObj, "DeliveryRetryPolicy", subValue->retryPolicy)) 744 { 745 subValue->retryPolicy = defaulRetryPolicy; 746 } 747 if (!json_util::getValueFromJsonObject(jsonObj, "EventFormatType", 748 subValue->eventFormatType)) 749 { 750 subValue->eventFormatType = defaulEventFormatType; 751 } 752 json_util::getValueFromJsonObject(jsonObj, "Context", 753 subValue->customText); 754 json_util::getValueFromJsonObject(jsonObj, "MessageIds", 755 subValue->registryMsgIds); 756 json_util::getValueFromJsonObject(jsonObj, "RegistryPrefixes", 757 subValue->registryPrefixes); 758 json_util::getValueFromJsonObject(jsonObj, "ResourceTypes", 759 subValue->resourceTypes); 760 json_util::getValueFromJsonObject(jsonObj, "HttpHeaders", 761 subValue->httpHeaders); 762 json_util::getValueFromJsonObject( 763 jsonObj, "MetricReportDefinitions", 764 subValue->metricReportDefinitions); 765 766 std::string id = addSubscription(subValue, false); 767 if (id.empty()) 768 { 769 BMCWEB_LOG_ERROR << "Failed to add subscription"; 770 } 771 } 772 return; 773 } 774 775 void updateSubscriptionData() 776 { 777 // Persist the config and subscription data. 778 nlohmann::json jsonData; 779 780 nlohmann::json& configObj = jsonData["Configuration"]; 781 configObj["ServiceEnabled"] = serviceEnabled; 782 configObj["DeliveryRetryAttempts"] = retryAttempts; 783 configObj["DeliveryRetryIntervalSeconds"] = retryTimeoutInterval; 784 785 nlohmann::json& subListArray = jsonData["Subscriptions"]; 786 subListArray = nlohmann::json::array(); 787 788 for (const auto& it : subscriptionsMap) 789 { 790 std::shared_ptr<Subscription> subValue = it.second; 791 // Don't preserve SSE connections. Its initiated from 792 // client side and can't be re-established from server. 793 if (subValue->subscriptionType == "SSE") 794 { 795 BMCWEB_LOG_DEBUG 796 << "The subscription type is SSE, so skipping."; 797 continue; 798 } 799 800 nlohmann::json entry; 801 entry["Context"] = subValue->customText; 802 entry["DeliveryRetryPolicy"] = subValue->retryPolicy; 803 entry["Destination"] = subValue->destinationUrl; 804 entry["EventFormatType"] = subValue->eventFormatType; 805 entry["HttpHeaders"] = subValue->httpHeaders; 806 entry["MessageIds"] = subValue->registryMsgIds; 807 entry["Protocol"] = subValue->protocol; 808 entry["RegistryPrefixes"] = subValue->registryPrefixes; 809 entry["ResourceTypes"] = subValue->resourceTypes; 810 entry["SubscriptionType"] = subValue->subscriptionType; 811 entry["MetricReportDefinitions"] = 812 subValue->metricReportDefinitions; 813 814 subListArray.push_back(entry); 815 } 816 817 const std::string tmpFile(std::string(eventServiceFile) + "_tmp"); 818 std::ofstream ofs(tmpFile, std::ios::out); 819 const auto& writeData = jsonData.dump( 820 2, ' ', true, nlohmann::json::error_handler_t::replace); 821 ofs << writeData; 822 ofs.close(); 823 824 BMCWEB_LOG_DEBUG << "EventService config updated to file."; 825 if (std::rename(tmpFile.c_str(), eventServiceFile) != 0) 826 { 827 BMCWEB_LOG_ERROR << "Error in renaming temporary file: " 828 << tmpFile.c_str(); 829 } 830 } 831 832 EventServiceConfig getEventServiceConfig() 833 { 834 return {serviceEnabled, retryAttempts, retryTimeoutInterval}; 835 } 836 837 void setEventServiceConfig(const EventServiceConfig& cfg) 838 { 839 bool updateConfig = false; 840 bool updateRetryCfg = false; 841 842 if (serviceEnabled != std::get<0>(cfg)) 843 { 844 serviceEnabled = std::get<0>(cfg); 845 if (serviceEnabled && noOfMetricReportSubscribers) 846 { 847 registerMetricReportSignal(); 848 } 849 else 850 { 851 unregisterMetricReportSignal(); 852 } 853 updateConfig = true; 854 } 855 856 if (retryAttempts != std::get<1>(cfg)) 857 { 858 retryAttempts = std::get<1>(cfg); 859 updateConfig = true; 860 updateRetryCfg = true; 861 } 862 863 if (retryTimeoutInterval != std::get<2>(cfg)) 864 { 865 retryTimeoutInterval = std::get<2>(cfg); 866 updateConfig = true; 867 updateRetryCfg = true; 868 } 869 870 if (updateConfig) 871 { 872 updateSubscriptionData(); 873 } 874 875 if (updateRetryCfg) 876 { 877 // Update the changed retry config to all subscriptions 878 for (const auto& it : 879 EventServiceManager::getInstance().subscriptionsMap) 880 { 881 std::shared_ptr<Subscription> entry = it.second; 882 entry->updateRetryConfig(retryAttempts, retryTimeoutInterval); 883 } 884 } 885 } 886 887 void updateNoOfSubscribersCount() 888 { 889 size_t eventLogSubCount = 0; 890 size_t metricReportSubCount = 0; 891 for (const auto& it : subscriptionsMap) 892 { 893 std::shared_ptr<Subscription> entry = it.second; 894 if (entry->eventFormatType == eventFormatType) 895 { 896 eventLogSubCount++; 897 } 898 else if (entry->eventFormatType == metricReportFormatType) 899 { 900 metricReportSubCount++; 901 } 902 } 903 904 noOfEventLogSubscribers = eventLogSubCount; 905 if (noOfMetricReportSubscribers != metricReportSubCount) 906 { 907 noOfMetricReportSubscribers = metricReportSubCount; 908 if (noOfMetricReportSubscribers) 909 { 910 registerMetricReportSignal(); 911 } 912 else 913 { 914 unregisterMetricReportSignal(); 915 } 916 } 917 } 918 919 std::shared_ptr<Subscription> getSubscription(const std::string& id) 920 { 921 auto obj = subscriptionsMap.find(id); 922 if (obj == subscriptionsMap.end()) 923 { 924 BMCWEB_LOG_ERROR << "No subscription exist with ID:" << id; 925 return nullptr; 926 } 927 std::shared_ptr<Subscription> subValue = obj->second; 928 return subValue; 929 } 930 931 std::string addSubscription(const std::shared_ptr<Subscription>& subValue, 932 const bool updateFile = true) 933 { 934 935 std::uniform_int_distribution<uint32_t> dist(0); 936 bmcweb::OpenSSLGenerator gen; 937 938 std::string id; 939 940 int retry = 3; 941 while (retry) 942 { 943 id = std::to_string(dist(gen)); 944 if (gen.error()) 945 { 946 retry = 0; 947 break; 948 } 949 auto inserted = subscriptionsMap.insert(std::pair(id, subValue)); 950 if (inserted.second) 951 { 952 break; 953 } 954 --retry; 955 } 956 957 if (retry <= 0) 958 { 959 BMCWEB_LOG_ERROR << "Failed to generate random number"; 960 return std::string(""); 961 } 962 963 updateNoOfSubscribersCount(); 964 965 if (updateFile) 966 { 967 updateSubscriptionData(); 968 } 969 970 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES 971 if (lastEventTStr.empty()) 972 { 973 cacheLastEventTimestamp(); 974 } 975 #endif 976 // Update retry configuration. 977 subValue->updateRetryConfig(retryAttempts, retryTimeoutInterval); 978 subValue->updateRetryPolicy(); 979 980 return id; 981 } 982 983 bool isSubscriptionExist(const std::string& id) 984 { 985 auto obj = subscriptionsMap.find(id); 986 if (obj == subscriptionsMap.end()) 987 { 988 return false; 989 } 990 return true; 991 } 992 993 void deleteSubscription(const std::string& id) 994 { 995 auto obj = subscriptionsMap.find(id); 996 if (obj != subscriptionsMap.end()) 997 { 998 subscriptionsMap.erase(obj); 999 updateNoOfSubscribersCount(); 1000 updateSubscriptionData(); 1001 } 1002 } 1003 1004 size_t getNumberOfSubscriptions() 1005 { 1006 return subscriptionsMap.size(); 1007 } 1008 1009 std::vector<std::string> getAllIDs() 1010 { 1011 std::vector<std::string> idList; 1012 for (const auto& it : subscriptionsMap) 1013 { 1014 idList.emplace_back(it.first); 1015 } 1016 return idList; 1017 } 1018 1019 bool isDestinationExist(const std::string& destUrl) 1020 { 1021 for (const auto& it : subscriptionsMap) 1022 { 1023 std::shared_ptr<Subscription> entry = it.second; 1024 if (entry->destinationUrl == destUrl) 1025 { 1026 BMCWEB_LOG_ERROR << "Destination exist already" << destUrl; 1027 return true; 1028 } 1029 } 1030 return false; 1031 } 1032 1033 void sendTestEventLog() 1034 { 1035 for (const auto& it : this->subscriptionsMap) 1036 { 1037 std::shared_ptr<Subscription> entry = it.second; 1038 entry->sendTestEventLog(); 1039 } 1040 } 1041 1042 void sendEvent(const nlohmann::json& eventMessageIn, 1043 const std::string& origin, const std::string& resType) 1044 { 1045 nlohmann::json eventRecord = nlohmann::json::array(); 1046 nlohmann::json eventMessage = eventMessageIn; 1047 // MemberId is 0 : since we are sending one event record. 1048 uint64_t memberId = 0; 1049 1050 nlohmann::json event = { 1051 {"EventId", eventId}, 1052 {"MemberId", memberId}, 1053 {"EventTimestamp", crow::utility::dateTimeNow()}, 1054 {"OriginOfCondition", origin}}; 1055 for (nlohmann::json::iterator it = event.begin(); it != event.end(); 1056 ++it) 1057 { 1058 eventMessage[it.key()] = it.value(); 1059 } 1060 eventRecord.push_back(eventMessage); 1061 1062 for (const auto& it : this->subscriptionsMap) 1063 { 1064 std::shared_ptr<Subscription> entry = it.second; 1065 bool isSubscribed = false; 1066 // Search the resourceTypes list for the subscription. 1067 // If resourceTypes list is empty, don't filter events 1068 // send everything. 1069 if (entry->resourceTypes.size()) 1070 { 1071 for (const auto& resource : entry->resourceTypes) 1072 { 1073 if (resType == resource) 1074 { 1075 BMCWEB_LOG_INFO << "ResourceType " << resource 1076 << " found in the subscribed list"; 1077 isSubscribed = true; 1078 break; 1079 } 1080 } 1081 } 1082 else // resourceTypes list is empty. 1083 { 1084 isSubscribed = true; 1085 } 1086 if (isSubscribed) 1087 { 1088 nlohmann::json msgJson = { 1089 {"@odata.type", "#Event.v1_4_0.Event"}, 1090 {"Name", "Event Log"}, 1091 {"Id", eventId}, 1092 {"Events", eventRecord}}; 1093 entry->sendEvent(msgJson.dump( 1094 2, ' ', true, nlohmann::json::error_handler_t::replace)); 1095 eventId++; // increament the eventId 1096 } 1097 else 1098 { 1099 BMCWEB_LOG_INFO << "Not subscribed to this resource"; 1100 } 1101 } 1102 } 1103 void sendBroadcastMsg(const std::string& broadcastMsg) 1104 { 1105 for (const auto& it : this->subscriptionsMap) 1106 { 1107 std::shared_ptr<Subscription> entry = it.second; 1108 nlohmann::json msgJson = { 1109 {"Timestamp", crow::utility::dateTimeNow()}, 1110 {"OriginOfCondition", "/ibm/v1/HMC/BroadcastService"}, 1111 {"Name", "Broadcast Message"}, 1112 {"Message", broadcastMsg}}; 1113 entry->sendEvent(msgJson.dump( 1114 2, ' ', true, nlohmann::json::error_handler_t::replace)); 1115 } 1116 } 1117 1118 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES 1119 void cacheLastEventTimestamp() 1120 { 1121 lastEventTStr.clear(); 1122 std::ifstream logStream(redfishEventLogFile); 1123 if (!logStream.good()) 1124 { 1125 BMCWEB_LOG_ERROR << " Redfish log file open failed \n"; 1126 return; 1127 } 1128 std::string logEntry; 1129 while (std::getline(logStream, logEntry)) 1130 { 1131 size_t space = logEntry.find_first_of(' '); 1132 if (space == std::string::npos) 1133 { 1134 // Shouldn't enter here but lets skip it. 1135 BMCWEB_LOG_DEBUG << "Invalid log entry found."; 1136 continue; 1137 } 1138 lastEventTStr = logEntry.substr(0, space); 1139 } 1140 BMCWEB_LOG_DEBUG << "Last Event time stamp set: " << lastEventTStr; 1141 } 1142 1143 void readEventLogsFromFile() 1144 { 1145 if (!serviceEnabled || !noOfEventLogSubscribers) 1146 { 1147 BMCWEB_LOG_DEBUG << "EventService disabled or no Subscriptions."; 1148 return; 1149 } 1150 std::ifstream logStream(redfishEventLogFile); 1151 if (!logStream.good()) 1152 { 1153 BMCWEB_LOG_ERROR << " Redfish log file open failed"; 1154 return; 1155 } 1156 1157 std::vector<EventLogObjectsType> eventRecords; 1158 1159 bool startLogCollection = false; 1160 bool firstEntry = true; 1161 1162 std::string logEntry; 1163 while (std::getline(logStream, logEntry)) 1164 { 1165 if (!startLogCollection && !lastEventTStr.empty()) 1166 { 1167 if (boost::starts_with(logEntry, lastEventTStr)) 1168 { 1169 startLogCollection = true; 1170 } 1171 continue; 1172 } 1173 1174 std::string idStr; 1175 if (!event_log::getUniqueEntryID(logEntry, idStr, firstEntry)) 1176 { 1177 continue; 1178 } 1179 firstEntry = false; 1180 1181 std::string timestamp; 1182 std::string messageID; 1183 std::vector<std::string> messageArgs; 1184 if (event_log::getEventLogParams(logEntry, timestamp, messageID, 1185 messageArgs) != 0) 1186 { 1187 BMCWEB_LOG_DEBUG << "Read eventLog entry params failed"; 1188 continue; 1189 } 1190 1191 std::string registryName; 1192 std::string messageKey; 1193 event_log::getRegistryAndMessageKey(messageID, registryName, 1194 messageKey); 1195 if (registryName.empty() || messageKey.empty()) 1196 { 1197 continue; 1198 } 1199 1200 lastEventTStr = timestamp; 1201 eventRecords.emplace_back(idStr, timestamp, messageID, registryName, 1202 messageKey, messageArgs); 1203 } 1204 1205 for (const auto& it : this->subscriptionsMap) 1206 { 1207 std::shared_ptr<Subscription> entry = it.second; 1208 if (entry->eventFormatType == "Event") 1209 { 1210 entry->filterAndSendEventLogs(eventRecords); 1211 } 1212 } 1213 } 1214 1215 static void watchRedfishEventLogFile() 1216 { 1217 if (!inotifyConn) 1218 { 1219 return; 1220 } 1221 1222 static std::array<char, 1024> readBuffer; 1223 1224 inotifyConn->async_read_some( 1225 boost::asio::buffer(readBuffer), 1226 [&](const boost::system::error_code& ec, 1227 const std::size_t& bytesTransferred) { 1228 if (ec) 1229 { 1230 BMCWEB_LOG_ERROR << "Callback Error: " << ec.message(); 1231 return; 1232 } 1233 std::size_t index = 0; 1234 while ((index + iEventSize) <= bytesTransferred) 1235 { 1236 struct inotify_event event; 1237 std::memcpy(&event, &readBuffer[index], iEventSize); 1238 if (event.wd == dirWatchDesc) 1239 { 1240 if ((event.len == 0) || 1241 (index + iEventSize + event.len > bytesTransferred)) 1242 { 1243 index += (iEventSize + event.len); 1244 continue; 1245 } 1246 1247 std::string fileName(&readBuffer[index + iEventSize], 1248 event.len); 1249 if (std::strcmp(fileName.c_str(), "redfish") != 0) 1250 { 1251 index += (iEventSize + event.len); 1252 continue; 1253 } 1254 1255 BMCWEB_LOG_DEBUG 1256 << "Redfish log file created/deleted. event.name: " 1257 << fileName; 1258 if (event.mask == IN_CREATE) 1259 { 1260 if (fileWatchDesc != -1) 1261 { 1262 BMCWEB_LOG_DEBUG 1263 << "Remove and Add inotify watcher on " 1264 "redfish event log file"; 1265 // Remove existing inotify watcher and add 1266 // with new redfish event log file. 1267 inotify_rm_watch(inotifyFd, fileWatchDesc); 1268 fileWatchDesc = -1; 1269 } 1270 1271 fileWatchDesc = inotify_add_watch( 1272 inotifyFd, redfishEventLogFile, IN_MODIFY); 1273 if (fileWatchDesc == -1) 1274 { 1275 BMCWEB_LOG_ERROR 1276 << "inotify_add_watch failed for " 1277 "redfish log file."; 1278 return; 1279 } 1280 1281 EventServiceManager::getInstance() 1282 .cacheLastEventTimestamp(); 1283 EventServiceManager::getInstance() 1284 .readEventLogsFromFile(); 1285 } 1286 else if ((event.mask == IN_DELETE) || 1287 (event.mask == IN_MOVED_TO)) 1288 { 1289 if (fileWatchDesc != -1) 1290 { 1291 inotify_rm_watch(inotifyFd, fileWatchDesc); 1292 fileWatchDesc = -1; 1293 } 1294 } 1295 } 1296 else if (event.wd == fileWatchDesc) 1297 { 1298 if (event.mask == IN_MODIFY) 1299 { 1300 EventServiceManager::getInstance() 1301 .readEventLogsFromFile(); 1302 } 1303 } 1304 index += (iEventSize + event.len); 1305 } 1306 1307 watchRedfishEventLogFile(); 1308 }); 1309 } 1310 1311 static int startEventLogMonitor(boost::asio::io_context& ioc) 1312 { 1313 inotifyConn.emplace(ioc); 1314 inotifyFd = inotify_init1(IN_NONBLOCK); 1315 if (inotifyFd == -1) 1316 { 1317 BMCWEB_LOG_ERROR << "inotify_init1 failed."; 1318 return -1; 1319 } 1320 1321 // Add watch on directory to handle redfish event log file 1322 // create/delete. 1323 dirWatchDesc = inotify_add_watch(inotifyFd, redfishEventLogDir, 1324 IN_CREATE | IN_MOVED_TO | IN_DELETE); 1325 if (dirWatchDesc == -1) 1326 { 1327 BMCWEB_LOG_ERROR 1328 << "inotify_add_watch failed for event log directory."; 1329 return -1; 1330 } 1331 1332 // Watch redfish event log file for modifications. 1333 fileWatchDesc = 1334 inotify_add_watch(inotifyFd, redfishEventLogFile, IN_MODIFY); 1335 if (fileWatchDesc == -1) 1336 { 1337 BMCWEB_LOG_ERROR 1338 << "inotify_add_watch failed for redfish log file."; 1339 // Don't return error if file not exist. 1340 // Watch on directory will handle create/delete of file. 1341 } 1342 1343 // monitor redfish event log file 1344 inotifyConn->assign(inotifyFd); 1345 watchRedfishEventLogFile(); 1346 1347 return 0; 1348 } 1349 1350 #endif 1351 1352 void getMetricReading(const std::string& service, 1353 const std::string& objPath, const std::string& intf) 1354 { 1355 std::size_t found = objPath.find_last_of('/'); 1356 if (found == std::string::npos) 1357 { 1358 BMCWEB_LOG_DEBUG << "Invalid objPath received"; 1359 return; 1360 } 1361 1362 std::string idStr = objPath.substr(found + 1); 1363 if (idStr.empty()) 1364 { 1365 BMCWEB_LOG_DEBUG << "Invalid ID in objPath"; 1366 return; 1367 } 1368 1369 crow::connections::systemBus->async_method_call( 1370 [idStr{std::move(idStr)}]( 1371 const boost::system::error_code ec, 1372 boost::container::flat_map< 1373 std::string, std::variant<int32_t, ReadingsObjType>>& 1374 resp) { 1375 if (ec) 1376 { 1377 BMCWEB_LOG_DEBUG 1378 << "D-Bus call failed to GetAll metric readings."; 1379 return; 1380 } 1381 1382 const int32_t* timestampPtr = 1383 std::get_if<int32_t>(&resp["Timestamp"]); 1384 if (!timestampPtr) 1385 { 1386 BMCWEB_LOG_DEBUG << "Failed to Get timestamp."; 1387 return; 1388 } 1389 1390 ReadingsObjType* readingsPtr = 1391 std::get_if<ReadingsObjType>(&resp["Readings"]); 1392 if (!readingsPtr) 1393 { 1394 BMCWEB_LOG_DEBUG << "Failed to Get Readings property."; 1395 return; 1396 } 1397 1398 if (!readingsPtr->size()) 1399 { 1400 BMCWEB_LOG_DEBUG << "No metrics report to be transferred"; 1401 return; 1402 } 1403 1404 for (const auto& it : 1405 EventServiceManager::getInstance().subscriptionsMap) 1406 { 1407 std::shared_ptr<Subscription> entry = it.second; 1408 if (entry->eventFormatType == metricReportFormatType) 1409 { 1410 entry->filterAndSendReports( 1411 idStr, crow::utility::getDateTime(*timestampPtr), 1412 *readingsPtr); 1413 } 1414 } 1415 }, 1416 service, objPath, "org.freedesktop.DBus.Properties", "GetAll", 1417 intf); 1418 } 1419 1420 void unregisterMetricReportSignal() 1421 { 1422 if (matchTelemetryMonitor) 1423 { 1424 BMCWEB_LOG_DEBUG << "Metrics report signal - Unregister"; 1425 matchTelemetryMonitor.reset(); 1426 matchTelemetryMonitor = nullptr; 1427 } 1428 } 1429 1430 void registerMetricReportSignal() 1431 { 1432 if (!serviceEnabled || matchTelemetryMonitor) 1433 { 1434 BMCWEB_LOG_DEBUG << "Not registering metric report signal."; 1435 return; 1436 } 1437 1438 BMCWEB_LOG_DEBUG << "Metrics report signal - Register"; 1439 std::string matchStr( 1440 "type='signal',member='ReportUpdate', " 1441 "interface='xyz.openbmc_project.MonitoringService.Report'"); 1442 1443 matchTelemetryMonitor = std::make_shared<sdbusplus::bus::match::match>( 1444 *crow::connections::systemBus, matchStr, 1445 [this](sdbusplus::message::message& msg) { 1446 if (msg.is_method_error()) 1447 { 1448 BMCWEB_LOG_ERROR << "TelemetryMonitor Signal error"; 1449 return; 1450 } 1451 1452 std::string service = msg.get_sender(); 1453 std::string objPath = msg.get_path(); 1454 std::string intf = msg.get_interface(); 1455 getMetricReading(service, objPath, intf); 1456 }); 1457 } 1458 1459 bool validateAndSplitUrl(const std::string& destUrl, std::string& urlProto, 1460 std::string& host, std::string& port, 1461 std::string& path) 1462 { 1463 // Validate URL using regex expression 1464 // Format: <protocol>://<host>:<port>/<path> 1465 // protocol: http/https 1466 const std::regex urlRegex( 1467 "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/" 1468 "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)"); 1469 std::cmatch match; 1470 if (!std::regex_match(destUrl.c_str(), match, urlRegex)) 1471 { 1472 BMCWEB_LOG_INFO << "Dest. url did not match "; 1473 return false; 1474 } 1475 1476 urlProto = std::string(match[1].first, match[1].second); 1477 if (urlProto == "http") 1478 { 1479 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING 1480 return false; 1481 #endif 1482 } 1483 1484 host = std::string(match[2].first, match[2].second); 1485 port = std::string(match[3].first, match[3].second); 1486 path = std::string(match[4].first, match[4].second); 1487 if (port.empty()) 1488 { 1489 if (urlProto == "http") 1490 { 1491 port = "80"; 1492 } 1493 else 1494 { 1495 port = "443"; 1496 } 1497 } 1498 if (path.empty()) 1499 { 1500 path = "/"; 1501 } 1502 return true; 1503 } 1504 }; 1505 1506 } // namespace redfish 1507