xref: /openbmc/bmcweb/features/redfish/lib/event_service.hpp (revision e5aaf047b6b41b0837ef0846cf5356c9a6bcb030)
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 "node.hpp"
18 
19 #include <boost/container/flat_map.hpp>
20 #include <error_messages.hpp>
21 #include <utils/json_utils.hpp>
22 #include <variant>
23 
24 namespace redfish
25 {
26 
27 static constexpr const std::array<const char*, 1> supportedEvtFormatTypes = {
28     "Event"};
29 static constexpr const std::array<const char*, 3> supportedRegPrefixes = {
30     "Base", "OpenBMC", "Task"};
31 static constexpr const std::array<const char*, 3> supportedRetryPolicies = {
32     "TerminateAfterRetries", "SuspendRetries", "RetryForever"};
33 
34 static constexpr const uint8_t maxNoOfSubscriptions = 20;
35 
36 struct EventSrvConfig
37 {
38     bool enabled;
39     uint32_t retryAttempts;
40     uint32_t retryTimeoutInterval;
41 };
42 
43 struct EventSrvSubscription
44 {
45     std::string destinationUrl;
46     std::string protocol;
47     std::string retryPolicy;
48     std::string customText;
49     std::string eventFormatType;
50     std::string subscriptionType;
51     std::vector<std::string> registryMsgIds;
52     std::vector<std::string> registryPrefixes;
53     std::vector<nlohmann::json> httpHeaders; // key-value pair
54 };
55 
56 EventSrvConfig configData;
57 boost::container::flat_map<std::string, EventSrvSubscription> subscriptionsMap;
58 
59 inline void initEventSrvStore()
60 {
61     // TODO: Read the persistent data from store and populate.
62     // Populating with default.
63     configData.enabled = true;
64     configData.retryAttempts = 3;
65     configData.retryTimeoutInterval = 30; // seconds
66 }
67 
68 inline void updateSubscriptionData()
69 {
70     // Persist the config and subscription data.
71     // TODO: subscriptionsMap & configData need to be
72     // written to Persist store.
73     return;
74 }
75 class EventService : public Node
76 {
77   public:
78     EventService(CrowApp& app) : Node(app, "/redfish/v1/EventService/")
79     {
80         initEventSrvStore();
81 
82         entityPrivileges = {
83             {boost::beast::http::verb::get, {{"Login"}}},
84             {boost::beast::http::verb::head, {{"Login"}}},
85             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
86             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
87             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
88             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
89     }
90 
91   private:
92     void doGet(crow::Response& res, const crow::Request& req,
93                const std::vector<std::string>& params) override
94     {
95         auto asyncResp = std::make_shared<AsyncResp>(res);
96         res.jsonValue = {
97             {"@odata.type", "#EventService.v1_5_0.EventService"},
98             {"Id", "EventService"},
99             {"Name", "Event Service"},
100             {"ServerSentEventUri",
101              "/redfish/v1/EventService/Subscriptions/SSE"},
102             {"Subscriptions",
103              {{"@odata.id", "/redfish/v1/EventService/Subscriptions"}}},
104             {"@odata.id", "/redfish/v1/EventService"}};
105 
106         asyncResp->res.jsonValue["Status"]["State"] = "Enabled";
107         asyncResp->res.jsonValue["ServiceEnabled"] = configData.enabled;
108         asyncResp->res.jsonValue["DeliveryRetryAttempts"] =
109             configData.retryAttempts;
110         asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
111             configData.retryTimeoutInterval;
112         asyncResp->res.jsonValue["EventFormatTypes"] = supportedEvtFormatTypes;
113         asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
114     }
115 
116     void doPatch(crow::Response& res, const crow::Request& req,
117                  const std::vector<std::string>& params) override
118     {
119         auto asyncResp = std::make_shared<AsyncResp>(res);
120 
121         std::optional<bool> serviceEnabled;
122         std::optional<uint32_t> retryAttemps;
123         std::optional<uint32_t> retryInterval;
124 
125         if (!json_util::readJson(req, res, "ServiceEnabled", serviceEnabled,
126                                  "DeliveryRetryAttempts", retryAttemps,
127                                  "DeliveryRetryIntervalSeconds", retryInterval))
128         {
129             return;
130         }
131 
132         if (serviceEnabled)
133         {
134             configData.enabled = *serviceEnabled;
135         }
136 
137         if (retryAttemps)
138         {
139             // Supported range [1-3]
140             if ((*retryAttemps < 1) || (*retryAttemps > 3))
141             {
142                 messages::queryParameterOutOfRange(
143                     asyncResp->res, std::to_string(*retryAttemps),
144                     "DeliveryRetryAttempts", "[1-3]");
145             }
146             else
147             {
148                 configData.retryAttempts = *retryAttemps;
149             }
150         }
151 
152         if (retryInterval)
153         {
154             // Supported range [30 - 180]
155             if ((*retryInterval < 30) || (*retryInterval > 180))
156             {
157                 messages::queryParameterOutOfRange(
158                     asyncResp->res, std::to_string(*retryInterval),
159                     "DeliveryRetryIntervalSeconds", "[30-180]");
160             }
161             else
162             {
163                 configData.retryTimeoutInterval = *retryInterval;
164             }
165         }
166 
167         updateSubscriptionData();
168     }
169 };
170 
171 class EventDestinationCollection : public Node
172 {
173   public:
174     EventDestinationCollection(CrowApp& app) :
175         Node(app, "/redfish/v1/EventService/Subscriptions/")
176     {
177         entityPrivileges = {
178             {boost::beast::http::verb::get, {{"Login"}}},
179             {boost::beast::http::verb::head, {{"Login"}}},
180             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
181             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
182             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
183             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
184     }
185 
186   private:
187     void doGet(crow::Response& res, const crow::Request& req,
188                const std::vector<std::string>& params) override
189     {
190         auto asyncResp = std::make_shared<AsyncResp>(res);
191 
192         res.jsonValue = {
193             {"@odata.type",
194              "#EventDestinationCollection.EventDestinationCollection"},
195             {"@odata.id", "/redfish/v1/EventService/Subscriptions"},
196             {"Name", "Event Destination Collections"}};
197 
198         nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
199         memberArray = nlohmann::json::array();
200         asyncResp->res.jsonValue["Members@odata.count"] =
201             subscriptionsMap.size();
202 
203         for (auto& it : subscriptionsMap)
204         {
205             memberArray.push_back(
206                 {{"@odata.id",
207                   "/redfish/v1/EventService/Subscriptions/" + it.first}});
208         }
209     }
210 
211     void doPost(crow::Response& res, const crow::Request& req,
212                 const std::vector<std::string>& params) override
213     {
214         auto asyncResp = std::make_shared<AsyncResp>(res);
215 
216         if (subscriptionsMap.size() >= maxNoOfSubscriptions)
217         {
218             messages::eventSubscriptionLimitExceeded(asyncResp->res);
219             return;
220         }
221         std::string destUrl;
222         std::string protocol;
223         std::optional<std::string> context;
224         std::optional<std::string> subscriptionType;
225         std::optional<std::string> eventFormatType;
226         std::optional<std::string> retryPolicy;
227         std::optional<std::vector<std::string>> msgIds;
228         std::optional<std::vector<std::string>> regPrefixes;
229         std::optional<std::vector<nlohmann::json>> headers;
230 
231         if (!json_util::readJson(
232                 req, res, "Destination", destUrl, "Context", context,
233                 "Protocol", protocol, "SubscriptionType", subscriptionType,
234                 "EventFormatType", eventFormatType, "HttpHeaders", headers,
235                 "RegistryPrefixes", regPrefixes, "MessageIds", msgIds,
236                 "DeliveryRetryPolicy", retryPolicy))
237         {
238             return;
239         }
240 
241         EventSrvSubscription subValue;
242 
243         // Validate the URL using regex expression
244         // Format: <protocol>://<host>:<port>/uri
245         // protocol: http/https, uri: can include params.
246         const std::regex urlRegex("(http|https)://([^/ :]+):?.*");
247         if (!std::regex_match(destUrl, urlRegex))
248         {
249             messages::propertyValueFormatError(asyncResp->res, destUrl,
250                                                "Destination");
251             return;
252         }
253         subValue.destinationUrl = destUrl;
254 
255         if (subscriptionType)
256         {
257             if (*subscriptionType != "RedfishEvent")
258             {
259                 messages::propertyValueNotInList(
260                     asyncResp->res, *subscriptionType, "SubscriptionType");
261                 return;
262             }
263             subValue.subscriptionType = *subscriptionType;
264         }
265         else
266         {
267             subValue.subscriptionType = "RedfishEvent"; // Default
268         }
269 
270         if (protocol != "Redfish")
271         {
272             messages::propertyValueNotInList(asyncResp->res, protocol,
273                                              "Protocol");
274             return;
275         }
276         subValue.protocol = protocol;
277 
278         if (eventFormatType)
279         {
280             if (std::find(supportedEvtFormatTypes.begin(),
281                           supportedEvtFormatTypes.end(),
282                           *eventFormatType) == supportedEvtFormatTypes.end())
283             {
284                 messages::propertyValueNotInList(
285                     asyncResp->res, *eventFormatType, "EventFormatType");
286                 return;
287             }
288             subValue.eventFormatType = *eventFormatType;
289         }
290         else
291         {
292             // If not specified, use default "Event"
293             subValue.eventFormatType.assign({"Event"});
294         }
295 
296         if (context)
297         {
298             subValue.customText = *context;
299         }
300 
301         if (headers)
302         {
303             subValue.httpHeaders = *headers;
304         }
305 
306         if (regPrefixes)
307         {
308             for (const std::string& it : *regPrefixes)
309             {
310                 if (std::find(supportedRegPrefixes.begin(),
311                               supportedRegPrefixes.end(),
312                               it) == supportedRegPrefixes.end())
313                 {
314                     messages::propertyValueNotInList(asyncResp->res, it,
315                                                      "RegistryPrefixes");
316                     return;
317                 }
318             }
319             subValue.registryPrefixes = *regPrefixes;
320         }
321 
322         if (msgIds)
323         {
324             // Do we need to loop-up MessageRegistry and validate
325             // data for authenticity??? Not mandate, i believe.
326             subValue.registryMsgIds = *msgIds;
327         }
328 
329         if (retryPolicy)
330         {
331             if (std::find(supportedRetryPolicies.begin(),
332                           supportedRetryPolicies.end(),
333                           *retryPolicy) == supportedRetryPolicies.end())
334             {
335                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
336                                                  "DeliveryRetryPolicy");
337                 return;
338             }
339             subValue.retryPolicy = *retryPolicy;
340         }
341         else
342         {
343             // Default "TerminateAfterRetries"
344             subValue.retryPolicy = "TerminateAfterRetries";
345         }
346 
347         std::srand(static_cast<uint32_t>(std::time(0)));
348         std::string id;
349 
350         int retry = 3;
351         while (retry)
352         {
353             id = std::to_string(std::rand());
354             auto inserted = subscriptionsMap.insert(std::pair(id, subValue));
355             if (inserted.second)
356             {
357                 break;
358             }
359             retry--;
360         };
361 
362         if (retry <= 0)
363         {
364             messages::internalError(asyncResp->res);
365             return;
366         }
367 
368         updateSubscriptionData();
369 
370         messages::created(asyncResp->res);
371         asyncResp->res.addHeader(
372             "Location", "/redfish/v1/EventService/Subscriptions/" + id);
373     }
374 };
375 
376 class EventDestination : public Node
377 {
378   public:
379     EventDestination(CrowApp& app) :
380         Node(app, "/redfish/v1/EventService/Subscriptions/<str>/",
381              std::string())
382     {
383         entityPrivileges = {
384             {boost::beast::http::verb::get, {{"Login"}}},
385             {boost::beast::http::verb::head, {{"Login"}}},
386             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
387             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
388             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
389             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
390     }
391 
392   private:
393     void doGet(crow::Response& res, const crow::Request& req,
394                const std::vector<std::string>& params) override
395     {
396         auto asyncResp = std::make_shared<AsyncResp>(res);
397         if (params.size() != 1)
398         {
399             messages::internalError(asyncResp->res);
400             return;
401         }
402 
403         const std::string& id = params[0];
404         auto obj = subscriptionsMap.find(id);
405         if (obj == subscriptionsMap.end())
406         {
407             res.result(boost::beast::http::status::not_found);
408             res.end();
409             return;
410         }
411 
412         EventSrvSubscription& subValue = obj->second;
413 
414         res.jsonValue = {
415             {"@odata.type", "#EventDestination.v1_7_0.EventDestination"},
416             {"Protocol", "Redfish"}};
417         asyncResp->res.jsonValue["@odata.id"] =
418             "/redfish/v1/EventService/Subscriptions/" + id;
419         asyncResp->res.jsonValue["Id"] = id;
420         asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
421         asyncResp->res.jsonValue["Destination"] = subValue.destinationUrl;
422         asyncResp->res.jsonValue["Context"] = subValue.customText;
423         asyncResp->res.jsonValue["SubscriptionType"] =
424             subValue.subscriptionType;
425         asyncResp->res.jsonValue["HttpHeaders"] = subValue.httpHeaders;
426         asyncResp->res.jsonValue["EventFormatType"] = subValue.eventFormatType;
427         asyncResp->res.jsonValue["RegistryPrefixes"] =
428             subValue.registryPrefixes;
429         asyncResp->res.jsonValue["MessageIds"] = subValue.registryMsgIds;
430         asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue.retryPolicy;
431     }
432 
433     void doPatch(crow::Response& res, const crow::Request& req,
434                  const std::vector<std::string>& params) override
435     {
436         auto asyncResp = std::make_shared<AsyncResp>(res);
437         if (params.size() != 1)
438         {
439             messages::internalError(asyncResp->res);
440             return;
441         }
442 
443         const std::string& id = params[0];
444         auto obj = subscriptionsMap.find(id);
445         if (obj == subscriptionsMap.end())
446         {
447             res.result(boost::beast::http::status::not_found);
448             res.end();
449             return;
450         }
451 
452         std::optional<std::string> context;
453         std::optional<std::string> retryPolicy;
454         std::optional<std::vector<nlohmann::json>> headers;
455 
456         if (!json_util::readJson(req, res, "Context", context,
457                                  "DeliveryRetryPolicy", retryPolicy,
458                                  "HttpHeaders", headers))
459         {
460             return;
461         }
462 
463         EventSrvSubscription& subValue = obj->second;
464 
465         if (context)
466         {
467             subValue.customText = *context;
468         }
469 
470         if (headers)
471         {
472             subValue.httpHeaders = *headers;
473         }
474 
475         if (retryPolicy)
476         {
477             if (std::find(supportedRetryPolicies.begin(),
478                           supportedRetryPolicies.end(),
479                           *retryPolicy) == supportedRetryPolicies.end())
480             {
481                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
482                                                  "DeliveryRetryPolicy");
483                 return;
484             }
485             subValue.retryPolicy = *retryPolicy;
486         }
487 
488         updateSubscriptionData();
489     }
490 
491     void doDelete(crow::Response& res, const crow::Request& req,
492                   const std::vector<std::string>& params) override
493     {
494         auto asyncResp = std::make_shared<AsyncResp>(res);
495 
496         if (params.size() != 1)
497         {
498             messages::internalError(asyncResp->res);
499             return;
500         }
501 
502         const std::string& id = params[0];
503         auto obj = subscriptionsMap.find(id);
504         if (obj == subscriptionsMap.end())
505         {
506             res.result(boost::beast::http::status::not_found);
507             res.end();
508             return;
509         }
510 
511         subscriptionsMap.erase(obj);
512 
513         updateSubscriptionData();
514     }
515 };
516 
517 } // namespace redfish
518