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 "event_service_manager.hpp" 18 19 namespace redfish 20 { 21 22 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = { 23 eventFormatType, metricReportFormatType}; 24 static constexpr const std::array<const char*, 3> supportedRegPrefixes = { 25 "Base", "OpenBMC", "Task"}; 26 static constexpr const std::array<const char*, 3> supportedRetryPolicies = { 27 "TerminateAfterRetries", "SuspendRetries", "RetryForever"}; 28 29 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE 30 static constexpr const std::array<const char*, 2> supportedResourceTypes = { 31 "IBMConfigFile", "Task"}; 32 #else 33 static constexpr const std::array<const char*, 1> supportedResourceTypes = { 34 "Task"}; 35 #endif 36 37 static constexpr const uint8_t maxNoOfSubscriptions = 20; 38 39 class EventService : public Node 40 { 41 public: 42 EventService(App& app) : Node(app, "/redfish/v1/EventService/") 43 { 44 entityPrivileges = { 45 {boost::beast::http::verb::get, {{"Login"}}}, 46 {boost::beast::http::verb::head, {{"Login"}}}, 47 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 48 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 49 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 50 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 51 } 52 53 private: 54 void doGet(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 55 const crow::Request&, const std::vector<std::string>&) override 56 { 57 58 asyncResp->res.jsonValue = { 59 {"@odata.type", "#EventService.v1_5_0.EventService"}, 60 {"Id", "EventService"}, 61 {"Name", "Event Service"}, 62 {"Subscriptions", 63 {{"@odata.id", "/redfish/v1/EventService/Subscriptions"}}}, 64 {"Actions", 65 {{"#EventService.SubmitTestEvent", 66 {{"target", "/redfish/v1/EventService/Actions/" 67 "EventService.SubmitTestEvent"}}}}}, 68 {"@odata.id", "/redfish/v1/EventService"}}; 69 70 const auto& [enabled, retryAttempts, retryTimeoutInterval] = 71 EventServiceManager::getInstance().getEventServiceConfig(); 72 73 asyncResp->res.jsonValue["Status"]["State"] = 74 (enabled ? "Enabled" : "Disabled"); 75 asyncResp->res.jsonValue["ServiceEnabled"] = enabled; 76 asyncResp->res.jsonValue["DeliveryRetryAttempts"] = retryAttempts; 77 asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] = 78 retryTimeoutInterval; 79 asyncResp->res.jsonValue["EventFormatTypes"] = supportedEvtFormatTypes; 80 asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes; 81 asyncResp->res.jsonValue["ResourceTypes"] = supportedResourceTypes; 82 83 nlohmann::json supportedSSEFilters = { 84 {"EventFormatType", true}, {"MessageId", true}, 85 {"MetricReportDefinition", true}, {"RegistryPrefix", true}, 86 {"OriginResource", false}, {"ResourceType", false}}; 87 88 asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] = 89 supportedSSEFilters; 90 } 91 92 void doPatch(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 93 const crow::Request& req, 94 const std::vector<std::string>&) override 95 { 96 97 std::optional<bool> serviceEnabled; 98 std::optional<uint32_t> retryAttemps; 99 std::optional<uint32_t> retryInterval; 100 101 if (!json_util::readJson(req, asyncResp->res, "ServiceEnabled", 102 serviceEnabled, "DeliveryRetryAttempts", 103 retryAttemps, "DeliveryRetryIntervalSeconds", 104 retryInterval)) 105 { 106 return; 107 } 108 109 auto [enabled, retryCount, retryTimeoutInterval] = 110 EventServiceManager::getInstance().getEventServiceConfig(); 111 112 if (serviceEnabled) 113 { 114 enabled = *serviceEnabled; 115 } 116 117 if (retryAttemps) 118 { 119 // Supported range [1-3] 120 if ((*retryAttemps < 1) || (*retryAttemps > 3)) 121 { 122 messages::queryParameterOutOfRange( 123 asyncResp->res, std::to_string(*retryAttemps), 124 "DeliveryRetryAttempts", "[1-3]"); 125 } 126 else 127 { 128 retryCount = *retryAttemps; 129 } 130 } 131 132 if (retryInterval) 133 { 134 // Supported range [30 - 180] 135 if ((*retryInterval < 30) || (*retryInterval > 180)) 136 { 137 messages::queryParameterOutOfRange( 138 asyncResp->res, std::to_string(*retryInterval), 139 "DeliveryRetryIntervalSeconds", "[30-180]"); 140 } 141 else 142 { 143 retryTimeoutInterval = *retryInterval; 144 } 145 } 146 147 EventServiceManager::getInstance().setEventServiceConfig( 148 std::make_tuple(enabled, retryCount, retryTimeoutInterval)); 149 } 150 }; 151 152 class SubmitTestEvent : public Node 153 { 154 public: 155 SubmitTestEvent(App& app) : 156 Node(app, 157 "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent/") 158 { 159 entityPrivileges = { 160 {boost::beast::http::verb::get, {{"Login"}}}, 161 {boost::beast::http::verb::head, {{"Login"}}}, 162 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 163 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 164 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 165 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 166 } 167 168 private: 169 void doPost(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 170 const crow::Request&, const std::vector<std::string>&) override 171 { 172 EventServiceManager::getInstance().sendTestEventLog(); 173 asyncResp->res.result(boost::beast::http::status::no_content); 174 } 175 }; 176 177 class EventDestinationCollection : public Node 178 { 179 public: 180 EventDestinationCollection(App& app) : 181 Node(app, "/redfish/v1/EventService/Subscriptions/") 182 { 183 entityPrivileges = { 184 {boost::beast::http::verb::get, {{"Login"}}}, 185 {boost::beast::http::verb::head, {{"Login"}}}, 186 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 187 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 188 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 189 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 190 } 191 192 private: 193 void doGet(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 194 const crow::Request&, const std::vector<std::string>&) override 195 { 196 197 asyncResp->res.jsonValue = { 198 {"@odata.type", 199 "#EventDestinationCollection.EventDestinationCollection"}, 200 {"@odata.id", "/redfish/v1/EventService/Subscriptions"}, 201 {"Name", "Event Destination Collections"}}; 202 203 nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"]; 204 205 std::vector<std::string> subscripIds = 206 EventServiceManager::getInstance().getAllIDs(); 207 memberArray = nlohmann::json::array(); 208 asyncResp->res.jsonValue["Members@odata.count"] = subscripIds.size(); 209 210 for (const std::string& id : subscripIds) 211 { 212 memberArray.push_back( 213 {{"@odata.id", 214 "/redfish/v1/EventService/Subscriptions/" + id}}); 215 } 216 } 217 218 void doPost(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 219 const crow::Request& req, 220 const std::vector<std::string>&) override 221 { 222 223 if (EventServiceManager::getInstance().getNumberOfSubscriptions() >= 224 maxNoOfSubscriptions) 225 { 226 messages::eventSubscriptionLimitExceeded(asyncResp->res); 227 return; 228 } 229 std::string destUrl; 230 std::string protocol; 231 std::optional<std::string> context; 232 std::optional<std::string> subscriptionType; 233 std::optional<std::string> eventFormatType2; 234 std::optional<std::string> retryPolicy; 235 std::optional<std::vector<std::string>> msgIds; 236 std::optional<std::vector<std::string>> regPrefixes; 237 std::optional<std::vector<std::string>> resTypes; 238 std::optional<std::vector<nlohmann::json>> headers; 239 std::optional<std::vector<nlohmann::json>> mrdJsonArray; 240 241 if (!json_util::readJson( 242 req, asyncResp->res, "Destination", destUrl, "Context", context, 243 "Protocol", protocol, "SubscriptionType", subscriptionType, 244 "EventFormatType", eventFormatType2, "HttpHeaders", headers, 245 "RegistryPrefixes", regPrefixes, "MessageIds", msgIds, 246 "DeliveryRetryPolicy", retryPolicy, "MetricReportDefinitions", 247 mrdJsonArray, "ResourceTypes", resTypes)) 248 { 249 return; 250 } 251 252 if (regPrefixes && msgIds) 253 { 254 if (regPrefixes->size() && msgIds->size()) 255 { 256 messages::mutualExclusiveProperties( 257 asyncResp->res, "RegistryPrefixes", "MessageIds"); 258 return; 259 } 260 } 261 262 // Validate the URL using regex expression 263 // Format: <protocol>://<host>:<port>/<uri> 264 // protocol: http/https 265 // host: Exclude ' ', ':', '#', '?' 266 // port: Empty or numeric value with ':' separator. 267 // uri: Start with '/' and Exclude '#', ' ' 268 // Can include query params(ex: '/event?test=1') 269 // TODO: Need to validate hostname extensively(as per rfc) 270 const std::regex urlRegex( 271 "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/" 272 "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)"); 273 std::cmatch match; 274 if (!std::regex_match(destUrl.c_str(), match, urlRegex)) 275 { 276 messages::propertyValueFormatError(asyncResp->res, destUrl, 277 "Destination"); 278 return; 279 } 280 281 std::string uriProto = std::string(match[1].first, match[1].second); 282 if (uriProto == "http") 283 { 284 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING 285 messages::propertyValueFormatError(asyncResp->res, destUrl, 286 "Destination"); 287 return; 288 #endif 289 } 290 291 std::string host = std::string(match[2].first, match[2].second); 292 std::string port = std::string(match[3].first, match[3].second); 293 std::string path = std::string(match[4].first, match[4].second); 294 if (port.empty()) 295 { 296 if (uriProto == "http") 297 { 298 port = "80"; 299 } 300 else 301 { 302 port = "443"; 303 } 304 } 305 if (path.empty()) 306 { 307 path = "/"; 308 } 309 310 std::shared_ptr<Subscription> subValue = 311 std::make_shared<Subscription>(host, port, path, uriProto); 312 313 subValue->destinationUrl = destUrl; 314 315 if (subscriptionType) 316 { 317 if (*subscriptionType != "RedfishEvent") 318 { 319 messages::propertyValueNotInList( 320 asyncResp->res, *subscriptionType, "SubscriptionType"); 321 return; 322 } 323 subValue->subscriptionType = *subscriptionType; 324 } 325 else 326 { 327 subValue->subscriptionType = "RedfishEvent"; // Default 328 } 329 330 if (protocol != "Redfish") 331 { 332 messages::propertyValueNotInList(asyncResp->res, protocol, 333 "Protocol"); 334 return; 335 } 336 subValue->protocol = protocol; 337 338 if (eventFormatType2) 339 { 340 if (std::find(supportedEvtFormatTypes.begin(), 341 supportedEvtFormatTypes.end(), 342 *eventFormatType2) == supportedEvtFormatTypes.end()) 343 { 344 messages::propertyValueNotInList( 345 asyncResp->res, *eventFormatType2, "EventFormatType"); 346 return; 347 } 348 subValue->eventFormatType = *eventFormatType2; 349 } 350 else 351 { 352 // If not specified, use default "Event" 353 subValue->eventFormatType = "Event"; 354 } 355 356 if (context) 357 { 358 subValue->customText = *context; 359 } 360 361 if (headers) 362 { 363 subValue->httpHeaders = *headers; 364 } 365 366 if (regPrefixes) 367 { 368 for (const std::string& it : *regPrefixes) 369 { 370 if (std::find(supportedRegPrefixes.begin(), 371 supportedRegPrefixes.end(), 372 it) == supportedRegPrefixes.end()) 373 { 374 messages::propertyValueNotInList(asyncResp->res, it, 375 "RegistryPrefixes"); 376 return; 377 } 378 } 379 subValue->registryPrefixes = *regPrefixes; 380 } 381 382 if (resTypes) 383 { 384 for (const std::string& it : *resTypes) 385 { 386 if (std::find(supportedResourceTypes.begin(), 387 supportedResourceTypes.end(), 388 it) == supportedResourceTypes.end()) 389 { 390 messages::propertyValueNotInList(asyncResp->res, it, 391 "ResourceTypes"); 392 return; 393 } 394 } 395 subValue->resourceTypes = *resTypes; 396 } 397 398 if (msgIds) 399 { 400 // Do we need to loop-up MessageRegistry and validate 401 // data for authenticity??? Not mandate, i believe. 402 subValue->registryMsgIds = *msgIds; 403 } 404 405 if (retryPolicy) 406 { 407 if (std::find(supportedRetryPolicies.begin(), 408 supportedRetryPolicies.end(), 409 *retryPolicy) == supportedRetryPolicies.end()) 410 { 411 messages::propertyValueNotInList(asyncResp->res, *retryPolicy, 412 "DeliveryRetryPolicy"); 413 return; 414 } 415 subValue->retryPolicy = *retryPolicy; 416 } 417 else 418 { 419 // Default "TerminateAfterRetries" 420 subValue->retryPolicy = "TerminateAfterRetries"; 421 } 422 423 if (mrdJsonArray) 424 { 425 for (nlohmann::json& mrdObj : *mrdJsonArray) 426 { 427 std::string mrdUri; 428 if (json_util::getValueFromJsonObject(mrdObj, "@odata.id", 429 mrdUri)) 430 { 431 subValue->metricReportDefinitions.emplace_back(mrdUri); 432 } 433 else 434 { 435 messages::propertyValueFormatError( 436 asyncResp->res, 437 mrdObj.dump(2, ' ', true, 438 nlohmann::json::error_handler_t::replace), 439 "MetricReportDefinitions"); 440 return; 441 } 442 } 443 } 444 445 std::string id = 446 EventServiceManager::getInstance().addSubscription(subValue); 447 if (id.empty()) 448 { 449 messages::internalError(asyncResp->res); 450 return; 451 } 452 453 messages::created(asyncResp->res); 454 asyncResp->res.addHeader( 455 "Location", "/redfish/v1/EventService/Subscriptions/" + id); 456 } 457 }; 458 459 class EventDestination : public Node 460 { 461 public: 462 EventDestination(App& app) : 463 Node(app, "/redfish/v1/EventService/Subscriptions/<str>/", 464 std::string()) 465 { 466 entityPrivileges = { 467 {boost::beast::http::verb::get, {{"Login"}}}, 468 {boost::beast::http::verb::head, {{"Login"}}}, 469 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 470 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 471 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 472 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 473 } 474 475 private: 476 void doGet(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 477 const crow::Request&, 478 const std::vector<std::string>& params) override 479 { 480 481 if (params.size() != 1) 482 { 483 messages::internalError(asyncResp->res); 484 return; 485 } 486 487 std::shared_ptr<Subscription> subValue = 488 EventServiceManager::getInstance().getSubscription(params[0]); 489 if (subValue == nullptr) 490 { 491 asyncResp->res.result(boost::beast::http::status::not_found); 492 return; 493 } 494 const std::string& id = params[0]; 495 496 asyncResp->res.jsonValue = { 497 {"@odata.type", "#EventDestination.v1_7_0.EventDestination"}, 498 {"Protocol", "Redfish"}}; 499 asyncResp->res.jsonValue["@odata.id"] = 500 "/redfish/v1/EventService/Subscriptions/" + id; 501 asyncResp->res.jsonValue["Id"] = id; 502 asyncResp->res.jsonValue["Name"] = "Event Destination " + id; 503 asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl; 504 asyncResp->res.jsonValue["Context"] = subValue->customText; 505 asyncResp->res.jsonValue["SubscriptionType"] = 506 subValue->subscriptionType; 507 asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders; 508 asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType; 509 asyncResp->res.jsonValue["RegistryPrefixes"] = 510 subValue->registryPrefixes; 511 asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes; 512 513 asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds; 514 asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy; 515 516 std::vector<nlohmann::json> mrdJsonArray; 517 for (const auto& mdrUri : subValue->metricReportDefinitions) 518 { 519 mrdJsonArray.push_back({{"@odata.id", mdrUri}}); 520 } 521 asyncResp->res.jsonValue["MetricReportDefinitions"] = mrdJsonArray; 522 } 523 524 void doPatch(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 525 const crow::Request& req, 526 const std::vector<std::string>& params) override 527 { 528 529 if (params.size() != 1) 530 { 531 messages::internalError(asyncResp->res); 532 return; 533 } 534 535 std::shared_ptr<Subscription> subValue = 536 EventServiceManager::getInstance().getSubscription(params[0]); 537 if (subValue == nullptr) 538 { 539 asyncResp->res.result(boost::beast::http::status::not_found); 540 return; 541 } 542 543 std::optional<std::string> context; 544 std::optional<std::string> retryPolicy; 545 std::optional<std::vector<nlohmann::json>> headers; 546 547 if (!json_util::readJson(req, asyncResp->res, "Context", context, 548 "DeliveryRetryPolicy", retryPolicy, 549 "HttpHeaders", headers)) 550 { 551 return; 552 } 553 554 if (context) 555 { 556 subValue->customText = *context; 557 } 558 559 if (headers) 560 { 561 subValue->httpHeaders = *headers; 562 } 563 564 if (retryPolicy) 565 { 566 if (std::find(supportedRetryPolicies.begin(), 567 supportedRetryPolicies.end(), 568 *retryPolicy) == supportedRetryPolicies.end()) 569 { 570 messages::propertyValueNotInList(asyncResp->res, *retryPolicy, 571 "DeliveryRetryPolicy"); 572 return; 573 } 574 subValue->retryPolicy = *retryPolicy; 575 subValue->updateRetryPolicy(); 576 } 577 578 EventServiceManager::getInstance().updateSubscriptionData(); 579 } 580 581 void doDelete(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 582 const crow::Request&, 583 const std::vector<std::string>& params) override 584 { 585 586 if (params.size() != 1) 587 { 588 messages::internalError(asyncResp->res); 589 return; 590 } 591 592 if (!EventServiceManager::getInstance().isSubscriptionExist(params[0])) 593 { 594 asyncResp->res.result(boost::beast::http::status::not_found); 595 return; 596 } 597 EventServiceManager::getInstance().deleteSubscription(params[0]); 598 } 599 }; 600 601 } // namespace redfish 602