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