xref: /openbmc/bmcweb/redfish-core/lib/aggregation_service.hpp (revision e8f66f0b0df2a483822b0c51b67c5b4e71c1d74b)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4 
5 #include "app.hpp"
6 #include "async_resp.hpp"
7 #include "error_messages.hpp"
8 #include "http_request.hpp"
9 #include "http_response.hpp"
10 #include "logging.hpp"
11 #include "ossl_random.hpp"
12 #include "query.hpp"
13 #include "redfish_aggregator.hpp"
14 #include "registries/privilege_registry.hpp"
15 #include "utility.hpp"
16 #include "utils/json_utils.hpp"
17 
18 #include <boost/beast/http/field.hpp>
19 #include <boost/beast/http/verb.hpp>
20 #include <boost/system/result.hpp>
21 #include <boost/url/format.hpp>
22 #include <boost/url/parse.hpp>
23 #include <boost/url/url.hpp>
24 #include <nlohmann/json.hpp>
25 
26 #include <cstddef>
27 #include <functional>
28 #include <memory>
29 #include <unordered_map>
30 #include <utility>
31 
32 namespace redfish
33 {
34 
handleAggregationServiceHead(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)35 inline void handleAggregationServiceHead(
36     App& app, const crow::Request& req,
37     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
38 {
39     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
40     {
41         return;
42     }
43     asyncResp->res.addHeader(
44         boost::beast::http::field::link,
45         "</redfish/v1/JsonSchemas/AggregationService/AggregationService.json>; rel=describedby");
46 }
47 
handleAggregationServiceGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)48 inline void handleAggregationServiceGet(
49     App& app, const crow::Request& req,
50     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
51 {
52     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
53     {
54         return;
55     }
56     asyncResp->res.addHeader(
57         boost::beast::http::field::link,
58         "</redfish/v1/JsonSchemas/AggregationService/AggregationService.json>; rel=describedby");
59     nlohmann::json& json = asyncResp->res.jsonValue;
60     json["@odata.id"] = "/redfish/v1/AggregationService";
61     json["@odata.type"] = "#AggregationService.v1_0_1.AggregationService";
62     json["Id"] = "AggregationService";
63     json["Name"] = "Aggregation Service";
64     json["Description"] = "Aggregation Service";
65     json["ServiceEnabled"] = true;
66     json["AggregationSources"]["@odata.id"] =
67         "/redfish/v1/AggregationService/AggregationSources";
68 }
69 
requestRoutesAggregationService(App & app)70 inline void requestRoutesAggregationService(App& app)
71 {
72     BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/")
73         .privileges(redfish::privileges::headAggregationService)
74         .methods(boost::beast::http::verb::head)(
75             std::bind_front(handleAggregationServiceHead, std::ref(app)));
76     BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/")
77         .privileges(redfish::privileges::getAggregationService)
78         .methods(boost::beast::http::verb::get)(
79             std::bind_front(handleAggregationServiceGet, std::ref(app)));
80 }
81 
populateAggregationSourceCollection(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)82 inline void populateAggregationSourceCollection(
83     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
84     const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
85 {
86     nlohmann::json::array_t members;
87     for (const auto& sat : satelliteInfo)
88     {
89         nlohmann::json::object_t member;
90         member["@odata.id"] = boost::urls::format(
91             "/redfish/v1/AggregationService/AggregationSources/{}", sat.first);
92         members.emplace_back(std::move(member));
93     }
94     asyncResp->res.jsonValue["Members@odata.count"] = members.size();
95     asyncResp->res.jsonValue["Members"] = std::move(members);
96 }
97 
handleAggregationSourceCollectionGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)98 inline void handleAggregationSourceCollectionGet(
99     App& app, const crow::Request& req,
100     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
101 {
102     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
103     {
104         return;
105     }
106     asyncResp->res.addHeader(
107         boost::beast::http::field::link,
108         "</redfish/v1/JsonSchemas/AggregationSourceCollection/AggregationSourceCollection.json>; rel=describedby");
109     nlohmann::json& json = asyncResp->res.jsonValue;
110     json["@odata.id"] = "/redfish/v1/AggregationService/AggregationSources";
111     json["@odata.type"] =
112         "#AggregationSourceCollection.AggregationSourceCollection";
113     json["Name"] = "Aggregation Source Collection";
114 
115     // Query D-Bus for satellite configs and add them to the Members array
116     RedfishAggregator::getInstance().getSatelliteConfigs(
117         std::bind_front(populateAggregationSourceCollection, asyncResp));
118 }
119 
handleAggregationSourceCollectionHead(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)120 inline void handleAggregationSourceCollectionHead(
121     App& app, const crow::Request& req,
122     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
123 {
124     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
125     {
126         return;
127     }
128     asyncResp->res.addHeader(
129         boost::beast::http::field::link,
130         "</redfish/v1/JsonSchemas/AggregationService/AggregationSourceCollection.json>; rel=describedby");
131 }
132 
requestRoutesAggregationSourceCollection(App & app)133 inline void requestRoutesAggregationSourceCollection(App& app)
134 {
135     BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/AggregationSources/")
136         .privileges(redfish::privileges::getAggregationSourceCollection)
137         .methods(boost::beast::http::verb::get)(std::bind_front(
138             handleAggregationSourceCollectionGet, std::ref(app)));
139 
140     BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/AggregationSources/")
141         .privileges(redfish::privileges::getAggregationSourceCollection)
142         .methods(boost::beast::http::verb::head)(std::bind_front(
143             handleAggregationSourceCollectionHead, std::ref(app)));
144 }
145 
populateAggregationSource(const std::string & aggregationSourceId,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)146 inline void populateAggregationSource(
147     const std::string& aggregationSourceId,
148     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
149     const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
150 {
151     asyncResp->res.addHeader(
152         boost::beast::http::field::link,
153         "</redfish/v1/JsonSchemas/AggregationSource/AggregationSource.json>; rel=describedby");
154 
155     const auto& sat = satelliteInfo.find(aggregationSourceId);
156     if (sat == satelliteInfo.end())
157     {
158         messages::resourceNotFound(asyncResp->res, "AggregationSource",
159                                    aggregationSourceId);
160         return;
161     }
162 
163     asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
164         "/redfish/v1/AggregationService/AggregationSources/{}",
165         aggregationSourceId);
166     asyncResp->res.jsonValue["@odata.type"] =
167         "#AggregationSource.v1_3_1.AggregationSource";
168     asyncResp->res.jsonValue["Id"] = aggregationSourceId;
169 
170     // TODO: We may want to change this whenever we support aggregating multiple
171     // satellite BMCs.  Otherwise all AggregationSource resources will have the
172     // same "Name".
173     // TODO: We should use the "Name" from the satellite config whenever we add
174     // support for including it in the data returned in satelliteInfo.
175     asyncResp->res.jsonValue["Name"] = "Aggregation source";
176     std::string hostName(sat->second.encoded_origin());
177     asyncResp->res.jsonValue["HostName"] = std::move(hostName);
178 
179     // Include UserName property, defaulting to null
180     auto& aggregator = RedfishAggregator::getInstance();
181     auto it = aggregator.aggregationSources.find(aggregationSourceId);
182     if (it != aggregator.aggregationSources.end() &&
183         !it->second.username.empty())
184     {
185         asyncResp->res.jsonValue["UserName"] = it->second.username;
186     }
187     else
188     {
189         asyncResp->res.jsonValue["UserName"] = nullptr;
190     }
191 
192     // The Redfish spec requires Password to be null in responses
193     asyncResp->res.jsonValue["Password"] = nullptr;
194 }
195 
handleAggregationSourceGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & aggregationSourceId)196 inline void handleAggregationSourceGet(
197     App& app, const crow::Request& req,
198     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
199     const std::string& aggregationSourceId)
200 {
201     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
202     {
203         return;
204     }
205 
206     // Query D-Bus for satellite config corresponding to the specified
207     // AggregationSource
208     RedfishAggregator::getInstance().getSatelliteConfigs(std::bind_front(
209         populateAggregationSource, aggregationSourceId, asyncResp));
210 }
211 
handleAggregationSourceHead(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & aggregationSourceId)212 inline void handleAggregationSourceHead(
213     App& app, const crow::Request& req,
214     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
215     const std::string& aggregationSourceId)
216 {
217     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
218     {
219         return;
220     }
221     asyncResp->res.addHeader(
222         boost::beast::http::field::link,
223         "</redfish/v1/JsonSchemas/AggregationService/AggregationSource.json>; rel=describedby");
224 
225     // Needed to prevent unused variable error
226     BMCWEB_LOG_DEBUG("Added link header to response from {}",
227                      aggregationSourceId);
228 }
229 
validateCredentialField(const std::optional<std::string> & field,const std::string & fieldName,crow::Response & res)230 inline bool validateCredentialField(const std::optional<std::string>& field,
231                                     const std::string& fieldName,
232                                     crow::Response& res)
233 {
234     if (!field.has_value())
235     {
236         return true; // Field not provided, that's okay
237     }
238 
239     if (field->empty())
240     {
241         messages::stringValueTooShort(res, fieldName, 1);
242         return false;
243     }
244 
245     if (field->find(':') != std::string::npos)
246     {
247         messages::propertyValueIncorrect(res, *field, fieldName);
248         return false;
249     }
250 
251     if (field->length() > 40)
252     {
253         messages::stringValueTooLong(res, fieldName, 40);
254         return false;
255     }
256 
257     return true;
258 }
259 
handleAggregationSourceCollectionPost(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)260 inline void handleAggregationSourceCollectionPost(
261     App& app, const crow::Request& req,
262     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
263 {
264     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
265     {
266         return;
267     }
268     std::string hostname;
269     std::optional<std::string> username;
270     std::optional<std::string> password;
271 
272     if (!json_util::readJsonPatch(req, asyncResp->res, "HostName", hostname,
273                                   "UserName", username, "Password", password))
274     {
275         return;
276     }
277 
278     boost::system::result<boost::urls::url> url =
279         boost::urls::parse_absolute_uri(hostname);
280     if (!url)
281     {
282         messages::propertyValueIncorrect(asyncResp->res, hostname, "HostName");
283         return;
284     }
285     url->normalize();
286     if (url->scheme() != "http" && url->scheme() != "https")
287     {
288         messages::propertyValueIncorrect(asyncResp->res, hostname, "HostName");
289         return;
290     }
291     crow::utility::setPortDefaults(*url);
292 
293     // Check for duplicate hostname
294     auto& aggregator = RedfishAggregator::getInstance();
295     for (const auto& [existingPrefix, existingSource] :
296          aggregator.aggregationSources)
297     {
298         if (existingSource.url == *url)
299         {
300             messages::resourceAlreadyExists(asyncResp->res, "AggregationSource",
301                                             "HostName", url->buffer());
302             return;
303         }
304     }
305 
306     // Validate username and password
307     if (!validateCredentialField(username, "UserName", asyncResp->res))
308     {
309         return;
310     }
311     if (!validateCredentialField(password, "Password", asyncResp->res))
312     {
313         return;
314     }
315 
316     std::string prefix = bmcweb::getRandomIdOfLength(8);
317     aggregator.aggregationSources.emplace(
318         prefix,
319         AggregationSource{*url, username.value_or(""), password.value_or("")});
320 
321     BMCWEB_LOG_DEBUG("Emplaced {} with url {}", prefix, url->buffer());
322     asyncResp->res.addHeader(
323         boost::beast::http::field::location,
324         boost::urls::format("/redfish/v1/AggregationSources/{}", prefix)
325             .buffer());
326     messages::created(asyncResp->res);
327 }
328 
handleAggregationSourcePatch(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & aggregationSourceId)329 inline void handleAggregationSourcePatch(
330     App& app, const crow::Request& req,
331     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
332     const std::string& aggregationSourceId)
333 {
334     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
335     {
336         return;
337     }
338 
339     std::optional<std::string> username;
340     std::optional<std::string> password;
341 
342     if (!json_util::readJsonPatch(req, asyncResp->res, "UserName", username,
343                                   "Password", password))
344     {
345         return;
346     }
347 
348     // Validate username and password
349     if (!validateCredentialField(username, "UserName", asyncResp->res))
350     {
351         return;
352     }
353     if (!validateCredentialField(password, "Password", asyncResp->res))
354     {
355         return;
356     }
357 
358     // Check if the aggregation source exists in writable sources
359     auto& aggregator = RedfishAggregator::getInstance();
360     auto it = aggregator.aggregationSources.find(aggregationSourceId);
361     if (it != aggregator.aggregationSources.end())
362     {
363         // Update only the fields that were provided
364         if (username.has_value())
365         {
366             it->second.username = *username;
367         }
368         if (password.has_value())
369         {
370             it->second.password = *password;
371         }
372 
373         messages::success(asyncResp->res);
374         return;
375     }
376 
377     // Not in writable sources, query D-Bus to check if it exists in
378     // Entity Manager sources
379     RedfishAggregator::getInstance().getSatelliteConfigs(
380         [asyncResp, aggregationSourceId](
381             const std::unordered_map<std::string, boost::urls::url>&
382                 satelliteInfo) {
383             // Check if it exists in Entity Manager sources
384             if (satelliteInfo.contains(aggregationSourceId))
385             {
386                 // Source exists but is read-only (from Entity Manager)
387                 messages::propertyNotWritable(asyncResp->res, "UserName");
388                 return;
389             }
390 
391             // Doesn't exist anywhere
392             messages::resourceNotFound(asyncResp->res, "AggregationSource",
393                                        aggregationSourceId);
394         });
395 }
396 
handleAggregationSourceDelete(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & aggregationSourceId)397 inline void handleAggregationSourceDelete(
398     App& app, const crow::Request& req,
399     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
400     const std::string& aggregationSourceId)
401 {
402     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
403     {
404         return;
405     }
406     asyncResp->res.addHeader(
407         boost::beast::http::field::link,
408         "</redfish/v1/JsonSchemas/AggregationService/AggregationSource.json>; rel=describedby");
409 
410     size_t deleted = RedfishAggregator::getInstance().aggregationSources.erase(
411         aggregationSourceId);
412     if (deleted == 0)
413     {
414         messages::resourceNotFound(asyncResp->res, "AggregationSource",
415                                    aggregationSourceId);
416         return;
417     }
418 
419     messages::success(asyncResp->res);
420 }
421 
requestRoutesAggregationSource(App & app)422 inline void requestRoutesAggregationSource(App& app)
423 {
424     BMCWEB_ROUTE(app,
425                  "/redfish/v1/AggregationService/AggregationSources/<str>/")
426         .privileges(redfish::privileges::getAggregationSource)
427         .methods(boost::beast::http::verb::get)(
428             std::bind_front(handleAggregationSourceGet, std::ref(app)));
429 
430     BMCWEB_ROUTE(app,
431                  "/redfish/v1/AggregationService/AggregationSources/<str>/")
432         .privileges(redfish::privileges::patchAggregationSource)
433         .methods(boost::beast::http::verb::patch)(
434             std::bind_front(handleAggregationSourcePatch, std::ref(app)));
435 
436     BMCWEB_ROUTE(app,
437                  "/redfish/v1/AggregationService/AggregationSources/<str>/")
438         .privileges(redfish::privileges::deleteAggregationSource)
439         .methods(boost::beast::http::verb::delete_)(
440             std::bind_front(handleAggregationSourceDelete, std::ref(app)));
441 
442     BMCWEB_ROUTE(app,
443                  "/redfish/v1/AggregationService/AggregationSources/<str>/")
444         .privileges(redfish::privileges::headAggregationSource)
445         .methods(boost::beast::http::verb::head)(
446             std::bind_front(handleAggregationSourceHead, std::ref(app)));
447 
448     BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/AggregationSources/")
449         .privileges(redfish::privileges::postAggregationSourceCollection)
450         .methods(boost::beast::http::verb::post)(std::bind_front(
451             handleAggregationSourceCollectionPost, std::ref(app)));
452 }
453 
454 } // namespace redfish
455