blob: 65d3ac7634b7513fce041f012c24b8d3b3d66bfc [file] [log] [blame]
Ed Tanous40e9b922024-09-10 13:50:16 -07001// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright OpenBMC Authors
Ed Tanous6c068982023-02-07 15:44:38 -08003#pragma once
4
5#include "app.hpp"
Ed Tanousd7857202025-01-28 15:32:26 -08006#include "async_resp.hpp"
Ed Tanous6c068982023-02-07 15:44:38 -08007#include "error_messages.hpp"
8#include "http_request.hpp"
9#include "http_response.hpp"
Ed Tanousd7857202025-01-28 15:32:26 -080010#include "logging.hpp"
Ed Tanous66620682024-06-05 08:47:30 -070011#include "ossl_random.hpp"
Ed Tanous6c068982023-02-07 15:44:38 -080012#include "query.hpp"
Carson Labrado8b2521a2023-02-18 02:33:14 +000013#include "redfish_aggregator.hpp"
Ed Tanous6c068982023-02-07 15:44:38 -080014#include "registries/privilege_registry.hpp"
Ed Tanous66620682024-06-05 08:47:30 -070015#include "utility.hpp"
16#include "utils/json_utils.hpp"
Ed Tanous6c068982023-02-07 15:44:38 -080017
Ed Tanousd7857202025-01-28 15:32:26 -080018#include <boost/beast/http/field.hpp>
19#include <boost/beast/http/verb.hpp>
Ed Tanous66620682024-06-05 08:47:30 -070020#include <boost/system/result.hpp>
Ed Tanousef4c65b2023-04-24 15:28:50 -070021#include <boost/url/format.hpp>
Ed Tanous66620682024-06-05 08:47:30 -070022#include <boost/url/parse.hpp>
Ed Tanousd7857202025-01-28 15:32:26 -080023#include <boost/url/url.hpp>
Ed Tanous6c068982023-02-07 15:44:38 -080024#include <nlohmann/json.hpp>
25
Ed Tanous66620682024-06-05 08:47:30 -070026#include <cstddef>
Ed Tanous6c068982023-02-07 15:44:38 -080027#include <functional>
28#include <memory>
Ed Tanousd7857202025-01-28 15:32:26 -080029#include <unordered_map>
30#include <utility>
Ed Tanous6c068982023-02-07 15:44:38 -080031
32namespace redfish
33{
34
35inline 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
48inline 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;
Carson Labrado5315c1b2023-02-18 01:02:18 +000066 json["AggregationSources"]["@odata.id"] =
67 "/redfish/v1/AggregationService/AggregationSources";
Ed Tanous6c068982023-02-07 15:44:38 -080068}
69
Carson Labrado8b2521a2023-02-18 02:33:14 +000070inline void requestRoutesAggregationService(App& app)
Ed Tanous6c068982023-02-07 15:44:38 -080071{
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
Carson Labrado8b2521a2023-02-18 02:33:14 +000082inline void populateAggregationSourceCollection(
83 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
Ed Tanous81c4e332023-05-18 10:30:34 -070084 const boost::system::error_code& ec,
Carson Labrado8b2521a2023-02-18 02:33:14 +000085 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 }
Ed Tanous82b286f2025-05-06 13:29:48 -070093 nlohmann::json::array_t members;
Carson Labrado8b2521a2023-02-18 02:33:14 +000094 for (const auto& sat : satelliteInfo)
95 {
96 nlohmann::json::object_t member;
Ed Tanousef4c65b2023-04-24 15:28:50 -070097 member["@odata.id"] = boost::urls::format(
98 "/redfish/v1/AggregationService/AggregationSources/{}", sat.first);
Patrick Williamsad539542023-05-12 10:10:08 -050099 members.emplace_back(std::move(member));
Carson Labrado8b2521a2023-02-18 02:33:14 +0000100 }
101 asyncResp->res.jsonValue["Members@odata.count"] = members.size();
102 asyncResp->res.jsonValue["Members"] = std::move(members);
103}
104
105inline void handleAggregationSourceCollectionGet(
Carson Labrado5315c1b2023-02-18 01:02:18 +0000106 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";
Carson Labrado5315c1b2023-02-18 01:02:18 +0000121
Carson Labrado8b2521a2023-02-18 02:33:14 +0000122 // Query D-Bus for satellite configs and add them to the Members array
Ed Tanous66620682024-06-05 08:47:30 -0700123 RedfishAggregator::getInstance().getSatelliteConfigs(
Carson Labrado8b2521a2023-02-18 02:33:14 +0000124 std::bind_front(populateAggregationSourceCollection, asyncResp));
Carson Labrado5315c1b2023-02-18 01:02:18 +0000125}
126
Carson Labrado8b2521a2023-02-18 02:33:14 +0000127inline 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
140inline void requestRoutesAggregationSourceCollection(App& app)
Carson Labrado5315c1b2023-02-18 01:02:18 +0000141{
142 BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/AggregationSources/")
Carson Labrado8b2521a2023-02-18 02:33:14 +0000143 .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
153inline void populateAggregationSource(
154 const std::string& aggregationSourceId,
155 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
Ed Tanous81c4e332023-05-18 10:30:34 -0700156 const boost::system::error_code& ec,
Carson Labrado8b2521a2023-02-18 02:33:14 +0000157 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
Ed Tanousef4c65b2023-04-24 15:28:50 -0700178 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
179 "/redfish/v1/AggregationService/AggregationSources/{}",
180 aggregationSourceId);
Carson Labrado8b2521a2023-02-18 02:33:14 +0000181 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
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700194 // 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
Carson Labrado8b2521a2023-02-18 02:33:14 +0000207 // The Redfish spec requires Password to be null in responses
208 asyncResp->res.jsonValue["Password"] = nullptr;
209}
210
211inline 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
Ed Tanous66620682024-06-05 08:47:30 -0700223 RedfishAggregator::getInstance().getSatelliteConfigs(std::bind_front(
Carson Labrado8b2521a2023-02-18 02:33:14 +0000224 populateAggregationSource, aggregationSourceId, asyncResp));
225}
226
227inline 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
Ed Tanous62598e32023-07-17 17:06:25 -0700241 BMCWEB_LOG_DEBUG("Added link header to response from {}",
242 aggregationSourceId);
Carson Labrado8b2521a2023-02-18 02:33:14 +0000243}
244
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700245inline 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
Ed Tanous66620682024-06-05 08:47:30 -0700275inline 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;
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700284 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))
Ed Tanous66620682024-06-05 08:47:30 -0700289 {
290 return;
291 }
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700292
Ed Tanous66620682024-06-05 08:47:30 -0700293 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);
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700307
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
Ed Tanous66620682024-06-05 08:47:30 -0700331 std::string prefix = bmcweb::getRandomIdOfLength(8);
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700332 aggregator.aggregationSources.emplace(
333 prefix,
334 AggregationSource{*url, username.value_or(""), password.value_or("")});
335
Kamran Hasan71526112025-08-28 21:31:34 -0700336 BMCWEB_LOG_DEBUG("Emplaced {} with url {}", prefix, url->buffer());
Ed Tanous66620682024-06-05 08:47:30 -0700337 asyncResp->res.addHeader(
338 boost::beast::http::field::location,
339 boost::urls::format("/redfish/v1/AggregationSources/{}", prefix)
340 .buffer());
Ed Tanous66620682024-06-05 08:47:30 -0700341 messages::created(asyncResp->res);
342}
343
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700344inline 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
Ed Tanous66620682024-06-05 08:47:30 -0700420inline 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
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700433 size_t deleted = RedfishAggregator::getInstance().aggregationSources.erase(
434 aggregationSourceId);
Ed Tanous66620682024-06-05 08:47:30 -0700435 if (deleted == 0)
436 {
437 messages::resourceNotFound(asyncResp->res, "AggregationSource",
438 aggregationSourceId);
439 return;
440 }
441
442 messages::success(asyncResp->res);
443}
444
Carson Labrado8b2521a2023-02-18 02:33:14 +0000445inline void requestRoutesAggregationSource(App& app)
446{
447 BMCWEB_ROUTE(app,
448 "/redfish/v1/AggregationService/AggregationSources/<str>/")
449 .privileges(redfish::privileges::getAggregationSource)
Carson Labrado5315c1b2023-02-18 01:02:18 +0000450 .methods(boost::beast::http::verb::get)(
Carson Labrado8b2521a2023-02-18 02:33:14 +0000451 std::bind_front(handleAggregationSourceGet, std::ref(app)));
Ed Tanous66620682024-06-05 08:47:30 -0700452
453 BMCWEB_ROUTE(app,
454 "/redfish/v1/AggregationService/AggregationSources/<str>/")
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700455 .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>/")
Ed Tanous66620682024-06-05 08:47:30 -0700461 .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)));
Carson Labrado5315c1b2023-02-18 01:02:18 +0000475}
476
Ed Tanous6c068982023-02-07 15:44:38 -0800477} // namespace redfish