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