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_service_store.hpp"
20 #include "filter_expr_executor.hpp"
21 #include "generated/enums/event.hpp"
22 #include "generated/enums/log_entry.hpp"
23 #include "http_client.hpp"
24 #include "metric_report.hpp"
25 #include "ossl_random.hpp"
26 #include "persistent_data.hpp"
27 #include "registries.hpp"
28 #include "registries_selector.hpp"
29 #include "str_utility.hpp"
30 #include "utility.hpp"
31 #include "utils/json_utils.hpp"
32 #include "utils/time_utils.hpp"
33
34 #include <sys/inotify.h>
35
36 #include <boost/asio/io_context.hpp>
37 #include <boost/circular_buffer.hpp>
38 #include <boost/container/flat_map.hpp>
39 #include <boost/url/format.hpp>
40 #include <boost/url/url_view_base.hpp>
41 #include <sdbusplus/bus/match.hpp>
42
43 #include <algorithm>
44 #include <cstdlib>
45 #include <ctime>
46 #include <fstream>
47 #include <memory>
48 #include <ranges>
49 #include <span>
50
51 namespace redfish
52 {
53
54 static constexpr const char* eventFormatType = "Event";
55 static constexpr const char* metricReportFormatType = "MetricReport";
56
57 static constexpr const char* subscriptionTypeSSE = "SSE";
58 static constexpr const char* eventServiceFile =
59 "/var/lib/bmcweb/eventservice_config.json";
60
61 static constexpr const uint8_t maxNoOfSubscriptions = 20;
62 static constexpr const uint8_t maxNoOfSSESubscriptions = 10;
63
64 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
65 static std::optional<boost::asio::posix::stream_descriptor> inotifyConn;
66 static constexpr const char* redfishEventLogDir = "/var/log";
67 static constexpr const char* redfishEventLogFile = "/var/log/redfish";
68 static constexpr const size_t iEventSize = sizeof(inotify_event);
69
70 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
71 static int inotifyFd = -1;
72 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
73 static int dirWatchDesc = -1;
74 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
75 static int fileWatchDesc = -1;
76 struct EventLogObjectsType
77 {
78 std::string id;
79 std::string timestamp;
80 std::string messageId;
81 std::vector<std::string> messageArgs;
82 };
83
84 namespace registries
85 {
86 static const Message*
getMsgFromRegistry(const std::string & messageKey,const std::span<const MessageEntry> & registry)87 getMsgFromRegistry(const std::string& messageKey,
88 const std::span<const MessageEntry>& registry)
89 {
90 std::span<const MessageEntry>::iterator messageIt = std::ranges::find_if(
91 registry, [&messageKey](const MessageEntry& messageEntry) {
92 return messageKey == messageEntry.first;
93 });
94 if (messageIt != registry.end())
95 {
96 return &messageIt->second;
97 }
98
99 return nullptr;
100 }
101
formatMessage(std::string_view messageID)102 static const Message* formatMessage(std::string_view messageID)
103 {
104 // Redfish MessageIds are in the form
105 // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find
106 // the right Message
107 std::vector<std::string> fields;
108 fields.reserve(4);
109
110 bmcweb::split(fields, messageID, '.');
111 if (fields.size() != 4)
112 {
113 return nullptr;
114 }
115 const std::string& registryName = fields[0];
116 const std::string& messageKey = fields[3];
117
118 // Find the right registry and check it for the MessageKey
119 return getMsgFromRegistry(messageKey, getRegistryFromPrefix(registryName));
120 }
121 } // namespace registries
122
123 namespace event_log
124 {
getUniqueEntryID(const std::string & logEntry,std::string & entryID)125 inline bool getUniqueEntryID(const std::string& logEntry, std::string& entryID)
126 {
127 static time_t prevTs = 0;
128 static int index = 0;
129
130 // Get the entry timestamp
131 std::time_t curTs = 0;
132 std::tm timeStruct = {};
133 std::istringstream entryStream(logEntry);
134 if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S"))
135 {
136 curTs = std::mktime(&timeStruct);
137 if (curTs == -1)
138 {
139 return false;
140 }
141 }
142 // If the timestamp isn't unique, increment the index
143 index = (curTs == prevTs) ? index + 1 : 0;
144
145 // Save the timestamp
146 prevTs = curTs;
147
148 entryID = std::to_string(curTs);
149 if (index > 0)
150 {
151 entryID += "_" + std::to_string(index);
152 }
153 return true;
154 }
155
getEventLogParams(const std::string & logEntry,std::string & timestamp,std::string & messageID,std::vector<std::string> & messageArgs)156 inline int getEventLogParams(const std::string& logEntry,
157 std::string& timestamp, std::string& messageID,
158 std::vector<std::string>& messageArgs)
159 {
160 // The redfish log format is "<Timestamp> <MessageId>,<MessageArgs>"
161 // First get the Timestamp
162 size_t space = logEntry.find_first_of(' ');
163 if (space == std::string::npos)
164 {
165 BMCWEB_LOG_ERROR("EventLog Params: could not find first space: {}",
166 logEntry);
167 return -EINVAL;
168 }
169 timestamp = logEntry.substr(0, space);
170 // Then get the log contents
171 size_t entryStart = logEntry.find_first_not_of(' ', space);
172 if (entryStart == std::string::npos)
173 {
174 BMCWEB_LOG_ERROR("EventLog Params: could not find log contents: {}",
175 logEntry);
176 return -EINVAL;
177 }
178 std::string_view entry(logEntry);
179 entry.remove_prefix(entryStart);
180 // Use split to separate the entry into its fields
181 std::vector<std::string> logEntryFields;
182 bmcweb::split(logEntryFields, entry, ',');
183 // We need at least a MessageId to be valid
184 if (logEntryFields.empty())
185 {
186 BMCWEB_LOG_ERROR("EventLog Params: could not find entry fields: {}",
187 logEntry);
188 return -EINVAL;
189 }
190 messageID = logEntryFields[0];
191
192 // Get the MessageArgs from the log if there are any
193 if (logEntryFields.size() > 1)
194 {
195 const std::string& messageArgsStart = logEntryFields[1];
196 // If the first string is empty, assume there are no MessageArgs
197 if (!messageArgsStart.empty())
198 {
199 messageArgs.assign(logEntryFields.begin() + 1,
200 logEntryFields.end());
201 }
202 }
203
204 return 0;
205 }
206
getRegistryAndMessageKey(const std::string & messageID,std::string & registryName,std::string & messageKey)207 inline void getRegistryAndMessageKey(const std::string& messageID,
208 std::string& registryName,
209 std::string& messageKey)
210 {
211 // Redfish MessageIds are in the form
212 // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find
213 // the right Message
214 std::vector<std::string> fields;
215 fields.reserve(4);
216 bmcweb::split(fields, messageID, '.');
217 if (fields.size() == 4)
218 {
219 registryName = fields[0];
220 messageKey = fields[3];
221 }
222 }
223
formatEventLogEntry(const std::string & logEntryID,const std::string & messageID,const std::span<std::string_view> messageArgs,std::string timestamp,const std::string & customText,nlohmann::json::object_t & logEntryJson)224 inline int formatEventLogEntry(
225 const std::string& logEntryID, const std::string& messageID,
226 const std::span<std::string_view> messageArgs, std::string timestamp,
227 const std::string& customText, nlohmann::json::object_t& logEntryJson)
228 {
229 // Get the Message from the MessageRegistry
230 const registries::Message* message = registries::formatMessage(messageID);
231
232 if (message == nullptr)
233 {
234 return -1;
235 }
236
237 std::string msg =
238 redfish::registries::fillMessageArgs(messageArgs, message->message);
239 if (msg.empty())
240 {
241 return -1;
242 }
243
244 // Get the Created time from the timestamp. The log timestamp is in
245 // RFC3339 format which matches the Redfish format except for the
246 // fractional seconds between the '.' and the '+', so just remove them.
247 std::size_t dot = timestamp.find_first_of('.');
248 std::size_t plus = timestamp.find_first_of('+', dot);
249 if (dot != std::string::npos && plus != std::string::npos)
250 {
251 timestamp.erase(dot, plus - dot);
252 }
253
254 // Fill in the log entry with the gathered data
255 logEntryJson["EventId"] = logEntryID;
256
257 logEntryJson["Severity"] = message->messageSeverity;
258 logEntryJson["Message"] = std::move(msg);
259 logEntryJson["MessageId"] = messageID;
260 logEntryJson["MessageArgs"] = messageArgs;
261 logEntryJson["EventTimestamp"] = std::move(timestamp);
262 logEntryJson["Context"] = customText;
263 return 0;
264 }
265
266 } // namespace event_log
267
268 class Subscription : public persistent_data::UserSubscription
269 {
270 public:
271 Subscription(const Subscription&) = delete;
272 Subscription& operator=(const Subscription&) = delete;
273 Subscription(Subscription&&) = delete;
274 Subscription& operator=(Subscription&&) = delete;
275
Subscription(const boost::urls::url_view_base & url,boost::asio::io_context & ioc)276 Subscription(const boost::urls::url_view_base& url,
277 boost::asio::io_context& ioc) :
278 policy(std::make_shared<crow::ConnectionPolicy>())
279 {
280 destinationUrl = url;
281 client.emplace(ioc, policy);
282 // Subscription constructor
283 policy->invalidResp = retryRespHandler;
284 }
285
Subscription(crow::sse_socket::Connection & connIn)286 explicit Subscription(crow::sse_socket::Connection& connIn) :
287 sseConn(&connIn)
288 {}
289
290 ~Subscription() = default;
291
sendEventToSubscriber(std::string && msg)292 bool sendEventToSubscriber(std::string&& msg)
293 {
294 persistent_data::EventServiceConfig eventServiceConfig =
295 persistent_data::EventServiceStore::getInstance()
296 .getEventServiceConfig();
297 if (!eventServiceConfig.enabled)
298 {
299 return false;
300 }
301
302 if (client)
303 {
304 client->sendData(
305 std::move(msg), destinationUrl,
306 static_cast<ensuressl::VerifyCertificate>(verifyCertificate),
307 httpHeaders, boost::beast::http::verb::post);
308 return true;
309 }
310
311 if (sseConn != nullptr)
312 {
313 eventSeqNum++;
314 sseConn->sendSseEvent(std::to_string(eventSeqNum), msg);
315 }
316 return true;
317 }
318
eventMatchesFilter(const nlohmann::json::object_t & eventMessage,std::string_view resType)319 bool eventMatchesFilter(const nlohmann::json::object_t& eventMessage,
320 std::string_view resType)
321 {
322 // If resourceTypes list is empty, assume all
323 if (!resourceTypes.empty())
324 {
325 // Search the resourceTypes list for the subscription.
326 auto resourceTypeIndex = std::ranges::find_if(
327 resourceTypes, [resType](const std::string& rtEntry) {
328 return rtEntry == resType;
329 });
330 if (resourceTypeIndex == resourceTypes.end())
331 {
332 BMCWEB_LOG_DEBUG("Not subscribed to this resource");
333 return false;
334 }
335 BMCWEB_LOG_DEBUG("ResourceType {} found in the subscribed list",
336 resType);
337 }
338
339 // If registryMsgIds list is empty, assume all
340 if (!registryMsgIds.empty())
341 {
342 auto eventJson = eventMessage.find("MessageId");
343 if (eventJson == eventMessage.end())
344 {
345 BMCWEB_LOG_DEBUG("'MessageId' not present");
346 return false;
347 }
348
349 const std::string* messageId =
350 eventJson->second.get_ptr<const std::string*>();
351 if (messageId == nullptr)
352 {
353 BMCWEB_LOG_ERROR("EventType wasn't a string???");
354 return false;
355 }
356
357 std::string registry;
358 std::string messageKey;
359 event_log::getRegistryAndMessageKey(*messageId, registry,
360 messageKey);
361
362 BMCWEB_LOG_DEBUG("extracted registry {}", registry);
363 BMCWEB_LOG_DEBUG("extracted message key {}", messageKey);
364
365 auto obj = std::ranges::find(registryMsgIds, registry);
366 if (obj == registryMsgIds.end())
367 {
368 BMCWEB_LOG_DEBUG("did not find registry {} in registryMsgIds",
369 registry);
370 BMCWEB_LOG_DEBUG("registryMsgIds has {} entries",
371 registryMsgIds.size());
372 return false;
373 }
374 }
375
376 if (filter)
377 {
378 if (!memberMatches(eventMessage, *filter))
379 {
380 BMCWEB_LOG_DEBUG("Filter didn't match");
381 return false;
382 }
383 }
384
385 return true;
386 }
387
sendTestEventLog()388 bool sendTestEventLog()
389 {
390 nlohmann::json::array_t logEntryArray;
391 nlohmann::json& logEntryJson = logEntryArray.emplace_back();
392
393 logEntryJson["EventId"] = "TestID";
394 logEntryJson["Severity"] = log_entry::EventSeverity::OK;
395 logEntryJson["Message"] = "Generated test event";
396 logEntryJson["MessageId"] = "OpenBMC.0.2.TestEventLog";
397 // MemberId is 0 : since we are sending one event record.
398 logEntryJson["MemberId"] = 0;
399 logEntryJson["MessageArgs"] = nlohmann::json::array();
400 logEntryJson["EventTimestamp"] =
401 redfish::time_utils::getDateTimeOffsetNow().first;
402 logEntryJson["Context"] = customText;
403
404 nlohmann::json msg;
405 msg["@odata.type"] = "#Event.v1_4_0.Event";
406 msg["Id"] = std::to_string(eventSeqNum);
407 msg["Name"] = "Event Log";
408 msg["Events"] = logEntryArray;
409
410 std::string strMsg =
411 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
412 return sendEventToSubscriber(std::move(strMsg));
413 }
414
filterAndSendEventLogs(const std::vector<EventLogObjectsType> & eventRecords)415 void filterAndSendEventLogs(
416 const std::vector<EventLogObjectsType>& eventRecords)
417 {
418 nlohmann::json::array_t logEntryArray;
419 for (const EventLogObjectsType& logEntry : eventRecords)
420 {
421 std::vector<std::string_view> messageArgsView(
422 logEntry.messageArgs.begin(), logEntry.messageArgs.end());
423
424 nlohmann::json::object_t bmcLogEntry;
425 if (event_log::formatEventLogEntry(
426 logEntry.id, logEntry.messageId, messageArgsView,
427 logEntry.timestamp, customText, bmcLogEntry) != 0)
428 {
429 BMCWEB_LOG_DEBUG("Read eventLog entry failed");
430 continue;
431 }
432
433 if (!eventMatchesFilter(bmcLogEntry, ""))
434 {
435 BMCWEB_LOG_DEBUG("Event {} did not match the filter",
436 nlohmann::json(bmcLogEntry).dump());
437 continue;
438 }
439
440 logEntryArray.emplace_back(std::move(bmcLogEntry));
441 }
442
443 if (logEntryArray.empty())
444 {
445 BMCWEB_LOG_DEBUG("No log entries available to be transferred.");
446 return;
447 }
448
449 nlohmann::json msg;
450 msg["@odata.type"] = "#Event.v1_4_0.Event";
451 msg["Id"] = std::to_string(eventSeqNum);
452 msg["Name"] = "Event Log";
453 msg["Events"] = std::move(logEntryArray);
454 std::string strMsg =
455 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
456 sendEventToSubscriber(std::move(strMsg));
457 eventSeqNum++;
458 }
459
filterAndSendReports(const std::string & reportId,const telemetry::TimestampReadings & var)460 void filterAndSendReports(const std::string& reportId,
461 const telemetry::TimestampReadings& var)
462 {
463 boost::urls::url mrdUri = boost::urls::format(
464 "/redfish/v1/TelemetryService/MetricReportDefinitions/{}",
465 reportId);
466
467 // Empty list means no filter. Send everything.
468 if (!metricReportDefinitions.empty())
469 {
470 if (std::ranges::find(metricReportDefinitions, mrdUri.buffer()) ==
471 metricReportDefinitions.end())
472 {
473 return;
474 }
475 }
476
477 nlohmann::json msg;
478 if (!telemetry::fillReport(msg, reportId, var))
479 {
480 BMCWEB_LOG_ERROR("Failed to fill the MetricReport for DBus "
481 "Report with id {}",
482 reportId);
483 return;
484 }
485
486 // Context is set by user during Event subscription and it must be
487 // set for MetricReport response.
488 if (!customText.empty())
489 {
490 msg["Context"] = customText;
491 }
492
493 std::string strMsg =
494 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
495 sendEventToSubscriber(std::move(strMsg));
496 }
497
updateRetryConfig(uint32_t retryAttempts,uint32_t retryTimeoutInterval)498 void updateRetryConfig(uint32_t retryAttempts,
499 uint32_t retryTimeoutInterval)
500 {
501 if (policy == nullptr)
502 {
503 BMCWEB_LOG_DEBUG("Retry policy was nullptr, ignoring set");
504 return;
505 }
506 policy->maxRetryAttempts = retryAttempts;
507 policy->retryIntervalSecs = std::chrono::seconds(retryTimeoutInterval);
508 }
509
getEventSeqNum() const510 uint64_t getEventSeqNum() const
511 {
512 return eventSeqNum;
513 }
514
setSubscriptionId(const std::string & id2)515 void setSubscriptionId(const std::string& id2)
516 {
517 BMCWEB_LOG_DEBUG("Subscription ID: {}", id2);
518 subId = id2;
519 }
520
getSubscriptionId()521 std::string getSubscriptionId()
522 {
523 return subId;
524 }
525
matchSseId(const crow::sse_socket::Connection & thisConn)526 bool matchSseId(const crow::sse_socket::Connection& thisConn)
527 {
528 return &thisConn == sseConn;
529 }
530
531 // Check used to indicate what response codes are valid as part of our retry
532 // policy. 2XX is considered acceptable
retryRespHandler(unsigned int respCode)533 static boost::system::error_code retryRespHandler(unsigned int respCode)
534 {
535 BMCWEB_LOG_DEBUG(
536 "Checking response code validity for SubscriptionEvent");
537 if ((respCode < 200) || (respCode >= 300))
538 {
539 return boost::system::errc::make_error_code(
540 boost::system::errc::result_out_of_range);
541 }
542
543 // Return 0 if the response code is valid
544 return boost::system::errc::make_error_code(
545 boost::system::errc::success);
546 }
547
548 private:
549 std::string subId;
550 uint64_t eventSeqNum = 1;
551 boost::urls::url host;
552 std::shared_ptr<crow::ConnectionPolicy> policy;
553 crow::sse_socket::Connection* sseConn = nullptr;
554
555 std::optional<crow::HttpClient> client;
556
557 public:
558 std::optional<filter_ast::LogicalAnd> filter;
559 };
560
561 class EventServiceManager
562 {
563 private:
564 bool serviceEnabled = false;
565 uint32_t retryAttempts = 0;
566 uint32_t retryTimeoutInterval = 0;
567
568 std::streampos redfishLogFilePosition{0};
569 size_t noOfEventLogSubscribers{0};
570 size_t noOfMetricReportSubscribers{0};
571 std::shared_ptr<sdbusplus::bus::match_t> matchTelemetryMonitor;
572 boost::container::flat_map<std::string, std::shared_ptr<Subscription>>
573 subscriptionsMap;
574
575 uint64_t eventId{1};
576
577 struct Event
578 {
579 std::string id;
580 nlohmann::json message;
581 };
582
583 constexpr static size_t maxMessages = 200;
584 boost::circular_buffer<Event> messages{maxMessages};
585
586 boost::asio::io_context& ioc;
587
588 public:
589 EventServiceManager(const EventServiceManager&) = delete;
590 EventServiceManager& operator=(const EventServiceManager&) = delete;
591 EventServiceManager(EventServiceManager&&) = delete;
592 EventServiceManager& operator=(EventServiceManager&&) = delete;
593 ~EventServiceManager() = default;
594
EventServiceManager(boost::asio::io_context & iocIn)595 explicit EventServiceManager(boost::asio::io_context& iocIn) : ioc(iocIn)
596 {
597 // Load config from persist store.
598 initConfig();
599 }
600
601 static EventServiceManager&
getInstance(boost::asio::io_context * ioc=nullptr)602 getInstance(boost::asio::io_context* ioc = nullptr)
603 {
604 static EventServiceManager handler(*ioc);
605 return handler;
606 }
607
initConfig()608 void initConfig()
609 {
610 loadOldBehavior();
611
612 persistent_data::EventServiceConfig eventServiceConfig =
613 persistent_data::EventServiceStore::getInstance()
614 .getEventServiceConfig();
615
616 serviceEnabled = eventServiceConfig.enabled;
617 retryAttempts = eventServiceConfig.retryAttempts;
618 retryTimeoutInterval = eventServiceConfig.retryTimeoutInterval;
619
620 for (const auto& it : persistent_data::EventServiceStore::getInstance()
621 .subscriptionsConfigMap)
622 {
623 std::shared_ptr<persistent_data::UserSubscription> newSub =
624 it.second;
625
626 boost::system::result<boost::urls::url> url =
627 boost::urls::parse_absolute_uri(newSub->destinationUrl);
628
629 if (!url)
630 {
631 BMCWEB_LOG_ERROR(
632 "Failed to validate and split destination url");
633 continue;
634 }
635 std::shared_ptr<Subscription> subValue =
636 std::make_shared<Subscription>(*url, ioc);
637
638 subValue->id = newSub->id;
639 subValue->destinationUrl = newSub->destinationUrl;
640 subValue->protocol = newSub->protocol;
641 subValue->verifyCertificate = newSub->verifyCertificate;
642 subValue->retryPolicy = newSub->retryPolicy;
643 subValue->customText = newSub->customText;
644 subValue->eventFormatType = newSub->eventFormatType;
645 subValue->subscriptionType = newSub->subscriptionType;
646 subValue->registryMsgIds = newSub->registryMsgIds;
647 subValue->registryPrefixes = newSub->registryPrefixes;
648 subValue->resourceTypes = newSub->resourceTypes;
649 subValue->httpHeaders = newSub->httpHeaders;
650 subValue->metricReportDefinitions = newSub->metricReportDefinitions;
651
652 if (subValue->id.empty())
653 {
654 BMCWEB_LOG_ERROR("Failed to add subscription");
655 }
656 subscriptionsMap.insert(std::pair(subValue->id, subValue));
657
658 updateNoOfSubscribersCount();
659
660 if constexpr (!BMCWEB_REDFISH_DBUS_LOG)
661 {
662 cacheRedfishLogFile();
663 }
664
665 // Update retry configuration.
666 subValue->updateRetryConfig(retryAttempts, retryTimeoutInterval);
667 }
668 }
669
loadOldBehavior()670 static void loadOldBehavior()
671 {
672 std::ifstream eventConfigFile(eventServiceFile);
673 if (!eventConfigFile.good())
674 {
675 BMCWEB_LOG_DEBUG("Old eventService config not exist");
676 return;
677 }
678 auto jsonData = nlohmann::json::parse(eventConfigFile, nullptr, false);
679 if (jsonData.is_discarded())
680 {
681 BMCWEB_LOG_ERROR("Old eventService config parse error.");
682 return;
683 }
684
685 const nlohmann::json::object_t* obj =
686 jsonData.get_ptr<const nlohmann::json::object_t*>();
687 for (const auto& item : *obj)
688 {
689 if (item.first == "Configuration")
690 {
691 persistent_data::EventServiceStore::getInstance()
692 .getEventServiceConfig()
693 .fromJson(item.second);
694 }
695 else if (item.first == "Subscriptions")
696 {
697 for (const auto& elem : item.second)
698 {
699 std::shared_ptr<persistent_data::UserSubscription>
700 newSubscription =
701 persistent_data::UserSubscription::fromJson(elem,
702 true);
703 if (newSubscription == nullptr)
704 {
705 BMCWEB_LOG_ERROR("Problem reading subscription "
706 "from old persistent store");
707 continue;
708 }
709
710 std::uniform_int_distribution<uint32_t> dist(0);
711 bmcweb::OpenSSLGenerator gen;
712
713 std::string id;
714
715 int retry = 3;
716 while (retry != 0)
717 {
718 id = std::to_string(dist(gen));
719 if (gen.error())
720 {
721 retry = 0;
722 break;
723 }
724 newSubscription->id = id;
725 auto inserted =
726 persistent_data::EventServiceStore::getInstance()
727 .subscriptionsConfigMap.insert(
728 std::pair(id, newSubscription));
729 if (inserted.second)
730 {
731 break;
732 }
733 --retry;
734 }
735
736 if (retry <= 0)
737 {
738 BMCWEB_LOG_ERROR(
739 "Failed to generate random number from old "
740 "persistent store");
741 continue;
742 }
743 }
744 }
745
746 persistent_data::getConfig().writeData();
747 std::error_code ec;
748 std::filesystem::remove(eventServiceFile, ec);
749 if (ec)
750 {
751 BMCWEB_LOG_DEBUG(
752 "Failed to remove old event service file. Ignoring");
753 }
754 else
755 {
756 BMCWEB_LOG_DEBUG("Remove old eventservice config");
757 }
758 }
759 }
760
updateSubscriptionData() const761 void updateSubscriptionData() const
762 {
763 persistent_data::EventServiceStore::getInstance()
764 .eventServiceConfig.enabled = serviceEnabled;
765 persistent_data::EventServiceStore::getInstance()
766 .eventServiceConfig.retryAttempts = retryAttempts;
767 persistent_data::EventServiceStore::getInstance()
768 .eventServiceConfig.retryTimeoutInterval = retryTimeoutInterval;
769
770 persistent_data::getConfig().writeData();
771 }
772
setEventServiceConfig(const persistent_data::EventServiceConfig & cfg)773 void setEventServiceConfig(const persistent_data::EventServiceConfig& cfg)
774 {
775 bool updateConfig = false;
776 bool updateRetryCfg = false;
777
778 if (serviceEnabled != cfg.enabled)
779 {
780 serviceEnabled = cfg.enabled;
781 if (serviceEnabled && noOfMetricReportSubscribers != 0U)
782 {
783 registerMetricReportSignal();
784 }
785 else
786 {
787 unregisterMetricReportSignal();
788 }
789 updateConfig = true;
790 }
791
792 if (retryAttempts != cfg.retryAttempts)
793 {
794 retryAttempts = cfg.retryAttempts;
795 updateConfig = true;
796 updateRetryCfg = true;
797 }
798
799 if (retryTimeoutInterval != cfg.retryTimeoutInterval)
800 {
801 retryTimeoutInterval = cfg.retryTimeoutInterval;
802 updateConfig = true;
803 updateRetryCfg = true;
804 }
805
806 if (updateConfig)
807 {
808 updateSubscriptionData();
809 }
810
811 if (updateRetryCfg)
812 {
813 // Update the changed retry config to all subscriptions
814 for (const auto& it :
815 EventServiceManager::getInstance().subscriptionsMap)
816 {
817 Subscription& entry = *it.second;
818 entry.updateRetryConfig(retryAttempts, retryTimeoutInterval);
819 }
820 }
821 }
822
updateNoOfSubscribersCount()823 void updateNoOfSubscribersCount()
824 {
825 size_t eventLogSubCount = 0;
826 size_t metricReportSubCount = 0;
827 for (const auto& it : subscriptionsMap)
828 {
829 std::shared_ptr<Subscription> entry = it.second;
830 if (entry->eventFormatType == eventFormatType)
831 {
832 eventLogSubCount++;
833 }
834 else if (entry->eventFormatType == metricReportFormatType)
835 {
836 metricReportSubCount++;
837 }
838 }
839
840 noOfEventLogSubscribers = eventLogSubCount;
841 if (noOfMetricReportSubscribers != metricReportSubCount)
842 {
843 noOfMetricReportSubscribers = metricReportSubCount;
844 if (noOfMetricReportSubscribers != 0U)
845 {
846 registerMetricReportSignal();
847 }
848 else
849 {
850 unregisterMetricReportSignal();
851 }
852 }
853 }
854
getSubscription(const std::string & id)855 std::shared_ptr<Subscription> getSubscription(const std::string& id)
856 {
857 auto obj = subscriptionsMap.find(id);
858 if (obj == subscriptionsMap.end())
859 {
860 BMCWEB_LOG_ERROR("No subscription exist with ID:{}", id);
861 return nullptr;
862 }
863 std::shared_ptr<Subscription> subValue = obj->second;
864 return subValue;
865 }
866
867 std::string
addSubscriptionInternal(const std::shared_ptr<Subscription> & subValue)868 addSubscriptionInternal(const std::shared_ptr<Subscription>& subValue)
869 {
870 std::uniform_int_distribution<uint32_t> dist(0);
871 bmcweb::OpenSSLGenerator gen;
872
873 std::string id;
874
875 int retry = 3;
876 while (retry != 0)
877 {
878 id = std::to_string(dist(gen));
879 if (gen.error())
880 {
881 retry = 0;
882 break;
883 }
884 auto inserted = subscriptionsMap.insert(std::pair(id, subValue));
885 if (inserted.second)
886 {
887 break;
888 }
889 --retry;
890 }
891
892 if (retry <= 0)
893 {
894 BMCWEB_LOG_ERROR("Failed to generate random number");
895 return "";
896 }
897
898 std::shared_ptr<persistent_data::UserSubscription> newSub =
899 std::make_shared<persistent_data::UserSubscription>();
900 newSub->id = id;
901 newSub->destinationUrl = subValue->destinationUrl;
902 newSub->protocol = subValue->protocol;
903 newSub->retryPolicy = subValue->retryPolicy;
904 newSub->customText = subValue->customText;
905 newSub->eventFormatType = subValue->eventFormatType;
906 newSub->subscriptionType = subValue->subscriptionType;
907 newSub->registryMsgIds = subValue->registryMsgIds;
908 newSub->registryPrefixes = subValue->registryPrefixes;
909 newSub->resourceTypes = subValue->resourceTypes;
910 newSub->httpHeaders = subValue->httpHeaders;
911 newSub->metricReportDefinitions = subValue->metricReportDefinitions;
912 persistent_data::EventServiceStore::getInstance()
913 .subscriptionsConfigMap.emplace(newSub->id, newSub);
914
915 updateNoOfSubscribersCount();
916
917 if constexpr (!BMCWEB_REDFISH_DBUS_LOG)
918 {
919 if (redfishLogFilePosition != 0)
920 {
921 cacheRedfishLogFile();
922 }
923 }
924 // Update retry configuration.
925 subValue->updateRetryConfig(retryAttempts, retryTimeoutInterval);
926
927 // Set Subscription ID for back trace
928 subValue->setSubscriptionId(id);
929
930 return id;
931 }
932
933 std::string
addSSESubscription(const std::shared_ptr<Subscription> & subValue,std::string_view lastEventId)934 addSSESubscription(const std::shared_ptr<Subscription>& subValue,
935 std::string_view lastEventId)
936 {
937 std::string id = addSubscriptionInternal(subValue);
938
939 if (!lastEventId.empty())
940 {
941 BMCWEB_LOG_INFO("Attempting to find message for last id {}",
942 lastEventId);
943 boost::circular_buffer<Event>::iterator lastEvent =
944 std::find_if(messages.begin(), messages.end(),
945 [&lastEventId](const Event& event) {
946 return event.id == lastEventId;
947 });
948 // Can't find a matching ID
949 if (lastEvent == messages.end())
950 {
951 nlohmann::json msg = messages::eventBufferExceeded();
952 // If the buffer overloaded, send all messages.
953 subValue->sendEventToSubscriber(msg);
954 lastEvent = messages.begin();
955 }
956 else
957 {
958 // Skip the last event the user already has
959 lastEvent++;
960 }
961
962 for (boost::circular_buffer<Event>::const_iterator event =
963 lastEvent;
964 lastEvent != messages.end(); lastEvent++)
965 {
966 subValue->sendEventToSubscriber(event->message);
967 }
968 }
969 return id;
970 }
971
972 std::string
addPushSubscription(const std::shared_ptr<Subscription> & subValue)973 addPushSubscription(const std::shared_ptr<Subscription>& subValue)
974 {
975 std::string id = addSubscriptionInternal(subValue);
976
977 updateSubscriptionData();
978 return id;
979 }
980
isSubscriptionExist(const std::string & id)981 bool isSubscriptionExist(const std::string& id)
982 {
983 auto obj = subscriptionsMap.find(id);
984 return obj != subscriptionsMap.end();
985 }
986
deleteSubscription(const std::string & id)987 void deleteSubscription(const std::string& id)
988 {
989 auto obj = subscriptionsMap.find(id);
990 if (obj != subscriptionsMap.end())
991 {
992 subscriptionsMap.erase(obj);
993 auto obj2 = persistent_data::EventServiceStore::getInstance()
994 .subscriptionsConfigMap.find(id);
995 persistent_data::EventServiceStore::getInstance()
996 .subscriptionsConfigMap.erase(obj2);
997 updateNoOfSubscribersCount();
998 updateSubscriptionData();
999 }
1000 }
1001
deleteSseSubscription(const crow::sse_socket::Connection & thisConn)1002 void deleteSseSubscription(const crow::sse_socket::Connection& thisConn)
1003 {
1004 for (auto it = subscriptionsMap.begin(); it != subscriptionsMap.end();)
1005 {
1006 std::shared_ptr<Subscription> entry = it->second;
1007 bool entryIsThisConn = entry->matchSseId(thisConn);
1008 if (entryIsThisConn)
1009 {
1010 persistent_data::EventServiceStore::getInstance()
1011 .subscriptionsConfigMap.erase(
1012 it->second->getSubscriptionId());
1013 it = subscriptionsMap.erase(it);
1014 return;
1015 }
1016 it++;
1017 }
1018 }
1019
getNumberOfSubscriptions() const1020 size_t getNumberOfSubscriptions() const
1021 {
1022 return subscriptionsMap.size();
1023 }
1024
getNumberOfSSESubscriptions() const1025 size_t getNumberOfSSESubscriptions() const
1026 {
1027 auto size = std::ranges::count_if(
1028 subscriptionsMap,
1029 [](const std::pair<std::string, std::shared_ptr<Subscription>>&
1030 entry) {
1031 return (entry.second->subscriptionType == subscriptionTypeSSE);
1032 });
1033 return static_cast<size_t>(size);
1034 }
1035
getAllIDs()1036 std::vector<std::string> getAllIDs()
1037 {
1038 std::vector<std::string> idList;
1039 for (const auto& it : subscriptionsMap)
1040 {
1041 idList.emplace_back(it.first);
1042 }
1043 return idList;
1044 }
1045
sendTestEventLog()1046 bool sendTestEventLog()
1047 {
1048 for (const auto& it : subscriptionsMap)
1049 {
1050 std::shared_ptr<Subscription> entry = it.second;
1051 if (!entry->sendTestEventLog())
1052 {
1053 return false;
1054 }
1055 }
1056 return true;
1057 }
1058
sendEvent(nlohmann::json::object_t eventMessage,std::string_view origin,std::string_view resourceType)1059 void sendEvent(nlohmann::json::object_t eventMessage,
1060 std::string_view origin, std::string_view resourceType)
1061 {
1062 eventMessage["EventId"] = eventId;
1063
1064 eventMessage["EventTimestamp"] =
1065 redfish::time_utils::getDateTimeOffsetNow().first;
1066 eventMessage["OriginOfCondition"] = origin;
1067
1068 // MemberId is 0 : since we are sending one event record.
1069 eventMessage["MemberId"] = 0;
1070
1071 messages.push_back(Event(std::to_string(eventId), eventMessage));
1072
1073 for (auto& it : subscriptionsMap)
1074 {
1075 std::shared_ptr<Subscription>& entry = it.second;
1076 if (!entry->eventMatchesFilter(eventMessage, resourceType))
1077 {
1078 BMCWEB_LOG_DEBUG("Filter didn't match");
1079 continue;
1080 }
1081
1082 nlohmann::json::array_t eventRecord;
1083 eventRecord.emplace_back(eventMessage);
1084
1085 nlohmann::json msgJson;
1086
1087 msgJson["@odata.type"] = "#Event.v1_4_0.Event";
1088 msgJson["Name"] = "Event Log";
1089 msgJson["Id"] = eventId;
1090 msgJson["Events"] = std::move(eventRecord);
1091
1092 std::string strMsg = msgJson.dump(
1093 2, ' ', true, nlohmann::json::error_handler_t::replace);
1094 entry->sendEventToSubscriber(std::move(strMsg));
1095 eventId++; // increment the eventId
1096 }
1097 }
1098
resetRedfishFilePosition()1099 void resetRedfishFilePosition()
1100 {
1101 // Control would be here when Redfish file is created.
1102 // Reset File Position as new file is created
1103 redfishLogFilePosition = 0;
1104 }
1105
cacheRedfishLogFile()1106 void cacheRedfishLogFile()
1107 {
1108 // Open the redfish file and read till the last record.
1109
1110 std::ifstream logStream(redfishEventLogFile);
1111 if (!logStream.good())
1112 {
1113 BMCWEB_LOG_ERROR(" Redfish log file open failed ");
1114 return;
1115 }
1116 std::string logEntry;
1117 while (std::getline(logStream, logEntry))
1118 {
1119 redfishLogFilePosition = logStream.tellg();
1120 }
1121 }
1122
readEventLogsFromFile()1123 void readEventLogsFromFile()
1124 {
1125 std::ifstream logStream(redfishEventLogFile);
1126 if (!logStream.good())
1127 {
1128 BMCWEB_LOG_ERROR(" Redfish log file open failed");
1129 return;
1130 }
1131
1132 std::vector<EventLogObjectsType> eventRecords;
1133
1134 std::string logEntry;
1135
1136 BMCWEB_LOG_DEBUG("Redfish log file: seek to {}",
1137 static_cast<int>(redfishLogFilePosition));
1138
1139 // Get the read pointer to the next log to be read.
1140 logStream.seekg(redfishLogFilePosition);
1141
1142 while (std::getline(logStream, logEntry))
1143 {
1144 BMCWEB_LOG_DEBUG("Redfish log file: found new event log entry");
1145 // Update Pointer position
1146 redfishLogFilePosition = logStream.tellg();
1147
1148 std::string idStr;
1149 if (!event_log::getUniqueEntryID(logEntry, idStr))
1150 {
1151 BMCWEB_LOG_DEBUG(
1152 "Redfish log file: could not get unique entry id for {}",
1153 logEntry);
1154 continue;
1155 }
1156
1157 if (!serviceEnabled || noOfEventLogSubscribers == 0)
1158 {
1159 // If Service is not enabled, no need to compute
1160 // the remaining items below.
1161 // But, Loop must continue to keep track of Timestamp
1162 BMCWEB_LOG_DEBUG(
1163 "Redfish log file: no subscribers / event service not enabled");
1164 continue;
1165 }
1166
1167 std::string timestamp;
1168 std::string messageID;
1169 std::vector<std::string> messageArgs;
1170 if (event_log::getEventLogParams(logEntry, timestamp, messageID,
1171 messageArgs) != 0)
1172 {
1173 BMCWEB_LOG_DEBUG("Read eventLog entry params failed for {}",
1174 logEntry);
1175 continue;
1176 }
1177
1178 eventRecords.emplace_back(idStr, timestamp, messageID, messageArgs);
1179 }
1180
1181 if (!serviceEnabled || noOfEventLogSubscribers == 0)
1182 {
1183 BMCWEB_LOG_DEBUG("EventService disabled or no Subscriptions.");
1184 return;
1185 }
1186
1187 if (eventRecords.empty())
1188 {
1189 // No Records to send
1190 BMCWEB_LOG_DEBUG("No log entries available to be transferred.");
1191 return;
1192 }
1193
1194 for (const auto& it : subscriptionsMap)
1195 {
1196 std::shared_ptr<Subscription> entry = it.second;
1197 if (entry->eventFormatType == "Event")
1198 {
1199 entry->filterAndSendEventLogs(eventRecords);
1200 }
1201 }
1202 }
1203
watchRedfishEventLogFile()1204 static void watchRedfishEventLogFile()
1205 {
1206 if (!inotifyConn)
1207 {
1208 BMCWEB_LOG_ERROR("inotify Connection is not present");
1209 return;
1210 }
1211
1212 static std::array<char, 1024> readBuffer;
1213
1214 inotifyConn->async_read_some(
1215 boost::asio::buffer(readBuffer),
1216 [&](const boost::system::error_code& ec,
1217 const std::size_t& bytesTransferred) {
1218 if (ec == boost::asio::error::operation_aborted)
1219 {
1220 BMCWEB_LOG_DEBUG("Inotify was canceled (shutdown?)");
1221 return;
1222 }
1223 if (ec)
1224 {
1225 BMCWEB_LOG_ERROR("Callback Error: {}", ec.message());
1226 return;
1227 }
1228
1229 BMCWEB_LOG_DEBUG("reading {} via inotify", bytesTransferred);
1230
1231 std::size_t index = 0;
1232 while ((index + iEventSize) <= bytesTransferred)
1233 {
1234 struct inotify_event event
1235 {};
1236 std::memcpy(&event, &readBuffer[index], iEventSize);
1237 if (event.wd == dirWatchDesc)
1238 {
1239 if ((event.len == 0) ||
1240 (index + iEventSize + event.len > bytesTransferred))
1241 {
1242 index += (iEventSize + event.len);
1243 continue;
1244 }
1245
1246 std::string fileName(&readBuffer[index + iEventSize]);
1247 if (fileName != "redfish")
1248 {
1249 index += (iEventSize + event.len);
1250 continue;
1251 }
1252
1253 BMCWEB_LOG_DEBUG(
1254 "Redfish log file created/deleted. event.name: {}",
1255 fileName);
1256 if (event.mask == IN_CREATE)
1257 {
1258 if (fileWatchDesc != -1)
1259 {
1260 BMCWEB_LOG_DEBUG(
1261 "Remove and Add inotify watcher on "
1262 "redfish event log file");
1263 // Remove existing inotify watcher and add
1264 // with new redfish event log file.
1265 inotify_rm_watch(inotifyFd, fileWatchDesc);
1266 fileWatchDesc = -1;
1267 }
1268
1269 fileWatchDesc = inotify_add_watch(
1270 inotifyFd, redfishEventLogFile, IN_MODIFY);
1271 if (fileWatchDesc == -1)
1272 {
1273 BMCWEB_LOG_ERROR("inotify_add_watch failed for "
1274 "redfish log file.");
1275 return;
1276 }
1277
1278 EventServiceManager::getInstance()
1279 .resetRedfishFilePosition();
1280 EventServiceManager::getInstance()
1281 .readEventLogsFromFile();
1282 }
1283 else if ((event.mask == IN_DELETE) ||
1284 (event.mask == IN_MOVED_TO))
1285 {
1286 if (fileWatchDesc != -1)
1287 {
1288 inotify_rm_watch(inotifyFd, fileWatchDesc);
1289 fileWatchDesc = -1;
1290 }
1291 }
1292 }
1293 else if (event.wd == fileWatchDesc)
1294 {
1295 if (event.mask == IN_MODIFY)
1296 {
1297 EventServiceManager::getInstance()
1298 .readEventLogsFromFile();
1299 }
1300 }
1301 index += (iEventSize + event.len);
1302 }
1303
1304 watchRedfishEventLogFile();
1305 });
1306 }
1307
startEventLogMonitor(boost::asio::io_context & ioc)1308 static int startEventLogMonitor(boost::asio::io_context& ioc)
1309 {
1310 BMCWEB_LOG_DEBUG("starting Event Log Monitor");
1311
1312 inotifyConn.emplace(ioc);
1313 inotifyFd = inotify_init1(IN_NONBLOCK);
1314 if (inotifyFd == -1)
1315 {
1316 BMCWEB_LOG_ERROR("inotify_init1 failed.");
1317 return -1;
1318 }
1319
1320 // Add watch on directory to handle redfish event log file
1321 // create/delete.
1322 dirWatchDesc = inotify_add_watch(inotifyFd, redfishEventLogDir,
1323 IN_CREATE | IN_MOVED_TO | IN_DELETE);
1324 if (dirWatchDesc == -1)
1325 {
1326 BMCWEB_LOG_ERROR(
1327 "inotify_add_watch failed for event log directory.");
1328 return -1;
1329 }
1330
1331 // Watch redfish event log file for modifications.
1332 fileWatchDesc =
1333 inotify_add_watch(inotifyFd, redfishEventLogFile, IN_MODIFY);
1334 if (fileWatchDesc == -1)
1335 {
1336 BMCWEB_LOG_ERROR("inotify_add_watch failed for redfish log file.");
1337 // Don't return error if file not exist.
1338 // Watch on directory will handle create/delete of file.
1339 }
1340
1341 // monitor redfish event log file
1342 inotifyConn->assign(inotifyFd);
1343 watchRedfishEventLogFile();
1344
1345 return 0;
1346 }
1347
stopEventLogMonitor()1348 static void stopEventLogMonitor()
1349 {
1350 inotifyConn.reset();
1351 }
1352
getReadingsForReport(sdbusplus::message_t & msg)1353 static void getReadingsForReport(sdbusplus::message_t& msg)
1354 {
1355 if (msg.is_method_error())
1356 {
1357 BMCWEB_LOG_ERROR("TelemetryMonitor Signal error");
1358 return;
1359 }
1360
1361 sdbusplus::message::object_path path(msg.get_path());
1362 std::string id = path.filename();
1363 if (id.empty())
1364 {
1365 BMCWEB_LOG_ERROR("Failed to get Id from path");
1366 return;
1367 }
1368
1369 std::string interface;
1370 dbus::utility::DBusPropertiesMap props;
1371 std::vector<std::string> invalidProps;
1372 msg.read(interface, props, invalidProps);
1373
1374 auto found = std::ranges::find_if(props, [](const auto& x) {
1375 return x.first == "Readings";
1376 });
1377 if (found == props.end())
1378 {
1379 BMCWEB_LOG_INFO("Failed to get Readings from Report properties");
1380 return;
1381 }
1382
1383 const telemetry::TimestampReadings* readings =
1384 std::get_if<telemetry::TimestampReadings>(&found->second);
1385 if (readings == nullptr)
1386 {
1387 BMCWEB_LOG_INFO("Failed to get Readings from Report properties");
1388 return;
1389 }
1390
1391 for (const auto& it :
1392 EventServiceManager::getInstance().subscriptionsMap)
1393 {
1394 Subscription& entry = *it.second;
1395 if (entry.eventFormatType == metricReportFormatType)
1396 {
1397 entry.filterAndSendReports(id, *readings);
1398 }
1399 }
1400 }
1401
unregisterMetricReportSignal()1402 void unregisterMetricReportSignal()
1403 {
1404 if (matchTelemetryMonitor)
1405 {
1406 BMCWEB_LOG_DEBUG("Metrics report signal - Unregister");
1407 matchTelemetryMonitor.reset();
1408 matchTelemetryMonitor = nullptr;
1409 }
1410 }
1411
registerMetricReportSignal()1412 void registerMetricReportSignal()
1413 {
1414 if (!serviceEnabled || matchTelemetryMonitor)
1415 {
1416 BMCWEB_LOG_DEBUG("Not registering metric report signal.");
1417 return;
1418 }
1419
1420 BMCWEB_LOG_DEBUG("Metrics report signal - Register");
1421 std::string matchStr = "type='signal',member='PropertiesChanged',"
1422 "interface='org.freedesktop.DBus.Properties',"
1423 "arg0=xyz.openbmc_project.Telemetry.Report";
1424
1425 matchTelemetryMonitor = std::make_shared<sdbusplus::bus::match_t>(
1426 *crow::connections::systemBus, matchStr, getReadingsForReport);
1427 }
1428 };
1429
1430 } // namespace redfish
1431