diff --git a/agent-ovs/Makefile.am b/agent-ovs/Makefile.am index e6fd06a60..08f40cd5c 100644 --- a/agent-ovs/Makefile.am +++ b/agent-ovs/Makefile.am @@ -134,6 +134,7 @@ libopflex_agent_la_include_HEADERS = \ lib/include/opflexagent/FSPacketDropLogConfigSource.h \ lib/include/opflexagent/PacketDropLogConfig.h \ lib/include/opflexagent/Faults.h \ + lib/include/opflexagent/EventNotificationManager.h \ lib/include/opflexagent/PrometheusManager.h noinst_HEADERS = \ @@ -231,6 +232,7 @@ libopflex_agent_la_SOURCES = \ lib/SnatSource.cpp \ lib/FSNetpolSource.cpp \ lib/FSPacketDropLogConfigSource.cpp \ + lib/EventNotificationManager.cpp \ lib/AgentPrometheusManager.cpp libopflex_agent_la_LDFLAGS = -shared -version-info ${VERSION_INFO} @@ -483,6 +485,7 @@ agent_test_SOURCES = \ lib/test/SnatManager_test.cpp \ lib/test/QosManager_test.cpp \ lib/test/FaultManager_test.cpp \ + lib/test/EventNotificationManager_test.cpp \ server/test/AgentStats_test.cpp \ server/ServerPrometheusManager.cpp \ cmd/test/agent_test.cpp diff --git a/agent-ovs/lib/Agent.cpp b/agent-ovs/lib/Agent.cpp index dfa4122a1..a5eade7a1 100644 --- a/agent-ovs/lib/Agent.cpp +++ b/agent-ovs/lib/Agent.cpp @@ -65,7 +65,9 @@ Agent::Agent(OFFramework& framework_, const LogParams& _logParams) endpointManager(*this, framework, policyManager, prometheusManager), serviceManager(*this, framework, prometheusManager), extraConfigManager(framework), - notifServer(agent_io),rendererFwdMode(opflex_elem_t::INVALID_MODE), + notifServer(agent_io), + eventNotificationManager(*this, ""), + rendererFwdMode(opflex_elem_t::INVALID_MODE), faultManager(*this, framework), sysStatsManager(this), started(false), presetFwdMode(opflex_elem_t::INVALID_MODE), @@ -155,6 +157,7 @@ void Agent::setProperties(const boost::property_tree::ptree& properties) { static const std::string NETPOL_SOURCE_PATH("netpol-sources.filesystem"); static const std::string DROP_LOG_CFG_SOURCE_FSPATH("drop-log-config-sources.filesystem"); static const std::string FAULT_SOURCE_FSPATH("host-agent-fault-sources.filesystem"); + static const std::string EVENT_NOTIFICATION_DIR("event-notifications.filesystem"); static const std::string PACKET_EVENT_NOTIF_SOCK("packet-event-notif.socket-name"); static const std::string OPFLEX_PEERS("opflex.peers"); static const std::string OPFLEX_SSL_MODE("opflex.ssl.mode"); @@ -358,6 +361,13 @@ void Agent::setProperties(const boost::property_tree::ptree& properties) { hostAgentFaultPaths.insert(v.second.data()); } + optional eventNotificationDir = + properties.get_optional(EVENT_NOTIFICATION_DIR); + if (eventNotificationDir) { + eventNotificationPath = eventNotificationDir.get(); + eventNotificationManager.setEventsDirectory(eventNotificationPath); + } + optional packetEventNotifSock = properties.get_child_optional(PACKET_EVENT_NOTIF_SOCK); @@ -766,6 +776,11 @@ void Agent::start() { new FSFaultSource(&faultManager, fsWatcher, path, *this); faultSources.emplace_back(source); } + + if (!eventNotificationPath.empty()) { + eventNotificationManager.start(); + } + fsWatcher.start(); for (const host_t& h : opflexPeers) @@ -826,6 +841,10 @@ void Agent::stop() { } catch (const std::runtime_error& e) { LOG(WARNING) << "failed to stop fswatcher: " << e.what(); } + + if (!eventNotificationPath.empty()) { + eventNotificationManager.stop(); + } notifServer.stop(); endpointManager.stop(); diff --git a/agent-ovs/lib/EndpointManager.cpp b/agent-ovs/lib/EndpointManager.cpp index 19ad4a531..ec05ca15c 100644 --- a/agent-ovs/lib/EndpointManager.cpp +++ b/agent-ovs/lib/EndpointManager.cpp @@ -1596,6 +1596,7 @@ void EndpointManager::configUpdated(const URI& uri) { if (!config) { LOG(WARNING) << "Platform config has been deleted. Disconnect from existing peers and fallback to configured list"; agent.updateResetTime(); + agent.getEventNotificationManager().handlePlatformConfigDeleted(uri); framework.resetAllUnconfiguredPeers(); } } diff --git a/agent-ovs/lib/EventNotificationManager.cpp b/agent-ovs/lib/EventNotificationManager.cpp new file mode 100644 index 000000000..9f4c49b6a --- /dev/null +++ b/agent-ovs/lib/EventNotificationManager.cpp @@ -0,0 +1,427 @@ +/* -*- C++ -*-; c-basic-offset: 4; indent-tabs-mode: nil */ +/* + * Implementation for EventNotificationManager class. + * + * Copyright (c) 2024 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace opflexagent { + +using std::string; +using std::unique_ptr; +using std::make_unique; +using std::lock_guard; +using std::mutex; +using boost::optional; +namespace fs = boost::filesystem; +using opflex::modb::URI; + +EventNotificationManager::EventNotificationManager(Agent& agent_, + const string& eventsDir_) + : agent(agent_), eventsDir(eventsDir_), started(false) { + fsWatcher = make_unique(); +} + +EventNotificationManager::~EventNotificationManager() { + stop(); +} + +void EventNotificationManager::start() { + if (started) return; + + if (eventsDir.empty()) { + LOG(DEBUG) << "Events directory not configured, EventNotificationManager disabled"; + return; + } + + if (!fs::exists(eventsDir)) { + LOG(INFO) << "Events directory " << eventsDir << " does not exist, creating it"; + try { + fs::create_directories(eventsDir); + } catch (const fs::filesystem_error& e) { + LOG(ERROR) << "Failed to create events directory: " << e.what(); + return; + } + } + + LOG(INFO) << "Starting EventNotificationManager with events directory: " << eventsDir; + + fsWatcher->addWatch(eventsDir, *this); + fsWatcher->setInitialScan(true); + fsWatcher->start(); + + started = true; +} + +void EventNotificationManager::stop() { + if (!started) return; + + LOG(INFO) << "Stopping EventNotificationManager"; + fsWatcher->stop(); + + lock_guard guard(subscriptions_mutex); + activeSubscriptions.clear(); + + started = false; +} + +void EventNotificationManager::setEventsDirectory(const string& eventsDir_) { + eventsDir = eventsDir_; +} + +void EventNotificationManager::updated(const fs::path& filePath) { + string filename = filePath.filename().string(); + + if (filename.size() < 14 || filename.substr(filename.size() - 14) != ".subscriptions") { + return; + } + + LOG(DEBUG) << "Subscription file updated: " << filePath; + + auto subscription = parseSubscriptionFile(filePath); + if (subscription) { + lock_guard guard(subscriptions_mutex); + activeSubscriptions[subscription->uuid] = std::move(subscription); + LOG(INFO) << "Loaded subscription file: " << filePath; + } else { + LOG(ERROR) << "Failed to parse subscription file: " << filePath; + } +} + +void EventNotificationManager::deleted(const fs::path& filePath) { + string filename = filePath.filename().string(); + + if (filename.size() < 14 || filename.substr(filename.size() - 14) != ".subscriptions") { + return; + } + + LOG(DEBUG) << "Subscription file deleted: " << filePath; + + string prefix = filename.substr(0, filename.find(".subscriptions")); + + lock_guard guard(subscriptions_mutex); + for (auto it = activeSubscriptions.begin(); it != activeSubscriptions.end(); ++it) { + if (it->second->filePath == filePath) { + LOG(INFO) << "Removed subscription for UUID: " << it->first; + activeSubscriptions.erase(it); + break; + } + } + + fs::path notificationPath = getNotificationPath(filePath); + if (fs::exists(notificationPath)) { + try { + fs::remove(notificationPath); + LOG(DEBUG) << "Removed corresponding notification file: " << notificationPath; + } catch (const fs::filesystem_error& e) { + LOG(WARNING) << "Failed to remove notification file " << notificationPath + << ": " << e.what(); + } + } +} + +void EventNotificationManager::handlePlatformConfigDeleted(const URI& uri) { + lock_guard guard(subscriptions_mutex); + + for (const auto& pair : activeSubscriptions) { + const SubscriptionFile& subFile = *pair.second; + + for (const auto& subscription : subFile.subscriptions) { + if (matchesSubscription(subscription, uri, SubscriptionState::DELETED)) { + NotificationFile notification; + notification.uuid = subFile.uuid; + + EventEntry event(uri); + event.timestamp = getCurrentTimestamp(subFile.timeZone, subFile.timeFormat); + event.state = SubscriptionState::DELETED; + + fs::path notificationPath = getNotificationPath(subFile.filePath); + + notification.events.push_back(event); + + writeNotificationFile(notification, notificationPath); + LOG(INFO) << "Generated notification for PlatformConfig deleted event in: " + << notificationPath; + break; + } + } + } +} + +unique_ptr +EventNotificationManager::parseSubscriptionFile(const fs::path& filePath) { + std::ifstream file(filePath.string()); + if (!file.is_open()) { + LOG(ERROR) << "Could not open subscription file: " << filePath; + return nullptr; + } + + string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + file.close(); + + rapidjson::Document document; + document.Parse(content.c_str()); + + if (document.HasParseError()) { + LOG(ERROR) << "JSON parse error in " << filePath << ": " + << rapidjson::GetParseError_En(document.GetParseError()); + return nullptr; + } + + if (!document.IsObject() || !document.HasMember("uuid") || + !document.HasMember("subscriptions")) { + LOG(ERROR) << "Invalid subscription file format: " << filePath; + return nullptr; + } + + auto result = make_unique(); + result->filePath = filePath; + result->uuid = document["uuid"].GetString(); + + if (document.HasMember("time-zone")) { + result->timeZone = document["time-zone"].GetString(); + } + + if (document.HasMember("time-format")) { + result->timeFormat = document["time-format"].GetString(); + } + + const auto& subscriptions = document["subscriptions"]; + if (!subscriptions.IsArray()) { + LOG(ERROR) << "subscriptions field must be an array in: " << filePath; + return nullptr; + } + + for (const auto& sub : subscriptions.GetArray()) { + if (!sub.IsObject() || !sub.HasMember("type") || !sub.HasMember("subject")) { + LOG(WARNING) << "Skipping invalid subscription entry in: " << filePath; + continue; + } + + Subscription subscription; + subscription.type = parseSubscriptionType(sub["type"].GetString()); + subscription.subject = sub["subject"].GetString(); + + if (sub.HasMember("state")) { + subscription.state = parseSubscriptionState(sub["state"].GetString()); + } else { + subscription.state = SubscriptionState::ANY; + } + + if (sub.HasMember("uri") && subscription.type == SubscrationType::URI) { + try { + subscription.uri = URI(sub["uri"].GetString()); + } catch (const std::exception& e) { + LOG(WARNING) << "Invalid URI in subscription: " << sub["uri"].GetString(); + continue; + } + } + + result->subscriptions.push_back(subscription); + } + + return result; +} + +unique_ptr +EventNotificationManager::parseNotificationFile(const fs::path& filePath) { + std::ifstream file(filePath.string()); + if (!file.is_open()) { + return nullptr; + } + + string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + file.close(); + + rapidjson::Document document; + document.Parse(content.c_str()); + + if (document.HasParseError() || !document.IsObject() || + !document.HasMember("uuid") || !document.HasMember("events")) { + return nullptr; + } + + auto result = make_unique(); + result->uuid = document["uuid"].GetString(); + + const auto& events = document["events"]; + if (events.IsArray()) { + for (const auto& event : events.GetArray()) { + if (event.IsObject() && event.HasMember("uri") && + event.HasMember("timestamp") && event.HasMember("state")) { + EventEntry entry(URI(event["uri"].GetString())); + entry.timestamp = event["timestamp"].GetString(); + entry.state = parseSubscriptionState(event["state"].GetString()); + result->events.push_back(entry); + } + } + } + + return result; +} + +void EventNotificationManager::writeNotificationFile(const NotificationFile& notification, + const fs::path& filePath) { + rapidjson::Document document; + document.SetObject(); + auto& allocator = document.GetAllocator(); + + document.AddMember("uuid", rapidjson::Value(notification.uuid.c_str(), allocator), allocator); + + rapidjson::Value events(rapidjson::kArrayType); + for (const auto& event : notification.events) { + rapidjson::Value eventObj(rapidjson::kObjectType); + eventObj.AddMember("uri", rapidjson::Value(event.uri.toString().c_str(), allocator), allocator); + eventObj.AddMember("timestamp", rapidjson::Value(event.timestamp.c_str(), allocator), allocator); + eventObj.AddMember("state", rapidjson::Value(subscriptionStateToString(event.state).c_str(), allocator), allocator); + events.PushBack(eventObj, allocator); + } + document.AddMember("events", events, allocator); + + FILE* fp = fopen(filePath.string().c_str(), "w"); + if (!fp) { + LOG(ERROR) << "Could not open notification file for writing: " << filePath; + return; + } + + char writeBuffer[65536]; + rapidjson::FileWriteStream os(fp, writeBuffer, sizeof(writeBuffer)); + rapidjson::PrettyWriter writer(os); + document.Accept(writer); + + fclose(fp); +} + +fs::path EventNotificationManager::getNotificationPath(const fs::path& subscriptionPath) { + string filename = subscriptionPath.filename().string(); + string prefix = filename.substr(0, filename.find(".subscriptions")); + return subscriptionPath.parent_path() / (prefix + ".notifications"); +} + +bool EventNotificationManager::matchesSubscription(const Subscription& subscription, + const URI& eventUri, + SubscriptionState eventState) { + if (subscription.state != SubscriptionState::ANY && + subscription.state != eventState) { + return false; + } + + if (subscription.type == SubscrationType::URI) { + return subscription.uri && subscription.uri.get() == eventUri; + } else if (subscription.type == SubscrationType::CLASS) { + // TODO: The implementation should probably get the MO from + // the MODB using the URI, and get the subject. This + // will do for now. + vector elements; + eventUri.getElements(elements); + auto rit = elements.rbegin(); + for (; rit != elements.rend(); ++rit) { + if ((*rit) == subscription.subject) { + return true; + } + } + } + + return false; +} + +string EventNotificationManager::getCurrentTimestamp(const optional& timeZone, + const optional& timeFormat) { + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast( + now.time_since_epoch()) % 1000; + + std::stringstream ss; + + // Determine time format to use + string format = "%Y-%m-%d %H:%M:%S"; + bool includeMs = true; + + if (timeFormat) { + if (timeFormat.get() == "ISO8601") { + format = "%Y-%m-%dT%H:%M:%S"; + includeMs = true; + } else if (timeFormat.get() == "RFC3339") { + format = "%Y-%m-%dT%H:%M:%S"; + includeMs = true; + } else if (timeFormat.get() == "UNIX") { + ss << time_t; + if (includeMs) { + ss << '.' << std::setfill('0') << std::setw(3) << ms.count(); + } + return ss.str(); + } else { + format = timeFormat.get(); + } + } + + // Handle timezone - for now, support UTC and local time + std::tm* timeinfo; + if (timeZone && timeZone.get() == "UTC") { + timeinfo = std::gmtime(&time_t); + } else { + timeinfo = std::localtime(&time_t); + } + + ss << std::put_time(timeinfo, format.c_str()); + + if (includeMs && timeFormat && (timeFormat.get() == "ISO8601" || timeFormat.get() == "RFC3339")) { + ss << '.' << std::setfill('0') << std::setw(3) << ms.count(); + if (timeZone && timeZone.get() == "UTC") { + ss << "Z"; + } + } else if (includeMs && (!timeFormat || timeFormat.get().find("%f") == string::npos)) { + ss << '.' << std::setfill('0') << std::setw(3) << ms.count(); + } + + return ss.str(); +} + +EventNotificationManager::SubscriptionState +EventNotificationManager::parseSubscriptionState(const string& stateStr) { + if (stateStr == "created") return SubscriptionState::CREATED; + if (stateStr == "updated") return SubscriptionState::UPDATED; + if (stateStr == "deleted") return SubscriptionState::DELETED; + return SubscriptionState::ANY; +} + +string EventNotificationManager::subscriptionStateToString(SubscriptionState state) { + switch (state) { + case SubscriptionState::CREATED: return "created"; + case SubscriptionState::UPDATED: return "updated"; + case SubscriptionState::DELETED: return "deleted"; + case SubscriptionState::ANY: return "any"; + } + return "any"; +} + +EventNotificationManager::SubscrationType +EventNotificationManager::parseSubscriptionType(const string& typeStr) { + if (typeStr == "uri") return SubscrationType::URI; + return SubscrationType::CLASS; +} + +} /* namespace opflexagent */ diff --git a/agent-ovs/lib/include/opflexagent/Agent.h b/agent-ovs/lib/include/opflexagent/Agent.h index 6a6e252e1..b7d72c08e 100644 --- a/agent-ovs/lib/include/opflexagent/Agent.h +++ b/agent-ovs/lib/include/opflexagent/Agent.h @@ -29,6 +29,7 @@ #include #include #include +#include #include @@ -244,6 +245,11 @@ typedef opflex::ofcore::OFConstants::OpflexElementMode opflex_elem_t; */ FaultManager& getFaultManager() { return faultManager; } + /** + * Get the event notification manager object for this agent + */ + EventNotificationManager& getEventNotificationManager() { return eventNotificationManager; } + /** * Get packet event notification socket file name */ @@ -349,6 +355,7 @@ typedef opflex::ofcore::OFConstants::OpflexElementMode opflex_elem_t; SnatManager snatManager; NotifServer notifServer; FSWatcher fsWatcher; + EventNotificationManager eventNotificationManager; opflex_elem_t rendererFwdMode; FaultManager faultManager; SysStatsManager sysStatsManager; @@ -399,6 +406,7 @@ typedef opflex::ofcore::OFConstants::OpflexElementMode opflex_elem_t; std::vector> learningBridgeSources; std::string dropLogCfgSourcePath; std::set hostAgentFaultPaths; + std::string eventNotificationPath; std::string packetEventNotifSockPath; std::unique_ptr dropLogCfgSource; diff --git a/agent-ovs/lib/include/opflexagent/EventNotificationManager.h b/agent-ovs/lib/include/opflexagent/EventNotificationManager.h new file mode 100644 index 000000000..18d43df31 --- /dev/null +++ b/agent-ovs/lib/include/opflexagent/EventNotificationManager.h @@ -0,0 +1,237 @@ +/* -*- C++ -*-; c-basic-offset: 4; indent-tabs-mode: nil */ +/* + * Include file for Event Notification Manager + * + * Copyright (c) 2024 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +#pragma once +#ifndef OPFLEXAGENT_EVENTNOTIFICATIONMANAGER_H +#define OPFLEXAGENT_EVENTNOTIFICATIONMANAGER_H + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace opflexagent { + +class Agent; + +/** + * Manages event notifications based on subscription files and generates + * notification files when subscribed events occur. + */ +class EventNotificationManager : private boost::noncopyable, + public FSWatcher::Watcher { +public: + /** + * Construct a new EventNotificationManager + * + * @param agent Reference to the agent + * @param eventsDir Path to the events directory containing subscription files + */ + EventNotificationManager(Agent& agent, const std::string& eventsDir); + + /** + * Destructor + */ + virtual ~EventNotificationManager(); + + /** + * Start the event notification manager + */ + void start(); + + /** + * Stop the event notification manager + */ + void stop(); + + /** + * Set the events directory path + * + * @param eventsDir Path to the events directory + */ + void setEventsDirectory(const std::string& eventsDir); + + /** + * Handle platform config deleted event + * + * @param uri URI of the deleted platform config object + */ + void handlePlatformConfigDeleted(const opflex::modb::URI& uri); + + // FSWatcher::Watcher interface + virtual void updated(const boost::filesystem::path& filePath) override; + virtual void deleted(const boost::filesystem::path& filePath) override; + +private: + /** + * Subscription state for event notifications + */ + enum class SubscriptionState { + CREATED, + UPDATED, + DELETED, + ANY + }; + + /** + * Subscription type for managed objects + */ + enum class SubscrationType { + CLASS, + URI + }; + + /** + * Individual subscription entry + */ + struct Subscription { + SubscrationType type; + SubscriptionState state; + boost::optional uri; + std::string subject; + }; + + /** + * Complete subscription file contents + */ + struct SubscriptionFile { + std::string uuid; + std::vector subscriptions; + boost::filesystem::path filePath; + boost::optional timeZone; + boost::optional timeFormat; + }; + + /** + * Event entry for notification files + */ + struct EventEntry { + opflex::modb::URI uri; + std::string timestamp; + SubscriptionState state; + // Constructor taking a string to initialize uri_member + EventEntry(const std::string& uri_str) : uri(uri_str) {}; + + // Constructor taking an existing URI object + EventEntry(const opflex::modb::URI& uri_obj) : uri(uri_obj) {}; + }; + + /** + * Notification file contents + */ + struct NotificationFile { + std::string uuid; + std::vector events; + }; + + /** + * Parse a subscription file + * + * @param filePath Path to the subscription file + * @return Parsed subscription data or nullptr if parsing failed + */ + std::unique_ptr parseSubscriptionFile( + const boost::filesystem::path& filePath); + + /** + * Parse a notification file + * + * @param filePath Path to the notification file + * @return Parsed notification data or nullptr if parsing failed + */ + std::unique_ptr parseNotificationFile( + const boost::filesystem::path& filePath); + + /** + * Write a notification file + * + * @param notification Notification data to write + * @param filePath Path where to write the notification file + */ + void writeNotificationFile(const NotificationFile& notification, + const boost::filesystem::path& filePath); + + /** + * Get the notification file path for a given subscription file + * + * @param subscriptionPath Path to the subscription file + * @return Path to the corresponding notification file + */ + boost::filesystem::path getNotificationPath( + const boost::filesystem::path& subscriptionPath); + + /** + * Check if a subscription matches the given event + * + * @param subscription The subscription to check + * @param eventUri URI of the event + * @param eventState State of the event + * @return true if the subscription matches + */ + bool matchesSubscription(const Subscription& subscription, + const opflex::modb::URI& eventUri, + SubscriptionState eventState); + + /** + * Get current timestamp in log format + * + * @param timeZone Optional timezone specification (e.g., "UTC", "America/New_York") + * @param timeFormat Optional time format specification (e.g., "%Y-%m-%d %H:%M:%S", "ISO8601") + * @return Formatted timestamp string + */ + std::string getCurrentTimestamp(const boost::optional& timeZone = boost::none, + const boost::optional& timeFormat = boost::none); + + /** + * Convert string to subscription state enum + * + * @param stateStr String representation of state + * @return SubscriptionState enum value + */ + SubscriptionState parseSubscriptionState(const std::string& stateStr); + + /** + * Convert subscription state enum to string + * + * @param state SubscriptionState enum value + * @return String representation + */ + std::string subscriptionStateToString(SubscriptionState state); + + /** + * Convert string to subscription type enum + * + * @param typeStr String representation of type + * @return SubscrationType enum value + */ + SubscrationType parseSubscriptionType(const std::string& typeStr); + + Agent& agent; + std::string eventsDir; + std::unique_ptr fsWatcher; + + std::mutex subscriptions_mutex; + std::unordered_map> activeSubscriptions; + + bool started; +}; + +} /* namespace opflexagent */ + +#endif /* OPFLEXAGENT_EVENTNOTIFICATIONMANAGER_H */ diff --git a/agent-ovs/lib/test/EventNotificationManager_test.cpp b/agent-ovs/lib/test/EventNotificationManager_test.cpp new file mode 100644 index 000000000..c801816c4 --- /dev/null +++ b/agent-ovs/lib/test/EventNotificationManager_test.cpp @@ -0,0 +1,331 @@ +/* -*- C++ -*-; c-basic-offset: 4; indent-tabs-mode: nil */ +/* + * Test suite for EventNotificationManager + * + * Copyright (c) 2024 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace opflexagent { + +using std::string; +using std::shared_ptr; +using opflex::modb::URI; +using opflex::modb::URIBuilder; + +namespace fs = boost::filesystem; + +class EventNotificationManagerFixture : public BaseFixture { +public: + EventNotificationManagerFixture() + : BaseFixture(), + temp_dir(fs::temp_directory_path() / fs::unique_path()), + eventMgr(agent, temp_dir.string() + "/events") { + + fs::create_directories(temp_dir / "events"); + eventMgr.setEventsDirectory(temp_dir.string() + "/events"); + } + + virtual ~EventNotificationManagerFixture() { + eventMgr.stop(); + fs::remove_all(temp_dir); + } + + void writeSubscriptionFile(const string& filename, const string& content) { + fs::path subscriptionPath = temp_dir / "events" / filename; + std::ofstream file(subscriptionPath.string()); + file << content; + file.close(); + } + + bool notificationFileExists(const string& filename) { + fs::path notificationPath = temp_dir / "events" / filename; + return fs::exists(notificationPath); + } + + string readNotificationFile(const string& filename) { + fs::path notificationPath = temp_dir / "events" / filename; + if (!fs::exists(notificationPath)) { + return ""; + } + + std::ifstream file(notificationPath.string()); + string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + file.close(); + return content; + } + +protected: + fs::path temp_dir; + EventNotificationManager eventMgr; +}; + +BOOST_AUTO_TEST_SUITE(EventNotificationManager_test) + +BOOST_FIXTURE_TEST_CASE(basic_startup_shutdown, EventNotificationManagerFixture) { + eventMgr.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + eventMgr.stop(); +} + +BOOST_FIXTURE_TEST_CASE(subscription_file_parsing, EventNotificationManagerFixture) { + string subscriptionContent = R"({ + "uuid": "test-uuid-123", + "subscriptions": [ + { + "type": "class", + "state": "deleted", + "subject": "PlatformConfig" + } + ] + })"; + + writeSubscriptionFile("test.subscriptions", subscriptionContent); + + eventMgr.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + URI platformConfigUri = URIBuilder() + .addElement("PolicyUniverse") + .addElement("PlatformConfig") + .addElement("test-config") + .build(); + + eventMgr.handlePlatformConfigDeleted(platformConfigUri); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + BOOST_CHECK(notificationFileExists("test.notifications")); + + string notificationContent = readNotificationFile("test.notifications"); + BOOST_CHECK(!notificationContent.empty()); + BOOST_CHECK(notificationContent.find("test-uuid-123") != string::npos); + BOOST_CHECK(notificationContent.find("deleted") != string::npos); + BOOST_CHECK(notificationContent.find("PolicyUniverse/PlatformConfig/test-config/") != string::npos); +} + +BOOST_FIXTURE_TEST_CASE(uri_based_subscription, EventNotificationManagerFixture) { + URI specificConfigUri = URIBuilder() + .addElement("PolicyUniverse") + .addElement("PlatformConfig") + .addElement("specific-config") + .build(); + + string subscriptionContent = R"({ + "uuid": "uri-test-uuid", + "subscriptions": [ + { + "type": "uri", + "state": "deleted", + "uri": ")" + specificConfigUri.toString() + R"(", + "subject": "PlatformConfig" + } + ] + })"; + + writeSubscriptionFile("uri_test.subscriptions", subscriptionContent); + + eventMgr.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Test with matching URI + eventMgr.handlePlatformConfigDeleted(specificConfigUri); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + BOOST_CHECK(notificationFileExists("uri_test.notifications")); + + // Test with non-matching URI + URI differentConfigUri = URIBuilder() + .addElement("PolicyUniverse") + .addElement("PlatformConfig") + .addElement("different-config") + .build(); + + // Remove existing notification file + fs::remove(temp_dir / "events" / "uri_test.notifications"); + + eventMgr.handlePlatformConfigDeleted(differentConfigUri); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + BOOST_CHECK(!notificationFileExists("uri_test.notifications")); +} + +BOOST_FIXTURE_TEST_CASE(state_filtering, EventNotificationManagerFixture) { + string subscriptionContent = R"({ + "uuid": "state-filter-uuid", + "subscriptions": [ + { + "type": "class", + "state": "created", + "subject": "PlatformConfig" + } + ] + })"; + + writeSubscriptionFile("state_filter.subscriptions", subscriptionContent); + + eventMgr.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + URI configUri = URIBuilder() + .addElement("PolicyUniverse") + .addElement("PlatformConfig") + .addElement("test-config") + .build(); + + // Should not generate notification for deleted event when subscription is for created + eventMgr.handlePlatformConfigDeleted(configUri); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + BOOST_CHECK(!notificationFileExists("state_filter.notifications")); +} + +BOOST_FIXTURE_TEST_CASE(any_state_subscription, EventNotificationManagerFixture) { + string subscriptionContent = R"({ + "uuid": "any-state-uuid", + "subscriptions": [ + { + "type": "class", + "subject": "PlatformConfig" + } + ] + })"; + + writeSubscriptionFile("any_state.subscriptions", subscriptionContent); + + eventMgr.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + URI configUri = URIBuilder() + .addElement("PolicyUniverse") + .addElement("PlatformConfig") + .addElement("test-config") + .build(); + + eventMgr.handlePlatformConfigDeleted(configUri); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + BOOST_CHECK(notificationFileExists("any_state.notifications")); +} + +BOOST_FIXTURE_TEST_CASE(multiple_subscriptions, EventNotificationManagerFixture) { + string subscription1 = R"({ + "uuid": "multi-uuid-1", + "subscriptions": [ + { + "type": "class", + "state": "deleted", + "subject": "PlatformConfig" + } + ] + })"; + + string subscription2 = R"({ + "uuid": "multi-uuid-2", + "subscriptions": [ + { + "type": "class", + "state": "deleted", + "subject": "PlatformConfig" + } + ] + })"; + + writeSubscriptionFile("multi1.subscriptions", subscription1); + writeSubscriptionFile("multi2.subscriptions", subscription2); + + eventMgr.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + URI configUri = URIBuilder() + .addElement("PolicyUniverse") + .addElement("PlatformConfig") + .addElement("test-config") + .build(); + + eventMgr.handlePlatformConfigDeleted(configUri); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + BOOST_CHECK(notificationFileExists("multi1.notifications")); + BOOST_CHECK(notificationFileExists("multi2.notifications")); +} + +BOOST_FIXTURE_TEST_CASE(invalid_subscription_file, EventNotificationManagerFixture) { + string invalidJson = R"({ + "invalid": "json structure" + })"; + + writeSubscriptionFile("invalid.subscriptions", invalidJson); + + eventMgr.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + URI configUri = URIBuilder() + .addElement("PolicyUniverse") + .addElement("PlatformConfig") + .addElement("test-config") + .build(); + + // Should not crash or generate notification for invalid subscription + eventMgr.handlePlatformConfigDeleted(configUri); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + BOOST_CHECK(!notificationFileExists("invalid.notifications")); +} + +BOOST_FIXTURE_TEST_CASE(file_deletion, EventNotificationManagerFixture) { + string subscriptionContent = R"({ + "uuid": "delete-test-uuid", + "subscriptions": [ + { + "type": "class", + "state": "deleted", + "subject": "PlatformConfig" + } + ] + })"; + + writeSubscriptionFile("delete_test.subscriptions", subscriptionContent); + + eventMgr.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Generate a notification first + URI configUri = URIBuilder() + .addElement("PolicyUniverse") + .addElement("PlatformConfig") + .addElement("test-config") + .build(); + + eventMgr.handlePlatformConfigDeleted(configUri); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + BOOST_CHECK(notificationFileExists("delete_test.notifications")); + + // Remove subscription file + fs::remove(temp_dir / "events" / "delete_test.subscriptions"); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Notification file should be removed as well + BOOST_CHECK(!notificationFileExists("delete_test.notifications")); +} + +BOOST_AUTO_TEST_SUITE_END() + +} /* namespace opflexagent */