blob: f5034593b70ba67e021ecbedf02406cda066fd05 [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,
Carson Labrado8b2521a2023-02-18 02:33:14 +000084 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
85{
Ed Tanous82b286f2025-05-06 13:29:48 -070086 nlohmann::json::array_t members;
Carson Labrado8b2521a2023-02-18 02:33:14 +000087 for (const auto& sat : satelliteInfo)
88 {
89 nlohmann::json::object_t member;
Ed Tanousef4c65b2023-04-24 15:28:50 -070090 member["@odata.id"] = boost::urls::format(
91 "/redfish/v1/AggregationService/AggregationSources/{}", sat.first);
Patrick Williamsad539542023-05-12 10:10:08 -050092 members.emplace_back(std::move(member));
Carson Labrado8b2521a2023-02-18 02:33:14 +000093 }
94 asyncResp->res.jsonValue["Members@odata.count"] = members.size();
95 asyncResp->res.jsonValue["Members"] = std::move(members);
96}
97
98inline void handleAggregationSourceCollectionGet(
Carson Labrado5315c1b2023-02-18 01:02:18 +000099 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";
Carson Labrado5315c1b2023-02-18 01:02:18 +0000114
Carson Labrado8b2521a2023-02-18 02:33:14 +0000115 // Query D-Bus for satellite configs and add them to the Members array
Ed Tanous66620682024-06-05 08:47:30 -0700116 RedfishAggregator::getInstance().getSatelliteConfigs(
Carson Labrado8b2521a2023-02-18 02:33:14 +0000117 std::bind_front(populateAggregationSourceCollection, asyncResp));
Carson Labrado5315c1b2023-02-18 01:02:18 +0000118}
119
Carson Labrado8b2521a2023-02-18 02:33:14 +0000120inline 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
133inline void requestRoutesAggregationSourceCollection(App& app)
Carson Labrado5315c1b2023-02-18 01:02:18 +0000134{
135 BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/AggregationSources/")
Carson Labrado8b2521a2023-02-18 02:33:14 +0000136 .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
146inline void populateAggregationSource(
147 const std::string& aggregationSourceId,
148 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
Carson Labrado8b2521a2023-02-18 02:33:14 +0000149 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
Carson Labrado8b2521a2023-02-18 02:33:14 +0000155 const auto& sat = satelliteInfo.find(aggregationSourceId);
156 if (sat == satelliteInfo.end())
157 {
158 messages::resourceNotFound(asyncResp->res, "AggregationSource",
159 aggregationSourceId);
160 return;
161 }
162
Ed Tanousef4c65b2023-04-24 15:28:50 -0700163 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
164 "/redfish/v1/AggregationService/AggregationSources/{}",
165 aggregationSourceId);
Carson Labrado8b2521a2023-02-18 02:33:14 +0000166 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
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700179 // 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
Carson Labrado8b2521a2023-02-18 02:33:14 +0000192 // The Redfish spec requires Password to be null in responses
193 asyncResp->res.jsonValue["Password"] = nullptr;
194}
195
196inline 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
Ed Tanous66620682024-06-05 08:47:30 -0700208 RedfishAggregator::getInstance().getSatelliteConfigs(std::bind_front(
Carson Labrado8b2521a2023-02-18 02:33:14 +0000209 populateAggregationSource, aggregationSourceId, asyncResp));
210}
211
212inline 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
Ed Tanous62598e32023-07-17 17:06:25 -0700226 BMCWEB_LOG_DEBUG("Added link header to response from {}",
227 aggregationSourceId);
Carson Labrado8b2521a2023-02-18 02:33:14 +0000228}
229
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700230inline 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
Ed Tanous66620682024-06-05 08:47:30 -0700260inline 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;
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700269 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))
Ed Tanous66620682024-06-05 08:47:30 -0700274 {
275 return;
276 }
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700277
Ed Tanous66620682024-06-05 08:47:30 -0700278 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);
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700292
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
Ed Tanous66620682024-06-05 08:47:30 -0700316 std::string prefix = bmcweb::getRandomIdOfLength(8);
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700317 aggregator.aggregationSources.emplace(
318 prefix,
319 AggregationSource{*url, username.value_or(""), password.value_or("")});
320
Kamran Hasan71526112025-08-28 21:31:34 -0700321 BMCWEB_LOG_DEBUG("Emplaced {} with url {}", prefix, url->buffer());
Ed Tanous66620682024-06-05 08:47:30 -0700322 asyncResp->res.addHeader(
323 boost::beast::http::field::location,
324 boost::urls::format("/redfish/v1/AggregationSources/{}", prefix)
325 .buffer());
Ed Tanous66620682024-06-05 08:47:30 -0700326 messages::created(asyncResp->res);
327}
328
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700329inline 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](
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700381 const std::unordered_map<std::string, boost::urls::url>&
382 satelliteInfo) {
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700383 // 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
Ed Tanous66620682024-06-05 08:47:30 -0700397inline 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
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700410 size_t deleted = RedfishAggregator::getInstance().aggregationSources.erase(
411 aggregationSourceId);
Ed Tanous66620682024-06-05 08:47:30 -0700412 if (deleted == 0)
413 {
414 messages::resourceNotFound(asyncResp->res, "AggregationSource",
415 aggregationSourceId);
416 return;
417 }
418
419 messages::success(asyncResp->res);
420}
421
Carson Labrado8b2521a2023-02-18 02:33:14 +0000422inline void requestRoutesAggregationSource(App& app)
423{
424 BMCWEB_ROUTE(app,
425 "/redfish/v1/AggregationService/AggregationSources/<str>/")
426 .privileges(redfish::privileges::getAggregationSource)
Carson Labrado5315c1b2023-02-18 01:02:18 +0000427 .methods(boost::beast::http::verb::get)(
Carson Labrado8b2521a2023-02-18 02:33:14 +0000428 std::bind_front(handleAggregationSourceGet, std::ref(app)));
Ed Tanous66620682024-06-05 08:47:30 -0700429
430 BMCWEB_ROUTE(app,
431 "/redfish/v1/AggregationService/AggregationSources/<str>/")
Kamran Hasan2682a0e2025-09-17 19:50:38 -0700432 .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>/")
Ed Tanous66620682024-06-05 08:47:30 -0700438 .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)));
Carson Labrado5315c1b2023-02-18 01:02:18 +0000452}
453
Ed Tanous6c068982023-02-07 15:44:38 -0800454} // namespace redfish