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