xref: /openbmc/bmcweb/features/redfish/include/event_service_manager.hpp (revision 2185ddeac3ed0c49da6399af635f31544e382373)
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 "dbus_utility.hpp"
18 #include "error_messages.hpp"
19 #include "event_log.hpp"
20 #include "event_matches_filter.hpp"
21 #include "event_service_store.hpp"
22 #include "filesystem_log_watcher.hpp"
23 #include "metric_report.hpp"
24 #include "ossl_random.hpp"
25 #include "persistent_data.hpp"
26 #include "subscription.hpp"
27 #include "utils/time_utils.hpp"
28 
29 #include <sys/inotify.h>
30 
31 #include <boost/asio/io_context.hpp>
32 #include <boost/circular_buffer.hpp>
33 #include <boost/container/flat_map.hpp>
34 #include <boost/url/format.hpp>
35 #include <boost/url/url_view_base.hpp>
36 #include <sdbusplus/bus/match.hpp>
37 
38 #include <algorithm>
39 #include <cstdlib>
40 #include <ctime>
41 #include <format>
42 #include <fstream>
43 #include <memory>
44 #include <string>
45 #include <string_view>
46 #include <utility>
47 
48 namespace redfish
49 {
50 
51 static constexpr const char* eventFormatType = "Event";
52 static constexpr const char* metricReportFormatType = "MetricReport";
53 
54 static constexpr const char* eventServiceFile =
55     "/var/lib/bmcweb/eventservice_config.json";
56 
57 static constexpr const char* redfishEventLogFile = "/var/log/redfish";
58 
59 class EventServiceManager
60 {
61   private:
62     bool serviceEnabled = false;
63     uint32_t retryAttempts = 0;
64     uint32_t retryTimeoutInterval = 0;
65 
66     std::streampos redfishLogFilePosition{0};
67     size_t noOfEventLogSubscribers{0};
68     size_t noOfMetricReportSubscribers{0};
69     std::shared_ptr<sdbusplus::bus::match_t> matchTelemetryMonitor;
70     boost::container::flat_map<std::string, std::shared_ptr<Subscription>>
71         subscriptionsMap;
72 
73     uint64_t eventId{1};
74 
75     struct Event
76     {
77         std::string id;
78         nlohmann::json message;
79     };
80 
81     constexpr static size_t maxMessages = 200;
82     boost::circular_buffer<Event> messages{maxMessages};
83 
84     boost::asio::io_context& ioc;
85 
86   public:
87     EventServiceManager(const EventServiceManager&) = delete;
88     EventServiceManager& operator=(const EventServiceManager&) = delete;
89     EventServiceManager(EventServiceManager&&) = delete;
90     EventServiceManager& operator=(EventServiceManager&&) = delete;
91     ~EventServiceManager() = default;
92 
93     explicit EventServiceManager(boost::asio::io_context& iocIn) : ioc(iocIn)
94     {
95         // Load config from persist store.
96         initConfig();
97     }
98 
99     static EventServiceManager&
100         getInstance(boost::asio::io_context* ioc = nullptr)
101     {
102         static EventServiceManager handler(*ioc);
103         return handler;
104     }
105 
106     void initConfig()
107     {
108         loadOldBehavior();
109 
110         persistent_data::EventServiceConfig eventServiceConfig =
111             persistent_data::EventServiceStore::getInstance()
112                 .getEventServiceConfig();
113 
114         serviceEnabled = eventServiceConfig.enabled;
115         retryAttempts = eventServiceConfig.retryAttempts;
116         retryTimeoutInterval = eventServiceConfig.retryTimeoutInterval;
117 
118         for (const auto& it : persistent_data::EventServiceStore::getInstance()
119                                   .subscriptionsConfigMap)
120         {
121             std::shared_ptr<persistent_data::UserSubscription> newSub =
122                 it.second;
123 
124             boost::system::result<boost::urls::url> url =
125                 boost::urls::parse_absolute_uri(newSub->destinationUrl);
126 
127             if (!url)
128             {
129                 BMCWEB_LOG_ERROR(
130                     "Failed to validate and split destination url");
131                 continue;
132             }
133             std::shared_ptr<Subscription> subValue =
134                 std::make_shared<Subscription>(newSub, *url, ioc);
135             std::string id = subValue->userSub->id;
136             subValue->deleter = [id]() {
137                 EventServiceManager::getInstance().deleteSubscription(id);
138             };
139 
140             subscriptionsMap.emplace(id, subValue);
141 
142             updateNoOfSubscribersCount();
143 
144             if constexpr (!BMCWEB_REDFISH_DBUS_LOG)
145             {
146                 cacheRedfishLogFile();
147             }
148 
149             // Update retry configuration.
150             subValue->updateRetryConfig(retryAttempts, retryTimeoutInterval);
151         }
152     }
153 
154     static void loadOldBehavior()
155     {
156         std::ifstream eventConfigFile(eventServiceFile);
157         if (!eventConfigFile.good())
158         {
159             BMCWEB_LOG_DEBUG("Old eventService config not exist");
160             return;
161         }
162         auto jsonData = nlohmann::json::parse(eventConfigFile, nullptr, false);
163         if (jsonData.is_discarded())
164         {
165             BMCWEB_LOG_ERROR("Old eventService config parse error.");
166             return;
167         }
168 
169         const nlohmann::json::object_t* obj =
170             jsonData.get_ptr<const nlohmann::json::object_t*>();
171         for (const auto& item : *obj)
172         {
173             if (item.first == "Configuration")
174             {
175                 persistent_data::EventServiceStore::getInstance()
176                     .getEventServiceConfig()
177                     .fromJson(item.second);
178             }
179             else if (item.first == "Subscriptions")
180             {
181                 for (const auto& elem : item.second)
182                 {
183                     std::optional<persistent_data::UserSubscription>
184                         newSubscription =
185                             persistent_data::UserSubscription::fromJson(elem,
186                                                                         true);
187                     if (!newSubscription)
188                     {
189                         BMCWEB_LOG_ERROR("Problem reading subscription "
190                                          "from old persistent store");
191                         continue;
192                     }
193                     persistent_data::UserSubscription& newSub =
194                         *newSubscription;
195 
196                     std::uniform_int_distribution<uint32_t> dist(0);
197                     bmcweb::OpenSSLGenerator gen;
198 
199                     std::string id;
200 
201                     int retry = 3;
202                     while (retry != 0)
203                     {
204                         id = std::to_string(dist(gen));
205                         if (gen.error())
206                         {
207                             retry = 0;
208                             break;
209                         }
210                         newSub.id = id;
211                         auto inserted =
212                             persistent_data::EventServiceStore::getInstance()
213                                 .subscriptionsConfigMap.insert(std::pair(
214                                     id, std::make_shared<
215                                             persistent_data::UserSubscription>(
216                                             newSub)));
217                         if (inserted.second)
218                         {
219                             break;
220                         }
221                         --retry;
222                     }
223 
224                     if (retry <= 0)
225                     {
226                         BMCWEB_LOG_ERROR(
227                             "Failed to generate random number from old "
228                             "persistent store");
229                         continue;
230                     }
231                 }
232             }
233 
234             persistent_data::getConfig().writeData();
235             std::error_code ec;
236             std::filesystem::remove(eventServiceFile, ec);
237             if (ec)
238             {
239                 BMCWEB_LOG_DEBUG(
240                     "Failed to remove old event service file.  Ignoring");
241             }
242             else
243             {
244                 BMCWEB_LOG_DEBUG("Remove old eventservice config");
245             }
246         }
247     }
248 
249     void updateSubscriptionData() const
250     {
251         persistent_data::EventServiceStore::getInstance()
252             .eventServiceConfig.enabled = serviceEnabled;
253         persistent_data::EventServiceStore::getInstance()
254             .eventServiceConfig.retryAttempts = retryAttempts;
255         persistent_data::EventServiceStore::getInstance()
256             .eventServiceConfig.retryTimeoutInterval = retryTimeoutInterval;
257 
258         persistent_data::getConfig().writeData();
259     }
260 
261     void setEventServiceConfig(const persistent_data::EventServiceConfig& cfg)
262     {
263         bool updateConfig = false;
264         bool updateRetryCfg = false;
265 
266         if (serviceEnabled != cfg.enabled)
267         {
268             serviceEnabled = cfg.enabled;
269             if (serviceEnabled && noOfMetricReportSubscribers != 0U)
270             {
271                 registerMetricReportSignal();
272             }
273             else
274             {
275                 unregisterMetricReportSignal();
276             }
277             updateConfig = true;
278         }
279 
280         if (retryAttempts != cfg.retryAttempts)
281         {
282             retryAttempts = cfg.retryAttempts;
283             updateConfig = true;
284             updateRetryCfg = true;
285         }
286 
287         if (retryTimeoutInterval != cfg.retryTimeoutInterval)
288         {
289             retryTimeoutInterval = cfg.retryTimeoutInterval;
290             updateConfig = true;
291             updateRetryCfg = true;
292         }
293 
294         if (updateConfig)
295         {
296             updateSubscriptionData();
297         }
298 
299         if (updateRetryCfg)
300         {
301             // Update the changed retry config to all subscriptions
302             for (const auto& it :
303                  EventServiceManager::getInstance().subscriptionsMap)
304             {
305                 Subscription& entry = *it.second;
306                 entry.updateRetryConfig(retryAttempts, retryTimeoutInterval);
307             }
308         }
309     }
310 
311     void updateNoOfSubscribersCount()
312     {
313         size_t eventLogSubCount = 0;
314         size_t metricReportSubCount = 0;
315         for (const auto& it : subscriptionsMap)
316         {
317             std::shared_ptr<Subscription> entry = it.second;
318             if (entry->userSub->eventFormatType == eventFormatType)
319             {
320                 eventLogSubCount++;
321             }
322             else if (entry->userSub->eventFormatType == metricReportFormatType)
323             {
324                 metricReportSubCount++;
325             }
326         }
327 
328         noOfEventLogSubscribers = eventLogSubCount;
329         if (noOfMetricReportSubscribers != metricReportSubCount)
330         {
331             noOfMetricReportSubscribers = metricReportSubCount;
332             if (noOfMetricReportSubscribers != 0U)
333             {
334                 registerMetricReportSignal();
335             }
336             else
337             {
338                 unregisterMetricReportSignal();
339             }
340         }
341     }
342 
343     std::shared_ptr<Subscription> getSubscription(const std::string& id)
344     {
345         auto obj = subscriptionsMap.find(id);
346         if (obj == subscriptionsMap.end())
347         {
348             BMCWEB_LOG_ERROR("No subscription exist with ID:{}", id);
349             return nullptr;
350         }
351         std::shared_ptr<Subscription> subValue = obj->second;
352         return subValue;
353     }
354 
355     std::string
356         addSubscriptionInternal(const std::shared_ptr<Subscription>& subValue)
357     {
358         std::uniform_int_distribution<uint32_t> dist(0);
359         bmcweb::OpenSSLGenerator gen;
360 
361         std::string id;
362 
363         int retry = 3;
364         while (retry != 0)
365         {
366             id = std::to_string(dist(gen));
367             if (gen.error())
368             {
369                 retry = 0;
370                 break;
371             }
372             auto inserted = subscriptionsMap.insert(std::pair(id, subValue));
373             if (inserted.second)
374             {
375                 break;
376             }
377             --retry;
378         }
379 
380         if (retry <= 0)
381         {
382             BMCWEB_LOG_ERROR("Failed to generate random number");
383             return "";
384         }
385 
386         // Set Subscription ID for back trace
387         subValue->userSub->id = id;
388 
389         persistent_data::EventServiceStore::getInstance()
390             .subscriptionsConfigMap.emplace(id, subValue->userSub);
391 
392         updateNoOfSubscribersCount();
393 
394         if constexpr (!BMCWEB_REDFISH_DBUS_LOG)
395         {
396             if (redfishLogFilePosition != 0)
397             {
398                 cacheRedfishLogFile();
399             }
400         }
401         // Update retry configuration.
402         subValue->updateRetryConfig(retryAttempts, retryTimeoutInterval);
403 
404         return id;
405     }
406 
407     std::string
408         addSSESubscription(const std::shared_ptr<Subscription>& subValue,
409                            std::string_view lastEventId)
410     {
411         std::string id = addSubscriptionInternal(subValue);
412 
413         if (!lastEventId.empty())
414         {
415             BMCWEB_LOG_INFO("Attempting to find message for last id {}",
416                             lastEventId);
417             boost::circular_buffer<Event>::iterator lastEvent =
418                 std::find_if(messages.begin(), messages.end(),
419                              [&lastEventId](const Event& event) {
420                                  return event.id == lastEventId;
421                              });
422             // Can't find a matching ID
423             if (lastEvent == messages.end())
424             {
425                 nlohmann::json msg = messages::eventBufferExceeded();
426                 // If the buffer overloaded, send all messages.
427                 subValue->sendEventToSubscriber(msg);
428                 lastEvent = messages.begin();
429             }
430             else
431             {
432                 // Skip the last event the user already has
433                 lastEvent++;
434             }
435 
436             for (boost::circular_buffer<Event>::const_iterator event =
437                      lastEvent;
438                  lastEvent != messages.end(); lastEvent++)
439             {
440                 subValue->sendEventToSubscriber(event->message);
441             }
442         }
443         return id;
444     }
445 
446     std::string
447         addPushSubscription(const std::shared_ptr<Subscription>& subValue)
448     {
449         std::string id = addSubscriptionInternal(subValue);
450         subValue->deleter = [id]() {
451             EventServiceManager::getInstance().deleteSubscription(id);
452         };
453         updateSubscriptionData();
454         return id;
455     }
456 
457     bool isSubscriptionExist(const std::string& id)
458     {
459         auto obj = subscriptionsMap.find(id);
460         return obj != subscriptionsMap.end();
461     }
462 
463     bool deleteSubscription(const std::string& id)
464     {
465         auto obj = subscriptionsMap.find(id);
466         if (obj == subscriptionsMap.end())
467         {
468             BMCWEB_LOG_WARNING("Could not find subscription with id {}", id);
469             return false;
470         }
471         subscriptionsMap.erase(obj);
472         auto& event = persistent_data::EventServiceStore::getInstance();
473         auto persistentObj = event.subscriptionsConfigMap.find(id);
474         if (persistentObj == event.subscriptionsConfigMap.end())
475         {
476             BMCWEB_LOG_ERROR("Subscription wasn't in persistent data");
477             return true;
478         }
479         persistent_data::EventServiceStore::getInstance()
480             .subscriptionsConfigMap.erase(persistentObj);
481         updateNoOfSubscribersCount();
482         updateSubscriptionData();
483 
484         return true;
485     }
486 
487     void deleteSseSubscription(const crow::sse_socket::Connection& thisConn)
488     {
489         for (auto it = subscriptionsMap.begin(); it != subscriptionsMap.end();)
490         {
491             std::shared_ptr<Subscription> entry = it->second;
492             bool entryIsThisConn = entry->matchSseId(thisConn);
493             if (entryIsThisConn)
494             {
495                 persistent_data::EventServiceStore::getInstance()
496                     .subscriptionsConfigMap.erase(entry->userSub->id);
497                 it = subscriptionsMap.erase(it);
498                 return;
499             }
500             it++;
501         }
502     }
503 
504     size_t getNumberOfSubscriptions() const
505     {
506         return subscriptionsMap.size();
507     }
508 
509     size_t getNumberOfSSESubscriptions() const
510     {
511         auto size = std::ranges::count_if(
512             subscriptionsMap,
513             [](const std::pair<std::string, std::shared_ptr<Subscription>>&
514                    entry) {
515                 return (entry.second->userSub->subscriptionType ==
516                         subscriptionTypeSSE);
517             });
518         return static_cast<size_t>(size);
519     }
520 
521     std::vector<std::string> getAllIDs()
522     {
523         std::vector<std::string> idList;
524         for (const auto& it : subscriptionsMap)
525         {
526             idList.emplace_back(it.first);
527         }
528         return idList;
529     }
530 
531     bool sendTestEventLog()
532     {
533         for (const auto& it : subscriptionsMap)
534         {
535             std::shared_ptr<Subscription> entry = it.second;
536             if (!entry->sendTestEventLog())
537             {
538                 return false;
539             }
540         }
541         return true;
542     }
543 
544     void sendEvent(nlohmann::json::object_t eventMessage,
545                    std::string_view origin, std::string_view resourceType)
546     {
547         eventMessage["EventId"] = eventId;
548 
549         eventMessage["EventTimestamp"] =
550             redfish::time_utils::getDateTimeOffsetNow().first;
551         eventMessage["OriginOfCondition"] = origin;
552 
553         // MemberId is 0 : since we are sending one event record.
554         eventMessage["MemberId"] = "0";
555 
556         messages.push_back(Event(std::to_string(eventId), eventMessage));
557 
558         for (auto& it : subscriptionsMap)
559         {
560             std::shared_ptr<Subscription>& entry = it.second;
561             if (!eventMatchesFilter(*entry->userSub, eventMessage,
562                                     resourceType))
563             {
564                 BMCWEB_LOG_DEBUG("Filter didn't match");
565                 continue;
566             }
567 
568             nlohmann::json::array_t eventRecord;
569             eventRecord.emplace_back(eventMessage);
570 
571             nlohmann::json msgJson;
572 
573             msgJson["@odata.type"] = "#Event.v1_4_0.Event";
574             msgJson["Name"] = "Event Log";
575             msgJson["Id"] = eventId;
576             msgJson["Events"] = std::move(eventRecord);
577 
578             std::string strMsg = msgJson.dump(
579                 2, ' ', true, nlohmann::json::error_handler_t::replace);
580             entry->sendEventToSubscriber(std::move(strMsg));
581             eventId++; // increment the eventId
582         }
583     }
584 
585     void resetRedfishFilePosition()
586     {
587         // Control would be here when Redfish file is created.
588         // Reset File Position as new file is created
589         redfishLogFilePosition = 0;
590     }
591 
592     void cacheRedfishLogFile()
593     {
594         // Open the redfish file and read till the last record.
595 
596         std::ifstream logStream(redfishEventLogFile);
597         if (!logStream.good())
598         {
599             BMCWEB_LOG_ERROR(" Redfish log file open failed ");
600             return;
601         }
602         std::string logEntry;
603         while (std::getline(logStream, logEntry))
604         {
605             redfishLogFilePosition = logStream.tellg();
606         }
607     }
608 
609     void readEventLogsFromFile()
610     {
611         std::ifstream logStream(redfishEventLogFile);
612         if (!logStream.good())
613         {
614             BMCWEB_LOG_ERROR(" Redfish log file open failed");
615             return;
616         }
617 
618         std::vector<EventLogObjectsType> eventRecords;
619 
620         std::string logEntry;
621 
622         BMCWEB_LOG_DEBUG("Redfish log file: seek to {}",
623                          static_cast<int>(redfishLogFilePosition));
624 
625         // Get the read pointer to the next log to be read.
626         logStream.seekg(redfishLogFilePosition);
627 
628         while (std::getline(logStream, logEntry))
629         {
630             BMCWEB_LOG_DEBUG("Redfish log file: found new event log entry");
631             // Update Pointer position
632             redfishLogFilePosition = logStream.tellg();
633 
634             std::string idStr;
635             if (!event_log::getUniqueEntryID(logEntry, idStr))
636             {
637                 BMCWEB_LOG_DEBUG(
638                     "Redfish log file: could not get unique entry id for {}",
639                     logEntry);
640                 continue;
641             }
642 
643             if (!serviceEnabled || noOfEventLogSubscribers == 0)
644             {
645                 // If Service is not enabled, no need to compute
646                 // the remaining items below.
647                 // But, Loop must continue to keep track of Timestamp
648                 BMCWEB_LOG_DEBUG(
649                     "Redfish log file: no subscribers / event service not enabled");
650                 continue;
651             }
652 
653             std::string timestamp;
654             std::string messageID;
655             std::vector<std::string> messageArgs;
656             if (event_log::getEventLogParams(logEntry, timestamp, messageID,
657                                              messageArgs) != 0)
658             {
659                 BMCWEB_LOG_DEBUG("Read eventLog entry params failed for {}",
660                                  logEntry);
661                 continue;
662             }
663 
664             eventRecords.emplace_back(idStr, timestamp, messageID, messageArgs);
665         }
666 
667         if (!serviceEnabled || noOfEventLogSubscribers == 0)
668         {
669             BMCWEB_LOG_DEBUG("EventService disabled or no Subscriptions.");
670             return;
671         }
672 
673         if (eventRecords.empty())
674         {
675             // No Records to send
676             BMCWEB_LOG_DEBUG("No log entries available to be transferred.");
677             return;
678         }
679 
680         for (const auto& it : subscriptionsMap)
681         {
682             std::shared_ptr<Subscription> entry = it.second;
683             if (entry->userSub->eventFormatType == "Event")
684             {
685                 entry->filterAndSendEventLogs(eventRecords);
686             }
687         }
688     }
689 
690     static void getReadingsForReport(sdbusplus::message_t& msg)
691     {
692         if (msg.is_method_error())
693         {
694             BMCWEB_LOG_ERROR("TelemetryMonitor Signal error");
695             return;
696         }
697 
698         sdbusplus::message::object_path path(msg.get_path());
699         std::string id = path.filename();
700         if (id.empty())
701         {
702             BMCWEB_LOG_ERROR("Failed to get Id from path");
703             return;
704         }
705 
706         std::string interface;
707         dbus::utility::DBusPropertiesMap props;
708         std::vector<std::string> invalidProps;
709         msg.read(interface, props, invalidProps);
710 
711         auto found = std::ranges::find_if(props, [](const auto& x) {
712             return x.first == "Readings";
713         });
714         if (found == props.end())
715         {
716             BMCWEB_LOG_INFO("Failed to get Readings from Report properties");
717             return;
718         }
719 
720         const telemetry::TimestampReadings* readings =
721             std::get_if<telemetry::TimestampReadings>(&found->second);
722         if (readings == nullptr)
723         {
724             BMCWEB_LOG_INFO("Failed to get Readings from Report properties");
725             return;
726         }
727 
728         for (const auto& it :
729              EventServiceManager::getInstance().subscriptionsMap)
730         {
731             Subscription& entry = *it.second;
732             if (entry.userSub->eventFormatType == metricReportFormatType)
733             {
734                 entry.filterAndSendReports(id, *readings);
735             }
736         }
737     }
738 
739     void unregisterMetricReportSignal()
740     {
741         if (matchTelemetryMonitor)
742         {
743             BMCWEB_LOG_DEBUG("Metrics report signal - Unregister");
744             matchTelemetryMonitor.reset();
745             matchTelemetryMonitor = nullptr;
746         }
747     }
748 
749     void registerMetricReportSignal()
750     {
751         if (!serviceEnabled || matchTelemetryMonitor)
752         {
753             BMCWEB_LOG_DEBUG("Not registering metric report signal.");
754             return;
755         }
756 
757         BMCWEB_LOG_DEBUG("Metrics report signal - Register");
758         std::string matchStr = "type='signal',member='PropertiesChanged',"
759                                "interface='org.freedesktop.DBus.Properties',"
760                                "arg0=xyz.openbmc_project.Telemetry.Report";
761 
762         matchTelemetryMonitor = std::make_shared<sdbusplus::bus::match_t>(
763             *crow::connections::systemBus, matchStr, getReadingsForReport);
764     }
765 };
766 
767 } // namespace redfish
768