/* Copyright (c) 2020 Intel Corporation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #pragma once #include "dbus_utility.hpp" #include "error_messages.hpp" #include "event_log.hpp" #include "event_matches_filter.hpp" #include "event_service_store.hpp" #include "metric_report.hpp" #include "ossl_random.hpp" #include "persistent_data.hpp" #include "subscription.hpp" #include "utils/time_utils.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace redfish { static constexpr const char* eventFormatType = "Event"; static constexpr const char* metricReportFormatType = "MetricReport"; static constexpr const char* eventServiceFile = "/var/lib/bmcweb/eventservice_config.json"; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static std::optional inotifyConn; static constexpr const char* redfishEventLogDir = "/var/log"; static constexpr const char* redfishEventLogFile = "/var/log/redfish"; static constexpr const size_t iEventSize = sizeof(inotify_event); // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static int inotifyFd = -1; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static int dirWatchDesc = -1; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static int fileWatchDesc = -1; class EventServiceManager { private: bool serviceEnabled = false; uint32_t retryAttempts = 0; uint32_t retryTimeoutInterval = 0; std::streampos redfishLogFilePosition{0}; size_t noOfEventLogSubscribers{0}; size_t noOfMetricReportSubscribers{0}; std::shared_ptr matchTelemetryMonitor; boost::container::flat_map> subscriptionsMap; uint64_t eventId{1}; struct Event { std::string id; nlohmann::json message; }; constexpr static size_t maxMessages = 200; boost::circular_buffer messages{maxMessages}; boost::asio::io_context& ioc; public: EventServiceManager(const EventServiceManager&) = delete; EventServiceManager& operator=(const EventServiceManager&) = delete; EventServiceManager(EventServiceManager&&) = delete; EventServiceManager& operator=(EventServiceManager&&) = delete; ~EventServiceManager() = default; explicit EventServiceManager(boost::asio::io_context& iocIn) : ioc(iocIn) { // Load config from persist store. initConfig(); } static EventServiceManager& getInstance(boost::asio::io_context* ioc = nullptr) { static EventServiceManager handler(*ioc); return handler; } void initConfig() { loadOldBehavior(); persistent_data::EventServiceConfig eventServiceConfig = persistent_data::EventServiceStore::getInstance() .getEventServiceConfig(); serviceEnabled = eventServiceConfig.enabled; retryAttempts = eventServiceConfig.retryAttempts; retryTimeoutInterval = eventServiceConfig.retryTimeoutInterval; for (const auto& it : persistent_data::EventServiceStore::getInstance() .subscriptionsConfigMap) { std::shared_ptr newSub = it.second; boost::system::result url = boost::urls::parse_absolute_uri(newSub->destinationUrl); if (!url) { BMCWEB_LOG_ERROR( "Failed to validate and split destination url"); continue; } std::shared_ptr subValue = std::make_shared(newSub, *url, ioc); std::string id = subValue->userSub->id; subValue->deleter = [id]() { EventServiceManager::getInstance().deleteSubscription(id); }; subscriptionsMap.emplace(id, subValue); updateNoOfSubscribersCount(); if constexpr (!BMCWEB_REDFISH_DBUS_LOG) { cacheRedfishLogFile(); } // Update retry configuration. subValue->updateRetryConfig(retryAttempts, retryTimeoutInterval); } } static void loadOldBehavior() { std::ifstream eventConfigFile(eventServiceFile); if (!eventConfigFile.good()) { BMCWEB_LOG_DEBUG("Old eventService config not exist"); return; } auto jsonData = nlohmann::json::parse(eventConfigFile, nullptr, false); if (jsonData.is_discarded()) { BMCWEB_LOG_ERROR("Old eventService config parse error."); return; } const nlohmann::json::object_t* obj = jsonData.get_ptr(); for (const auto& item : *obj) { if (item.first == "Configuration") { persistent_data::EventServiceStore::getInstance() .getEventServiceConfig() .fromJson(item.second); } else if (item.first == "Subscriptions") { for (const auto& elem : item.second) { std::optional newSubscription = persistent_data::UserSubscription::fromJson(elem, true); if (!newSubscription) { BMCWEB_LOG_ERROR("Problem reading subscription " "from old persistent store"); continue; } persistent_data::UserSubscription& newSub = *newSubscription; std::uniform_int_distribution dist(0); bmcweb::OpenSSLGenerator gen; std::string id; int retry = 3; while (retry != 0) { id = std::to_string(dist(gen)); if (gen.error()) { retry = 0; break; } newSub.id = id; auto inserted = persistent_data::EventServiceStore::getInstance() .subscriptionsConfigMap.insert(std::pair( id, std::make_shared< persistent_data::UserSubscription>( newSub))); if (inserted.second) { break; } --retry; } if (retry <= 0) { BMCWEB_LOG_ERROR( "Failed to generate random number from old " "persistent store"); continue; } } } persistent_data::getConfig().writeData(); std::error_code ec; std::filesystem::remove(eventServiceFile, ec); if (ec) { BMCWEB_LOG_DEBUG( "Failed to remove old event service file. Ignoring"); } else { BMCWEB_LOG_DEBUG("Remove old eventservice config"); } } } void updateSubscriptionData() const { persistent_data::EventServiceStore::getInstance() .eventServiceConfig.enabled = serviceEnabled; persistent_data::EventServiceStore::getInstance() .eventServiceConfig.retryAttempts = retryAttempts; persistent_data::EventServiceStore::getInstance() .eventServiceConfig.retryTimeoutInterval = retryTimeoutInterval; persistent_data::getConfig().writeData(); } void setEventServiceConfig(const persistent_data::EventServiceConfig& cfg) { bool updateConfig = false; bool updateRetryCfg = false; if (serviceEnabled != cfg.enabled) { serviceEnabled = cfg.enabled; if (serviceEnabled && noOfMetricReportSubscribers != 0U) { registerMetricReportSignal(); } else { unregisterMetricReportSignal(); } updateConfig = true; } if (retryAttempts != cfg.retryAttempts) { retryAttempts = cfg.retryAttempts; updateConfig = true; updateRetryCfg = true; } if (retryTimeoutInterval != cfg.retryTimeoutInterval) { retryTimeoutInterval = cfg.retryTimeoutInterval; updateConfig = true; updateRetryCfg = true; } if (updateConfig) { updateSubscriptionData(); } if (updateRetryCfg) { // Update the changed retry config to all subscriptions for (const auto& it : EventServiceManager::getInstance().subscriptionsMap) { Subscription& entry = *it.second; entry.updateRetryConfig(retryAttempts, retryTimeoutInterval); } } } void updateNoOfSubscribersCount() { size_t eventLogSubCount = 0; size_t metricReportSubCount = 0; for (const auto& it : subscriptionsMap) { std::shared_ptr entry = it.second; if (entry->userSub->eventFormatType == eventFormatType) { eventLogSubCount++; } else if (entry->userSub->eventFormatType == metricReportFormatType) { metricReportSubCount++; } } noOfEventLogSubscribers = eventLogSubCount; if (noOfMetricReportSubscribers != metricReportSubCount) { noOfMetricReportSubscribers = metricReportSubCount; if (noOfMetricReportSubscribers != 0U) { registerMetricReportSignal(); } else { unregisterMetricReportSignal(); } } } std::shared_ptr getSubscription(const std::string& id) { auto obj = subscriptionsMap.find(id); if (obj == subscriptionsMap.end()) { BMCWEB_LOG_ERROR("No subscription exist with ID:{}", id); return nullptr; } std::shared_ptr subValue = obj->second; return subValue; } std::string addSubscriptionInternal(const std::shared_ptr& subValue) { std::uniform_int_distribution dist(0); bmcweb::OpenSSLGenerator gen; std::string id; int retry = 3; while (retry != 0) { id = std::to_string(dist(gen)); if (gen.error()) { retry = 0; break; } auto inserted = subscriptionsMap.insert(std::pair(id, subValue)); if (inserted.second) { break; } --retry; } if (retry <= 0) { BMCWEB_LOG_ERROR("Failed to generate random number"); return ""; } // Set Subscription ID for back trace subValue->userSub->id = id; persistent_data::EventServiceStore::getInstance() .subscriptionsConfigMap.emplace(id, subValue->userSub); updateNoOfSubscribersCount(); if constexpr (!BMCWEB_REDFISH_DBUS_LOG) { if (redfishLogFilePosition != 0) { cacheRedfishLogFile(); } } // Update retry configuration. subValue->updateRetryConfig(retryAttempts, retryTimeoutInterval); return id; } std::string addSSESubscription(const std::shared_ptr& subValue, std::string_view lastEventId) { std::string id = addSubscriptionInternal(subValue); if (!lastEventId.empty()) { BMCWEB_LOG_INFO("Attempting to find message for last id {}", lastEventId); boost::circular_buffer::iterator lastEvent = std::find_if(messages.begin(), messages.end(), [&lastEventId](const Event& event) { return event.id == lastEventId; }); // Can't find a matching ID if (lastEvent == messages.end()) { nlohmann::json msg = messages::eventBufferExceeded(); // If the buffer overloaded, send all messages. subValue->sendEventToSubscriber(msg); lastEvent = messages.begin(); } else { // Skip the last event the user already has lastEvent++; } for (boost::circular_buffer::const_iterator event = lastEvent; lastEvent != messages.end(); lastEvent++) { subValue->sendEventToSubscriber(event->message); } } return id; } std::string addPushSubscription(const std::shared_ptr& subValue) { std::string id = addSubscriptionInternal(subValue); subValue->deleter = [id]() { EventServiceManager::getInstance().deleteSubscription(id); }; updateSubscriptionData(); return id; } bool isSubscriptionExist(const std::string& id) { auto obj = subscriptionsMap.find(id); return obj != subscriptionsMap.end(); } bool deleteSubscription(const std::string& id) { auto obj = subscriptionsMap.find(id); if (obj == subscriptionsMap.end()) { BMCWEB_LOG_WARNING("Could not find subscription with id {}", id); return false; } subscriptionsMap.erase(obj); auto& event = persistent_data::EventServiceStore::getInstance(); auto persistentObj = event.subscriptionsConfigMap.find(id); if (persistentObj == event.subscriptionsConfigMap.end()) { BMCWEB_LOG_ERROR("Subscription wasn't in persistent data"); return true; } persistent_data::EventServiceStore::getInstance() .subscriptionsConfigMap.erase(persistentObj); updateNoOfSubscribersCount(); updateSubscriptionData(); return true; } void deleteSseSubscription(const crow::sse_socket::Connection& thisConn) { for (auto it = subscriptionsMap.begin(); it != subscriptionsMap.end();) { std::shared_ptr entry = it->second; bool entryIsThisConn = entry->matchSseId(thisConn); if (entryIsThisConn) { persistent_data::EventServiceStore::getInstance() .subscriptionsConfigMap.erase(entry->userSub->id); it = subscriptionsMap.erase(it); return; } it++; } } size_t getNumberOfSubscriptions() const { return subscriptionsMap.size(); } size_t getNumberOfSSESubscriptions() const { auto size = std::ranges::count_if( subscriptionsMap, [](const std::pair>& entry) { return (entry.second->userSub->subscriptionType == subscriptionTypeSSE); }); return static_cast(size); } std::vector getAllIDs() { std::vector idList; for (const auto& it : subscriptionsMap) { idList.emplace_back(it.first); } return idList; } bool sendTestEventLog() { for (const auto& it : subscriptionsMap) { std::shared_ptr entry = it.second; if (!entry->sendTestEventLog()) { return false; } } return true; } void sendEvent(nlohmann::json::object_t eventMessage, std::string_view origin, std::string_view resourceType) { eventMessage["EventId"] = eventId; eventMessage["EventTimestamp"] = redfish::time_utils::getDateTimeOffsetNow().first; eventMessage["OriginOfCondition"] = origin; // MemberId is 0 : since we are sending one event record. eventMessage["MemberId"] = "0"; messages.push_back(Event(std::to_string(eventId), eventMessage)); for (auto& it : subscriptionsMap) { std::shared_ptr& entry = it.second; if (!eventMatchesFilter(*entry->userSub, eventMessage, resourceType)) { BMCWEB_LOG_DEBUG("Filter didn't match"); continue; } nlohmann::json::array_t eventRecord; eventRecord.emplace_back(eventMessage); nlohmann::json msgJson; msgJson["@odata.type"] = "#Event.v1_4_0.Event"; msgJson["Name"] = "Event Log"; msgJson["Id"] = eventId; msgJson["Events"] = std::move(eventRecord); std::string strMsg = msgJson.dump( 2, ' ', true, nlohmann::json::error_handler_t::replace); entry->sendEventToSubscriber(std::move(strMsg)); eventId++; // increment the eventId } } void resetRedfishFilePosition() { // Control would be here when Redfish file is created. // Reset File Position as new file is created redfishLogFilePosition = 0; } void cacheRedfishLogFile() { // Open the redfish file and read till the last record. std::ifstream logStream(redfishEventLogFile); if (!logStream.good()) { BMCWEB_LOG_ERROR(" Redfish log file open failed "); return; } std::string logEntry; while (std::getline(logStream, logEntry)) { redfishLogFilePosition = logStream.tellg(); } } void readEventLogsFromFile() { std::ifstream logStream(redfishEventLogFile); if (!logStream.good()) { BMCWEB_LOG_ERROR(" Redfish log file open failed"); return; } std::vector eventRecords; std::string logEntry; BMCWEB_LOG_DEBUG("Redfish log file: seek to {}", static_cast(redfishLogFilePosition)); // Get the read pointer to the next log to be read. logStream.seekg(redfishLogFilePosition); while (std::getline(logStream, logEntry)) { BMCWEB_LOG_DEBUG("Redfish log file: found new event log entry"); // Update Pointer position redfishLogFilePosition = logStream.tellg(); std::string idStr; if (!event_log::getUniqueEntryID(logEntry, idStr)) { BMCWEB_LOG_DEBUG( "Redfish log file: could not get unique entry id for {}", logEntry); continue; } if (!serviceEnabled || noOfEventLogSubscribers == 0) { // If Service is not enabled, no need to compute // the remaining items below. // But, Loop must continue to keep track of Timestamp BMCWEB_LOG_DEBUG( "Redfish log file: no subscribers / event service not enabled"); continue; } std::string timestamp; std::string messageID; std::vector messageArgs; if (event_log::getEventLogParams(logEntry, timestamp, messageID, messageArgs) != 0) { BMCWEB_LOG_DEBUG("Read eventLog entry params failed for {}", logEntry); continue; } eventRecords.emplace_back(idStr, timestamp, messageID, messageArgs); } if (!serviceEnabled || noOfEventLogSubscribers == 0) { BMCWEB_LOG_DEBUG("EventService disabled or no Subscriptions."); return; } if (eventRecords.empty()) { // No Records to send BMCWEB_LOG_DEBUG("No log entries available to be transferred."); return; } for (const auto& it : subscriptionsMap) { std::shared_ptr entry = it.second; if (entry->userSub->eventFormatType == "Event") { entry->filterAndSendEventLogs(eventRecords); } } } static void watchRedfishEventLogFile() { if (!inotifyConn) { BMCWEB_LOG_ERROR("inotify Connection is not present"); return; } static std::array readBuffer; inotifyConn->async_read_some( boost::asio::buffer(readBuffer), [&](const boost::system::error_code& ec, const std::size_t& bytesTransferred) { if (ec == boost::asio::error::operation_aborted) { BMCWEB_LOG_DEBUG("Inotify was canceled (shutdown?)"); return; } if (ec) { BMCWEB_LOG_ERROR("Callback Error: {}", ec.message()); return; } BMCWEB_LOG_DEBUG("reading {} via inotify", bytesTransferred); std::size_t index = 0; while ((index + iEventSize) <= bytesTransferred) { struct inotify_event event {}; std::memcpy(&event, &readBuffer[index], iEventSize); if (event.wd == dirWatchDesc) { if ((event.len == 0) || (index + iEventSize + event.len > bytesTransferred)) { index += (iEventSize + event.len); continue; } std::string fileName(&readBuffer[index + iEventSize]); if (fileName != "redfish") { index += (iEventSize + event.len); continue; } BMCWEB_LOG_DEBUG( "Redfish log file created/deleted. event.name: {}", fileName); if (event.mask == IN_CREATE) { if (fileWatchDesc != -1) { BMCWEB_LOG_DEBUG( "Remove and Add inotify watcher on " "redfish event log file"); // Remove existing inotify watcher and add // with new redfish event log file. inotify_rm_watch(inotifyFd, fileWatchDesc); fileWatchDesc = -1; } fileWatchDesc = inotify_add_watch( inotifyFd, redfishEventLogFile, IN_MODIFY); if (fileWatchDesc == -1) { BMCWEB_LOG_ERROR("inotify_add_watch failed for " "redfish log file."); return; } EventServiceManager::getInstance() .resetRedfishFilePosition(); EventServiceManager::getInstance() .readEventLogsFromFile(); } else if ((event.mask == IN_DELETE) || (event.mask == IN_MOVED_TO)) { if (fileWatchDesc != -1) { inotify_rm_watch(inotifyFd, fileWatchDesc); fileWatchDesc = -1; } } } else if (event.wd == fileWatchDesc) { if (event.mask == IN_MODIFY) { EventServiceManager::getInstance() .readEventLogsFromFile(); } } index += (iEventSize + event.len); } watchRedfishEventLogFile(); }); } static int startEventLogMonitor(boost::asio::io_context& ioc) { BMCWEB_LOG_DEBUG("starting Event Log Monitor"); inotifyConn.emplace(ioc); inotifyFd = inotify_init1(IN_NONBLOCK); if (inotifyFd == -1) { BMCWEB_LOG_ERROR("inotify_init1 failed."); return -1; } // Add watch on directory to handle redfish event log file // create/delete. dirWatchDesc = inotify_add_watch(inotifyFd, redfishEventLogDir, IN_CREATE | IN_MOVED_TO | IN_DELETE); if (dirWatchDesc == -1) { BMCWEB_LOG_ERROR( "inotify_add_watch failed for event log directory."); return -1; } // Watch redfish event log file for modifications. fileWatchDesc = inotify_add_watch(inotifyFd, redfishEventLogFile, IN_MODIFY); if (fileWatchDesc == -1) { BMCWEB_LOG_ERROR("inotify_add_watch failed for redfish log file."); // Don't return error if file not exist. // Watch on directory will handle create/delete of file. } // monitor redfish event log file inotifyConn->assign(inotifyFd); watchRedfishEventLogFile(); return 0; } static void stopEventLogMonitor() { inotifyConn.reset(); } static void getReadingsForReport(sdbusplus::message_t& msg) { if (msg.is_method_error()) { BMCWEB_LOG_ERROR("TelemetryMonitor Signal error"); return; } sdbusplus::message::object_path path(msg.get_path()); std::string id = path.filename(); if (id.empty()) { BMCWEB_LOG_ERROR("Failed to get Id from path"); return; } std::string interface; dbus::utility::DBusPropertiesMap props; std::vector invalidProps; msg.read(interface, props, invalidProps); auto found = std::ranges::find_if(props, [](const auto& x) { return x.first == "Readings"; }); if (found == props.end()) { BMCWEB_LOG_INFO("Failed to get Readings from Report properties"); return; } const telemetry::TimestampReadings* readings = std::get_if(&found->second); if (readings == nullptr) { BMCWEB_LOG_INFO("Failed to get Readings from Report properties"); return; } for (const auto& it : EventServiceManager::getInstance().subscriptionsMap) { Subscription& entry = *it.second; if (entry.userSub->eventFormatType == metricReportFormatType) { entry.filterAndSendReports(id, *readings); } } } void unregisterMetricReportSignal() { if (matchTelemetryMonitor) { BMCWEB_LOG_DEBUG("Metrics report signal - Unregister"); matchTelemetryMonitor.reset(); matchTelemetryMonitor = nullptr; } } void registerMetricReportSignal() { if (!serviceEnabled || matchTelemetryMonitor) { BMCWEB_LOG_DEBUG("Not registering metric report signal."); return; } BMCWEB_LOG_DEBUG("Metrics report signal - Register"); std::string matchStr = "type='signal',member='PropertiesChanged'," "interface='org.freedesktop.DBus.Properties'," "arg0=xyz.openbmc_project.Telemetry.Report"; matchTelemetryMonitor = std::make_shared( *crow::connections::systemBus, matchStr, getReadingsForReport); } }; } // namespace redfish