1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 // SPDX-FileCopyrightText: Copyright 2020 Intel Corporation
4 #pragma once
5 #include "app.hpp"
6 #include "async_resp.hpp"
7 #include "dbus_singleton.hpp"
8 #include "dbus_utility.hpp"
9 #include "error_messages.hpp"
10 #include "event_service_manager.hpp"
11 #include "event_service_store.hpp"
12 #include "generated/enums/event_destination.hpp"
13 #include "http/utility.hpp"
14 #include "http_request.hpp"
15 #include "io_context_singleton.hpp"
16 #include "logging.hpp"
17 #include "query.hpp"
18 #include "registries.hpp"
19 #include "registries/privilege_registry.hpp"
20 #include "registries_selector.hpp"
21 #include "snmp_trap_event_clients.hpp"
22 #include "subscription.hpp"
23 #include "utils/json_utils.hpp"
24
25 #include <asm-generic/errno.h>
26
27 #include <boost/beast/http/fields.hpp>
28 #include <boost/beast/http/status.hpp>
29 #include <boost/beast/http/verb.hpp>
30 #include <boost/system/error_code.hpp>
31 #include <boost/system/result.hpp>
32 #include <boost/url/format.hpp>
33 #include <boost/url/parse.hpp>
34 #include <boost/url/url.hpp>
35 #include <sdbusplus/message/native_types.hpp>
36
37 #include <algorithm>
38 #include <array>
39 #include <cerrno>
40 #include <cstddef>
41 #include <cstdint>
42 #include <memory>
43 #include <optional>
44 #include <ranges>
45 #include <span>
46 #include <string>
47 #include <utility>
48 #include <vector>
49
50 namespace redfish
51 {
52
53 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = {
54 eventFormatType, metricReportFormatType};
55 static constexpr const std::array<const char*, 4> supportedRegPrefixes = {
56 "Base", "OpenBMC", "TaskEvent", "HeartbeatEvent"};
57 static constexpr const std::array<const char*, 3> supportedRetryPolicies = {
58 "TerminateAfterRetries", "SuspendRetries", "RetryForever"};
59
60 static constexpr const std::array<const char*, 2> supportedResourceTypes = {
61 "Task", "Heartbeat"};
62
requestRoutesEventService(App & app)63 inline void requestRoutesEventService(App& app)
64 {
65 BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
66 .privileges(redfish::privileges::getEventService)
67 .methods(
68 boost::beast::http::verb::
69 get)([&app](
70 const crow::Request& req,
71 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
72 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
73 {
74 return;
75 }
76
77 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/EventService";
78 asyncResp->res.jsonValue["@odata.type"] =
79 "#EventService.v1_5_0.EventService";
80 asyncResp->res.jsonValue["Id"] = "EventService";
81 asyncResp->res.jsonValue["Name"] = "Event Service";
82 asyncResp->res.jsonValue["ServerSentEventUri"] =
83 "/redfish/v1/EventService/SSE";
84
85 asyncResp->res.jsonValue["Subscriptions"]["@odata.id"] =
86 "/redfish/v1/EventService/Subscriptions";
87 asyncResp->res.jsonValue["Actions"]["#EventService.SubmitTestEvent"]
88 ["target"] =
89 "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent";
90
91 const persistent_data::EventServiceConfig eventServiceConfig =
92 persistent_data::EventServiceStore::getInstance()
93 .getEventServiceConfig();
94
95 asyncResp->res.jsonValue["Status"]["State"] =
96 (eventServiceConfig.enabled ? "Enabled" : "Disabled");
97 asyncResp->res.jsonValue["ServiceEnabled"] =
98 eventServiceConfig.enabled;
99 asyncResp->res.jsonValue["DeliveryRetryAttempts"] =
100 eventServiceConfig.retryAttempts;
101 asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
102 eventServiceConfig.retryTimeoutInterval;
103 asyncResp->res.jsonValue["EventFormatTypes"] =
104 supportedEvtFormatTypes;
105 asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
106 asyncResp->res.jsonValue["ResourceTypes"] = supportedResourceTypes;
107
108 nlohmann::json::object_t supportedSSEFilters;
109 supportedSSEFilters["EventFormatType"] = true;
110 supportedSSEFilters["MessageId"] = true;
111 supportedSSEFilters["MetricReportDefinition"] = true;
112 supportedSSEFilters["RegistryPrefix"] = true;
113 supportedSSEFilters["OriginResource"] = false;
114 supportedSSEFilters["ResourceType"] = false;
115
116 asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] =
117 std::move(supportedSSEFilters);
118 });
119
120 BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
121 .privileges(redfish::privileges::patchEventService)
122 .methods(boost::beast::http::verb::patch)(
123 [&app](const crow::Request& req,
124 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
125 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
126 {
127 return;
128 }
129 std::optional<bool> serviceEnabled;
130 std::optional<uint32_t> retryAttemps;
131 std::optional<uint32_t> retryInterval;
132 if (!json_util::readJsonPatch( //
133 req, asyncResp->res, //
134 "DeliveryRetryAttempts", retryAttemps, //
135 "DeliveryRetryIntervalSeconds", retryInterval, //
136 "ServiceEnabled", serviceEnabled //
137 ))
138 {
139 return;
140 }
141
142 persistent_data::EventServiceConfig eventServiceConfig =
143 persistent_data::EventServiceStore::getInstance()
144 .getEventServiceConfig();
145
146 if (serviceEnabled)
147 {
148 eventServiceConfig.enabled = *serviceEnabled;
149 }
150
151 if (retryAttemps)
152 {
153 // Supported range [1-3]
154 if ((*retryAttemps < 1) || (*retryAttemps > 3))
155 {
156 messages::queryParameterOutOfRange(
157 asyncResp->res, std::to_string(*retryAttemps),
158 "DeliveryRetryAttempts", "[1-3]");
159 }
160 else
161 {
162 eventServiceConfig.retryAttempts = *retryAttemps;
163 }
164 }
165
166 if (retryInterval)
167 {
168 // Supported range [5 - 180]
169 if ((*retryInterval < 5) || (*retryInterval > 180))
170 {
171 messages::queryParameterOutOfRange(
172 asyncResp->res, std::to_string(*retryInterval),
173 "DeliveryRetryIntervalSeconds", "[5-180]");
174 }
175 else
176 {
177 eventServiceConfig.retryTimeoutInterval =
178 *retryInterval;
179 }
180 }
181
182 EventServiceManager::getInstance().setEventServiceConfig(
183 eventServiceConfig);
184 });
185 }
186
requestRoutesSubmitTestEvent(App & app)187 inline void requestRoutesSubmitTestEvent(App& app)
188 {
189 BMCWEB_ROUTE(
190 app, "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent/")
191 .privileges(redfish::privileges::postEventService)
192 .methods(boost::beast::http::verb::post)(
193 [&app](const crow::Request& req,
194 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
195 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
196 {
197 return;
198 }
199
200 // From the Redfish spec on EventId
201 // A service can ignore this value and replace it with its own.
202 // note that this parameter is intentionally ignored
203
204 std::optional<std::string> eventId;
205 TestEvent testEvent;
206 // clang-format off
207 if (!json_util::readJsonAction(
208 req, asyncResp->res,
209 "EventGroupId", testEvent.eventGroupId,
210 "EventId", eventId,
211 "EventTimestamp", testEvent.eventTimestamp,
212 "Message", testEvent.message,
213 "MessageArgs", testEvent.messageArgs,
214 "MessageId", testEvent.messageId,
215 "OriginOfCondition", testEvent.originOfCondition,
216 "Resolution", testEvent.resolution,
217 "Severity", testEvent.severity))
218 {
219 return;
220 }
221 // clang-format on
222
223 if (!EventServiceManager::getInstance().sendTestEventLog(
224 testEvent))
225 {
226 messages::serviceDisabled(asyncResp->res,
227 "/redfish/v1/EventService/");
228 return;
229 }
230 asyncResp->res.result(boost::beast::http::status::no_content);
231 });
232 }
233
doSubscriptionCollection(const boost::system::error_code & ec,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const dbus::utility::ManagedObjectType & resp)234 inline void doSubscriptionCollection(
235 const boost::system::error_code& ec,
236 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
237 const dbus::utility::ManagedObjectType& resp)
238 {
239 if (ec)
240 {
241 if (ec.value() == EBADR || ec.value() == EHOSTUNREACH)
242 {
243 // This is an optional process so just return if it isn't there
244 return;
245 }
246
247 BMCWEB_LOG_ERROR("D-Bus response error on GetManagedObjects {}", ec);
248 messages::internalError(asyncResp->res);
249 return;
250 }
251 nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
252 for (const auto& objpath : resp)
253 {
254 sdbusplus::message::object_path path(objpath.first);
255 const std::string snmpId = path.filename();
256 if (snmpId.empty())
257 {
258 BMCWEB_LOG_ERROR("The SNMP client ID is wrong");
259 messages::internalError(asyncResp->res);
260 return;
261 }
262
263 getSnmpSubscriptionList(asyncResp, snmpId, memberArray);
264 }
265 }
266
requestRoutesEventDestinationCollection(App & app)267 inline void requestRoutesEventDestinationCollection(App& app)
268 {
269 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
270 .privileges(redfish::privileges::getEventDestinationCollection)
271 .methods(boost::beast::http::verb::get)(
272 [&app](const crow::Request& req,
273 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
274 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
275 {
276 return;
277 }
278 asyncResp->res.jsonValue["@odata.type"] =
279 "#EventDestinationCollection.EventDestinationCollection";
280 asyncResp->res.jsonValue["@odata.id"] =
281 "/redfish/v1/EventService/Subscriptions";
282 asyncResp->res.jsonValue["Name"] =
283 "Event Destination Collections";
284
285 nlohmann::json& memberArray =
286 asyncResp->res.jsonValue["Members"];
287
288 std::vector<std::string> subscripIds =
289 EventServiceManager::getInstance().getAllIDs();
290 memberArray = nlohmann::json::array();
291 asyncResp->res.jsonValue["Members@odata.count"] =
292 subscripIds.size();
293
294 for (const std::string& id : subscripIds)
295 {
296 nlohmann::json::object_t member;
297 member["@odata.id"] = boost::urls::format(
298 "/redfish/v1/EventService/Subscriptions/{}" + id);
299 memberArray.emplace_back(std::move(member));
300 }
301 crow::connections::systemBus->async_method_call(
302 [asyncResp](const boost::system::error_code& ec,
303 const dbus::utility::ManagedObjectType& resp) {
304 doSubscriptionCollection(ec, asyncResp, resp);
305 },
306 "xyz.openbmc_project.Network.SNMP",
307 "/xyz/openbmc_project/network/snmp/manager",
308 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
309 });
310
311 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
312 .privileges(redfish::privileges::postEventDestinationCollection)
313 .methods(
314 boost::beast::http::verb::
315 post)([&app](
316 const crow::Request& req,
317 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
318 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
319 {
320 return;
321 }
322 if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
323 maxNoOfSubscriptions)
324 {
325 messages::eventSubscriptionLimitExceeded(asyncResp->res);
326 return;
327 }
328 std::string destUrl;
329 std::string protocol;
330 std::optional<bool> verifyCertificate;
331 std::optional<std::string> context;
332 std::optional<std::string> subscriptionType;
333 std::optional<std::string> eventFormatType2;
334 std::optional<std::string> retryPolicy;
335 std::optional<bool> sendHeartbeat;
336 std::optional<uint64_t> hbIntervalMinutes;
337 std::optional<std::vector<std::string>> msgIds;
338 std::optional<std::vector<std::string>> regPrefixes;
339 std::optional<std::vector<std::string>> originResources;
340 std::optional<std::vector<std::string>> resTypes;
341 std::optional<std::vector<nlohmann::json::object_t>> headers;
342 std::optional<std::vector<nlohmann::json::object_t>> mrdJsonArray;
343
344 if (!json_util::readJsonPatch( //
345 req, asyncResp->res, //
346 "Context", context, //
347 "DeliveryRetryPolicy", retryPolicy, //
348 "Destination", destUrl, //
349 "EventFormatType", eventFormatType2, //
350 "HeartbeatIntervalMinutes", hbIntervalMinutes, //
351 "HttpHeaders", headers, //
352 "MessageIds", msgIds, //
353 "MetricReportDefinitions", mrdJsonArray, //
354 "OriginResources", originResources, //
355 "Protocol", protocol, //
356 "RegistryPrefixes", regPrefixes, //
357 "ResourceTypes", resTypes, //
358 "SendHeartbeat", sendHeartbeat, //
359 "SubscriptionType", subscriptionType, //
360 "VerifyCertificate", verifyCertificate //
361 ))
362 {
363 return;
364 }
365 // clang-format on
366
367 // https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
368 static constexpr const uint16_t maxDestinationSize = 2000;
369 if (destUrl.size() > maxDestinationSize)
370 {
371 messages::stringValueTooLong(asyncResp->res, "Destination",
372 maxDestinationSize);
373 return;
374 }
375
376 if (regPrefixes && msgIds)
377 {
378 if (!regPrefixes->empty() && !msgIds->empty())
379 {
380 messages::propertyValueConflict(
381 asyncResp->res, "MessageIds", "RegistryPrefixes");
382 return;
383 }
384 }
385
386 boost::system::result<boost::urls::url> url =
387 boost::urls::parse_absolute_uri(destUrl);
388 if (!url)
389 {
390 BMCWEB_LOG_WARNING(
391 "Failed to validate and split destination url");
392 messages::propertyValueFormatError(asyncResp->res, destUrl,
393 "Destination");
394 return;
395 }
396 url->normalize();
397
398 // port_number returns zero if it is not a valid representable port
399 if (url->has_port() && url->port_number() == 0)
400 {
401 BMCWEB_LOG_WARNING("{} is an invalid port in destination url",
402 url->port());
403 messages::propertyValueFormatError(asyncResp->res, destUrl,
404 "Destination");
405 return;
406 }
407
408 crow::utility::setProtocolDefaults(*url, protocol);
409 crow::utility::setPortDefaults(*url);
410
411 if (url->path().empty())
412 {
413 url->set_path("/");
414 }
415
416 if (url->has_userinfo())
417 {
418 messages::propertyValueFormatError(asyncResp->res, destUrl,
419 "Destination");
420 return;
421 }
422
423 if (protocol == "SNMPv2c")
424 {
425 if (context)
426 {
427 messages::propertyValueConflict(asyncResp->res, "Context",
428 "Protocol");
429 return;
430 }
431 if (eventFormatType2)
432 {
433 messages::propertyValueConflict(
434 asyncResp->res, "EventFormatType", "Protocol");
435 return;
436 }
437 if (retryPolicy)
438 {
439 messages::propertyValueConflict(asyncResp->res,
440 "RetryPolicy", "Protocol");
441 return;
442 }
443 if (sendHeartbeat)
444 {
445 messages::propertyValueConflict(
446 asyncResp->res, "SendHeartbeat", "Protocol");
447 return;
448 }
449 if (hbIntervalMinutes)
450 {
451 messages::propertyValueConflict(
452 asyncResp->res, "HeartbeatIntervalMinutes", "Protocol");
453 return;
454 }
455 if (msgIds)
456 {
457 messages::propertyValueConflict(asyncResp->res,
458 "MessageIds", "Protocol");
459 return;
460 }
461 if (regPrefixes)
462 {
463 messages::propertyValueConflict(
464 asyncResp->res, "RegistryPrefixes", "Protocol");
465 return;
466 }
467 if (resTypes)
468 {
469 messages::propertyValueConflict(
470 asyncResp->res, "ResourceTypes", "Protocol");
471 return;
472 }
473 if (headers)
474 {
475 messages::propertyValueConflict(asyncResp->res,
476 "HttpHeaders", "Protocol");
477 return;
478 }
479 if (mrdJsonArray)
480 {
481 messages::propertyValueConflict(
482 asyncResp->res, "MetricReportDefinitions", "Protocol");
483 return;
484 }
485 if (url->scheme() != "snmp")
486 {
487 messages::propertyValueConflict(asyncResp->res,
488 "Destination", "Protocol");
489 return;
490 }
491
492 addSnmpTrapClient(asyncResp, url->host_address(),
493 url->port_number());
494 return;
495 }
496
497 std::shared_ptr<Subscription> subValue =
498 std::make_shared<Subscription>(
499 std::make_shared<persistent_data::UserSubscription>(), *url,
500 getIoContext());
501
502 if (subscriptionType)
503 {
504 if (*subscriptionType != "RedfishEvent")
505 {
506 messages::propertyValueNotInList(
507 asyncResp->res, *subscriptionType, "SubscriptionType");
508 return;
509 }
510 subValue->userSub->subscriptionType = *subscriptionType;
511 }
512 else
513 {
514 // Default
515 subValue->userSub->subscriptionType = "RedfishEvent";
516 }
517
518 if (protocol != "Redfish")
519 {
520 messages::propertyValueNotInList(asyncResp->res, protocol,
521 "Protocol");
522 return;
523 }
524 subValue->userSub->protocol = protocol;
525
526 if (verifyCertificate)
527 {
528 subValue->userSub->verifyCertificate = *verifyCertificate;
529 }
530
531 if (eventFormatType2)
532 {
533 if (std::ranges::find(supportedEvtFormatTypes,
534 *eventFormatType2) ==
535 supportedEvtFormatTypes.end())
536 {
537 messages::propertyValueNotInList(
538 asyncResp->res, *eventFormatType2, "EventFormatType");
539 return;
540 }
541 subValue->userSub->eventFormatType = *eventFormatType2;
542 }
543 else
544 {
545 // If not specified, use default "Event"
546 subValue->userSub->eventFormatType = "Event";
547 }
548
549 if (context)
550 {
551 // This value is selected arbitrarily.
552 constexpr const size_t maxContextSize = 256;
553 if (context->size() > maxContextSize)
554 {
555 messages::stringValueTooLong(asyncResp->res, "Context",
556 maxContextSize);
557 return;
558 }
559 subValue->userSub->customText = *context;
560 }
561
562 if (headers)
563 {
564 size_t cumulativeLen = 0;
565
566 for (const nlohmann::json::object_t& headerChunk : *headers)
567 {
568 for (const auto& item : headerChunk)
569 {
570 const std::string* value =
571 item.second.get_ptr<const std::string*>();
572 if (value == nullptr)
573 {
574 messages::propertyValueFormatError(
575 asyncResp->res, item.second,
576 "HttpHeaders/" + item.first);
577 return;
578 }
579 // Adding a new json value is the size of the key, +
580 // the size of the value + 2 * 2 quotes for each, +
581 // the colon and space between. example:
582 // "key": "value"
583 cumulativeLen += item.first.size() + value->size() + 6;
584 // This value is selected to mirror http_connection.hpp
585 constexpr const uint16_t maxHeaderSizeED = 8096;
586 if (cumulativeLen > maxHeaderSizeED)
587 {
588 messages::arraySizeTooLong(
589 asyncResp->res, "HttpHeaders", maxHeaderSizeED);
590 return;
591 }
592 subValue->userSub->httpHeaders.set(item.first, *value);
593 }
594 }
595 }
596
597 if (regPrefixes)
598 {
599 for (const std::string& it : *regPrefixes)
600 {
601 if (std::ranges::find(supportedRegPrefixes, it) ==
602 supportedRegPrefixes.end())
603 {
604 messages::propertyValueNotInList(asyncResp->res, it,
605 "RegistryPrefixes");
606 return;
607 }
608 }
609 subValue->userSub->registryPrefixes = *regPrefixes;
610 }
611
612 if (originResources)
613 {
614 subValue->userSub->originResources = *originResources;
615 }
616
617 if (resTypes)
618 {
619 for (const std::string& it : *resTypes)
620 {
621 if (std::ranges::find(supportedResourceTypes, it) ==
622 supportedResourceTypes.end())
623 {
624 messages::propertyValueNotInList(asyncResp->res, it,
625 "ResourceTypes");
626 return;
627 }
628 }
629 subValue->userSub->resourceTypes = *resTypes;
630 }
631
632 if (msgIds)
633 {
634 std::vector<std::string> registryPrefix;
635
636 // If no registry prefixes are mentioned, consider all
637 // supported prefixes
638 if (subValue->userSub->registryPrefixes.empty())
639 {
640 registryPrefix.assign(supportedRegPrefixes.begin(),
641 supportedRegPrefixes.end());
642 }
643 else
644 {
645 registryPrefix = subValue->userSub->registryPrefixes;
646 }
647
648 for (const std::string& id : *msgIds)
649 {
650 bool validId = false;
651
652 // Check for Message ID in each of the selected Registry
653 for (const std::string& it : registryPrefix)
654 {
655 const std::span<const redfish::registries::MessageEntry>
656 registry =
657 redfish::registries::getRegistryFromPrefix(it);
658
659 if (std::ranges::any_of(
660 registry,
661 [&id](const redfish::registries::MessageEntry&
662 messageEntry) {
663 return id == messageEntry.first;
664 }))
665 {
666 validId = true;
667 break;
668 }
669 }
670
671 if (!validId)
672 {
673 messages::propertyValueNotInList(asyncResp->res, id,
674 "MessageIds");
675 return;
676 }
677 }
678
679 subValue->userSub->registryMsgIds = *msgIds;
680 }
681
682 if (retryPolicy)
683 {
684 if (std::ranges::find(supportedRetryPolicies, *retryPolicy) ==
685 supportedRetryPolicies.end())
686 {
687 messages::propertyValueNotInList(
688 asyncResp->res, *retryPolicy, "DeliveryRetryPolicy");
689 return;
690 }
691 subValue->userSub->retryPolicy = *retryPolicy;
692 }
693 else
694 {
695 // Default "TerminateAfterRetries"
696 subValue->userSub->retryPolicy = "TerminateAfterRetries";
697 }
698 if (sendHeartbeat)
699 {
700 subValue->userSub->sendHeartbeat = *sendHeartbeat;
701 }
702 if (hbIntervalMinutes)
703 {
704 if (*hbIntervalMinutes < 1 || *hbIntervalMinutes > 65535)
705 {
706 messages::propertyValueOutOfRange(
707 asyncResp->res, *hbIntervalMinutes,
708 "HeartbeatIntervalMinutes");
709 return;
710 }
711 subValue->userSub->hbIntervalMinutes = *hbIntervalMinutes;
712 }
713
714 if (mrdJsonArray)
715 {
716 for (nlohmann::json::object_t& mrdObj : *mrdJsonArray)
717 {
718 std::string mrdUri;
719
720 if (!json_util::readJsonObject(mrdObj, asyncResp->res,
721 "@odata.id", mrdUri))
722
723 {
724 return;
725 }
726 subValue->userSub->metricReportDefinitions.emplace_back(
727 mrdUri);
728 }
729 }
730
731 std::string id =
732 EventServiceManager::getInstance().addPushSubscription(
733 subValue);
734 if (id.empty())
735 {
736 messages::internalError(asyncResp->res);
737 return;
738 }
739
740 messages::created(asyncResp->res);
741 asyncResp->res.addHeader(
742 "Location", "/redfish/v1/EventService/Subscriptions/" + id);
743
744 // schedule a heartbeat
745 if (subValue->userSub->sendHeartbeat)
746 {
747 subValue->scheduleNextHeartbeatEvent();
748 }
749 });
750 }
751
requestRoutesEventDestination(App & app)752 inline void requestRoutesEventDestination(App& app)
753 {
754 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
755 .privileges(redfish::privileges::getEventDestination)
756 .methods(boost::beast::http::verb::get)(
757 [&app](const crow::Request& req,
758 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
759 const std::string& param) {
760 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
761 {
762 return;
763 }
764
765 if (param.starts_with("snmp"))
766 {
767 getSnmpTrapClient(asyncResp, param);
768 return;
769 }
770
771 std::shared_ptr<Subscription> subValue =
772 EventServiceManager::getInstance().getSubscription(param);
773 if (subValue == nullptr)
774 {
775 asyncResp->res.result(
776 boost::beast::http::status::not_found);
777 return;
778 }
779 const std::string& id = param;
780
781 const persistent_data::UserSubscription& userSub =
782 *subValue->userSub;
783
784 nlohmann::json& jVal = asyncResp->res.jsonValue;
785 jVal["@odata.type"] =
786 "#EventDestination.v1_14_1.EventDestination";
787 jVal["Protocol"] =
788 event_destination::EventDestinationProtocol::Redfish;
789 jVal["@odata.id"] = boost::urls::format(
790 "/redfish/v1/EventService/Subscriptions/{}", id);
791 jVal["Id"] = id;
792 jVal["Name"] = "Event Destination " + id;
793 jVal["Destination"] = userSub.destinationUrl;
794 jVal["Context"] = userSub.customText;
795 jVal["SubscriptionType"] = userSub.subscriptionType;
796 jVal["HttpHeaders"] = nlohmann::json::array();
797 jVal["EventFormatType"] = userSub.eventFormatType;
798 jVal["RegistryPrefixes"] = userSub.registryPrefixes;
799 jVal["ResourceTypes"] = userSub.resourceTypes;
800
801 jVal["MessageIds"] = userSub.registryMsgIds;
802 jVal["DeliveryRetryPolicy"] = userSub.retryPolicy;
803 jVal["SendHeartbeat"] = userSub.sendHeartbeat;
804 jVal["HeartbeatIntervalMinutes"] = userSub.hbIntervalMinutes;
805 jVal["VerifyCertificate"] = userSub.verifyCertificate;
806
807 nlohmann::json::array_t mrdJsonArray;
808 for (const auto& mdrUri : userSub.metricReportDefinitions)
809 {
810 nlohmann::json::object_t mdr;
811 mdr["@odata.id"] = mdrUri;
812 mrdJsonArray.emplace_back(std::move(mdr));
813 }
814 jVal["MetricReportDefinitions"] = mrdJsonArray;
815 });
816 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
817 // The below privilege is wrong, it should be ConfigureManager OR
818 // ConfigureSelf
819 // https://github.com/openbmc/bmcweb/issues/220
820 //.privileges(redfish::privileges::patchEventDestination)
821 .privileges({{"ConfigureManager"}})
822 .methods(boost::beast::http::verb::patch)(
823 [&app](const crow::Request& req,
824 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
825 const std::string& param) {
826 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
827 {
828 return;
829 }
830 std::shared_ptr<Subscription> subValue =
831 EventServiceManager::getInstance().getSubscription(param);
832 if (subValue == nullptr)
833 {
834 asyncResp->res.result(
835 boost::beast::http::status::not_found);
836 return;
837 }
838
839 std::optional<std::string> context;
840 std::optional<std::string> retryPolicy;
841 std::optional<bool> sendHeartbeat;
842 std::optional<uint64_t> hbIntervalMinutes;
843 std::optional<bool> verifyCertificate;
844 std::optional<std::vector<nlohmann::json::object_t>> headers;
845
846 if (!json_util::readJsonPatch( //
847 req, asyncResp->res, //
848 "Context", context, //
849 "DeliveryRetryPolicy", retryPolicy, //
850 "HeartbeatIntervalMinutes", hbIntervalMinutes, //
851 "HttpHeaders", headers, //
852 "SendHeartbeat", sendHeartbeat, //
853 "VerifyCertificate", verifyCertificate //
854 ))
855 {
856 return;
857 }
858
859 if (context)
860 {
861 subValue->userSub->customText = *context;
862 }
863
864 if (headers)
865 {
866 boost::beast::http::fields fields;
867 for (const nlohmann::json::object_t& headerChunk : *headers)
868 {
869 for (const auto& it : headerChunk)
870 {
871 const std::string* value =
872 it.second.get_ptr<const std::string*>();
873 if (value == nullptr)
874 {
875 messages::propertyValueFormatError(
876 asyncResp->res, it.second,
877 "HttpHeaders/" + it.first);
878 return;
879 }
880 fields.set(it.first, *value);
881 }
882 }
883 subValue->userSub->httpHeaders = std::move(fields);
884 }
885
886 if (retryPolicy)
887 {
888 if (std::ranges::find(supportedRetryPolicies,
889 *retryPolicy) ==
890 supportedRetryPolicies.end())
891 {
892 messages::propertyValueNotInList(asyncResp->res,
893 *retryPolicy,
894 "DeliveryRetryPolicy");
895 return;
896 }
897 subValue->userSub->retryPolicy = *retryPolicy;
898 }
899
900 if (sendHeartbeat)
901 {
902 subValue->userSub->sendHeartbeat = *sendHeartbeat;
903 }
904 if (hbIntervalMinutes)
905 {
906 if (*hbIntervalMinutes < 1 || *hbIntervalMinutes > 65535)
907 {
908 messages::propertyValueOutOfRange(
909 asyncResp->res, *hbIntervalMinutes,
910 "HeartbeatIntervalMinutes");
911 return;
912 }
913 subValue->userSub->hbIntervalMinutes = *hbIntervalMinutes;
914 }
915
916 if (hbIntervalMinutes || sendHeartbeat)
917 {
918 // if Heartbeat interval or send heart were changed, cancel
919 // the heartbeat timer if running and start a new heartbeat
920 // if needed
921 subValue->heartbeatParametersChanged();
922 }
923
924 if (verifyCertificate)
925 {
926 subValue->userSub->verifyCertificate = *verifyCertificate;
927 }
928
929 EventServiceManager::getInstance().updateSubscriptionData();
930 });
931 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
932 // The below privilege is wrong, it should be ConfigureManager OR
933 // ConfigureSelf
934 // https://github.com/openbmc/bmcweb/issues/220
935 //.privileges(redfish::privileges::deleteEventDestination)
936 .privileges({{"ConfigureManager"}})
937 .methods(boost::beast::http::verb::delete_)(
938 [&app](const crow::Request& req,
939 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
940 const std::string& param) {
941 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
942 {
943 return;
944 }
945 EventServiceManager& event = EventServiceManager::getInstance();
946 if (param.starts_with("snmp"))
947 {
948 deleteSnmpTrapClient(asyncResp, param);
949 event.deleteSubscription(param);
950 return;
951 }
952
953 if (!event.deleteSubscription(param))
954 {
955 messages::resourceNotFound(asyncResp->res,
956 "EventDestination", param);
957 return;
958 }
959 messages::success(asyncResp->res);
960 });
961 }
962
963 } // namespace redfish
964