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