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 19 #include <boost/asio/io_context.hpp> 20 #include <boost/container/flat_map.hpp> 21 #include <error_messages.hpp> 22 #include <http_client.hpp> 23 #include <utils/json_utils.hpp> 24 25 #include <cstdlib> 26 #include <ctime> 27 #include <fstream> 28 #include <memory> 29 #include <variant> 30 31 namespace redfish 32 { 33 34 using ReadingsObjType = 35 std::vector<std::tuple<std::string, std::string, double, std::string>>; 36 using EventServiceConfig = std::tuple<bool, uint32_t, uint32_t>; 37 38 static constexpr const char* eventFormatType = "Event"; 39 static constexpr const char* metricReportFormatType = "MetricReport"; 40 41 static constexpr const char* eventServiceFile = 42 "/var/lib/bmcweb/eventservice_config.json"; 43 44 class Subscription 45 { 46 public: 47 std::string id; 48 std::string destinationUrl; 49 std::string protocol; 50 std::string retryPolicy; 51 std::string customText; 52 std::string eventFormatType; 53 std::string subscriptionType; 54 std::vector<std::string> registryMsgIds; 55 std::vector<std::string> registryPrefixes; 56 std::vector<nlohmann::json> httpHeaders; // key-value pair 57 std::vector<nlohmann::json> metricReportDefinitions; 58 59 Subscription(const Subscription&) = delete; 60 Subscription& operator=(const Subscription&) = delete; 61 Subscription(Subscription&&) = delete; 62 Subscription& operator=(Subscription&&) = delete; 63 64 Subscription(const std::string& inHost, const std::string& inPort, 65 const std::string& inPath, const std::string& inUriProto) : 66 eventSeqNum(1), 67 host(inHost), port(inPort), path(inPath), uriProto(inUriProto) 68 { 69 conn = std::make_shared<crow::HttpClient>( 70 crow::connections::systemBus->get_io_context(), host, port, path); 71 } 72 ~Subscription() 73 {} 74 75 void sendEvent(const std::string& msg) 76 { 77 std::vector<std::pair<std::string, std::string>> reqHeaders; 78 for (const auto& header : httpHeaders) 79 { 80 for (const auto& item : header.items()) 81 { 82 std::string key = item.key(); 83 std::string val = item.value(); 84 reqHeaders.emplace_back(std::pair(key, val)); 85 } 86 } 87 conn->setHeaders(reqHeaders); 88 conn->sendData(msg); 89 } 90 91 void sendTestEventLog() 92 { 93 nlohmann::json logEntryArray; 94 logEntryArray.push_back({}); 95 nlohmann::json& logEntryJson = logEntryArray.back(); 96 97 logEntryJson = {{"EventId", "TestID"}, 98 {"EventType", "Event"}, 99 {"Severity", "OK"}, 100 {"Message", "Generated test event"}, 101 {"MessageId", "OpenBMC.0.1.TestEventLog"}, 102 {"MessageArgs", nlohmann::json::array()}, 103 {"EventTimestamp", crow::utility::dateTimeNow()}, 104 {"Context", customText}}; 105 106 nlohmann::json msg = {{"@odata.type", "#Event.v1_4_0.Event"}, 107 {"Id", std::to_string(eventSeqNum)}, 108 {"Name", "Event Log"}, 109 {"Events", logEntryArray}}; 110 111 this->sendEvent(msg.dump()); 112 this->eventSeqNum++; 113 } 114 115 void filterAndSendReports(const std::string& id, 116 const std::string& readingsTs, 117 const ReadingsObjType& readings) 118 { 119 std::string metricReportDef = 120 "/redfish/v1/TelemetryService/MetricReportDefinitions/" + id; 121 122 // Empty list means no filter. Send everything. 123 if (metricReportDefinitions.size()) 124 { 125 if (std::find(metricReportDefinitions.begin(), 126 metricReportDefinitions.end(), 127 metricReportDef) == metricReportDefinitions.end()) 128 { 129 return; 130 } 131 } 132 133 nlohmann::json metricValuesArray = nlohmann::json::array(); 134 for (const auto& it : readings) 135 { 136 metricValuesArray.push_back({}); 137 nlohmann::json& entry = metricValuesArray.back(); 138 139 entry = {{"MetricId", std::get<0>(it)}, 140 {"MetricProperty", std::get<1>(it)}, 141 {"MetricValue", std::to_string(std::get<2>(it))}, 142 {"Timestamp", std::get<3>(it)}}; 143 } 144 145 nlohmann::json msg = { 146 {"@odata.id", "/redfish/v1/TelemetryService/MetricReports/" + id}, 147 {"@odata.type", "#MetricReport.v1_3_0.MetricReport"}, 148 {"Id", id}, 149 {"Name", id}, 150 {"Timestamp", readingsTs}, 151 {"MetricReportDefinition", {{"@odata.id", metricReportDef}}}, 152 {"MetricValues", metricValuesArray}}; 153 154 this->sendEvent(msg.dump()); 155 } 156 157 private: 158 uint64_t eventSeqNum; 159 std::string host; 160 std::string port; 161 std::string path; 162 std::string uriProto; 163 std::shared_ptr<crow::HttpClient> conn; 164 }; 165 166 static constexpr const bool defaultEnabledState = true; 167 static constexpr const uint32_t defaultRetryAttempts = 3; 168 static constexpr const uint32_t defaultRetryInterval = 30; 169 static constexpr const char* defaulEventFormatType = "Event"; 170 static constexpr const char* defaulSubscriptionType = "RedfishEvent"; 171 static constexpr const char* defaulRetryPolicy = "TerminateAfterRetries"; 172 173 class EventServiceManager 174 { 175 private: 176 bool serviceEnabled; 177 uint32_t retryAttempts; 178 uint32_t retryTimeoutInterval; 179 180 EventServiceManager(const EventServiceManager&) = delete; 181 EventServiceManager& operator=(const EventServiceManager&) = delete; 182 EventServiceManager(EventServiceManager&&) = delete; 183 EventServiceManager& operator=(EventServiceManager&&) = delete; 184 185 EventServiceManager() : 186 noOfEventLogSubscribers(0), noOfMetricReportSubscribers(0) 187 { 188 // Load config from persist store. 189 initConfig(); 190 } 191 192 size_t noOfEventLogSubscribers; 193 size_t noOfMetricReportSubscribers; 194 std::shared_ptr<sdbusplus::bus::match::match> matchTelemetryMonitor; 195 boost::container::flat_map<std::string, std::shared_ptr<Subscription>> 196 subscriptionsMap; 197 198 public: 199 static EventServiceManager& getInstance() 200 { 201 static EventServiceManager handler; 202 return handler; 203 } 204 205 void loadDefaultConfig() 206 { 207 serviceEnabled = defaultEnabledState; 208 retryAttempts = defaultRetryAttempts; 209 retryTimeoutInterval = defaultRetryInterval; 210 } 211 212 void initConfig() 213 { 214 std::ifstream eventConfigFile(eventServiceFile); 215 if (!eventConfigFile.good()) 216 { 217 BMCWEB_LOG_DEBUG << "EventService config not exist"; 218 loadDefaultConfig(); 219 return; 220 } 221 auto jsonData = nlohmann::json::parse(eventConfigFile, nullptr, false); 222 if (jsonData.is_discarded()) 223 { 224 BMCWEB_LOG_ERROR << "EventService config parse error."; 225 loadDefaultConfig(); 226 return; 227 } 228 229 nlohmann::json jsonConfig; 230 if (json_util::getValueFromJsonObject(jsonData, "Configuration", 231 jsonConfig)) 232 { 233 if (!json_util::getValueFromJsonObject(jsonConfig, "ServiceEnabled", 234 serviceEnabled)) 235 { 236 serviceEnabled = defaultEnabledState; 237 } 238 if (!json_util::getValueFromJsonObject( 239 jsonConfig, "DeliveryRetryAttempts", retryAttempts)) 240 { 241 retryAttempts = defaultRetryAttempts; 242 } 243 if (!json_util::getValueFromJsonObject( 244 jsonConfig, "DeliveryRetryIntervalSeconds", 245 retryTimeoutInterval)) 246 { 247 retryTimeoutInterval = defaultRetryInterval; 248 } 249 } 250 else 251 { 252 loadDefaultConfig(); 253 } 254 255 nlohmann::json subscriptionsList; 256 if (!json_util::getValueFromJsonObject(jsonData, "Subscriptions", 257 subscriptionsList)) 258 { 259 BMCWEB_LOG_DEBUG << "EventService: Subscriptions not exist."; 260 return; 261 } 262 263 for (nlohmann::json& jsonObj : subscriptionsList) 264 { 265 std::string protocol; 266 if (!json_util::getValueFromJsonObject(jsonObj, "Protocol", 267 protocol)) 268 { 269 BMCWEB_LOG_DEBUG << "Invalid subscription Protocol exist."; 270 continue; 271 } 272 std::string destination; 273 if (!json_util::getValueFromJsonObject(jsonObj, "Destination", 274 destination)) 275 { 276 BMCWEB_LOG_DEBUG << "Invalid subscription destination exist."; 277 continue; 278 } 279 std::string host; 280 std::string urlProto; 281 std::string port; 282 std::string path; 283 bool status = 284 validateAndSplitUrl(destination, urlProto, host, port, path); 285 286 if (!status) 287 { 288 BMCWEB_LOG_ERROR 289 << "Failed to validate and split destination url"; 290 continue; 291 } 292 std::shared_ptr<Subscription> subValue = 293 std::make_shared<Subscription>(host, port, path, urlProto); 294 295 subValue->destinationUrl = destination; 296 subValue->protocol = protocol; 297 if (!json_util::getValueFromJsonObject( 298 jsonObj, "DeliveryRetryPolicy", subValue->retryPolicy)) 299 { 300 subValue->retryPolicy = defaulRetryPolicy; 301 } 302 if (!json_util::getValueFromJsonObject(jsonObj, "EventFormatType", 303 subValue->eventFormatType)) 304 { 305 subValue->eventFormatType = defaulEventFormatType; 306 } 307 if (!json_util::getValueFromJsonObject(jsonObj, "SubscriptionType", 308 subValue->subscriptionType)) 309 { 310 subValue->subscriptionType = defaulSubscriptionType; 311 } 312 313 json_util::getValueFromJsonObject(jsonObj, "Context", 314 subValue->customText); 315 json_util::getValueFromJsonObject(jsonObj, "MessageIds", 316 subValue->registryMsgIds); 317 json_util::getValueFromJsonObject(jsonObj, "RegistryPrefixes", 318 subValue->registryPrefixes); 319 json_util::getValueFromJsonObject(jsonObj, "HttpHeaders", 320 subValue->httpHeaders); 321 json_util::getValueFromJsonObject( 322 jsonObj, "MetricReportDefinitions", 323 subValue->metricReportDefinitions); 324 325 std::string id = addSubscription(subValue, false); 326 if (id.empty()) 327 { 328 BMCWEB_LOG_ERROR << "Failed to add subscription"; 329 } 330 } 331 return; 332 } 333 334 void updateSubscriptionData() 335 { 336 // Persist the config and subscription data. 337 nlohmann::json jsonData; 338 339 nlohmann::json& configObj = jsonData["Configuration"]; 340 configObj["ServiceEnabled"] = serviceEnabled; 341 configObj["DeliveryRetryAttempts"] = retryAttempts; 342 configObj["DeliveryRetryIntervalSeconds"] = retryTimeoutInterval; 343 344 nlohmann::json& subListArray = jsonData["Subscriptions"]; 345 subListArray = nlohmann::json::array(); 346 347 for (const auto& it : subscriptionsMap) 348 { 349 nlohmann::json entry; 350 std::shared_ptr<Subscription> subValue = it.second; 351 352 entry["Context"] = subValue->customText; 353 entry["DeliveryRetryPolicy"] = subValue->retryPolicy; 354 entry["Destination"] = subValue->destinationUrl; 355 entry["EventFormatType"] = subValue->eventFormatType; 356 entry["HttpHeaders"] = subValue->httpHeaders; 357 entry["MessageIds"] = subValue->registryMsgIds; 358 entry["Protocol"] = subValue->protocol; 359 entry["RegistryPrefixes"] = subValue->registryPrefixes; 360 entry["SubscriptionType"] = subValue->subscriptionType; 361 entry["MetricReportDefinitions"] = 362 subValue->metricReportDefinitions; 363 364 subListArray.push_back(entry); 365 } 366 367 const std::string tmpFile(std::string(eventServiceFile) + "_tmp"); 368 std::ofstream ofs(tmpFile, std::ios::out); 369 const auto& writeData = jsonData.dump(); 370 ofs << writeData; 371 ofs.close(); 372 373 BMCWEB_LOG_DEBUG << "EventService config updated to file."; 374 if (std::rename(tmpFile.c_str(), eventServiceFile) != 0) 375 { 376 BMCWEB_LOG_ERROR << "Error in renaming temporary file: " 377 << tmpFile.c_str(); 378 } 379 } 380 381 EventServiceConfig getEventServiceConfig() 382 { 383 return {serviceEnabled, retryAttempts, retryTimeoutInterval}; 384 } 385 386 void setEventServiceConfig(const EventServiceConfig& cfg) 387 { 388 bool updateConfig = false; 389 390 if (serviceEnabled != std::get<0>(cfg)) 391 { 392 serviceEnabled = std::get<0>(cfg); 393 if (serviceEnabled && noOfMetricReportSubscribers) 394 { 395 registerMetricReportSignal(); 396 } 397 else 398 { 399 unregisterMetricReportSignal(); 400 } 401 updateConfig = true; 402 } 403 404 if (retryAttempts != std::get<1>(cfg)) 405 { 406 retryAttempts = std::get<1>(cfg); 407 updateConfig = true; 408 } 409 410 if (retryTimeoutInterval != std::get<2>(cfg)) 411 { 412 retryTimeoutInterval = std::get<2>(cfg); 413 updateConfig = true; 414 } 415 416 if (updateConfig) 417 { 418 updateSubscriptionData(); 419 } 420 } 421 422 void updateNoOfSubscribersCount() 423 { 424 size_t eventLogSubCount = 0; 425 size_t metricReportSubCount = 0; 426 for (const auto& it : subscriptionsMap) 427 { 428 std::shared_ptr<Subscription> entry = it.second; 429 if (entry->eventFormatType == eventFormatType) 430 { 431 eventLogSubCount++; 432 } 433 else if (entry->eventFormatType == metricReportFormatType) 434 { 435 metricReportSubCount++; 436 } 437 } 438 439 noOfEventLogSubscribers = eventLogSubCount; 440 if (noOfMetricReportSubscribers != metricReportSubCount) 441 { 442 noOfMetricReportSubscribers = metricReportSubCount; 443 if (noOfMetricReportSubscribers) 444 { 445 registerMetricReportSignal(); 446 } 447 else 448 { 449 unregisterMetricReportSignal(); 450 } 451 } 452 } 453 454 std::shared_ptr<Subscription> getSubscription(const std::string& id) 455 { 456 auto obj = subscriptionsMap.find(id); 457 if (obj == subscriptionsMap.end()) 458 { 459 BMCWEB_LOG_ERROR << "No subscription exist with ID:" << id; 460 return nullptr; 461 } 462 std::shared_ptr<Subscription> subValue = obj->second; 463 return subValue; 464 } 465 466 std::string addSubscription(const std::shared_ptr<Subscription> subValue, 467 const bool updateFile = true) 468 { 469 std::srand(static_cast<uint32_t>(std::time(0))); 470 std::string id; 471 472 int retry = 3; 473 while (retry) 474 { 475 id = std::to_string(std::rand()); 476 auto inserted = subscriptionsMap.insert(std::pair(id, subValue)); 477 if (inserted.second) 478 { 479 break; 480 } 481 --retry; 482 }; 483 484 if (retry <= 0) 485 { 486 BMCWEB_LOG_ERROR << "Failed to generate random number"; 487 return std::string(""); 488 } 489 490 updateNoOfSubscribersCount(); 491 492 if (updateFile) 493 { 494 updateSubscriptionData(); 495 } 496 return id; 497 } 498 499 bool isSubscriptionExist(const std::string& id) 500 { 501 auto obj = subscriptionsMap.find(id); 502 if (obj == subscriptionsMap.end()) 503 { 504 return false; 505 } 506 return true; 507 } 508 509 void deleteSubscription(const std::string& id) 510 { 511 auto obj = subscriptionsMap.find(id); 512 if (obj != subscriptionsMap.end()) 513 { 514 subscriptionsMap.erase(obj); 515 updateNoOfSubscribersCount(); 516 updateSubscriptionData(); 517 } 518 } 519 520 size_t getNumberOfSubscriptions() 521 { 522 return subscriptionsMap.size(); 523 } 524 525 std::vector<std::string> getAllIDs() 526 { 527 std::vector<std::string> idList; 528 for (const auto& it : subscriptionsMap) 529 { 530 idList.emplace_back(it.first); 531 } 532 return idList; 533 } 534 535 bool isDestinationExist(const std::string& destUrl) 536 { 537 for (const auto& it : subscriptionsMap) 538 { 539 std::shared_ptr<Subscription> entry = it.second; 540 if (entry->destinationUrl == destUrl) 541 { 542 BMCWEB_LOG_ERROR << "Destination exist already" << destUrl; 543 return true; 544 } 545 } 546 return false; 547 } 548 549 void sendTestEventLog() 550 { 551 for (const auto& it : this->subscriptionsMap) 552 { 553 std::shared_ptr<Subscription> entry = it.second; 554 entry->sendTestEventLog(); 555 } 556 } 557 558 void getMetricReading(const std::string& service, 559 const std::string& objPath, const std::string& intf) 560 { 561 std::size_t found = objPath.find_last_of("/"); 562 if (found == std::string::npos) 563 { 564 BMCWEB_LOG_DEBUG << "Invalid objPath received"; 565 return; 566 } 567 568 std::string idStr = objPath.substr(found + 1); 569 if (idStr.empty()) 570 { 571 BMCWEB_LOG_DEBUG << "Invalid ID in objPath"; 572 return; 573 } 574 575 crow::connections::systemBus->async_method_call( 576 [idStr{std::move(idStr)}]( 577 const boost::system::error_code ec, 578 boost::container::flat_map< 579 std::string, std::variant<std::string, ReadingsObjType>>& 580 resp) { 581 if (ec) 582 { 583 BMCWEB_LOG_DEBUG 584 << "D-Bus call failed to GetAll metric readings."; 585 return; 586 } 587 588 const std::string* timestampPtr = 589 std::get_if<std::string>(&resp["Timestamp"]); 590 if (!timestampPtr) 591 { 592 BMCWEB_LOG_DEBUG << "Failed to Get timestamp."; 593 return; 594 } 595 596 ReadingsObjType* readingsPtr = 597 std::get_if<ReadingsObjType>(&resp["Readings"]); 598 if (!readingsPtr) 599 { 600 BMCWEB_LOG_DEBUG << "Failed to Get Readings property."; 601 return; 602 } 603 604 if (!readingsPtr->size()) 605 { 606 BMCWEB_LOG_DEBUG << "No metrics report to be transferred"; 607 return; 608 } 609 610 for (const auto& it : 611 EventServiceManager::getInstance().subscriptionsMap) 612 { 613 std::shared_ptr<Subscription> entry = it.second; 614 if (entry->eventFormatType == metricReportFormatType) 615 { 616 entry->filterAndSendReports(idStr, *timestampPtr, 617 *readingsPtr); 618 } 619 } 620 }, 621 service, objPath, "org.freedesktop.DBus.Properties", "GetAll", 622 intf); 623 } 624 625 void unregisterMetricReportSignal() 626 { 627 if (matchTelemetryMonitor) 628 { 629 BMCWEB_LOG_DEBUG << "Metrics report signal - Unregister"; 630 matchTelemetryMonitor.reset(); 631 matchTelemetryMonitor = nullptr; 632 } 633 } 634 635 void registerMetricReportSignal() 636 { 637 if (!serviceEnabled || matchTelemetryMonitor) 638 { 639 BMCWEB_LOG_DEBUG << "Not registering metric report signal."; 640 return; 641 } 642 643 BMCWEB_LOG_DEBUG << "Metrics report signal - Register"; 644 std::string matchStr( 645 "type='signal',member='ReportUpdate', " 646 "interface='xyz.openbmc_project.MonitoringService.Report'"); 647 648 matchTelemetryMonitor = std::make_shared<sdbusplus::bus::match::match>( 649 *crow::connections::systemBus, matchStr, 650 [this](sdbusplus::message::message& msg) { 651 if (msg.is_method_error()) 652 { 653 BMCWEB_LOG_ERROR << "TelemetryMonitor Signal error"; 654 return; 655 } 656 657 std::string service = msg.get_sender(); 658 std::string objPath = msg.get_path(); 659 std::string intf = msg.get_interface(); 660 getMetricReading(service, objPath, intf); 661 }); 662 } 663 664 bool validateAndSplitUrl(const std::string& destUrl, std::string& urlProto, 665 std::string& host, std::string& port, 666 std::string& path) 667 { 668 // Validate URL using regex expression 669 // Format: <protocol>://<host>:<port>/<path> 670 // protocol: http/https 671 const std::regex urlRegex( 672 "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/" 673 "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)"); 674 std::cmatch match; 675 if (!std::regex_match(destUrl.c_str(), match, urlRegex)) 676 { 677 BMCWEB_LOG_INFO << "Dest. url did not match "; 678 return false; 679 } 680 681 urlProto = std::string(match[1].first, match[1].second); 682 if (urlProto == "http") 683 { 684 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING 685 return false; 686 #endif 687 } 688 689 host = std::string(match[2].first, match[2].second); 690 port = std::string(match[3].first, match[3].second); 691 path = std::string(match[4].first, match[4].second); 692 if (port.empty()) 693 { 694 if (urlProto == "http") 695 { 696 port = "80"; 697 } 698 else 699 { 700 port = "443"; 701 } 702 } 703 if (path.empty()) 704 { 705 path = "/"; 706 } 707 return true; 708 } 709 }; 710 711 } // namespace redfish 712