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