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