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