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