xref: /openbmc/bmcweb/features/redfish/include/event_service_manager.hpp (revision e9a14131650d30389eaf9dc38a3c32f1cb552f52)
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 #include "registries.hpp"
19 #include "registries/base_message_registry.hpp"
20 #include "registries/openbmc_message_registry.hpp"
21 
22 #include <sys/inotify.h>
23 
24 #include <boost/container/flat_map.hpp>
25 #include <cstdlib>
26 #include <ctime>
27 #include <error_messages.hpp>
28 #include <http_client.hpp>
29 #include <memory>
30 #include <utils/json_utils.hpp>
31 #include <variant>
32 
33 namespace redfish
34 {
35 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES
36 constexpr const char* redfishEventLogFile = "/var/log/redfish";
37 constexpr const uint32_t inotifyFileAction = IN_MODIFY;
38 std::shared_ptr<boost::asio::posix::stream_descriptor> inotifyConn = nullptr;
39 
40 // <ID, timestamp, RedfishLogId, registryPrefix, MessageId, MessageArgs>
41 using EventLogObjectsType =
42     std::tuple<std::string, std::string, std::string, std::string, std::string,
43                boost::beast::span<std::string>>;
44 
45 namespace message_registries
46 {
47 static const Message*
48     getMsgFromRegistry(const std::string& messageKey,
49                        const boost::beast::span<const MessageEntry>& registry)
50 {
51     boost::beast::span<const MessageEntry>::const_iterator messageIt =
52         std::find_if(registry.cbegin(), registry.cend(),
53                      [&messageKey](const MessageEntry& messageEntry) {
54                          return !messageKey.compare(messageEntry.first);
55                      });
56     if (messageIt != registry.cend())
57     {
58         return &messageIt->second;
59     }
60 
61     return nullptr;
62 }
63 
64 static const Message* formatMessage(const std::string_view& messageID)
65 {
66     // Redfish MessageIds are in the form
67     // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find
68     // the right Message
69     std::vector<std::string> fields;
70     fields.reserve(4);
71     boost::split(fields, messageID, boost::is_any_of("."));
72     if (fields.size() != 4)
73     {
74         return nullptr;
75     }
76     std::string& registryName = fields[0];
77     std::string& messageKey = fields[3];
78 
79     // Find the right registry and check it for the MessageKey
80     if (std::string(base::header.registryPrefix) == registryName)
81     {
82         return getMsgFromRegistry(
83             messageKey, boost::beast::span<const MessageEntry>(base::registry));
84     }
85     if (std::string(openbmc::header.registryPrefix) == registryName)
86     {
87         return getMsgFromRegistry(
88             messageKey,
89             boost::beast::span<const MessageEntry>(openbmc::registry));
90     }
91     return nullptr;
92 }
93 } // namespace message_registries
94 
95 namespace event_log
96 {
97 bool getUniqueEntryID(const std::string& logEntry, std::string& entryID,
98                       const bool firstEntry = true)
99 {
100     static time_t prevTs = 0;
101     static int index = 0;
102     if (firstEntry)
103     {
104         prevTs = 0;
105     }
106 
107     // Get the entry timestamp
108     std::time_t curTs = 0;
109     std::tm timeStruct = {};
110     std::istringstream entryStream(logEntry);
111     if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S"))
112     {
113         curTs = std::mktime(&timeStruct);
114         if (curTs == -1)
115         {
116             return false;
117         }
118     }
119     // If the timestamp isn't unique, increment the index
120     index = (curTs == prevTs) ? index + 1 : 0;
121 
122     // Save the timestamp
123     prevTs = curTs;
124 
125     entryID = std::to_string(curTs);
126     if (index > 0)
127     {
128         entryID += "_" + std::to_string(index);
129     }
130     return true;
131 }
132 
133 int getEventLogParams(const std::string& logEntry, std::string& timestamp,
134                       std::string& messageID,
135                       boost::beast::span<std::string>& messageArgs)
136 {
137     // The redfish log format is "<Timestamp> <MessageId>,<MessageArgs>"
138     // First get the Timestamp
139     size_t space = logEntry.find_first_of(" ");
140     if (space == std::string::npos)
141     {
142         return -EINVAL;
143     }
144     timestamp = logEntry.substr(0, space);
145     // Then get the log contents
146     size_t entryStart = logEntry.find_first_not_of(" ", space);
147     if (entryStart == std::string::npos)
148     {
149         return -EINVAL;
150     }
151     std::string_view entry(logEntry);
152     entry.remove_prefix(entryStart);
153     // Use split to separate the entry into its fields
154     std::vector<std::string> logEntryFields;
155     boost::split(logEntryFields, entry, boost::is_any_of(","),
156                  boost::token_compress_on);
157     // We need at least a MessageId to be valid
158     if (logEntryFields.size() < 1)
159     {
160         return -EINVAL;
161     }
162     messageID = logEntryFields[0];
163 
164     // Get the MessageArgs from the log if there are any
165     if (logEntryFields.size() > 1)
166     {
167         std::string& messageArgsStart = logEntryFields[1];
168         // If the first string is empty, assume there are no MessageArgs
169         std::size_t messageArgsSize = 0;
170         if (!messageArgsStart.empty())
171         {
172             messageArgsSize = logEntryFields.size() - 1;
173         }
174 
175         messageArgs = boost::beast::span(&messageArgsStart, messageArgsSize);
176     }
177 
178     return 0;
179 }
180 
181 void getRegistryAndMessageKey(const std::string& messageID,
182                               std::string& registryName,
183                               std::string& messageKey)
184 {
185     // Redfish MessageIds are in the form
186     // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find
187     // the right Message
188     std::vector<std::string> fields;
189     fields.reserve(4);
190     boost::split(fields, messageID, boost::is_any_of("."));
191     if (fields.size() == 4)
192     {
193         registryName = fields[0];
194         messageKey = fields[3];
195     }
196 }
197 
198 int formatEventLogEntry(const std::string& logEntryID,
199                         const std::string& messageID,
200                         const boost::beast::span<std::string>& messageArgs,
201                         std::string timestamp, const std::string customText,
202                         nlohmann::json& logEntryJson)
203 {
204     // Get the Message from the MessageRegistry
205     const message_registries::Message* message =
206         message_registries::formatMessage(messageID);
207 
208     std::string msg;
209     std::string severity;
210     if (message != nullptr)
211     {
212         msg = message->message;
213         severity = message->severity;
214     }
215 
216     // Fill the MessageArgs into the Message
217     int i = 0;
218     for (const std::string& messageArg : messageArgs)
219     {
220         std::string argStr = "%" + std::to_string(++i);
221         size_t argPos = msg.find(argStr);
222         if (argPos != std::string::npos)
223         {
224             msg.replace(argPos, argStr.length(), messageArg);
225         }
226     }
227 
228     // Get the Created time from the timestamp. The log timestamp is in
229     // RFC3339 format which matches the Redfish format except for the
230     // fractional seconds between the '.' and the '+', so just remove them.
231     std::size_t dot = timestamp.find_first_of(".");
232     std::size_t plus = timestamp.find_first_of("+");
233     if (dot != std::string::npos && plus != std::string::npos)
234     {
235         timestamp.erase(dot, plus - dot);
236     }
237 
238     // Fill in the log entry with the gathered data
239     logEntryJson = {{"EventId", logEntryID},
240                     {"EventType", "Event"},
241                     {"Severity", std::move(severity)},
242                     {"Message", std::move(msg)},
243                     {"MessageId", std::move(messageID)},
244                     {"MessageArgs", std::move(messageArgs)},
245                     {"EventTimestamp", std::move(timestamp)},
246                     {"Context", customText}};
247     return 0;
248 }
249 #endif
250 
251 } // namespace event_log
252 
253 class Subscription
254 {
255   public:
256     std::string id;
257     std::string destinationUrl;
258     std::string protocol;
259     std::string retryPolicy;
260     std::string customText;
261     std::string eventFormatType;
262     std::string subscriptionType;
263     std::vector<std::string> registryMsgIds;
264     std::vector<std::string> registryPrefixes;
265     std::vector<nlohmann::json> httpHeaders; // key-value pair
266 
267     Subscription(const Subscription&) = delete;
268     Subscription& operator=(const Subscription&) = delete;
269     Subscription(Subscription&&) = delete;
270     Subscription& operator=(Subscription&&) = delete;
271 
272     Subscription(const std::string& inHost, const std::string& inPort,
273                  const std::string& inPath, const std::string& inUriProto) :
274         eventSeqNum(1),
275         host(inHost), port(inPort), path(inPath), uriProto(inUriProto)
276     {
277         conn = std::make_shared<crow::HttpClient>(
278             crow::connections::systemBus->get_io_context(), host, port, path);
279     }
280     ~Subscription()
281     {
282     }
283 
284     void sendEvent(const std::string& msg)
285     {
286         std::vector<std::pair<std::string, std::string>> reqHeaders;
287         for (const auto& header : httpHeaders)
288         {
289             for (const auto& item : header.items())
290             {
291                 std::string key = item.key();
292                 std::string val = item.value();
293                 reqHeaders.emplace_back(std::pair(key, val));
294             }
295         }
296         conn->setHeaders(reqHeaders);
297         conn->sendData(msg);
298     }
299 
300     void sendTestEventLog()
301     {
302         nlohmann::json logEntryArray;
303         logEntryArray.push_back({});
304         nlohmann::json& logEntryJson = logEntryArray.back();
305 
306         logEntryJson = {{"EventId", "TestID"},
307                         {"EventType", "Event"},
308                         {"Severity", "OK"},
309                         {"Message", "Generated test event"},
310                         {"MessageId", "OpenBMC.0.1.TestEventLog"},
311                         {"MessageArgs", nlohmann::json::array()},
312                         {"EventTimestamp", crow::utility::dateTimeNow()},
313                         {"Context", customText}};
314 
315         nlohmann::json msg = {{"@odata.type", "#Event.v1_4_0.Event"},
316                               {"Id", std::to_string(eventSeqNum)},
317                               {"Name", "Event Log"},
318                               {"Events", logEntryArray}};
319 
320         this->sendEvent(msg.dump());
321         this->eventSeqNum++;
322     }
323 
324 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES
325     void filterAndSendEventLogs(
326         const std::vector<EventLogObjectsType>& eventRecords)
327     {
328         nlohmann::json logEntryArray;
329         for (const EventLogObjectsType& logEntry : eventRecords)
330         {
331             const std::string& idStr = std::get<0>(logEntry);
332             const std::string& timestamp = std::get<1>(logEntry);
333             const std::string& messageID = std::get<2>(logEntry);
334             const std::string& registryName = std::get<3>(logEntry);
335             const std::string& messageKey = std::get<4>(logEntry);
336             const boost::beast::span<std::string>& messageArgs =
337                 std::get<5>(logEntry);
338 
339             // If registryPrefixes list is empty, don't filter events
340             // send everything.
341             if (registryPrefixes.size())
342             {
343                 auto obj = std::find(registryPrefixes.begin(),
344                                      registryPrefixes.end(), registryName);
345                 if (obj == registryPrefixes.end())
346                 {
347                     continue;
348                 }
349             }
350 
351             // If registryMsgIds list is empty, don't filter events
352             // send everything.
353             if (registryMsgIds.size())
354             {
355                 auto obj = std::find(registryMsgIds.begin(),
356                                      registryMsgIds.end(), messageKey);
357                 if (obj == registryMsgIds.end())
358                 {
359                     continue;
360                 }
361             }
362 
363             logEntryArray.push_back({});
364             nlohmann::json& bmcLogEntry = logEntryArray.back();
365             if (event_log::formatEventLogEntry(idStr, messageID, messageArgs,
366                                                timestamp, customText,
367                                                bmcLogEntry) != 0)
368             {
369                 BMCWEB_LOG_DEBUG << "Read eventLog entry failed";
370                 continue;
371             }
372         }
373 
374         if (logEntryArray.size() < 1)
375         {
376             BMCWEB_LOG_DEBUG << "No log entries available to be transferred.";
377             return;
378         }
379 
380         nlohmann::json msg = {{"@odata.type", "#Event.v1_4_0.Event"},
381                               {"Id", std::to_string(eventSeqNum)},
382                               {"Name", "Event Log"},
383                               {"Events", logEntryArray}};
384 
385         this->sendEvent(msg.dump());
386         this->eventSeqNum++;
387     }
388 #endif
389 
390   private:
391     uint64_t eventSeqNum;
392     std::string host;
393     std::string port;
394     std::string path;
395     std::string uriProto;
396     std::shared_ptr<crow::HttpClient> conn;
397 };
398 
399 class EventServiceManager
400 {
401   private:
402     EventServiceManager(const EventServiceManager&) = delete;
403     EventServiceManager& operator=(const EventServiceManager&) = delete;
404     EventServiceManager(EventServiceManager&&) = delete;
405     EventServiceManager& operator=(EventServiceManager&&) = delete;
406 
407     EventServiceManager()
408     {
409         // TODO: Read the persistent data from store and populate.
410         // Populating with default.
411         enabled = true;
412         retryAttempts = 3;
413         retryTimeoutInterval = 30; // seconds
414     }
415 
416     std::string lastEventTStr;
417     boost::container::flat_map<std::string, std::shared_ptr<Subscription>>
418         subscriptionsMap;
419 
420   public:
421     bool enabled;
422     uint32_t retryAttempts;
423     uint32_t retryTimeoutInterval;
424 
425     static EventServiceManager& getInstance()
426     {
427         static EventServiceManager handler;
428         return handler;
429     }
430 
431     void updateSubscriptionData()
432     {
433         // Persist the config and subscription data.
434         // TODO: subscriptionsMap & configData need to be
435         // written to Persist store.
436         return;
437     }
438 
439     std::shared_ptr<Subscription> getSubscription(const std::string& id)
440     {
441         auto obj = subscriptionsMap.find(id);
442         if (obj == subscriptionsMap.end())
443         {
444             BMCWEB_LOG_ERROR << "No subscription exist with ID:" << id;
445             return nullptr;
446         }
447         std::shared_ptr<Subscription> subValue = obj->second;
448         return subValue;
449     }
450 
451     std::string addSubscription(const std::shared_ptr<Subscription> subValue)
452     {
453         std::srand(static_cast<uint32_t>(std::time(0)));
454         std::string id;
455 
456         int retry = 3;
457         while (retry)
458         {
459             id = std::to_string(std::rand());
460             auto inserted = subscriptionsMap.insert(std::pair(id, subValue));
461             if (inserted.second)
462             {
463                 break;
464             }
465             --retry;
466         };
467 
468         if (retry <= 0)
469         {
470             BMCWEB_LOG_ERROR << "Failed to generate random number";
471             return std::string("");
472         }
473 
474         updateSubscriptionData();
475 
476 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES
477         if (lastEventTStr.empty())
478         {
479             cacheLastEventTimestamp();
480         }
481 #endif
482         return id;
483     }
484 
485     bool isSubscriptionExist(const std::string& id)
486     {
487         auto obj = subscriptionsMap.find(id);
488         if (obj == subscriptionsMap.end())
489         {
490             return false;
491         }
492         return true;
493     }
494 
495     void deleteSubscription(const std::string& id)
496     {
497         auto obj = subscriptionsMap.find(id);
498         if (obj != subscriptionsMap.end())
499         {
500             subscriptionsMap.erase(obj);
501             updateSubscriptionData();
502         }
503     }
504 
505     size_t getNumberOfSubscriptions()
506     {
507         return subscriptionsMap.size();
508     }
509 
510     std::vector<std::string> getAllIDs()
511     {
512         std::vector<std::string> idList;
513         for (const auto& it : subscriptionsMap)
514         {
515             idList.emplace_back(it.first);
516         }
517         return idList;
518     }
519 
520     bool isDestinationExist(const std::string& destUrl)
521     {
522         for (const auto& it : subscriptionsMap)
523         {
524             std::shared_ptr<Subscription> entry = it.second;
525             if (entry->destinationUrl == destUrl)
526             {
527                 BMCWEB_LOG_ERROR << "Destination exist already" << destUrl;
528                 return true;
529             }
530         }
531         return false;
532     }
533 
534     void sendTestEventLog()
535     {
536         for (const auto& it : this->subscriptionsMap)
537         {
538             std::shared_ptr<Subscription> entry = it.second;
539             entry->sendTestEventLog();
540         }
541     }
542 
543 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES
544     void cacheLastEventTimestamp()
545     {
546         std::ifstream logStream(redfishEventLogFile);
547         if (!logStream.good())
548         {
549             BMCWEB_LOG_ERROR << " Redfish log file open failed \n";
550             return;
551         }
552         std::string logEntry;
553         while (std::getline(logStream, logEntry))
554         {
555             size_t space = logEntry.find_first_of(" ");
556             if (space == std::string::npos)
557             {
558                 // Shouldn't enter here but lets skip it.
559                 BMCWEB_LOG_DEBUG << "Invalid log entry found.";
560                 continue;
561             }
562             lastEventTStr = logEntry.substr(0, space);
563         }
564         BMCWEB_LOG_DEBUG << "Last Event time stamp set: " << lastEventTStr;
565     }
566 
567     void readEventLogsFromFile()
568     {
569         if (!getNumberOfSubscriptions())
570         {
571             // no subscriptions. Just return.
572             BMCWEB_LOG_DEBUG << "No Subscriptions\n";
573             return;
574         }
575         std::ifstream logStream(redfishEventLogFile);
576         if (!logStream.good())
577         {
578             BMCWEB_LOG_ERROR << " Redfish log file open failed";
579             return;
580         }
581 
582         std::vector<EventLogObjectsType> eventRecords;
583 
584         bool startLogCollection = false;
585         bool firstEntry = true;
586 
587         std::string logEntry;
588         while (std::getline(logStream, logEntry))
589         {
590             if (!startLogCollection)
591             {
592                 if (boost::starts_with(logEntry, lastEventTStr))
593                 {
594                     startLogCollection = true;
595                 }
596                 continue;
597             }
598 
599             std::string idStr;
600             if (!event_log::getUniqueEntryID(logEntry, idStr, firstEntry))
601             {
602                 continue;
603             }
604             firstEntry = false;
605 
606             std::string timestamp;
607             std::string messageID;
608             boost::beast::span<std::string> messageArgs;
609             if (event_log::getEventLogParams(logEntry, timestamp, messageID,
610                                              messageArgs) != 0)
611             {
612                 BMCWEB_LOG_DEBUG << "Read eventLog entry params failed";
613                 continue;
614             }
615 
616             std::string registryName;
617             std::string messageKey;
618             event_log::getRegistryAndMessageKey(messageID, registryName,
619                                                 messageKey);
620             if (registryName.empty() || messageKey.empty())
621             {
622                 continue;
623             }
624 
625             lastEventTStr = timestamp;
626             eventRecords.emplace_back(idStr, timestamp, messageID, registryName,
627                                       messageKey, messageArgs);
628         }
629 
630         for (const auto& it : this->subscriptionsMap)
631         {
632             std::shared_ptr<Subscription> entry = it.second;
633             if (entry->eventFormatType == "Event")
634             {
635                 entry->filterAndSendEventLogs(eventRecords);
636             }
637         }
638     }
639 
640     static void watchRedfishEventLogFile()
641     {
642         if (inotifyConn == nullptr)
643         {
644             return;
645         }
646 
647         static std::array<char, 1024> readBuffer;
648 
649         inotifyConn->async_read_some(
650             boost::asio::buffer(readBuffer),
651             [&](const boost::system::error_code& ec,
652                 const std::size_t& bytesTransferred) {
653                 if (ec)
654                 {
655                     BMCWEB_LOG_ERROR << "Callback Error: " << ec.message();
656                     return;
657                 }
658                 std::size_t index = 0;
659                 while ((index + sizeof(inotify_event)) <= bytesTransferred)
660                 {
661                     struct inotify_event event;
662                     std::memcpy(&event, &readBuffer[index],
663                                 sizeof(inotify_event));
664                     if (event.mask == inotifyFileAction)
665                     {
666                         EventServiceManager::getInstance()
667                             .readEventLogsFromFile();
668                     }
669                     index += (sizeof(inotify_event) + event.len);
670                 }
671 
672                 watchRedfishEventLogFile();
673             });
674     }
675 
676     static int startEventLogMonitor(boost::asio::io_service& ioc)
677     {
678         inotifyConn =
679             std::make_shared<boost::asio::posix::stream_descriptor>(ioc);
680         int fd = inotify_init1(IN_NONBLOCK);
681         if (fd == -1)
682         {
683             BMCWEB_LOG_ERROR << "inotify_init1 failed.";
684             return -1;
685         }
686         auto wd = inotify_add_watch(fd, redfishEventLogFile, inotifyFileAction);
687         if (wd == -1)
688         {
689             BMCWEB_LOG_ERROR
690                 << "inotify_add_watch failed for redfish log file.";
691             return -1;
692         }
693 
694         // monitor redfish event log file
695         inotifyConn->assign(fd);
696         watchRedfishEventLogFile();
697 
698         return 0;
699     }
700 
701 #endif
702 };
703 
704 } // namespace redfish
705