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/asio/io_context.hpp>
20 #include <boost/container/flat_map.hpp>
21 #include <error_messages.hpp>
22 #include <http_client.hpp>
23 #include <utils/json_utils.hpp>
24 
25 #include <cstdlib>
26 #include <ctime>
27 #include <fstream>
28 #include <memory>
29 #include <variant>
30 
31 namespace redfish
32 {
33 
34 using ReadingsObjType =
35     std::vector<std::tuple<std::string, std::string, double, std::string>>;
36 using EventServiceConfig = std::tuple<bool, uint32_t, uint32_t>;
37 
38 static constexpr const char* eventFormatType = "Event";
39 static constexpr const char* metricReportFormatType = "MetricReport";
40 
41 static constexpr const char* eventServiceFile =
42     "/var/lib/bmcweb/eventservice_config.json";
43 
44 class Subscription
45 {
46   public:
47     std::string id;
48     std::string destinationUrl;
49     std::string protocol;
50     std::string retryPolicy;
51     std::string customText;
52     std::string eventFormatType;
53     std::string subscriptionType;
54     std::vector<std::string> registryMsgIds;
55     std::vector<std::string> registryPrefixes;
56     std::vector<nlohmann::json> httpHeaders; // key-value pair
57     std::vector<nlohmann::json> metricReportDefinitions;
58 
59     Subscription(const Subscription&) = delete;
60     Subscription& operator=(const Subscription&) = delete;
61     Subscription(Subscription&&) = delete;
62     Subscription& operator=(Subscription&&) = delete;
63 
64     Subscription(const std::string& inHost, const std::string& inPort,
65                  const std::string& inPath, const std::string& inUriProto) :
66         eventSeqNum(1),
67         host(inHost), port(inPort), path(inPath), uriProto(inUriProto)
68     {
69         conn = std::make_shared<crow::HttpClient>(
70             crow::connections::systemBus->get_io_context(), host, port, path);
71     }
72     ~Subscription()
73     {}
74 
75     void sendEvent(const std::string& msg)
76     {
77         std::vector<std::pair<std::string, std::string>> reqHeaders;
78         for (const auto& header : httpHeaders)
79         {
80             for (const auto& item : header.items())
81             {
82                 std::string key = item.key();
83                 std::string val = item.value();
84                 reqHeaders.emplace_back(std::pair(key, val));
85             }
86         }
87         conn->setHeaders(reqHeaders);
88         conn->sendData(msg);
89     }
90 
91     void sendTestEventLog()
92     {
93         nlohmann::json logEntryArray;
94         logEntryArray.push_back({});
95         nlohmann::json& logEntryJson = logEntryArray.back();
96 
97         logEntryJson = {{"EventId", "TestID"},
98                         {"EventType", "Event"},
99                         {"Severity", "OK"},
100                         {"Message", "Generated test event"},
101                         {"MessageId", "OpenBMC.0.1.TestEventLog"},
102                         {"MessageArgs", nlohmann::json::array()},
103                         {"EventTimestamp", crow::utility::dateTimeNow()},
104                         {"Context", customText}};
105 
106         nlohmann::json msg = {{"@odata.type", "#Event.v1_4_0.Event"},
107                               {"Id", std::to_string(eventSeqNum)},
108                               {"Name", "Event Log"},
109                               {"Events", logEntryArray}};
110 
111         this->sendEvent(msg.dump());
112         this->eventSeqNum++;
113     }
114 
115     void filterAndSendReports(const std::string& id,
116                               const std::string& readingsTs,
117                               const ReadingsObjType& readings)
118     {
119         std::string metricReportDef =
120             "/redfish/v1/TelemetryService/MetricReportDefinitions/" + id;
121 
122         // Empty list means no filter. Send everything.
123         if (metricReportDefinitions.size())
124         {
125             if (std::find(metricReportDefinitions.begin(),
126                           metricReportDefinitions.end(),
127                           metricReportDef) == metricReportDefinitions.end())
128             {
129                 return;
130             }
131         }
132 
133         nlohmann::json metricValuesArray = nlohmann::json::array();
134         for (const auto& it : readings)
135         {
136             metricValuesArray.push_back({});
137             nlohmann::json& entry = metricValuesArray.back();
138 
139             entry = {{"MetricId", std::get<0>(it)},
140                      {"MetricProperty", std::get<1>(it)},
141                      {"MetricValue", std::to_string(std::get<2>(it))},
142                      {"Timestamp", std::get<3>(it)}};
143         }
144 
145         nlohmann::json msg = {
146             {"@odata.id", "/redfish/v1/TelemetryService/MetricReports/" + id},
147             {"@odata.type", "#MetricReport.v1_3_0.MetricReport"},
148             {"Id", id},
149             {"Name", id},
150             {"Timestamp", readingsTs},
151             {"MetricReportDefinition", {{"@odata.id", metricReportDef}}},
152             {"MetricValues", metricValuesArray}};
153 
154         this->sendEvent(msg.dump());
155     }
156 
157   private:
158     uint64_t eventSeqNum;
159     std::string host;
160     std::string port;
161     std::string path;
162     std::string uriProto;
163     std::shared_ptr<crow::HttpClient> conn;
164 };
165 
166 static constexpr const bool defaultEnabledState = true;
167 static constexpr const uint32_t defaultRetryAttempts = 3;
168 static constexpr const uint32_t defaultRetryInterval = 30;
169 static constexpr const char* defaulEventFormatType = "Event";
170 static constexpr const char* defaulSubscriptionType = "RedfishEvent";
171 static constexpr const char* defaulRetryPolicy = "TerminateAfterRetries";
172 
173 class EventServiceManager
174 {
175   private:
176     bool serviceEnabled;
177     uint32_t retryAttempts;
178     uint32_t retryTimeoutInterval;
179 
180     EventServiceManager(const EventServiceManager&) = delete;
181     EventServiceManager& operator=(const EventServiceManager&) = delete;
182     EventServiceManager(EventServiceManager&&) = delete;
183     EventServiceManager& operator=(EventServiceManager&&) = delete;
184 
185     EventServiceManager() :
186         noOfEventLogSubscribers(0), noOfMetricReportSubscribers(0)
187     {
188         // Load config from persist store.
189         initConfig();
190     }
191 
192     size_t noOfEventLogSubscribers;
193     size_t noOfMetricReportSubscribers;
194     std::shared_ptr<sdbusplus::bus::match::match> matchTelemetryMonitor;
195     boost::container::flat_map<std::string, std::shared_ptr<Subscription>>
196         subscriptionsMap;
197 
198   public:
199     static EventServiceManager& getInstance()
200     {
201         static EventServiceManager handler;
202         return handler;
203     }
204 
205     void loadDefaultConfig()
206     {
207         serviceEnabled = defaultEnabledState;
208         retryAttempts = defaultRetryAttempts;
209         retryTimeoutInterval = defaultRetryInterval;
210     }
211 
212     void initConfig()
213     {
214         std::ifstream eventConfigFile(eventServiceFile);
215         if (!eventConfigFile.good())
216         {
217             BMCWEB_LOG_DEBUG << "EventService config not exist";
218             loadDefaultConfig();
219             return;
220         }
221         auto jsonData = nlohmann::json::parse(eventConfigFile, nullptr, false);
222         if (jsonData.is_discarded())
223         {
224             BMCWEB_LOG_ERROR << "EventService config parse error.";
225             loadDefaultConfig();
226             return;
227         }
228 
229         nlohmann::json jsonConfig;
230         if (json_util::getValueFromJsonObject(jsonData, "Configuration",
231                                               jsonConfig))
232         {
233             if (!json_util::getValueFromJsonObject(jsonConfig, "ServiceEnabled",
234                                                    serviceEnabled))
235             {
236                 serviceEnabled = defaultEnabledState;
237             }
238             if (!json_util::getValueFromJsonObject(
239                     jsonConfig, "DeliveryRetryAttempts", retryAttempts))
240             {
241                 retryAttempts = defaultRetryAttempts;
242             }
243             if (!json_util::getValueFromJsonObject(
244                     jsonConfig, "DeliveryRetryIntervalSeconds",
245                     retryTimeoutInterval))
246             {
247                 retryTimeoutInterval = defaultRetryInterval;
248             }
249         }
250         else
251         {
252             loadDefaultConfig();
253         }
254 
255         nlohmann::json subscriptionsList;
256         if (!json_util::getValueFromJsonObject(jsonData, "Subscriptions",
257                                                subscriptionsList))
258         {
259             BMCWEB_LOG_DEBUG << "EventService: Subscriptions not exist.";
260             return;
261         }
262 
263         for (nlohmann::json& jsonObj : subscriptionsList)
264         {
265             std::string protocol;
266             if (!json_util::getValueFromJsonObject(jsonObj, "Protocol",
267                                                    protocol))
268             {
269                 BMCWEB_LOG_DEBUG << "Invalid subscription Protocol exist.";
270                 continue;
271             }
272             std::string destination;
273             if (!json_util::getValueFromJsonObject(jsonObj, "Destination",
274                                                    destination))
275             {
276                 BMCWEB_LOG_DEBUG << "Invalid subscription destination exist.";
277                 continue;
278             }
279             std::string host;
280             std::string urlProto;
281             std::string port;
282             std::string path;
283             bool status =
284                 validateAndSplitUrl(destination, urlProto, host, port, path);
285 
286             if (!status)
287             {
288                 BMCWEB_LOG_ERROR
289                     << "Failed to validate and split destination url";
290                 continue;
291             }
292             std::shared_ptr<Subscription> subValue =
293                 std::make_shared<Subscription>(host, port, path, urlProto);
294 
295             subValue->destinationUrl = destination;
296             subValue->protocol = protocol;
297             if (!json_util::getValueFromJsonObject(
298                     jsonObj, "DeliveryRetryPolicy", subValue->retryPolicy))
299             {
300                 subValue->retryPolicy = defaulRetryPolicy;
301             }
302             if (!json_util::getValueFromJsonObject(jsonObj, "EventFormatType",
303                                                    subValue->eventFormatType))
304             {
305                 subValue->eventFormatType = defaulEventFormatType;
306             }
307             if (!json_util::getValueFromJsonObject(jsonObj, "SubscriptionType",
308                                                    subValue->subscriptionType))
309             {
310                 subValue->subscriptionType = defaulSubscriptionType;
311             }
312 
313             json_util::getValueFromJsonObject(jsonObj, "Context",
314                                               subValue->customText);
315             json_util::getValueFromJsonObject(jsonObj, "MessageIds",
316                                               subValue->registryMsgIds);
317             json_util::getValueFromJsonObject(jsonObj, "RegistryPrefixes",
318                                               subValue->registryPrefixes);
319             json_util::getValueFromJsonObject(jsonObj, "HttpHeaders",
320                                               subValue->httpHeaders);
321             json_util::getValueFromJsonObject(
322                 jsonObj, "MetricReportDefinitions",
323                 subValue->metricReportDefinitions);
324 
325             std::string id = addSubscription(subValue, false);
326             if (id.empty())
327             {
328                 BMCWEB_LOG_ERROR << "Failed to add subscription";
329             }
330         }
331         return;
332     }
333 
334     void updateSubscriptionData()
335     {
336         // Persist the config and subscription data.
337         nlohmann::json jsonData;
338 
339         nlohmann::json& configObj = jsonData["Configuration"];
340         configObj["ServiceEnabled"] = serviceEnabled;
341         configObj["DeliveryRetryAttempts"] = retryAttempts;
342         configObj["DeliveryRetryIntervalSeconds"] = retryTimeoutInterval;
343 
344         nlohmann::json& subListArray = jsonData["Subscriptions"];
345         subListArray = nlohmann::json::array();
346 
347         for (const auto& it : subscriptionsMap)
348         {
349             nlohmann::json entry;
350             std::shared_ptr<Subscription> subValue = it.second;
351 
352             entry["Context"] = subValue->customText;
353             entry["DeliveryRetryPolicy"] = subValue->retryPolicy;
354             entry["Destination"] = subValue->destinationUrl;
355             entry["EventFormatType"] = subValue->eventFormatType;
356             entry["HttpHeaders"] = subValue->httpHeaders;
357             entry["MessageIds"] = subValue->registryMsgIds;
358             entry["Protocol"] = subValue->protocol;
359             entry["RegistryPrefixes"] = subValue->registryPrefixes;
360             entry["SubscriptionType"] = subValue->subscriptionType;
361             entry["MetricReportDefinitions"] =
362                 subValue->metricReportDefinitions;
363 
364             subListArray.push_back(entry);
365         }
366 
367         const std::string tmpFile(std::string(eventServiceFile) + "_tmp");
368         std::ofstream ofs(tmpFile, std::ios::out);
369         const auto& writeData = jsonData.dump();
370         ofs << writeData;
371         ofs.close();
372 
373         BMCWEB_LOG_DEBUG << "EventService config updated to file.";
374         if (std::rename(tmpFile.c_str(), eventServiceFile) != 0)
375         {
376             BMCWEB_LOG_ERROR << "Error in renaming temporary file: "
377                              << tmpFile.c_str();
378         }
379     }
380 
381     EventServiceConfig getEventServiceConfig()
382     {
383         return {serviceEnabled, retryAttempts, retryTimeoutInterval};
384     }
385 
386     void setEventServiceConfig(const EventServiceConfig& cfg)
387     {
388         bool updateConfig = false;
389 
390         if (serviceEnabled != std::get<0>(cfg))
391         {
392             serviceEnabled = std::get<0>(cfg);
393             if (serviceEnabled && noOfMetricReportSubscribers)
394             {
395                 registerMetricReportSignal();
396             }
397             else
398             {
399                 unregisterMetricReportSignal();
400             }
401             updateConfig = true;
402         }
403 
404         if (retryAttempts != std::get<1>(cfg))
405         {
406             retryAttempts = std::get<1>(cfg);
407             updateConfig = true;
408         }
409 
410         if (retryTimeoutInterval != std::get<2>(cfg))
411         {
412             retryTimeoutInterval = std::get<2>(cfg);
413             updateConfig = true;
414         }
415 
416         if (updateConfig)
417         {
418             updateSubscriptionData();
419         }
420     }
421 
422     void updateNoOfSubscribersCount()
423     {
424         size_t eventLogSubCount = 0;
425         size_t metricReportSubCount = 0;
426         for (const auto& it : subscriptionsMap)
427         {
428             std::shared_ptr<Subscription> entry = it.second;
429             if (entry->eventFormatType == eventFormatType)
430             {
431                 eventLogSubCount++;
432             }
433             else if (entry->eventFormatType == metricReportFormatType)
434             {
435                 metricReportSubCount++;
436             }
437         }
438 
439         noOfEventLogSubscribers = eventLogSubCount;
440         if (noOfMetricReportSubscribers != metricReportSubCount)
441         {
442             noOfMetricReportSubscribers = metricReportSubCount;
443             if (noOfMetricReportSubscribers)
444             {
445                 registerMetricReportSignal();
446             }
447             else
448             {
449                 unregisterMetricReportSignal();
450             }
451         }
452     }
453 
454     std::shared_ptr<Subscription> getSubscription(const std::string& id)
455     {
456         auto obj = subscriptionsMap.find(id);
457         if (obj == subscriptionsMap.end())
458         {
459             BMCWEB_LOG_ERROR << "No subscription exist with ID:" << id;
460             return nullptr;
461         }
462         std::shared_ptr<Subscription> subValue = obj->second;
463         return subValue;
464     }
465 
466     std::string addSubscription(const std::shared_ptr<Subscription> subValue,
467                                 const bool updateFile = true)
468     {
469         std::srand(static_cast<uint32_t>(std::time(0)));
470         std::string id;
471 
472         int retry = 3;
473         while (retry)
474         {
475             id = std::to_string(std::rand());
476             auto inserted = subscriptionsMap.insert(std::pair(id, subValue));
477             if (inserted.second)
478             {
479                 break;
480             }
481             --retry;
482         };
483 
484         if (retry <= 0)
485         {
486             BMCWEB_LOG_ERROR << "Failed to generate random number";
487             return std::string("");
488         }
489 
490         updateNoOfSubscribersCount();
491 
492         if (updateFile)
493         {
494             updateSubscriptionData();
495         }
496         return id;
497     }
498 
499     bool isSubscriptionExist(const std::string& id)
500     {
501         auto obj = subscriptionsMap.find(id);
502         if (obj == subscriptionsMap.end())
503         {
504             return false;
505         }
506         return true;
507     }
508 
509     void deleteSubscription(const std::string& id)
510     {
511         auto obj = subscriptionsMap.find(id);
512         if (obj != subscriptionsMap.end())
513         {
514             subscriptionsMap.erase(obj);
515             updateNoOfSubscribersCount();
516             updateSubscriptionData();
517         }
518     }
519 
520     size_t getNumberOfSubscriptions()
521     {
522         return subscriptionsMap.size();
523     }
524 
525     std::vector<std::string> getAllIDs()
526     {
527         std::vector<std::string> idList;
528         for (const auto& it : subscriptionsMap)
529         {
530             idList.emplace_back(it.first);
531         }
532         return idList;
533     }
534 
535     bool isDestinationExist(const std::string& destUrl)
536     {
537         for (const auto& it : subscriptionsMap)
538         {
539             std::shared_ptr<Subscription> entry = it.second;
540             if (entry->destinationUrl == destUrl)
541             {
542                 BMCWEB_LOG_ERROR << "Destination exist already" << destUrl;
543                 return true;
544             }
545         }
546         return false;
547     }
548 
549     void sendTestEventLog()
550     {
551         for (const auto& it : this->subscriptionsMap)
552         {
553             std::shared_ptr<Subscription> entry = it.second;
554             entry->sendTestEventLog();
555         }
556     }
557 
558     void getMetricReading(const std::string& service,
559                           const std::string& objPath, const std::string& intf)
560     {
561         std::size_t found = objPath.find_last_of("/");
562         if (found == std::string::npos)
563         {
564             BMCWEB_LOG_DEBUG << "Invalid objPath received";
565             return;
566         }
567 
568         std::string idStr = objPath.substr(found + 1);
569         if (idStr.empty())
570         {
571             BMCWEB_LOG_DEBUG << "Invalid ID in objPath";
572             return;
573         }
574 
575         crow::connections::systemBus->async_method_call(
576             [idStr{std::move(idStr)}](
577                 const boost::system::error_code ec,
578                 boost::container::flat_map<
579                     std::string, std::variant<std::string, ReadingsObjType>>&
580                     resp) {
581                 if (ec)
582                 {
583                     BMCWEB_LOG_DEBUG
584                         << "D-Bus call failed to GetAll metric readings.";
585                     return;
586                 }
587 
588                 const std::string* timestampPtr =
589                     std::get_if<std::string>(&resp["Timestamp"]);
590                 if (!timestampPtr)
591                 {
592                     BMCWEB_LOG_DEBUG << "Failed to Get timestamp.";
593                     return;
594                 }
595 
596                 ReadingsObjType* readingsPtr =
597                     std::get_if<ReadingsObjType>(&resp["Readings"]);
598                 if (!readingsPtr)
599                 {
600                     BMCWEB_LOG_DEBUG << "Failed to Get Readings property.";
601                     return;
602                 }
603 
604                 if (!readingsPtr->size())
605                 {
606                     BMCWEB_LOG_DEBUG << "No metrics report to be transferred";
607                     return;
608                 }
609 
610                 for (const auto& it :
611                      EventServiceManager::getInstance().subscriptionsMap)
612                 {
613                     std::shared_ptr<Subscription> entry = it.second;
614                     if (entry->eventFormatType == metricReportFormatType)
615                     {
616                         entry->filterAndSendReports(idStr, *timestampPtr,
617                                                     *readingsPtr);
618                     }
619                 }
620             },
621             service, objPath, "org.freedesktop.DBus.Properties", "GetAll",
622             intf);
623     }
624 
625     void unregisterMetricReportSignal()
626     {
627         if (matchTelemetryMonitor)
628         {
629             BMCWEB_LOG_DEBUG << "Metrics report signal - Unregister";
630             matchTelemetryMonitor.reset();
631             matchTelemetryMonitor = nullptr;
632         }
633     }
634 
635     void registerMetricReportSignal()
636     {
637         if (!serviceEnabled || matchTelemetryMonitor)
638         {
639             BMCWEB_LOG_DEBUG << "Not registering metric report signal.";
640             return;
641         }
642 
643         BMCWEB_LOG_DEBUG << "Metrics report signal - Register";
644         std::string matchStr(
645             "type='signal',member='ReportUpdate', "
646             "interface='xyz.openbmc_project.MonitoringService.Report'");
647 
648         matchTelemetryMonitor = std::make_shared<sdbusplus::bus::match::match>(
649             *crow::connections::systemBus, matchStr,
650             [this](sdbusplus::message::message& msg) {
651                 if (msg.is_method_error())
652                 {
653                     BMCWEB_LOG_ERROR << "TelemetryMonitor Signal error";
654                     return;
655                 }
656 
657                 std::string service = msg.get_sender();
658                 std::string objPath = msg.get_path();
659                 std::string intf = msg.get_interface();
660                 getMetricReading(service, objPath, intf);
661             });
662     }
663 
664     bool validateAndSplitUrl(const std::string& destUrl, std::string& urlProto,
665                              std::string& host, std::string& port,
666                              std::string& path)
667     {
668         // Validate URL using regex expression
669         // Format: <protocol>://<host>:<port>/<path>
670         // protocol: http/https
671         const std::regex urlRegex(
672             "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/"
673             "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)");
674         std::cmatch match;
675         if (!std::regex_match(destUrl.c_str(), match, urlRegex))
676         {
677             BMCWEB_LOG_INFO << "Dest. url did not match ";
678             return false;
679         }
680 
681         urlProto = std::string(match[1].first, match[1].second);
682         if (urlProto == "http")
683         {
684 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING
685             return false;
686 #endif
687         }
688 
689         host = std::string(match[2].first, match[2].second);
690         port = std::string(match[3].first, match[3].second);
691         path = std::string(match[4].first, match[4].second);
692         if (port.empty())
693         {
694             if (urlProto == "http")
695             {
696                 port = "80";
697             }
698             else
699             {
700                 port = "443";
701             }
702         }
703         if (path.empty())
704         {
705             path = "/";
706         }
707         return true;
708     }
709 };
710 
711 } // namespace redfish
712