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