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