1 #pragma once 2 3 #include <dbus_utility.hpp> 4 #include <error_messages.hpp> 5 #include <http_client.hpp> 6 #include <http_connection.hpp> 7 8 namespace redfish 9 { 10 11 enum class Result 12 { 13 LocalHandle, 14 NoLocalHandle 15 }; 16 17 static void addPrefixToItem(nlohmann::json& item, std::string_view prefix) 18 { 19 std::string* strValue = item.get_ptr<std::string*>(); 20 if (strValue == nullptr) 21 { 22 BMCWEB_LOG_CRITICAL << "Field wasn't a string????"; 23 return; 24 } 25 // Make sure the value is a properly formatted URI 26 auto parsed = boost::urls::parse_relative_ref(*strValue); 27 if (!parsed) 28 { 29 BMCWEB_LOG_CRITICAL << "Couldn't parse URI from resource " << *strValue; 30 return; 31 } 32 33 boost::urls::url_view thisUrl = *parsed; 34 35 // We don't need to add prefixes to these URIs since 36 // /redfish/v1/UpdateService/ itself is not a collection 37 // /redfish/v1/UpdateService/FirmwareInventory 38 // /redfish/v1/UpdateService/SoftwareInventory 39 if (crow::utility::readUrlSegments(thisUrl, "redfish", "v1", 40 "UpdateService", "FirmwareInventory") || 41 crow::utility::readUrlSegments(thisUrl, "redfish", "v1", 42 "UpdateService", "SoftwareInventory")) 43 { 44 BMCWEB_LOG_DEBUG << "Skipping UpdateService URI prefix fixing"; 45 return; 46 } 47 48 // We also need to aggregate FirmwareInventory and 49 // SoftwareInventory so add an extra offset 50 // /redfish/v1/UpdateService/FirmwareInventory/<id> 51 // /redfish/v1/UpdateService/SoftwareInventory/<id> 52 std::string collectionName; 53 std::string softwareItem; 54 if (crow::utility::readUrlSegments( 55 thisUrl, "redfish", "v1", "UpdateService", std::ref(collectionName), 56 std::ref(softwareItem), crow::utility::OrMorePaths())) 57 { 58 softwareItem.insert(0, "_"); 59 softwareItem.insert(0, prefix); 60 item = crow::utility::replaceUrlSegment(thisUrl, 4, softwareItem); 61 } 62 63 // A collection URI that ends with "/" such as 64 // "/redfish/v1/Chassis/" will have 4 segments so we need to 65 // make sure we don't try to add a prefix to an empty segment 66 if (crow::utility::readUrlSegments( 67 thisUrl, "redfish", "v1", std::ref(collectionName), 68 std::ref(softwareItem), crow::utility::OrMorePaths())) 69 { 70 softwareItem.insert(0, "_"); 71 softwareItem.insert(0, prefix); 72 item = crow::utility::replaceUrlSegment(thisUrl, 3, softwareItem); 73 } 74 } 75 76 // We need to attempt to update all URIs under Actions 77 static void addPrefixesToActions(nlohmann::json& json, std::string_view prefix) 78 { 79 nlohmann::json::object_t* object = 80 json.get_ptr<nlohmann::json::object_t*>(); 81 if (object != nullptr) 82 { 83 for (std::pair<const std::string, nlohmann::json>& item : *object) 84 { 85 std::string* strValue = item.second.get_ptr<std::string*>(); 86 if (strValue != nullptr) 87 { 88 addPrefixToItem(item.second, prefix); 89 } 90 else 91 { 92 addPrefixesToActions(item.second, prefix); 93 } 94 } 95 } 96 } 97 98 // Search the json for all URIs and add the supplied prefix if the URI is for 99 // and aggregated resource. 100 static void addPrefixes(nlohmann::json& json, std::string_view prefix) 101 { 102 nlohmann::json::object_t* object = 103 json.get_ptr<nlohmann::json::object_t*>(); 104 if (object != nullptr) 105 { 106 for (std::pair<const std::string, nlohmann::json>& item : *object) 107 { 108 if (item.first == "Actions") 109 { 110 addPrefixesToActions(item.second, prefix); 111 continue; 112 } 113 114 if ((item.first == "@odata.id") || (item.first.ends_with("URI"))) 115 { 116 addPrefixToItem(item.second, prefix); 117 } 118 // Recusively parse the rest of the json 119 addPrefixes(item.second, prefix); 120 } 121 return; 122 } 123 nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>(); 124 if (array != nullptr) 125 { 126 for (nlohmann::json& item : *array) 127 { 128 addPrefixes(item, prefix); 129 } 130 } 131 } 132 133 class RedfishAggregator 134 { 135 private: 136 const std::string retryPolicyName = "RedfishAggregation"; 137 const uint32_t retryAttempts = 5; 138 const uint32_t retryTimeoutInterval = 0; 139 const std::string id = "Aggregator"; 140 141 RedfishAggregator() 142 { 143 getSatelliteConfigs(constructorCallback); 144 145 // Setup the retry policy to be used by Redfish Aggregation 146 crow::HttpClient::getInstance().setRetryConfig( 147 retryAttempts, retryTimeoutInterval, aggregationRetryHandler, 148 retryPolicyName); 149 } 150 151 static inline boost::system::error_code 152 aggregationRetryHandler(unsigned int respCode) 153 { 154 // As a default, assume 200X is alright. 155 // We don't need to retry on a 404 156 if ((respCode < 200) || ((respCode >= 300) && (respCode != 404))) 157 { 158 return boost::system::errc::make_error_code( 159 boost::system::errc::result_out_of_range); 160 } 161 162 // Return 0 if the response code is valid 163 return boost::system::errc::make_error_code( 164 boost::system::errc::success); 165 } 166 167 // Dummy callback used by the Constructor so that it can report the number 168 // of satellite configs when the class is first created 169 static void constructorCallback( 170 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 171 { 172 BMCWEB_LOG_DEBUG << "There were " 173 << std::to_string(satelliteInfo.size()) 174 << " satellite configs found at startup"; 175 } 176 177 // Polls D-Bus to get all available satellite config information 178 // Expects a handler which interacts with the returned configs 179 static void getSatelliteConfigs( 180 const std::function<void( 181 const std::unordered_map<std::string, boost::urls::url>&)>& handler) 182 { 183 BMCWEB_LOG_DEBUG << "Gathering satellite configs"; 184 crow::connections::systemBus->async_method_call( 185 [handler](const boost::system::error_code ec, 186 const dbus::utility::ManagedObjectType& objects) { 187 if (ec) 188 { 189 BMCWEB_LOG_ERROR << "DBUS response error " << ec.value() << ", " 190 << ec.message(); 191 return; 192 } 193 194 // Maps a chosen alias representing a satellite BMC to a url 195 // containing the information required to create a http 196 // connection to the satellite 197 std::unordered_map<std::string, boost::urls::url> satelliteInfo; 198 199 findSatelliteConfigs(objects, satelliteInfo); 200 201 if (!satelliteInfo.empty()) 202 { 203 BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with " 204 << std::to_string(satelliteInfo.size()) 205 << " satellite BMCs"; 206 } 207 else 208 { 209 BMCWEB_LOG_DEBUG 210 << "No satellite BMCs detected. Redfish Aggregation not enabled"; 211 } 212 handler(satelliteInfo); 213 }, 214 "xyz.openbmc_project.EntityManager", "/", 215 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); 216 } 217 218 // Search D-Bus objects for satellite config objects and add their 219 // information if valid 220 static void findSatelliteConfigs( 221 const dbus::utility::ManagedObjectType& objects, 222 std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 223 { 224 for (const auto& objectPath : objects) 225 { 226 for (const auto& interface : objectPath.second) 227 { 228 if (interface.first == 229 "xyz.openbmc_project.Configuration.SatelliteController") 230 { 231 BMCWEB_LOG_DEBUG << "Found Satellite Controller at " 232 << objectPath.first.str; 233 234 if (!satelliteInfo.empty()) 235 { 236 BMCWEB_LOG_ERROR 237 << "Redfish Aggregation only supports one satellite!"; 238 BMCWEB_LOG_DEBUG << "Clearing all satellite data"; 239 satelliteInfo.clear(); 240 return; 241 } 242 243 // For now assume there will only be one satellite config. 244 // Assign it the name/prefix "5B247A" 245 addSatelliteConfig("5B247A", interface.second, 246 satelliteInfo); 247 } 248 } 249 } 250 } 251 252 // Parse the properties of a satellite config object and add the 253 // configuration if the properties are valid 254 static void addSatelliteConfig( 255 const std::string& name, 256 const dbus::utility::DBusPropertiesMap& properties, 257 std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 258 { 259 boost::urls::url url; 260 261 for (const auto& prop : properties) 262 { 263 if (prop.first == "Hostname") 264 { 265 const std::string* propVal = 266 std::get_if<std::string>(&prop.second); 267 if (propVal == nullptr) 268 { 269 BMCWEB_LOG_ERROR << "Invalid Hostname value"; 270 return; 271 } 272 url.set_host(*propVal); 273 } 274 275 else if (prop.first == "Port") 276 { 277 const uint64_t* propVal = std::get_if<uint64_t>(&prop.second); 278 if (propVal == nullptr) 279 { 280 BMCWEB_LOG_ERROR << "Invalid Port value"; 281 return; 282 } 283 284 if (*propVal > std::numeric_limits<uint16_t>::max()) 285 { 286 BMCWEB_LOG_ERROR << "Port value out of range"; 287 return; 288 } 289 url.set_port(static_cast<uint16_t>(*propVal)); 290 } 291 292 else if (prop.first == "AuthType") 293 { 294 const std::string* propVal = 295 std::get_if<std::string>(&prop.second); 296 if (propVal == nullptr) 297 { 298 BMCWEB_LOG_ERROR << "Invalid AuthType value"; 299 return; 300 } 301 302 // For now assume authentication not required to communicate 303 // with the satellite BMC 304 if (*propVal != "None") 305 { 306 BMCWEB_LOG_ERROR 307 << "Unsupported AuthType value: " << *propVal 308 << ", only \"none\" is supported"; 309 return; 310 } 311 url.set_scheme("http"); 312 } 313 } // Finished reading properties 314 315 // Make sure all required config information was made available 316 if (url.host().empty()) 317 { 318 BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Host"; 319 return; 320 } 321 322 if (!url.has_port()) 323 { 324 BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Port"; 325 return; 326 } 327 328 if (!url.has_scheme()) 329 { 330 BMCWEB_LOG_ERROR << "Satellite config " << name 331 << " missing AuthType"; 332 return; 333 } 334 335 std::string resultString; 336 auto result = satelliteInfo.insert_or_assign(name, std::move(url)); 337 if (result.second) 338 { 339 resultString = "Added new satellite config "; 340 } 341 else 342 { 343 resultString = "Updated existing satellite config "; 344 } 345 346 BMCWEB_LOG_DEBUG << resultString << name << " at " 347 << result.first->second.scheme() << "://" 348 << result.first->second.encoded_host_and_port(); 349 } 350 351 enum AggregationType 352 { 353 Collection, 354 Resource, 355 }; 356 357 static void 358 startAggregation(AggregationType isCollection, 359 const crow::Request& thisReq, 360 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 361 { 362 // Create a copy of thisReq so we we can still locally process the req 363 std::error_code ec; 364 auto localReq = std::make_shared<crow::Request>(thisReq.req, ec); 365 if (ec) 366 { 367 BMCWEB_LOG_ERROR << "Failed to create copy of request"; 368 if (isCollection != AggregationType::Collection) 369 { 370 messages::internalError(asyncResp->res); 371 } 372 return; 373 } 374 375 getSatelliteConfigs(std::bind_front(aggregateAndHandle, isCollection, 376 localReq, asyncResp)); 377 } 378 379 static void findSatelite( 380 const crow::Request& req, 381 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 382 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo, 383 std::string_view memberName) 384 { 385 // Determine if the resource ID begins with a known prefix 386 for (const auto& satellite : satelliteInfo) 387 { 388 std::string targetPrefix = satellite.first; 389 targetPrefix += "_"; 390 if (memberName.starts_with(targetPrefix)) 391 { 392 BMCWEB_LOG_DEBUG << "\"" << satellite.first 393 << "\" is a known prefix"; 394 395 // Remove the known prefix from the request's URI and 396 // then forward to the associated satellite BMC 397 getInstance().forwardRequest(req, asyncResp, satellite.first, 398 satelliteInfo); 399 return; 400 } 401 } 402 } 403 404 // Intended to handle an incoming request based on if Redfish Aggregation 405 // is enabled. Forwards request to satellite BMC if it exists. 406 static void aggregateAndHandle( 407 AggregationType isCollection, 408 const std::shared_ptr<crow::Request>& sharedReq, 409 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 410 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 411 { 412 if (sharedReq == nullptr) 413 { 414 return; 415 } 416 const crow::Request& thisReq = *sharedReq; 417 BMCWEB_LOG_DEBUG << "Aggregation is enabled, begin processing of " 418 << thisReq.target(); 419 420 // We previously determined the request is for a collection. No need to 421 // check again 422 if (isCollection == AggregationType::Collection) 423 { 424 // TODO: This should instead be handled so that we can 425 // aggregate the satellite resource collections 426 BMCWEB_LOG_DEBUG << "Aggregating a collection"; 427 return; 428 } 429 430 std::string updateServiceName; 431 std::string memberName; 432 if (crow::utility::readUrlSegments( 433 thisReq.urlView, "redfish", "v1", "UpdateService", 434 std::ref(updateServiceName), std::ref(memberName), 435 crow::utility::OrMorePaths())) 436 { 437 // Must be FirmwareInventory or SoftwareInventory 438 findSatelite(thisReq, asyncResp, satelliteInfo, memberName); 439 return; 440 } 441 442 std::string collectionName; 443 if (crow::utility::readUrlSegments( 444 thisReq.urlView, "redfish", "v1", std::ref(collectionName), 445 std::ref(memberName), crow::utility::OrMorePaths())) 446 { 447 findSatelite(thisReq, asyncResp, satelliteInfo, memberName); 448 } 449 } 450 451 // Attempt to forward a request to the satellite BMC associated with the 452 // prefix. 453 void forwardRequest( 454 const crow::Request& thisReq, 455 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 456 const std::string& prefix, 457 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 458 { 459 const auto& sat = satelliteInfo.find(prefix); 460 if (sat == satelliteInfo.end()) 461 { 462 // Realistically this shouldn't get called since we perform an 463 // earlier check to make sure the prefix exists 464 BMCWEB_LOG_ERROR << "Unrecognized satellite prefix \"" << prefix 465 << "\""; 466 return; 467 } 468 469 // We need to strip the prefix from the request's path 470 std::string targetURI(thisReq.target()); 471 size_t pos = targetURI.find(prefix + "_"); 472 if (pos == std::string::npos) 473 { 474 // If this fails then something went wrong 475 BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix 476 << "_\" from request URI"; 477 messages::internalError(asyncResp->res); 478 return; 479 } 480 targetURI.erase(pos, prefix.size() + 1); 481 482 std::function<void(crow::Response&)> cb = 483 std::bind_front(processResponse, prefix, asyncResp); 484 485 std::string data = thisReq.req.body(); 486 crow::HttpClient::getInstance().sendDataWithCallback( 487 data, id, std::string(sat->second.host()), 488 sat->second.port_number(), targetURI, thisReq.fields, 489 thisReq.method(), retryPolicyName, cb); 490 } 491 492 // Processes the response returned by a satellite BMC and loads its 493 // contents into asyncResp 494 static void 495 processResponse(std::string_view prefix, 496 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 497 crow::Response& resp) 498 { 499 // No processing needed if the request wasn't successful 500 if (resp.resultInt() != 200) 501 { 502 BMCWEB_LOG_DEBUG << "No need to parse satellite response"; 503 asyncResp->res.stringResponse = std::move(resp.stringResponse); 504 return; 505 } 506 507 // The resp will not have a json component 508 // We need to create a json from resp's stringResponse 509 if (resp.getHeaderValue("Content-Type") == "application/json") 510 { 511 nlohmann::json jsonVal = 512 nlohmann::json::parse(resp.body(), nullptr, false); 513 if (jsonVal.is_discarded()) 514 { 515 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON"; 516 messages::operationFailed(asyncResp->res); 517 return; 518 } 519 520 BMCWEB_LOG_DEBUG << "Successfully parsed satellite response"; 521 522 // TODO: For collections we want to add the satellite responses to 523 // our response rather than just straight overwriting them if our 524 // local handling was successful (i.e. would return a 200). 525 526 addPrefixes(jsonVal, prefix); 527 528 BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response"; 529 530 asyncResp->res.stringResponse.emplace( 531 boost::beast::http::response< 532 boost::beast::http::string_body>{}); 533 asyncResp->res.result(resp.result()); 534 asyncResp->res.jsonValue = std::move(jsonVal); 535 536 BMCWEB_LOG_DEBUG << "Finished writing asyncResp"; 537 } 538 else 539 { 540 if (!resp.body().empty()) 541 { 542 // We received a 200 response without the correct Content-Type 543 // so return an Operation Failed error 544 BMCWEB_LOG_ERROR 545 << "Satellite response must be of type \"application/json\""; 546 messages::operationFailed(asyncResp->res); 547 } 548 } 549 } 550 551 public: 552 RedfishAggregator(const RedfishAggregator&) = delete; 553 RedfishAggregator& operator=(const RedfishAggregator&) = delete; 554 RedfishAggregator(RedfishAggregator&&) = delete; 555 RedfishAggregator& operator=(RedfishAggregator&&) = delete; 556 ~RedfishAggregator() = default; 557 558 static RedfishAggregator& getInstance() 559 { 560 static RedfishAggregator handler; 561 return handler; 562 } 563 564 // Entry point to Redfish Aggregation 565 // Returns Result stating whether or not we still need to locally handle the 566 // request 567 static Result 568 beginAggregation(const crow::Request& thisReq, 569 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 570 { 571 using crow::utility::OrMorePaths; 572 using crow::utility::readUrlSegments; 573 const boost::urls::url_view& url = thisReq.urlView; 574 // UpdateService is the only top level resource that is not a Collection 575 if (readUrlSegments(url, "redfish", "v1", "UpdateService")) 576 { 577 return Result::LocalHandle; 578 } 579 if (readUrlSegments(url, "redfish", "v1", "UpdateService", 580 "SoftwareInventory") || 581 readUrlSegments(url, "redfish", "v1", "UpdateService", 582 "FirmwareInventory")) 583 { 584 startAggregation(AggregationType::Collection, thisReq, asyncResp); 585 return Result::LocalHandle; 586 } 587 588 // Is the request for a resource collection?: 589 // /redfish/v1/<resource> 590 // e.g. /redfish/v1/Chassis 591 std::string collectionName; 592 if (readUrlSegments(url, "redfish", "v1", std::ref(collectionName))) 593 { 594 startAggregation(AggregationType::Collection, thisReq, asyncResp); 595 return Result::LocalHandle; 596 } 597 598 // We know that the ID of an aggregated resource will begin with 599 // "5B247A". For the most part the URI will begin like this: 600 // /redfish/v1/<resource>/<resource ID> 601 // Note, FirmwareInventory and SoftwareInventory are "special" because 602 // they are two levels deep, but still need aggregated 603 // /redfish/v1/UpdateService/FirmwareInventory/<FirmwareInventory ID> 604 // /redfish/v1/UpdateService/SoftwareInventory/<SoftwareInventory ID> 605 std::string memberName; 606 if (readUrlSegments(url, "redfish", "v1", "UpdateService", 607 "SoftwareInventory", std::ref(memberName), 608 OrMorePaths()) || 609 readUrlSegments(url, "redfish", "v1", "UpdateService", 610 "FirmwareInventory", std::ref(memberName), 611 OrMorePaths()) || 612 readUrlSegments(url, "redfish", "v1", std::ref(collectionName), 613 std::ref(memberName), OrMorePaths())) 614 { 615 if (memberName.starts_with("5B247A")) 616 { 617 BMCWEB_LOG_DEBUG << "Need to forward a request"; 618 619 // Extract the prefix from the request's URI, retrieve the 620 // associated satellite config information, and then forward the 621 // request to that satellite. 622 startAggregation(AggregationType::Resource, thisReq, asyncResp); 623 return Result::NoLocalHandle; 624 } 625 return Result::LocalHandle; 626 } 627 628 BMCWEB_LOG_DEBUG << "Aggregation not required"; 629 return Result::LocalHandle; 630 } 631 }; 632 633 } // namespace redfish 634