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