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