xref: /openbmc/bmcweb/redfish-core/lib/event_service.hpp (revision 52cc112d962920b035c870127784bcbd98948fad)
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 "event_service_manager.hpp"
18 
19 namespace redfish
20 {
21 
22 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = {
23     eventFormatType, metricReportFormatType};
24 static constexpr const std::array<const char*, 3> supportedRegPrefixes = {
25     "Base", "OpenBMC", "Task"};
26 static constexpr const std::array<const char*, 3> supportedRetryPolicies = {
27     "TerminateAfterRetries", "SuspendRetries", "RetryForever"};
28 
29 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
30 static constexpr const std::array<const char*, 2> supportedResourceTypes = {
31     "IBMConfigFile", "Task"};
32 #else
33 static constexpr const std::array<const char*, 1> supportedResourceTypes = {
34     "Task"};
35 #endif
36 
37 static constexpr const uint8_t maxNoOfSubscriptions = 20;
38 
39 class EventService : public Node
40 {
41   public:
42     EventService(App& app) : Node(app, "/redfish/v1/EventService/")
43     {
44         entityPrivileges = {
45             {boost::beast::http::verb::get, {{"Login"}}},
46             {boost::beast::http::verb::head, {{"Login"}}},
47             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
48             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
49             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
50             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
51     }
52 
53   private:
54     void doGet(crow::Response& res, const crow::Request& req,
55                const std::vector<std::string>& params) override
56     {
57         auto asyncResp = std::make_shared<AsyncResp>(res);
58         res.jsonValue = {
59             {"@odata.type", "#EventService.v1_5_0.EventService"},
60             {"Id", "EventService"},
61             {"Name", "Event Service"},
62             {"ServerSentEventUri",
63              "/redfish/v1/EventService/Subscriptions/SSE"},
64             {"Subscriptions",
65              {{"@odata.id", "/redfish/v1/EventService/Subscriptions"}}},
66             {"Actions",
67              {{"#EventService.SubmitTestEvent",
68                {{"target", "/redfish/v1/EventService/Actions/"
69                            "EventService.SubmitTestEvent"}}}}},
70             {"@odata.id", "/redfish/v1/EventService"}};
71 
72         const auto& [enabled, retryAttempts, retryTimeoutInterval] =
73             EventServiceManager::getInstance().getEventServiceConfig();
74 
75         asyncResp->res.jsonValue["Status"]["State"] =
76             (enabled ? "Enabled" : "Disabled");
77         asyncResp->res.jsonValue["ServiceEnabled"] = enabled;
78         asyncResp->res.jsonValue["DeliveryRetryAttempts"] = retryAttempts;
79         asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
80             retryTimeoutInterval;
81         asyncResp->res.jsonValue["EventFormatTypes"] = supportedEvtFormatTypes;
82         asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
83         asyncResp->res.jsonValue["ResourceTypes"] = supportedResourceTypes;
84 
85         nlohmann::json supportedSSEFilters = {
86             {"EventFormatType", true},        {"MessageId", true},
87             {"MetricReportDefinition", true}, {"RegistryPrefix", true},
88             {"OriginResource", false},        {"ResourceType", false}};
89 
90         asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] =
91             supportedSSEFilters;
92     }
93 
94     void doPatch(crow::Response& res, const crow::Request& req,
95                  const std::vector<std::string>& params) override
96     {
97         auto asyncResp = std::make_shared<AsyncResp>(res);
98 
99         std::optional<bool> serviceEnabled;
100         std::optional<uint32_t> retryAttemps;
101         std::optional<uint32_t> retryInterval;
102 
103         if (!json_util::readJson(req, res, "ServiceEnabled", serviceEnabled,
104                                  "DeliveryRetryAttempts", retryAttemps,
105                                  "DeliveryRetryIntervalSeconds", retryInterval))
106         {
107             return;
108         }
109 
110         auto [enabled, retryCount, retryTimeoutInterval] =
111             EventServiceManager::getInstance().getEventServiceConfig();
112 
113         if (serviceEnabled)
114         {
115             enabled = *serviceEnabled;
116         }
117 
118         if (retryAttemps)
119         {
120             // Supported range [1-3]
121             if ((*retryAttemps < 1) || (*retryAttemps > 3))
122             {
123                 messages::queryParameterOutOfRange(
124                     asyncResp->res, std::to_string(*retryAttemps),
125                     "DeliveryRetryAttempts", "[1-3]");
126             }
127             else
128             {
129                 retryCount = *retryAttemps;
130             }
131         }
132 
133         if (retryInterval)
134         {
135             // Supported range [30 - 180]
136             if ((*retryInterval < 30) || (*retryInterval > 180))
137             {
138                 messages::queryParameterOutOfRange(
139                     asyncResp->res, std::to_string(*retryInterval),
140                     "DeliveryRetryIntervalSeconds", "[30-180]");
141             }
142             else
143             {
144                 retryTimeoutInterval = *retryInterval;
145             }
146         }
147 
148         EventServiceManager::getInstance().setEventServiceConfig(
149             std::make_tuple(enabled, retryCount, retryTimeoutInterval));
150     }
151 };
152 
153 class SubmitTestEvent : public Node
154 {
155   public:
156     SubmitTestEvent(App& app) :
157         Node(app,
158              "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent/")
159     {
160         entityPrivileges = {
161             {boost::beast::http::verb::get, {{"Login"}}},
162             {boost::beast::http::verb::head, {{"Login"}}},
163             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
164             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
165             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
166             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
167     }
168 
169   private:
170     void doPost(crow::Response& res, const crow::Request& req,
171                 const std::vector<std::string>& params) override
172     {
173         EventServiceManager::getInstance().sendTestEventLog();
174         res.result(boost::beast::http::status::no_content);
175         res.end();
176     }
177 };
178 
179 class EventDestinationCollection : public Node
180 {
181   public:
182     EventDestinationCollection(App& app) :
183         Node(app, "/redfish/v1/EventService/Subscriptions/")
184     {
185         entityPrivileges = {
186             {boost::beast::http::verb::get, {{"Login"}}},
187             {boost::beast::http::verb::head, {{"Login"}}},
188             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
189             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
190             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
191             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
192     }
193 
194   private:
195     void doGet(crow::Response& res, const crow::Request& req,
196                const std::vector<std::string>& params) override
197     {
198         auto asyncResp = std::make_shared<AsyncResp>(res);
199 
200         res.jsonValue = {
201             {"@odata.type",
202              "#EventDestinationCollection.EventDestinationCollection"},
203             {"@odata.id", "/redfish/v1/EventService/Subscriptions"},
204             {"Name", "Event Destination Collections"}};
205 
206         nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
207 
208         std::vector<std::string> subscripIds =
209             EventServiceManager::getInstance().getAllIDs();
210         memberArray = nlohmann::json::array();
211         asyncResp->res.jsonValue["Members@odata.count"] = subscripIds.size();
212 
213         for (const std::string& id : subscripIds)
214         {
215             memberArray.push_back(
216                 {{"@odata.id",
217                   "/redfish/v1/EventService/Subscriptions/" + id}});
218         }
219     }
220 
221     void doPost(crow::Response& res, const crow::Request& req,
222                 const std::vector<std::string>& params) override
223     {
224         auto asyncResp = std::make_shared<AsyncResp>(res);
225 
226         if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
227             maxNoOfSubscriptions)
228         {
229             messages::eventSubscriptionLimitExceeded(asyncResp->res);
230             return;
231         }
232         std::string destUrl;
233         std::string protocol;
234         std::optional<std::string> context;
235         std::optional<std::string> subscriptionType;
236         std::optional<std::string> eventFormatType;
237         std::optional<std::string> retryPolicy;
238         std::optional<std::vector<std::string>> msgIds;
239         std::optional<std::vector<std::string>> regPrefixes;
240         std::optional<std::vector<std::string>> resTypes;
241         std::optional<std::vector<nlohmann::json>> headers;
242         std::optional<std::vector<nlohmann::json>> mrdJsonArray;
243 
244         if (!json_util::readJson(
245                 req, res, "Destination", destUrl, "Context", context,
246                 "Protocol", protocol, "SubscriptionType", subscriptionType,
247                 "EventFormatType", eventFormatType, "HttpHeaders", headers,
248                 "RegistryPrefixes", regPrefixes, "MessageIds", msgIds,
249                 "DeliveryRetryPolicy", retryPolicy, "MetricReportDefinitions",
250                 mrdJsonArray, "ResourceTypes", resTypes))
251         {
252             return;
253         }
254 
255         // Validate the URL using regex expression
256         // Format: <protocol>://<host>:<port>/<uri>
257         // protocol: http/https
258         // host: Exclude ' ', ':', '#', '?'
259         // port: Empty or numeric value with ':' separator.
260         // uri: Start with '/' and Exclude '#', ' '
261         //      Can include query params(ex: '/event?test=1')
262         // TODO: Need to validate hostname extensively(as per rfc)
263         const std::regex urlRegex(
264             "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/"
265             "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)");
266         std::cmatch match;
267         if (!std::regex_match(destUrl.c_str(), match, urlRegex))
268         {
269             messages::propertyValueFormatError(asyncResp->res, destUrl,
270                                                "Destination");
271             return;
272         }
273 
274         std::string uriProto = std::string(match[1].first, match[1].second);
275         if (uriProto == "http")
276         {
277 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING
278             messages::propertyValueFormatError(asyncResp->res, destUrl,
279                                                "Destination");
280             return;
281 #endif
282         }
283 
284         std::string host = std::string(match[2].first, match[2].second);
285         std::string port = std::string(match[3].first, match[3].second);
286         std::string path = std::string(match[4].first, match[4].second);
287         if (port.empty())
288         {
289             if (uriProto == "http")
290             {
291                 port = "80";
292             }
293             else
294             {
295                 port = "443";
296             }
297         }
298         if (path.empty())
299         {
300             path = "/";
301         }
302 
303         std::shared_ptr<Subscription> subValue =
304             std::make_shared<Subscription>(host, port, path, uriProto);
305 
306         subValue->destinationUrl = destUrl;
307 
308         if (subscriptionType)
309         {
310             if (*subscriptionType != "RedfishEvent")
311             {
312                 messages::propertyValueNotInList(
313                     asyncResp->res, *subscriptionType, "SubscriptionType");
314                 return;
315             }
316             subValue->subscriptionType = *subscriptionType;
317         }
318         else
319         {
320             subValue->subscriptionType = "RedfishEvent"; // Default
321         }
322 
323         if (protocol != "Redfish")
324         {
325             messages::propertyValueNotInList(asyncResp->res, protocol,
326                                              "Protocol");
327             return;
328         }
329         subValue->protocol = protocol;
330 
331         if (eventFormatType)
332         {
333             if (std::find(supportedEvtFormatTypes.begin(),
334                           supportedEvtFormatTypes.end(),
335                           *eventFormatType) == supportedEvtFormatTypes.end())
336             {
337                 messages::propertyValueNotInList(
338                     asyncResp->res, *eventFormatType, "EventFormatType");
339                 return;
340             }
341             subValue->eventFormatType = *eventFormatType;
342         }
343         else
344         {
345             // If not specified, use default "Event"
346             subValue->eventFormatType.assign({"Event"});
347         }
348 
349         if (context)
350         {
351             subValue->customText = *context;
352         }
353 
354         if (headers)
355         {
356             subValue->httpHeaders = *headers;
357         }
358 
359         if (regPrefixes)
360         {
361             for (const std::string& it : *regPrefixes)
362             {
363                 if (std::find(supportedRegPrefixes.begin(),
364                               supportedRegPrefixes.end(),
365                               it) == supportedRegPrefixes.end())
366                 {
367                     messages::propertyValueNotInList(asyncResp->res, it,
368                                                      "RegistryPrefixes");
369                     return;
370                 }
371             }
372             subValue->registryPrefixes = *regPrefixes;
373         }
374 
375         if (resTypes)
376         {
377             for (const std::string& it : *resTypes)
378             {
379                 if (std::find(supportedResourceTypes.begin(),
380                               supportedResourceTypes.end(),
381                               it) == supportedResourceTypes.end())
382                 {
383                     messages::propertyValueNotInList(asyncResp->res, it,
384                                                      "ResourceTypes");
385                     return;
386                 }
387             }
388             subValue->resourceTypes = *resTypes;
389         }
390 
391         if (msgIds)
392         {
393             // Do we need to loop-up MessageRegistry and validate
394             // data for authenticity??? Not mandate, i believe.
395             subValue->registryMsgIds = *msgIds;
396         }
397 
398         if (retryPolicy)
399         {
400             if (std::find(supportedRetryPolicies.begin(),
401                           supportedRetryPolicies.end(),
402                           *retryPolicy) == supportedRetryPolicies.end())
403             {
404                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
405                                                  "DeliveryRetryPolicy");
406                 return;
407             }
408             subValue->retryPolicy = *retryPolicy;
409         }
410         else
411         {
412             // Default "TerminateAfterRetries"
413             subValue->retryPolicy = "TerminateAfterRetries";
414         }
415 
416         if (mrdJsonArray)
417         {
418             for (nlohmann::json& mrdObj : *mrdJsonArray)
419             {
420                 std::string mrdUri;
421                 if (json_util::getValueFromJsonObject(mrdObj, "@odata.id",
422                                                       mrdUri))
423                 {
424                     subValue->metricReportDefinitions.emplace_back(mrdUri);
425                 }
426                 else
427                 {
428                     messages::propertyValueFormatError(
429                         asyncResp->res, mrdObj.dump(),
430                         "MetricReportDefinitions");
431                     return;
432                 }
433             }
434         }
435 
436         std::string id =
437             EventServiceManager::getInstance().addSubscription(subValue);
438         if (id.empty())
439         {
440             messages::internalError(asyncResp->res);
441             return;
442         }
443 
444         messages::created(asyncResp->res);
445         asyncResp->res.addHeader(
446             "Location", "/redfish/v1/EventService/Subscriptions/" + id);
447     }
448 };
449 
450 class EventServiceSSE : public Node
451 {
452   public:
453     EventServiceSSE(App& app) :
454         Node(app, "/redfish/v1/EventService/Subscriptions/SSE/")
455     {
456         entityPrivileges = {
457             {boost::beast::http::verb::get, {{"ConfigureManager"}}},
458             {boost::beast::http::verb::head, {{"ConfigureManager"}}},
459             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
460             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
461             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
462             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
463     }
464 
465   private:
466     void doGet(crow::Response& res, const crow::Request& req,
467                const std::vector<std::string>& params) override
468     {
469         if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
470             maxNoOfSubscriptions)
471         {
472             messages::eventSubscriptionLimitExceeded(res);
473             res.end();
474             return;
475         }
476 
477         std::shared_ptr<crow::Request::Adaptor> sseConn =
478             std::make_shared<crow::Request::Adaptor>(std::move(req.socket()));
479         std::shared_ptr<Subscription> subValue =
480             std::make_shared<Subscription>(sseConn);
481 
482         // GET on this URI means, Its SSE subscriptionType.
483         subValue->subscriptionType = "SSE";
484 
485         // Default values
486         subValue->protocol = "Redfish";
487         subValue->retryPolicy = "TerminateAfterRetries";
488 
489         boost::urls::url_view::params_type::iterator it =
490             req.urlParams.find("$filter");
491         if (it == req.urlParams.end())
492         {
493             subValue->eventFormatType = "Event";
494         }
495 
496         else
497         {
498             std::string filters = it->value();
499             // Reading from query params.
500             bool status = readSSEQueryParams(
501                 filters, subValue->eventFormatType, subValue->registryMsgIds,
502                 subValue->registryPrefixes, subValue->metricReportDefinitions);
503 
504             if (!status)
505             {
506                 messages::invalidObject(res, filters);
507                 return;
508             }
509 
510             if (!subValue->eventFormatType.empty())
511             {
512                 if (std::find(supportedEvtFormatTypes.begin(),
513                               supportedEvtFormatTypes.end(),
514                               subValue->eventFormatType) ==
515                     supportedEvtFormatTypes.end())
516                 {
517                     messages::propertyValueNotInList(
518                         res, subValue->eventFormatType, "EventFormatType");
519                     return;
520                 }
521             }
522             else
523             {
524                 // If nothing specified, using default "Event"
525                 subValue->eventFormatType.assign({"Event"});
526             }
527 
528             if (!subValue->registryPrefixes.empty())
529             {
530                 for (const std::string& it : subValue->registryPrefixes)
531                 {
532                     if (std::find(supportedRegPrefixes.begin(),
533                                   supportedRegPrefixes.end(),
534                                   it) == supportedRegPrefixes.end())
535                     {
536                         messages::propertyValueNotInList(res, it,
537                                                          "RegistryPrefixes");
538                         return;
539                     }
540                 }
541             }
542         }
543 
544         std::string id =
545             EventServiceManager::getInstance().addSubscription(subValue, false);
546         if (id.empty())
547         {
548             messages::internalError(res);
549             res.end();
550             return;
551         }
552     }
553 };
554 
555 class EventDestination : public Node
556 {
557   public:
558     EventDestination(App& app) :
559         Node(app, "/redfish/v1/EventService/Subscriptions/<str>/",
560              std::string())
561     {
562         entityPrivileges = {
563             {boost::beast::http::verb::get, {{"Login"}}},
564             {boost::beast::http::verb::head, {{"Login"}}},
565             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
566             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
567             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
568             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
569     }
570 
571   private:
572     void doGet(crow::Response& res, const crow::Request& req,
573                const std::vector<std::string>& params) override
574     {
575         auto asyncResp = std::make_shared<AsyncResp>(res);
576         if (params.size() != 1)
577         {
578             messages::internalError(asyncResp->res);
579             return;
580         }
581 
582         std::shared_ptr<Subscription> subValue =
583             EventServiceManager::getInstance().getSubscription(params[0]);
584         if (subValue == nullptr)
585         {
586             res.result(boost::beast::http::status::not_found);
587             res.end();
588             return;
589         }
590         const std::string& id = params[0];
591 
592         res.jsonValue = {
593             {"@odata.type", "#EventDestination.v1_7_0.EventDestination"},
594             {"Protocol", "Redfish"}};
595         asyncResp->res.jsonValue["@odata.id"] =
596             "/redfish/v1/EventService/Subscriptions/" + id;
597         asyncResp->res.jsonValue["Id"] = id;
598         asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
599         asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl;
600         asyncResp->res.jsonValue["Context"] = subValue->customText;
601         asyncResp->res.jsonValue["SubscriptionType"] =
602             subValue->subscriptionType;
603         asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders;
604         asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType;
605         asyncResp->res.jsonValue["RegistryPrefixes"] =
606             subValue->registryPrefixes;
607         asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes;
608 
609         asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds;
610         asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy;
611 
612         std::vector<nlohmann::json> mrdJsonArray;
613         for (const auto& mdrUri : subValue->metricReportDefinitions)
614         {
615             mrdJsonArray.push_back({{"@odata.id", mdrUri}});
616         }
617         asyncResp->res.jsonValue["MetricReportDefinitions"] = mrdJsonArray;
618     }
619 
620     void doPatch(crow::Response& res, const crow::Request& req,
621                  const std::vector<std::string>& params) override
622     {
623         auto asyncResp = std::make_shared<AsyncResp>(res);
624         if (params.size() != 1)
625         {
626             messages::internalError(asyncResp->res);
627             return;
628         }
629 
630         std::shared_ptr<Subscription> subValue =
631             EventServiceManager::getInstance().getSubscription(params[0]);
632         if (subValue == nullptr)
633         {
634             res.result(boost::beast::http::status::not_found);
635             res.end();
636             return;
637         }
638 
639         std::optional<std::string> context;
640         std::optional<std::string> retryPolicy;
641         std::optional<std::vector<nlohmann::json>> headers;
642 
643         if (!json_util::readJson(req, res, "Context", context,
644                                  "DeliveryRetryPolicy", retryPolicy,
645                                  "HttpHeaders", headers))
646         {
647             return;
648         }
649 
650         if (context)
651         {
652             subValue->customText = *context;
653         }
654 
655         if (headers)
656         {
657             subValue->httpHeaders = *headers;
658         }
659 
660         if (retryPolicy)
661         {
662             if (std::find(supportedRetryPolicies.begin(),
663                           supportedRetryPolicies.end(),
664                           *retryPolicy) == supportedRetryPolicies.end())
665             {
666                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
667                                                  "DeliveryRetryPolicy");
668                 return;
669             }
670             subValue->retryPolicy = *retryPolicy;
671             subValue->updateRetryPolicy();
672         }
673 
674         EventServiceManager::getInstance().updateSubscriptionData();
675     }
676 
677     void doDelete(crow::Response& res, const crow::Request& req,
678                   const std::vector<std::string>& params) override
679     {
680         auto asyncResp = std::make_shared<AsyncResp>(res);
681 
682         if (params.size() != 1)
683         {
684             messages::internalError(asyncResp->res);
685             return;
686         }
687 
688         if (!EventServiceManager::getInstance().isSubscriptionExist(params[0]))
689         {
690             res.result(boost::beast::http::status::not_found);
691             res.end();
692             return;
693         }
694         EventServiceManager::getInstance().deleteSubscription(params[0]);
695     }
696 };
697 
698 } // namespace redfish
699