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