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