1 #pragma once
2
3 #include "aggregation_utils.hpp"
4 #include "dbus_utility.hpp"
5 #include "error_messages.hpp"
6 #include "http_client.hpp"
7 #include "http_connection.hpp"
8 #include "parsing.hpp"
9
10 #include <array>
11 #include <ranges>
12 #include <string_view>
13
14 namespace redfish
15 {
16
17 constexpr unsigned int aggregatorReadBodyLimit = 50 * 1024 * 1024; // 50MB
18
19 enum class Result
20 {
21 LocalHandle,
22 NoLocalHandle
23 };
24
25 enum class SearchType
26 {
27 Collection,
28 CollOrCon,
29 ContainsSubordinate,
30 Resource
31 };
32
33 // clang-format off
34 // These are all of the properties as of version 2022.2 of the Redfish Resource
35 // and Schema Guide whose Type is "string (URI)" and the name does not end in a
36 // case-insensitive form of "uri". That version of the schema is associated
37 // with version 1.16.0 of the Redfish Specification. Going forward, new URI
38 // properties should end in URI so this list should not need to be maintained as
39 // the spec is updated. NOTE: These have been pre-sorted in order to be
40 // compatible with binary search
41 constexpr std::array nonUriProperties{
42 "@Redfish.ActionInfo",
43 // "@odata.context", // We can't fix /redfish/v1/$metadata URIs
44 "@odata.id",
45 // "Destination", // Only used by EventService and won't be a Redfish URI
46 // "HostName", // Isn't actually a Redfish URI
47 "Image",
48 "MetricProperty",
49 // "OriginOfCondition", // Is URI when in request, but is object in response
50 "TaskMonitor",
51 "target", // normal string, but target URI for POST to invoke an action
52 };
53 // clang-format on
54
55 // Search the top collection array to determine if the passed URI is of a
56 // desired type
searchCollectionsArray(std::string_view uri,const SearchType searchType)57 inline bool searchCollectionsArray(std::string_view uri,
58 const SearchType searchType)
59 {
60 boost::system::result<boost::urls::url> parsedUrl =
61 boost::urls::parse_relative_ref(uri);
62 if (!parsedUrl)
63 {
64 BMCWEB_LOG_ERROR("Failed to get target URI from {}", uri);
65 return false;
66 }
67
68 parsedUrl->normalize();
69 boost::urls::segments_ref segments = parsedUrl->segments();
70 if (!segments.is_absolute())
71 {
72 return false;
73 }
74
75 // The passed URI must begin with "/redfish/v1", but we have to strip it
76 // from the URI since topCollections does not include it in its URIs.
77 if (segments.size() < 2)
78 {
79 return false;
80 }
81 if (segments.front() != "redfish")
82 {
83 return false;
84 }
85 segments.erase(segments.begin());
86 if (segments.front() != "v1")
87 {
88 return false;
89 }
90 segments.erase(segments.begin());
91
92 // Exclude the trailing "/" if it exists such as in "/redfish/v1/".
93 if (!segments.empty() && segments.back().empty())
94 {
95 segments.pop_back();
96 }
97
98 // If no segments then the passed URI was either "/redfish/v1" or
99 // "/redfish/v1/".
100 if (segments.empty())
101 {
102 return (searchType == SearchType::ContainsSubordinate) ||
103 (searchType == SearchType::CollOrCon);
104 }
105 std::string_view url = segments.buffer();
106 const auto* it = std::ranges::lower_bound(topCollections, url);
107 if (it == topCollections.end())
108 {
109 // parsedUrl is alphabetically after the last entry in the array so it
110 // can't be a top collection or up tree from a top collection
111 return false;
112 }
113
114 boost::urls::url collectionUrl(*it);
115 boost::urls::segments_view collectionSegments = collectionUrl.segments();
116 boost::urls::segments_view::iterator itCollection =
117 collectionSegments.begin();
118 const boost::urls::segments_view::const_iterator endCollection =
119 collectionSegments.end();
120
121 // Each segment in the passed URI should match the found collection
122 for (const auto& segment : segments)
123 {
124 if (itCollection == endCollection)
125 {
126 // Leftover segments means the target is for an aggregation
127 // supported resource
128 return searchType == SearchType::Resource;
129 }
130
131 if (segment != (*itCollection))
132 {
133 return false;
134 }
135 itCollection++;
136 }
137
138 // No remaining segments means the passed URI was a top level collection
139 if (searchType == SearchType::Collection)
140 {
141 return itCollection == endCollection;
142 }
143 if (searchType == SearchType::ContainsSubordinate)
144 {
145 return itCollection != endCollection;
146 }
147
148 // Return this check instead of "true" in case other SearchTypes get added
149 return searchType == SearchType::CollOrCon;
150 }
151
152 // Determines if the passed property contains a URI. Those property names
153 // either end with a case-insensitive version of "uri" or are specifically
154 // defined in the above array.
isPropertyUri(std::string_view propertyName)155 inline bool isPropertyUri(std::string_view propertyName)
156 {
157 if (propertyName.ends_with("uri") || propertyName.ends_with("Uri") ||
158 propertyName.ends_with("URI"))
159 {
160 return true;
161 }
162 return std::binary_search(nonUriProperties.begin(), nonUriProperties.end(),
163 propertyName);
164 }
165
addPrefixToStringItem(std::string & strValue,std::string_view prefix)166 inline void addPrefixToStringItem(std::string& strValue,
167 std::string_view prefix)
168 {
169 // Make sure the value is a properly formatted URI
170 auto parsed = boost::urls::parse_relative_ref(strValue);
171 if (!parsed)
172 {
173 // Note that DMTF URIs such as
174 // https://redfish.dmtf.org/registries/Base.1.15.0.json will fail this
175 // check and that's okay
176 BMCWEB_LOG_DEBUG("Couldn't parse URI from resource {}", strValue);
177 return;
178 }
179
180 boost::urls::url_view thisUrl = *parsed;
181
182 // We don't need to aggregate JsonSchemas due to potential issues such as
183 // version mismatches between aggregator and satellite BMCs. For now
184 // assume that the aggregator has all the schemas and versions that the
185 // aggregated server has.
186 if (crow::utility::readUrlSegments(thisUrl, "redfish", "v1", "JsonSchemas",
187 crow::utility::OrMorePaths()))
188 {
189 BMCWEB_LOG_DEBUG("Skipping JsonSchemas URI prefix fixing");
190 return;
191 }
192
193 // The first two segments should be "/redfish/v1". We need to check that
194 // before we can search topCollections
195 if (!crow::utility::readUrlSegments(thisUrl, "redfish", "v1",
196 crow::utility::OrMorePaths()))
197 {
198 return;
199 }
200
201 // Check array adding a segment each time until collection is identified
202 // Add prefix to segment after the collection
203 const boost::urls::segments_view urlSegments = thisUrl.segments();
204 bool addedPrefix = false;
205 boost::urls::url url("/");
206 boost::urls::segments_view::const_iterator it = urlSegments.begin();
207 const boost::urls::segments_view::const_iterator end = urlSegments.end();
208
209 // Skip past the leading "/redfish/v1"
210 it++;
211 it++;
212 for (; it != end; it++)
213 {
214 // Trailing "/" will result in an empty segment. In that case we need
215 // to return so we don't apply a prefix to top level collections such
216 // as "/redfish/v1/Chassis/"
217 if ((*it).empty())
218 {
219 return;
220 }
221
222 if (std::binary_search(topCollections.begin(), topCollections.end(),
223 url.buffer()))
224 {
225 std::string collectionItem(prefix);
226 collectionItem += "_" + (*it);
227 url.segments().push_back(collectionItem);
228 it++;
229 addedPrefix = true;
230 break;
231 }
232
233 url.segments().push_back(*it);
234 }
235
236 // Finish constructing the URL here (if needed) to avoid additional checks
237 for (; it != end; it++)
238 {
239 url.segments().push_back(*it);
240 }
241
242 if (addedPrefix)
243 {
244 url.segments().insert(url.segments().begin(), {"redfish", "v1"});
245 strValue = url.buffer();
246 }
247 }
248
addPrefixToItem(nlohmann::json & item,std::string_view prefix)249 inline void addPrefixToItem(nlohmann::json& item, std::string_view prefix)
250 {
251 std::string* strValue = item.get_ptr<std::string*>();
252 if (strValue == nullptr)
253 {
254 // Values for properties like "InvalidURI" and "ResourceMissingAtURI"
255 // from within the Base Registry are objects instead of strings and will
256 // fall into this check
257 BMCWEB_LOG_DEBUG("Field was not a string");
258 return;
259 }
260 addPrefixToStringItem(*strValue, prefix);
261 item = *strValue;
262 }
263
addAggregatedHeaders(crow::Response & asyncResp,const crow::Response & resp,std::string_view prefix)264 inline void addAggregatedHeaders(crow::Response& asyncResp,
265 const crow::Response& resp,
266 std::string_view prefix)
267 {
268 if (!resp.getHeaderValue("Content-Type").empty())
269 {
270 asyncResp.addHeader(boost::beast::http::field::content_type,
271 resp.getHeaderValue("Content-Type"));
272 }
273 if (!resp.getHeaderValue("Allow").empty())
274 {
275 asyncResp.addHeader(boost::beast::http::field::allow,
276 resp.getHeaderValue("Allow"));
277 }
278 std::string_view header = resp.getHeaderValue("Location");
279 if (!header.empty())
280 {
281 std::string location(header);
282 addPrefixToStringItem(location, prefix);
283 asyncResp.addHeader(boost::beast::http::field::location, location);
284 }
285 if (!resp.getHeaderValue("Retry-After").empty())
286 {
287 asyncResp.addHeader(boost::beast::http::field::retry_after,
288 resp.getHeaderValue("Retry-After"));
289 }
290 // TODO: we need special handling for Link Header Value
291 }
292
293 // Fix HTTP headers which appear in responses from Task resources among others
addPrefixToHeadersInResp(nlohmann::json & json,std::string_view prefix)294 inline void addPrefixToHeadersInResp(nlohmann::json& json,
295 std::string_view prefix)
296 {
297 // The passed in "HttpHeaders" should be an array of headers
298 nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>();
299 if (array == nullptr)
300 {
301 BMCWEB_LOG_ERROR("Field wasn't an array_t????");
302 return;
303 }
304
305 for (nlohmann::json& item : *array)
306 {
307 // Each header is a single string with the form "<Field>: <Value>"
308 std::string* strHeader = item.get_ptr<std::string*>();
309 if (strHeader == nullptr)
310 {
311 BMCWEB_LOG_CRITICAL("Field wasn't a string????");
312 continue;
313 }
314
315 constexpr std::string_view location = "Location: ";
316 if (strHeader->starts_with(location))
317 {
318 std::string header = strHeader->substr(location.size());
319 addPrefixToStringItem(header, prefix);
320 *strHeader = std::string(location) + header;
321 }
322 }
323 }
324
325 // Search the json for all URIs and add the supplied prefix if the URI is for
326 // an aggregated resource.
addPrefixes(nlohmann::json & json,std::string_view prefix)327 inline void addPrefixes(nlohmann::json& json, std::string_view prefix)
328 {
329 nlohmann::json::object_t* object =
330 json.get_ptr<nlohmann::json::object_t*>();
331 if (object != nullptr)
332 {
333 for (std::pair<const std::string, nlohmann::json>& item : *object)
334 {
335 if (isPropertyUri(item.first))
336 {
337 addPrefixToItem(item.second, prefix);
338 continue;
339 }
340
341 // "HttpHeaders" contains HTTP headers. Among those we need to
342 // attempt to fix the "Location" header
343 if (item.first == "HttpHeaders")
344 {
345 addPrefixToHeadersInResp(item.second, prefix);
346 continue;
347 }
348
349 // Recursively parse the rest of the json
350 addPrefixes(item.second, prefix);
351 }
352 return;
353 }
354 nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>();
355 if (array != nullptr)
356 {
357 for (nlohmann::json& item : *array)
358 {
359 addPrefixes(item, prefix);
360 }
361 }
362 }
363
aggregationRetryHandler(unsigned int respCode)364 inline boost::system::error_code aggregationRetryHandler(unsigned int respCode)
365 {
366 // Allow all response codes because we want to surface any satellite
367 // issue to the client
368 BMCWEB_LOG_DEBUG("Received {} response from satellite", respCode);
369 return boost::system::errc::make_error_code(boost::system::errc::success);
370 }
371
getAggregationPolicy()372 inline crow::ConnectionPolicy getAggregationPolicy()
373 {
374 return {.maxRetryAttempts = 0,
375 .requestByteLimit = aggregatorReadBodyLimit,
376 .maxConnections = 20,
377 .retryPolicyAction = "TerminateAfterRetries",
378 .retryIntervalSecs = std::chrono::seconds(0),
379 .invalidResp = aggregationRetryHandler};
380 }
381
382 class RedfishAggregator
383 {
384 private:
385 crow::HttpClient client;
386
387 // Dummy callback used by the Constructor so that it can report the number
388 // of satellite configs when the class is first created
constructorCallback(const boost::system::error_code & ec,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)389 static void constructorCallback(
390 const boost::system::error_code& ec,
391 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
392 {
393 if (ec)
394 {
395 BMCWEB_LOG_ERROR("Something went wrong while querying dbus!");
396 return;
397 }
398
399 BMCWEB_LOG_DEBUG("There were {} satellite configs found at startup",
400 std::to_string(satelliteInfo.size()));
401 }
402
403 // Search D-Bus objects for satellite config objects and add their
404 // information if valid
findSatelliteConfigs(const dbus::utility::ManagedObjectType & objects,std::unordered_map<std::string,boost::urls::url> & satelliteInfo)405 static void findSatelliteConfigs(
406 const dbus::utility::ManagedObjectType& objects,
407 std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
408 {
409 for (const auto& objectPath : objects)
410 {
411 for (const auto& interface : objectPath.second)
412 {
413 if (interface.first ==
414 "xyz.openbmc_project.Configuration.SatelliteController")
415 {
416 BMCWEB_LOG_DEBUG("Found Satellite Controller at {}",
417 objectPath.first.str);
418
419 if (!satelliteInfo.empty())
420 {
421 BMCWEB_LOG_ERROR(
422 "Redfish Aggregation only supports one satellite!");
423 BMCWEB_LOG_DEBUG("Clearing all satellite data");
424 satelliteInfo.clear();
425 return;
426 }
427
428 // For now assume there will only be one satellite config.
429 // Assign it the name/prefix "5B247A"
430 addSatelliteConfig("5B247A", interface.second,
431 satelliteInfo);
432 }
433 }
434 }
435 }
436
437 // Parse the properties of a satellite config object and add the
438 // configuration if the properties are valid
addSatelliteConfig(const std::string & name,const dbus::utility::DBusPropertiesMap & properties,std::unordered_map<std::string,boost::urls::url> & satelliteInfo)439 static void addSatelliteConfig(
440 const std::string& name,
441 const dbus::utility::DBusPropertiesMap& properties,
442 std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
443 {
444 boost::urls::url url;
445
446 for (const auto& prop : properties)
447 {
448 if (prop.first == "Hostname")
449 {
450 const std::string* propVal =
451 std::get_if<std::string>(&prop.second);
452 if (propVal == nullptr)
453 {
454 BMCWEB_LOG_ERROR("Invalid Hostname value");
455 return;
456 }
457 url.set_host(*propVal);
458 }
459
460 else if (prop.first == "Port")
461 {
462 const uint64_t* propVal = std::get_if<uint64_t>(&prop.second);
463 if (propVal == nullptr)
464 {
465 BMCWEB_LOG_ERROR("Invalid Port value");
466 return;
467 }
468
469 if (*propVal > std::numeric_limits<uint16_t>::max())
470 {
471 BMCWEB_LOG_ERROR("Port value out of range");
472 return;
473 }
474 url.set_port(std::to_string(static_cast<uint16_t>(*propVal)));
475 }
476
477 else if (prop.first == "AuthType")
478 {
479 const std::string* propVal =
480 std::get_if<std::string>(&prop.second);
481 if (propVal == nullptr)
482 {
483 BMCWEB_LOG_ERROR("Invalid AuthType value");
484 return;
485 }
486
487 // For now assume authentication not required to communicate
488 // with the satellite BMC
489 if (*propVal != "None")
490 {
491 BMCWEB_LOG_ERROR(
492 "Unsupported AuthType value: {}, only \"none\" is supported",
493 *propVal);
494 return;
495 }
496 url.set_scheme("http");
497 }
498 } // Finished reading properties
499
500 // Make sure all required config information was made available
501 if (url.host().empty())
502 {
503 BMCWEB_LOG_ERROR("Satellite config {} missing Host", name);
504 return;
505 }
506
507 if (!url.has_port())
508 {
509 BMCWEB_LOG_ERROR("Satellite config {} missing Port", name);
510 return;
511 }
512
513 if (!url.has_scheme())
514 {
515 BMCWEB_LOG_ERROR("Satellite config {} missing AuthType", name);
516 return;
517 }
518
519 std::string resultString;
520 auto result = satelliteInfo.insert_or_assign(name, std::move(url));
521 if (result.second)
522 {
523 resultString = "Added new satellite config ";
524 }
525 else
526 {
527 resultString = "Updated existing satellite config ";
528 }
529
530 BMCWEB_LOG_DEBUG("{}{} at {}://{}", resultString, name,
531 result.first->second.scheme(),
532 result.first->second.encoded_host_and_port());
533 }
534
535 enum AggregationType
536 {
537 Collection,
538 ContainsSubordinate,
539 Resource,
540 };
541
542 static void
startAggregation(AggregationType aggType,const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)543 startAggregation(AggregationType aggType, const crow::Request& thisReq,
544 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
545 {
546 if (thisReq.method() != boost::beast::http::verb::get)
547 {
548 if (aggType == AggregationType::Collection)
549 {
550 BMCWEB_LOG_DEBUG(
551 "Only aggregate GET requests to top level collections");
552 return;
553 }
554
555 if (aggType == AggregationType::ContainsSubordinate)
556 {
557 BMCWEB_LOG_DEBUG(
558 "Only aggregate GET requests when uptree of a top level collection");
559 return;
560 }
561 }
562
563 // Create a copy of thisReq so we we can still locally process the req
564 std::error_code ec;
565 auto localReq = std::make_shared<crow::Request>(thisReq.req, ec);
566 if (ec)
567 {
568 BMCWEB_LOG_ERROR("Failed to create copy of request");
569 if (aggType == AggregationType::Resource)
570 {
571 messages::internalError(asyncResp->res);
572 }
573 return;
574 }
575
576 getSatelliteConfigs(
577 std::bind_front(aggregateAndHandle, aggType, localReq, asyncResp));
578 }
579
findSatellite(const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo,std::string_view memberName)580 static void findSatellite(
581 const crow::Request& req,
582 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
583 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo,
584 std::string_view memberName)
585 {
586 // Determine if the resource ID begins with a known prefix
587 for (const auto& satellite : satelliteInfo)
588 {
589 std::string targetPrefix = satellite.first;
590 targetPrefix += "_";
591 if (memberName.starts_with(targetPrefix))
592 {
593 BMCWEB_LOG_DEBUG("\"{}\" is a known prefix", satellite.first);
594
595 // Remove the known prefix from the request's URI and
596 // then forward to the associated satellite BMC
597 getInstance().forwardRequest(req, asyncResp, satellite.first,
598 satelliteInfo);
599 return;
600 }
601 }
602
603 // We didn't recognize the prefix and need to return a 404
604 std::string nameStr = req.url().segments().back();
605 messages::resourceNotFound(asyncResp->res, "", nameStr);
606 }
607
608 // Intended to handle an incoming request based on if Redfish Aggregation
609 // is enabled. Forwards request to satellite BMC if it exists.
aggregateAndHandle(AggregationType aggType,const std::shared_ptr<crow::Request> & sharedReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const boost::system::error_code & ec,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)610 static void aggregateAndHandle(
611 AggregationType aggType,
612 const std::shared_ptr<crow::Request>& sharedReq,
613 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
614 const boost::system::error_code& ec,
615 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
616 {
617 if (sharedReq == nullptr)
618 {
619 return;
620 }
621 // Something went wrong while querying dbus
622 if (ec)
623 {
624 messages::internalError(asyncResp->res);
625 return;
626 }
627
628 // No satellite configs means we don't need to keep attempting to
629 // aggregate
630 if (satelliteInfo.empty())
631 {
632 // For collections or resources that can contain a subordinate
633 // top level collection we'll also handle the request locally so we
634 // don't need to write an error code
635 if (aggType == AggregationType::Resource)
636 {
637 std::string nameStr = sharedReq->url().segments().back();
638 messages::resourceNotFound(asyncResp->res, "", nameStr);
639 }
640 return;
641 }
642
643 const crow::Request& thisReq = *sharedReq;
644 BMCWEB_LOG_DEBUG("Aggregation is enabled, begin processing of {}",
645 thisReq.target());
646
647 // We previously determined the request is for a collection. No need to
648 // check again
649 if (aggType == AggregationType::Collection)
650 {
651 BMCWEB_LOG_DEBUG("Aggregating a collection");
652 // We need to use a specific response handler and send the
653 // request to all known satellites
654 getInstance().forwardCollectionRequests(thisReq, asyncResp,
655 satelliteInfo);
656 return;
657 }
658
659 // We previously determined the request may contain a subordinate
660 // collection. No need to check again
661 if (aggType == AggregationType::ContainsSubordinate)
662 {
663 BMCWEB_LOG_DEBUG(
664 "Aggregating what may have a subordinate collection");
665 // We need to use a specific response handler and send the
666 // request to all known satellites
667 getInstance().forwardContainsSubordinateRequests(thisReq, asyncResp,
668 satelliteInfo);
669 return;
670 }
671
672 const boost::urls::segments_view urlSegments = thisReq.url().segments();
673 boost::urls::url currentUrl("/");
674 boost::urls::segments_view::const_iterator it = urlSegments.begin();
675 boost::urls::segments_view::const_iterator end = urlSegments.end();
676
677 // Skip past the leading "/redfish/v1"
678 it++;
679 it++;
680 for (; it != end; it++)
681 {
682 if (std::binary_search(topCollections.begin(), topCollections.end(),
683 currentUrl.buffer()))
684 {
685 // We've matched a resource collection so this current segment
686 // must contain an aggregation prefix
687 findSatellite(thisReq, asyncResp, satelliteInfo, *it);
688 return;
689 }
690
691 currentUrl.segments().push_back(*it);
692 }
693
694 // We shouldn't reach this point since we should've hit one of the
695 // previous exits
696 messages::internalError(asyncResp->res);
697 }
698
699 // Attempt to forward a request to the satellite BMC associated with the
700 // prefix.
forwardRequest(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & prefix,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)701 void forwardRequest(
702 const crow::Request& thisReq,
703 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
704 const std::string& prefix,
705 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
706 {
707 const auto& sat = satelliteInfo.find(prefix);
708 if (sat == satelliteInfo.end())
709 {
710 // Realistically this shouldn't get called since we perform an
711 // earlier check to make sure the prefix exists
712 BMCWEB_LOG_ERROR("Unrecognized satellite prefix \"{}\"", prefix);
713 return;
714 }
715
716 // We need to strip the prefix from the request's path
717 boost::urls::url targetURI(thisReq.target());
718 std::string path = thisReq.url().path();
719 size_t pos = path.find(prefix + "_");
720 if (pos == std::string::npos)
721 {
722 // If this fails then something went wrong
723 BMCWEB_LOG_ERROR("Error removing prefix \"{}_\" from request URI",
724 prefix);
725 messages::internalError(asyncResp->res);
726 return;
727 }
728 path.erase(pos, prefix.size() + 1);
729
730 std::function<void(crow::Response&)> cb =
731 std::bind_front(processResponse, prefix, asyncResp);
732
733 std::string data = thisReq.body();
734 boost::urls::url url(sat->second);
735 url.set_path(path);
736 if (targetURI.has_query())
737 {
738 url.set_query(targetURI.query());
739 }
740 client.sendDataWithCallback(std::move(data), url,
741 ensuressl::VerifyCertificate::Verify,
742 thisReq.fields(), thisReq.method(), cb);
743 }
744
745 // Forward a request for a collection URI to each known satellite BMC
forwardCollectionRequests(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)746 void forwardCollectionRequests(
747 const crow::Request& thisReq,
748 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
749 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
750 {
751 for (const auto& sat : satelliteInfo)
752 {
753 std::function<void(crow::Response&)> cb = std::bind_front(
754 processCollectionResponse, sat.first, asyncResp);
755
756 boost::urls::url url(sat.second);
757 url.set_path(thisReq.url().path());
758 if (thisReq.url().has_query())
759 {
760 url.set_query(thisReq.url().query());
761 }
762 std::string data = thisReq.body();
763 client.sendDataWithCallback(std::move(data), url,
764 ensuressl::VerifyCertificate::Verify,
765 thisReq.fields(), thisReq.method(), cb);
766 }
767 }
768
769 // Forward request for a URI that is uptree of a top level collection to
770 // each known satellite BMC
forwardContainsSubordinateRequests(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)771 void forwardContainsSubordinateRequests(
772 const crow::Request& thisReq,
773 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
774 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
775 {
776 for (const auto& sat : satelliteInfo)
777 {
778 std::function<void(crow::Response&)> cb = std::bind_front(
779 processContainsSubordinateResponse, sat.first, asyncResp);
780
781 // will ignore an expanded resource in the response if that resource
782 // is not already supported by the aggregating BMC
783 // TODO: Improve the processing so that we don't have to strip query
784 // params in this specific case
785 boost::urls::url url(sat.second);
786 url.set_path(thisReq.url().path());
787
788 std::string data = thisReq.body();
789
790 client.sendDataWithCallback(std::move(data), url,
791 ensuressl::VerifyCertificate::Verify,
792 thisReq.fields(), thisReq.method(), cb);
793 }
794 }
795
796 public:
RedfishAggregator(boost::asio::io_context & ioc)797 explicit RedfishAggregator(boost::asio::io_context& ioc) :
798 client(ioc,
799 std::make_shared<crow::ConnectionPolicy>(getAggregationPolicy()))
800 {
801 getSatelliteConfigs(constructorCallback);
802 }
803 RedfishAggregator(const RedfishAggregator&) = delete;
804 RedfishAggregator& operator=(const RedfishAggregator&) = delete;
805 RedfishAggregator(RedfishAggregator&&) = delete;
806 RedfishAggregator& operator=(RedfishAggregator&&) = delete;
807 ~RedfishAggregator() = default;
808
getInstance(boost::asio::io_context * io=nullptr)809 static RedfishAggregator& getInstance(boost::asio::io_context* io = nullptr)
810 {
811 static RedfishAggregator handler(*io);
812 return handler;
813 }
814
815 // Polls D-Bus to get all available satellite config information
816 // Expects a handler which interacts with the returned configs
getSatelliteConfigs(std::function<void (const boost::system::error_code &,const std::unordered_map<std::string,boost::urls::url> &)> handler)817 static void getSatelliteConfigs(
818 std::function<
819 void(const boost::system::error_code&,
820 const std::unordered_map<std::string, boost::urls::url>&)>
821 handler)
822 {
823 BMCWEB_LOG_DEBUG("Gathering satellite configs");
824 sdbusplus::message::object_path path("/xyz/openbmc_project/inventory");
825 dbus::utility::getManagedObjects(
826 "xyz.openbmc_project.EntityManager", path,
827 [handler{std::move(handler)}](
828 const boost::system::error_code& ec,
829 const dbus::utility::ManagedObjectType& objects) {
830 std::unordered_map<std::string, boost::urls::url> satelliteInfo;
831 if (ec)
832 {
833 BMCWEB_LOG_ERROR("DBUS response error {}, {}", ec.value(),
834 ec.message());
835 handler(ec, satelliteInfo);
836 return;
837 }
838
839 // Maps a chosen alias representing a satellite BMC to a url
840 // containing the information required to create a http
841 // connection to the satellite
842 findSatelliteConfigs(objects, satelliteInfo);
843
844 if (!satelliteInfo.empty())
845 {
846 BMCWEB_LOG_DEBUG(
847 "Redfish Aggregation enabled with {} satellite BMCs",
848 std::to_string(satelliteInfo.size()));
849 }
850 else
851 {
852 BMCWEB_LOG_DEBUG(
853 "No satellite BMCs detected. Redfish Aggregation not enabled");
854 }
855 handler(ec, satelliteInfo);
856 });
857 }
858
859 // Processes the response returned by a satellite BMC and loads its
860 // contents into asyncResp
861 static void
processResponse(std::string_view prefix,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,crow::Response & resp)862 processResponse(std::string_view prefix,
863 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
864 crow::Response& resp)
865 {
866 // 429 and 502 mean we didn't actually send the request so don't
867 // overwrite the response headers in that case
868 if ((resp.result() == boost::beast::http::status::too_many_requests) ||
869 (resp.result() == boost::beast::http::status::bad_gateway))
870 {
871 asyncResp->res.result(resp.result());
872 return;
873 }
874
875 // We want to attempt prefix fixing regardless of response code
876 // The resp will not have a json component
877 // We need to create a json from resp's stringResponse
878 if (isJsonContentType(resp.getHeaderValue("Content-Type")))
879 {
880 nlohmann::json jsonVal =
881 nlohmann::json::parse(*resp.body(), nullptr, false);
882 if (jsonVal.is_discarded())
883 {
884 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON");
885 messages::operationFailed(asyncResp->res);
886 return;
887 }
888
889 BMCWEB_LOG_DEBUG("Successfully parsed satellite response");
890
891 addPrefixes(jsonVal, prefix);
892
893 BMCWEB_LOG_DEBUG("Added prefix to parsed satellite response");
894
895 asyncResp->res.result(resp.result());
896 asyncResp->res.jsonValue = std::move(jsonVal);
897
898 BMCWEB_LOG_DEBUG("Finished writing asyncResp");
899 }
900 else
901 {
902 // We allow any Content-Type that is not "application/json" now
903 asyncResp->res.result(resp.result());
904 asyncResp->res.copyBody(resp);
905 }
906 addAggregatedHeaders(asyncResp->res, resp, prefix);
907 }
908
909 // Processes the collection response returned by a satellite BMC and merges
910 // its "@odata.id" values
processCollectionResponse(const std::string & prefix,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,crow::Response & resp)911 static void processCollectionResponse(
912 const std::string& prefix,
913 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
914 crow::Response& resp)
915 {
916 // 429 and 502 mean we didn't actually send the request so don't
917 // overwrite the response headers in that case
918 if ((resp.result() == boost::beast::http::status::too_many_requests) ||
919 (resp.result() == boost::beast::http::status::bad_gateway))
920 {
921 return;
922 }
923
924 if (resp.resultInt() != 200)
925 {
926 BMCWEB_LOG_DEBUG(
927 "Collection resource does not exist in satellite BMC \"{}\"",
928 prefix);
929 // Return the error if we haven't had any successes
930 if (asyncResp->res.resultInt() != 200)
931 {
932 asyncResp->res.result(resp.result());
933 asyncResp->res.copyBody(resp);
934 }
935 return;
936 }
937
938 // The resp will not have a json component
939 // We need to create a json from resp's stringResponse
940 if (isJsonContentType(resp.getHeaderValue("Content-Type")))
941 {
942 nlohmann::json jsonVal =
943 nlohmann::json::parse(*resp.body(), nullptr, false);
944 if (jsonVal.is_discarded())
945 {
946 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON");
947
948 // Notify the user if doing so won't overwrite a valid response
949 if (asyncResp->res.resultInt() != 200)
950 {
951 messages::operationFailed(asyncResp->res);
952 }
953 return;
954 }
955
956 BMCWEB_LOG_DEBUG("Successfully parsed satellite response");
957
958 // Now we need to add the prefix to the URIs contained in the
959 // response.
960 addPrefixes(jsonVal, prefix);
961
962 BMCWEB_LOG_DEBUG("Added prefix to parsed satellite response");
963
964 // If this resource collection does not exist on the aggregating bmc
965 // and has not already been added from processing the response from
966 // a different satellite then we need to completely overwrite
967 // asyncResp
968 if (asyncResp->res.resultInt() != 200)
969 {
970 // We only want to aggregate collections that contain a
971 // "Members" array
972 if ((!jsonVal.contains("Members")) &&
973 (!jsonVal["Members"].is_array()))
974 {
975 BMCWEB_LOG_DEBUG(
976 "Skipping aggregating unsupported resource");
977 return;
978 }
979
980 BMCWEB_LOG_DEBUG(
981 "Collection does not exist, overwriting asyncResp");
982 asyncResp->res.result(resp.result());
983 asyncResp->res.jsonValue = std::move(jsonVal);
984 asyncResp->res.addHeader("Content-Type", "application/json");
985
986 BMCWEB_LOG_DEBUG("Finished overwriting asyncResp");
987 }
988 else
989 {
990 // We only want to aggregate collections that contain a
991 // "Members" array
992 if ((!asyncResp->res.jsonValue.contains("Members")) &&
993 (!asyncResp->res.jsonValue["Members"].is_array()))
994
995 {
996 BMCWEB_LOG_DEBUG(
997 "Skipping aggregating unsupported resource");
998 return;
999 }
1000
1001 BMCWEB_LOG_DEBUG(
1002 "Adding aggregated resources from \"{}\" to collection",
1003 prefix);
1004
1005 // TODO: This is a potential race condition with multiple
1006 // satellites and the aggregating bmc attempting to write to
1007 // update this array. May need to cascade calls to the next
1008 // satellite at the end of this function.
1009 // This is presumably not a concern when there is only a single
1010 // satellite since the aggregating bmc should have completed
1011 // before the response is received from the satellite.
1012
1013 auto& members = asyncResp->res.jsonValue["Members"];
1014 auto& satMembers = jsonVal["Members"];
1015 for (auto& satMem : satMembers)
1016 {
1017 members.emplace_back(std::move(satMem));
1018 }
1019 asyncResp->res.jsonValue["Members@odata.count"] =
1020 members.size();
1021
1022 // TODO: Do we need to sort() after updating the array?
1023 }
1024 }
1025 else
1026 {
1027 BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"",
1028 prefix);
1029 // We received a response that was not a json.
1030 // Notify the user only if we did not receive any valid responses
1031 // and if the resource collection does not already exist on the
1032 // aggregating BMC
1033 if (asyncResp->res.resultInt() != 200)
1034 {
1035 messages::operationFailed(asyncResp->res);
1036 }
1037 }
1038 } // End processCollectionResponse()
1039
1040 // Processes the response returned by a satellite BMC and merges any
1041 // properties whose "@odata.id" value is the URI or either a top level
1042 // collection or is uptree from a top level collection
processContainsSubordinateResponse(const std::string & prefix,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,crow::Response & resp)1043 static void processContainsSubordinateResponse(
1044 const std::string& prefix,
1045 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1046 crow::Response& resp)
1047 {
1048 // 429 and 502 mean we didn't actually send the request so don't
1049 // overwrite the response headers in that case
1050 if ((resp.result() == boost::beast::http::status::too_many_requests) ||
1051 (resp.result() == boost::beast::http::status::bad_gateway))
1052 {
1053 return;
1054 }
1055
1056 if (resp.resultInt() != 200)
1057 {
1058 BMCWEB_LOG_DEBUG(
1059 "Resource uptree from Collection does not exist in satellite BMC \"{}\"",
1060 prefix);
1061 // Return the error if we haven't had any successes
1062 if (asyncResp->res.resultInt() != 200)
1063 {
1064 asyncResp->res.result(resp.result());
1065 asyncResp->res.copyBody(resp);
1066 }
1067 return;
1068 }
1069
1070 // The resp will not have a json component
1071 // We need to create a json from resp's stringResponse
1072 if (isJsonContentType(resp.getHeaderValue("Content-Type")))
1073 {
1074 bool addedLinks = false;
1075 nlohmann::json jsonVal =
1076 nlohmann::json::parse(*resp.body(), nullptr, false);
1077 if (jsonVal.is_discarded())
1078 {
1079 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON");
1080
1081 // Notify the user if doing so won't overwrite a valid response
1082 if (asyncResp->res.resultInt() != 200)
1083 {
1084 messages::operationFailed(asyncResp->res);
1085 }
1086 return;
1087 }
1088
1089 BMCWEB_LOG_DEBUG("Successfully parsed satellite response");
1090
1091 // Parse response and add properties missing from the AsyncResp
1092 // Valid properties will be of the form <property>.@odata.id and
1093 // @odata.id is a <URI>. In other words, the json should contain
1094 // multiple properties such that
1095 // {"<property>":{"@odata.id": "<URI>"}}
1096 nlohmann::json::object_t* object =
1097 jsonVal.get_ptr<nlohmann::json::object_t*>();
1098 if (object == nullptr)
1099 {
1100 BMCWEB_LOG_ERROR("Parsed JSON was not an object?");
1101 return;
1102 }
1103
1104 for (std::pair<const std::string, nlohmann::json>& prop : *object)
1105 {
1106 if (!prop.second.contains("@odata.id"))
1107 {
1108 continue;
1109 }
1110
1111 std::string* strValue =
1112 prop.second["@odata.id"].get_ptr<std::string*>();
1113 if (strValue == nullptr)
1114 {
1115 BMCWEB_LOG_CRITICAL("Field wasn't a string????");
1116 continue;
1117 }
1118 if (!searchCollectionsArray(*strValue, SearchType::CollOrCon))
1119 {
1120 continue;
1121 }
1122
1123 addedLinks = true;
1124 if (!asyncResp->res.jsonValue.contains(prop.first))
1125 {
1126 // Only add the property if it did not already exist
1127 BMCWEB_LOG_DEBUG("Adding link for {} from BMC {}",
1128 *strValue, prefix);
1129 asyncResp->res.jsonValue[prop.first]["@odata.id"] =
1130 *strValue;
1131 continue;
1132 }
1133 }
1134
1135 // If we added links to a previously unsuccessful (non-200) response
1136 // then we need to make sure the response contains the bare minimum
1137 // amount of additional information that we'd expect to have been
1138 // populated.
1139 if (addedLinks && (asyncResp->res.resultInt() != 200))
1140 {
1141 // This resource didn't locally exist or an error
1142 // occurred while generating the response. Remove any
1143 // error messages and update the error code.
1144 asyncResp->res.jsonValue.erase(
1145 asyncResp->res.jsonValue.find("error"));
1146 asyncResp->res.result(resp.result());
1147
1148 const auto& it1 = object->find("@odata.id");
1149 if (it1 != object->end())
1150 {
1151 asyncResp->res.jsonValue["@odata.id"] = (it1->second);
1152 }
1153 const auto& it2 = object->find("@odata.type");
1154 if (it2 != object->end())
1155 {
1156 asyncResp->res.jsonValue["@odata.type"] = (it2->second);
1157 }
1158 const auto& it3 = object->find("Id");
1159 if (it3 != object->end())
1160 {
1161 asyncResp->res.jsonValue["Id"] = (it3->second);
1162 }
1163 const auto& it4 = object->find("Name");
1164 if (it4 != object->end())
1165 {
1166 asyncResp->res.jsonValue["Name"] = (it4->second);
1167 }
1168 }
1169 }
1170 else
1171 {
1172 BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"",
1173 prefix);
1174 // We received as response that was not a json
1175 // Notify the user only if we did not receive any valid responses,
1176 // and if the resource does not already exist on the aggregating BMC
1177 if (asyncResp->res.resultInt() != 200)
1178 {
1179 messages::operationFailed(asyncResp->res);
1180 }
1181 }
1182 }
1183
1184 // Entry point to Redfish Aggregation
1185 // Returns Result stating whether or not we still need to locally handle the
1186 // request
1187 static Result
beginAggregation(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)1188 beginAggregation(const crow::Request& thisReq,
1189 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
1190 {
1191 using crow::utility::OrMorePaths;
1192 using crow::utility::readUrlSegments;
1193 boost::urls::url_view url = thisReq.url();
1194
1195 // We don't need to aggregate JsonSchemas due to potential issues such
1196 // as version mismatches between aggregator and satellite BMCs. For
1197 // now assume that the aggregator has all the schemas and versions that
1198 // the aggregated server has.
1199 if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas",
1200 crow::utility::OrMorePaths()))
1201 {
1202 return Result::LocalHandle;
1203 }
1204
1205 // The first two segments should be "/redfish/v1". We need to check
1206 // that before we can search topCollections
1207 if (!crow::utility::readUrlSegments(url, "redfish", "v1",
1208 crow::utility::OrMorePaths()))
1209 {
1210 return Result::LocalHandle;
1211 }
1212
1213 // Parse the URI to see if it begins with a known top level collection
1214 // such as:
1215 // /redfish/v1/Chassis
1216 // /redfish/v1/UpdateService/FirmwareInventory
1217 const boost::urls::segments_view urlSegments = url.segments();
1218 boost::urls::url currentUrl("/");
1219 boost::urls::segments_view::const_iterator it = urlSegments.begin();
1220 boost::urls::segments_view::const_iterator end = urlSegments.end();
1221
1222 // Skip past the leading "/redfish/v1"
1223 it++;
1224 it++;
1225 for (; it != end; it++)
1226 {
1227 const std::string& collectionItem = *it;
1228 if (std::binary_search(topCollections.begin(), topCollections.end(),
1229 currentUrl.buffer()))
1230 {
1231 // We've matched a resource collection so this current segment
1232 // might contain an aggregation prefix
1233 // TODO: This needs to be rethought when we can support multiple
1234 // satellites due to
1235 // /redfish/v1/AggregationService/AggregationSources/5B247A
1236 // being a local resource describing the satellite
1237 if (collectionItem.starts_with("5B247A_"))
1238 {
1239 BMCWEB_LOG_DEBUG("Need to forward a request");
1240
1241 // Extract the prefix from the request's URI, retrieve the
1242 // associated satellite config information, and then forward
1243 // the request to that satellite.
1244 startAggregation(AggregationType::Resource, thisReq,
1245 asyncResp);
1246 return Result::NoLocalHandle;
1247 }
1248
1249 // Handle collection URI with a trailing backslash
1250 // e.g. /redfish/v1/Chassis/
1251 it++;
1252 if ((it == end) && collectionItem.empty())
1253 {
1254 startAggregation(AggregationType::Collection, thisReq,
1255 asyncResp);
1256 }
1257
1258 // We didn't recognize the prefix or it's a collection with a
1259 // trailing "/". In both cases we still want to locally handle
1260 // the request
1261 return Result::LocalHandle;
1262 }
1263
1264 currentUrl.segments().push_back(collectionItem);
1265 }
1266
1267 // If we made it here then currentUrl could contain a top level
1268 // collection URI without a trailing "/", e.g. /redfish/v1/Chassis
1269 if (std::binary_search(topCollections.begin(), topCollections.end(),
1270 currentUrl.buffer()))
1271 {
1272 startAggregation(AggregationType::Collection, thisReq, asyncResp);
1273 return Result::LocalHandle;
1274 }
1275
1276 // If nothing else then the request could be for a resource which has a
1277 // top level collection as a subordinate
1278 if (searchCollectionsArray(url.path(), SearchType::ContainsSubordinate))
1279 {
1280 startAggregation(AggregationType::ContainsSubordinate, thisReq,
1281 asyncResp);
1282 return Result::LocalHandle;
1283 }
1284
1285 BMCWEB_LOG_DEBUG("Aggregation not required for {}", url.buffer());
1286 return Result::LocalHandle;
1287 }
1288 };
1289
1290 } // namespace redfish
1291