From a20522debea43e71c0c29c4738129e1ced62cad7 Mon Sep 17 00:00:00 2001 From: Sonic Build Admin Date: Wed, 10 Sep 2025 12:55:48 +0000 Subject: [PATCH] Add c-api/Rust wrappers for ConfigDBConnector and EventPublisher This PR adds c-api/Rust wrappers for ConfigDBConnector and EventPublisher, so they will be available for Rust application development. Also add unit tests for c-api and Rust wrappers. --- Cargo.toml | 1 + common/Makefile.am | 4 +- common/c-api/configdbconnector.cpp | 185 ++++++++++++ common/c-api/configdbconnector.h | 63 ++++ common/c-api/events.cpp | 71 +++++ common/c-api/events.h | 35 +++ common/c-api/util.cpp | 37 +++ common/c-api/util.h | 20 ++ crates/swss-common/Cargo.toml | 1 + crates/swss-common/src/types.rs | 4 + .../src/types/configdbconnector.rs | 275 ++++++++++++++++++ crates/swss-common/src/types/events.rs | 111 +++++++ crates/swss-common/tests/logger.rs | 2 + crates/swss-common/tests/logger_fallback.rs | 12 +- .../tests/test_config_db_connector.rs | 163 +++++++++++ tests/c_api_ut.cpp | 174 +++++++++++ 16 files changed, 1156 insertions(+), 2 deletions(-) create mode 100644 common/c-api/configdbconnector.cpp create mode 100644 common/c-api/configdbconnector.h create mode 100644 common/c-api/events.cpp create mode 100644 common/c-api/events.h create mode 100644 crates/swss-common/src/types/configdbconnector.rs create mode 100644 crates/swss-common/src/types/events.rs create mode 100644 crates/swss-common/tests/test_config_db_connector.rs diff --git a/Cargo.toml b/Cargo.toml index 87a8614..3d0fcc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ serde_with = "3.12" getset = "0.1" lazy_static = "1.4" +serial_test = "3.0" # Internal dependencies swss-common = { version = "0.1.0", path = "crates/swss-common" } diff --git a/common/Makefile.am b/common/Makefile.am index 4c9a089..b44e840 100644 --- a/common/Makefile.am +++ b/common/Makefile.am @@ -72,6 +72,7 @@ common_libswsscommon_la_SOURCES = \ common/interface.h \ common/c-api/util.cpp \ common/c-api/dbconnector.cpp \ + common/c-api/configdbconnector.cpp \ common/c-api/consumerstatetable.cpp \ common/c-api/producerstatetable.cpp \ common/c-api/subscriberstatetable.cpp \ @@ -80,7 +81,8 @@ common_libswsscommon_la_SOURCES = \ common/c-api/zmqconsumerstatetable.cpp \ common/c-api/zmqproducerstatetable.cpp \ common/c-api/table.cpp \ - common/c-api/logger.cpp \ + common/c-api/logger.cpp \ + common/c-api/events.cpp \ common/performancetimer.cpp common_libswsscommon_la_CXXFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) $(LIBNL_CFLAGS) $(CODE_COVERAGE_CXXFLAGS) diff --git a/common/c-api/configdbconnector.cpp b/common/c-api/configdbconnector.cpp new file mode 100644 index 0000000..448e010 --- /dev/null +++ b/common/c-api/configdbconnector.cpp @@ -0,0 +1,185 @@ +#include +#include +#include +#include +#include + +#include "../configdb.h" +#include "configdbconnector.h" +#include "util.h" + +using namespace swss; +using namespace std; + +SWSSResult SWSSConfigDBConnector_new(uint8_t use_unix_socket_path, const char *netns, SWSSConfigDBConnector *outConfigDb) { + SWSSTry({ + string netns_str = netns ? string(netns) : ""; + *outConfigDb = (SWSSConfigDBConnector) new ConfigDBConnector_Native(use_unix_socket_path != 0, netns_str.c_str()); + }); +} + +SWSSResult SWSSConfigDBConnector_free(SWSSConfigDBConnector configDb) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" + SWSSTry(delete (ConfigDBConnector_Native *)configDb); +#pragma GCC diagnostic pop +} + +SWSSResult SWSSConfigDBConnector_connect(SWSSConfigDBConnector configDb, uint8_t wait_for_init, uint8_t retry_on) { + SWSSTry(((ConfigDBConnector_Native *)configDb)->connect(wait_for_init != 0, retry_on != 0)); +} + +SWSSResult SWSSConfigDBConnector_get_entry(SWSSConfigDBConnector configDb, const char *table, const char *key, SWSSFieldValueArray *outEntry) { + SWSSTry({ + auto entry_map = ((ConfigDBConnector_Native *)configDb)->get_entry(string(table), string(key)); + // Convert map to vector> for makeFieldValueArray + vector> pairs; + pairs.reserve(entry_map.size()); + for (auto &pair : entry_map) { + pairs.push_back(make_pair(pair.first, move(pair.second))); + } + + *outEntry = makeFieldValueArray(std::move(pairs)); + }); +} + +SWSSResult SWSSConfigDBConnector_get_keys(SWSSConfigDBConnector configDb, const char *table, uint8_t split, SWSSStringArray *outKeys) { + SWSSTry({ + auto keys = ((ConfigDBConnector_Native *)configDb)->get_keys(string(table), split != 0); + *outKeys = makeStringArray(std::move(keys)); + }); +} + +SWSSResult SWSSConfigDBConnector_get_table(SWSSConfigDBConnector configDb, const char *table, SWSSKeyOpFieldValuesArray *outTable) { + SWSSTry({ + auto table_map = ((ConfigDBConnector_Native *)configDb)->get_table(string(table)); + + // Convert map> to vector + vector table_entries; + table_entries.reserve(table_map.size()); + + for (auto &entry : table_map) { + SWSSKeyOpFieldValues kfv_entry; + kfv_entry.key = strdup(entry.first.c_str()); + kfv_entry.operation = SWSSKeyOperation_SET; // ConfigDB entries are always SET operations + + // Convert inner map to field-value array + vector> pairs; + pairs.reserve(entry.second.size()); + for (auto &field_pair : entry.second) { + pairs.push_back(make_pair(field_pair.first, move(field_pair.second))); + } + kfv_entry.fieldValues = makeFieldValueArray(std::move(pairs)); + + table_entries.push_back(kfv_entry); + } + + // Convert to SWSSKeyOpFieldValuesArray + SWSSKeyOpFieldValues *data = new SWSSKeyOpFieldValues[table_entries.size()]; + for (size_t i = 0; i < table_entries.size(); i++) { + data[i] = table_entries[i]; + } + + SWSSKeyOpFieldValuesArray out; + out.len = (uint64_t)table_entries.size(); + out.data = data; + *outTable = out; + }); +} + +SWSSResult SWSSConfigDBConnector_set_entry(SWSSConfigDBConnector configDb, const char *table, const char *key, const SWSSFieldValueArray *data) { + SWSSTry({ + // Convert SWSSFieldValueArray to map + map data_map; + auto field_values = takeFieldValueArray(*data); + for (auto &fv : field_values) { + data_map[fv.first] = fv.second; + } + + ((ConfigDBConnector_Native *)configDb)->set_entry(string(table), string(key), data_map); + }); +} + +SWSSResult SWSSConfigDBConnector_mod_entry(SWSSConfigDBConnector configDb, const char *table, const char *key, const SWSSFieldValueArray *data) { + SWSSTry({ + // Convert SWSSFieldValueArray to map + map data_map; + auto field_values = takeFieldValueArray(*data); + for (auto &fv : field_values) { + data_map[fv.first] = fv.second; + } + + ((ConfigDBConnector_Native *)configDb)->mod_entry(string(table), string(key), data_map); + }); +} + +SWSSResult SWSSConfigDBConnector_delete_table(SWSSConfigDBConnector configDb, const char *table) { + SWSSTry(((ConfigDBConnector_Native *)configDb)->delete_table(string(table))); +} + +SWSSResult SWSSConfigDBConnector_get_redis_client(SWSSConfigDBConnector configDb, const char *db_name, SWSSDBConnector *outDbConnector) { + SWSSTry({ + // Get the Redis client from the ConfigDBConnector's parent SonicV2Connector_Native + DBConnector& redis_client = ((ConfigDBConnector_Native *)configDb)->get_redis_client(string(db_name)); + + // Return a pointer to the existing DBConnector + // Note: This returns a reference to the existing connector, not a new one + *outDbConnector = (SWSSDBConnector)&redis_client; + }); +} + +SWSSResult SWSSConfigDBConnector_get_config(SWSSConfigDBConnector configDb, SWSSConfigMap *outConfig) { + SWSSTry({ + // Get the entire configuration as a nested map structure + auto config_map = ((ConfigDBConnector_Native *)configDb)->get_config(); + + // Convert map>> to SWSSConfigMap + vector config_tables; + config_tables.reserve(config_map.size()); + + for (auto &table_entry : config_map) { + SWSSConfigTable config_table; + config_table.table_name = strdup(table_entry.first.c_str()); + + // Convert map> to SWSSKeyOpFieldValuesArray + vector table_entries; + table_entries.reserve(table_entry.second.size()); + + for (auto &key_entry : table_entry.second) { + SWSSKeyOpFieldValues kfv_entry; + kfv_entry.key = strdup(key_entry.first.c_str()); + kfv_entry.operation = SWSSKeyOperation_SET; // Config entries are always SET operations + + // Convert map to SWSSFieldValueArray + vector> pairs; + pairs.reserve(key_entry.second.size()); + for (auto &field_pair : key_entry.second) { + pairs.push_back(make_pair(field_pair.first, move(field_pair.second))); + } + kfv_entry.fieldValues = makeFieldValueArray(std::move(pairs)); + + table_entries.push_back(kfv_entry); + } + + // Convert vector to array + SWSSKeyOpFieldValues *data = new SWSSKeyOpFieldValues[table_entries.size()]; + for (size_t i = 0; i < table_entries.size(); i++) { + data[i] = table_entries[i]; + } + + config_table.entries.len = (uint64_t)table_entries.size(); + config_table.entries.data = data; + + config_tables.push_back(config_table); + } + + // Convert vector to final array + SWSSConfigTable *config_data = new SWSSConfigTable[config_tables.size()]; + for (size_t i = 0; i < config_tables.size(); i++) { + config_data[i] = config_tables[i]; + } + + outConfig->len = (uint64_t)config_tables.size(); + outConfig->data = config_data; + }); +} diff --git a/common/c-api/configdbconnector.h b/common/c-api/configdbconnector.h new file mode 100644 index 0000000..5b407f0 --- /dev/null +++ b/common/c-api/configdbconnector.h @@ -0,0 +1,63 @@ +#ifndef SWSS_COMMON_C_API_CONFIGDBCONNECTOR_H +#define SWSS_COMMON_C_API_CONFIGDBCONNECTOR_H + +#include "result.h" +#include "util.h" +#include "dbconnector.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef struct SWSSConfigDBConnectorOpaque *SWSSConfigDBConnector; + +// Create a new ConfigDBConnector +// Pass 0 to use TCP connection, 1 to use Unix socket +SWSSResult SWSSConfigDBConnector_new(uint8_t use_unix_socket_path, const char *netns, SWSSConfigDBConnector *outConfigDb); + +// Free the ConfigDBConnector +SWSSResult SWSSConfigDBConnector_free(SWSSConfigDBConnector configDb); + +// Connect to ConfigDB +// wait_for_init: wait for CONFIG_DB_INITIALIZED flag if true +// retry_on: retry connection on failure if true +SWSSResult SWSSConfigDBConnector_connect(SWSSConfigDBConnector configDb, uint8_t wait_for_init, uint8_t retry_on); + +// Get a single entry from a table +// Result array must be freed using SWSSFieldValueArray_free() +SWSSResult SWSSConfigDBConnector_get_entry(SWSSConfigDBConnector configDb, const char *table, const char *key, SWSSFieldValueArray *outEntry); + +// Get all keys from a table +// Result array and all of its elements must be freed using appropriate free functions +SWSSResult SWSSConfigDBConnector_get_keys(SWSSConfigDBConnector configDb, const char *table, uint8_t split, SWSSStringArray *outKeys); + +// Get entire table as key-value pairs +// Result is a map-like structure where each entry contains a key and its field-value pairs +// Result array and all of its elements must be freed using appropriate free functions +SWSSResult SWSSConfigDBConnector_get_table(SWSSConfigDBConnector configDb, const char *table, SWSSKeyOpFieldValuesArray *outTable); + +// Set an entry in a table +SWSSResult SWSSConfigDBConnector_set_entry(SWSSConfigDBConnector configDb, const char *table, const char *key, const SWSSFieldValueArray *data); + +// Modify an entry in a table (update existing fields, add new ones) +SWSSResult SWSSConfigDBConnector_mod_entry(SWSSConfigDBConnector configDb, const char *table, const char *key, const SWSSFieldValueArray *data); + +// Delete an entire table +SWSSResult SWSSConfigDBConnector_delete_table(SWSSConfigDBConnector configDb, const char *table); + +// Get the Redis client for the specified database +// Returns a SWSSDBConnector that can be used to perform direct Redis operations +SWSSResult SWSSConfigDBConnector_get_redis_client(SWSSConfigDBConnector configDb, const char *db_name, SWSSDBConnector *outDbConnector); + +// Get the entire configuration as a nested structure +// Returns a nested table structure containing all configuration data +// The result represents a map of table_name -> (key -> (field -> value)) +SWSSResult SWSSConfigDBConnector_get_config(SWSSConfigDBConnector configDb, SWSSConfigMap *outConfig); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/common/c-api/events.cpp b/common/c-api/events.cpp new file mode 100644 index 0000000..916718a --- /dev/null +++ b/common/c-api/events.cpp @@ -0,0 +1,71 @@ +#include +#include +#include + +#include "../events.h" +#include "events.h" +#include "util.h" + +using namespace std; + +struct SWSSEventPublisherOpaque { + event_handle_t handle; + + SWSSEventPublisherOpaque(event_handle_t h) : handle(h) {} +}; + +SWSSResult SWSSEventPublisher_new(const char *event_source, SWSSEventPublisher *outPublisher) { + SWSSTry({ + string source_str = event_source ? string(event_source) : ""; + event_handle_t handle = events_init_publisher(source_str); + if (handle == nullptr) { + throw std::runtime_error("Failed to initialize event publisher"); + } + *outPublisher = new SWSSEventPublisherOpaque(handle); + }); +} + +SWSSResult SWSSEventPublisher_deinit(SWSSEventPublisher publisher) { + SWSSTry({ + if (publisher) { + events_deinit_publisher(publisher->handle); + publisher->handle = nullptr; + } + }); +} + +SWSSResult SWSSEventPublisher_free(SWSSEventPublisher publisher) { + SWSSTry({ + if (publisher) { + if (publisher->handle) { + events_deinit_publisher(publisher->handle); + } + delete publisher; + } + }); +} + +SWSSResult SWSSEventPublisher_publish(SWSSEventPublisher publisher, const char *event_tag, const SWSSFieldValueArray *params) { + SWSSTry({ + if (!publisher || !event_tag) { + throw std::invalid_argument("Invalid publisher or event_tag"); + } + + string tag_str(event_tag); + event_params_t event_params; + + // Convert SWSSFieldValueArray to event_params_t if params provided + if (params) { + for (uint64_t i = 0; i < params->len; i++) { + string key(params->data[i].field); + string value = takeString(std::move(params->data[i].value)); + event_params[key] = value; + } + } + + int result = event_publish(publisher->handle, tag_str, params ? &event_params : nullptr); + if (result != 0) { + throw std::runtime_error("Failed to publish event, error code: " + std::to_string(result)); + } + }); +} \ No newline at end of file diff --git a/common/c-api/events.h b/common/c-api/events.h new file mode 100644 index 0000000..c96c98a --- /dev/null +++ b/common/c-api/events.h @@ -0,0 +1,35 @@ +#ifndef SWSS_COMMON_C_API_EVENTS_H +#define SWSS_COMMON_C_API_EVENTS_H + +#include "result.h" +#include "util.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef struct SWSSEventPublisherOpaque *SWSSEventPublisher; + +// Initialize an event publisher for a given source +// event_source: The YANG module name for the event source +SWSSResult SWSSEventPublisher_new(const char *event_source, SWSSEventPublisher *outPublisher); + +// Deinitialize the event publisher (without freeing the handle) +SWSSResult SWSSEventPublisher_deinit(SWSSEventPublisher publisher); + +// Free the event publisher +SWSSResult SWSSEventPublisher_free(SWSSEventPublisher publisher); + +// Publish an event with the given tag and parameters +// publisher: Event publisher handle +// event_tag: Name of the YANG container that defines this event +// params: Parameters associated with the event (can be NULL) +SWSSResult SWSSEventPublisher_publish(SWSSEventPublisher publisher, const char *event_tag, const SWSSFieldValueArray *params); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/common/c-api/util.cpp b/common/c-api/util.cpp index bc17342..70162f1 100644 --- a/common/c-api/util.cpp +++ b/common/c-api/util.cpp @@ -33,3 +33,40 @@ void SWSSKeyOpFieldValuesArray_free(SWSSKeyOpFieldValuesArray kfvs) { void SWSSStringArray_free(SWSSStringArray arr) { delete[] arr.data; } + +void SWSSConfigMap_free(SWSSConfigMap config) { + if (config.data) { + for (uint64_t i = 0; i < config.len; i++) { + SWSSConfigTable &table = config.data[i]; + + // Free table name + free(const_cast(table.table_name)); + + // Free entries array and its contents + if (table.entries.data) { + for (uint64_t j = 0; j < table.entries.len; j++) { + SWSSKeyOpFieldValues &kfv = table.entries.data[j]; + + // Free key + free(const_cast(kfv.key)); + + // Free field-value array + if (kfv.fieldValues.data) { + for (uint64_t k = 0; k < kfv.fieldValues.len; k++) { + SWSSFieldValueTuple &fv = kfv.fieldValues.data[k]; + + // Free field name + free(const_cast(fv.field)); + + // Free value string - SWSSString_free handles NULL + SWSSString_free(fv.value); + } + delete[] kfv.fieldValues.data; + } + } + delete[] table.entries.data; + } + } + delete[] config.data; + } +} diff --git a/common/c-api/util.h b/common/c-api/util.h index 103755b..844b0cc 100644 --- a/common/c-api/util.h +++ b/common/c-api/util.h @@ -103,6 +103,26 @@ void SWSSKeyOpFieldValuesArray_free(SWSSKeyOpFieldValuesArray kfvs); // grained control of ownership). void SWSSStringArray_free(SWSSStringArray arr); +// FFI version of map> (key -> field -> value) +// This represents one table's configuration data +// table_name should be freed with libc's free() +// entries should be freed with SWSSKeyOpFieldValuesArray_free() +typedef struct { + const char *table_name; + SWSSKeyOpFieldValuesArray entries; +} SWSSConfigTable; + +// FFI version of map>> (table_name -> key -> field -> value) +// This represents the entire configuration +// data should be freed with SWSSConfigMap_free() +typedef struct { + uint64_t len; + SWSSConfigTable *data; +} SWSSConfigMap; + +// Free a SWSSConfigMap structure +void SWSSConfigMap_free(SWSSConfigMap config); + #ifdef __cplusplus } #endif diff --git a/crates/swss-common/Cargo.toml b/crates/swss-common/Cargo.toml index bfe17f2..07784fe 100644 --- a/crates/swss-common/Cargo.toml +++ b/crates/swss-common/Cargo.toml @@ -29,3 +29,4 @@ bindgen = "0.70.1" swss-common-testing = { path = "../swss-common-testing" } paste = "1.0.15" tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } +serial_test.workspace = true diff --git a/crates/swss-common/src/types.rs b/crates/swss-common/src/types.rs index 0bc286c..2f600d2 100644 --- a/crates/swss-common/src/types.rs +++ b/crates/swss-common/src/types.rs @@ -1,9 +1,11 @@ #[cfg(feature = "async")] mod async_util; +mod configdbconnector; mod consumerstatetable; mod cxxstring; mod dbconnector; +mod events; mod exception; mod logger; mod producerstatetable; @@ -14,9 +16,11 @@ mod zmqconsumerstatetable; mod zmqproducerstatetable; mod zmqserver; +pub use configdbconnector::{ConfigDBConnector, BorrowedDbConnector}; pub use consumerstatetable::ConsumerStateTable; pub use cxxstring::{CxxStr, CxxString}; pub use dbconnector::{DbConnectionInfo, DbConnector}; +pub use events::EventPublisher; pub use exception::{Exception, Result}; pub use logger::{link_to_swsscommon_logger, log_level, log_output, LoggerConfigChangeHandler}; pub use producerstatetable::ProducerStateTable; diff --git a/crates/swss-common/src/types/configdbconnector.rs b/crates/swss-common/src/types/configdbconnector.rs new file mode 100644 index 0000000..7e46df3 --- /dev/null +++ b/crates/swss-common/src/types/configdbconnector.rs @@ -0,0 +1,275 @@ +use super::*; +use crate::bindings::*; +use std::collections::HashMap; + +/// Rust wrapper around `swss::ConfigDBConnector_Native`. +#[derive(Debug)] +pub struct ConfigDBConnector { + pub(crate) ptr: SWSSConfigDBConnector, + use_unix_socket_path: bool, + netns: String, +} + +impl ConfigDBConnector { + /// Create a new ConfigDBConnector. + pub fn new(use_unix_socket_path: bool, netns: Option) -> Result { + let netns_str = netns.unwrap_or_default(); + let netns_cstr = cstr(&netns_str); + + let ptr = unsafe { + swss_try!(p_config_db => SWSSConfigDBConnector_new( + use_unix_socket_path as u8, + netns_cstr.as_ptr(), + p_config_db + ))? + }; + + Ok(Self { + ptr, + use_unix_socket_path, + netns: netns_str, + }) + } + + /// Connect to ConfigDB. + /// + /// # Arguments + /// * `wait_for_init` - Wait for CONFIG_DB_INITIALIZED flag if true + /// * `retry_on` - Retry connection on failure if true + pub fn connect(&self, wait_for_init: bool, retry_on: bool) -> Result<()> { + unsafe { + swss_try!(SWSSConfigDBConnector_connect( + self.ptr, + wait_for_init as u8, + retry_on as u8 + )) + } + } + + /// Get a single entry from a table. + pub fn get_entry(&self, table: &str, key: &str) -> Result> { + let table_cstr = cstr(table); + let key_cstr = cstr(key); + + unsafe { + let arr = swss_try!(p_arr => SWSSConfigDBConnector_get_entry( + self.ptr, + table_cstr.as_ptr(), + key_cstr.as_ptr(), + p_arr + ))?; + Ok(take_field_value_array(arr)) + } + } + + /// Get all keys from a table. + pub fn get_keys(&self, table: &str, split: bool) -> Result> { + let table_cstr = cstr(table); + + unsafe { + let arr = swss_try!(p_arr => SWSSConfigDBConnector_get_keys( + self.ptr, + table_cstr.as_ptr(), + split as u8, + p_arr + ))?; + Ok(take_string_array(arr)) + } + } + + /// Get entire table as key-value pairs. + /// Returns a HashMap where keys are table keys and values are field-value maps. + pub fn get_table(&self, table: &str) -> Result>> { + let table_cstr = cstr(table); + + unsafe { + let arr = swss_try!(p_arr => SWSSConfigDBConnector_get_table( + self.ptr, + table_cstr.as_ptr(), + p_arr + ))?; + + let kfvs = take_key_op_field_values_array(arr); + let mut table_map = HashMap::new(); + + for kfv in kfvs { + table_map.insert(kfv.key, kfv.field_values); + } + + Ok(table_map) + } + } + + /// Set an entry in a table. + pub fn set_entry(&self, table: &str, key: &str, data: I) -> Result<()> + where + I: IntoIterator, + F: AsRef<[u8]>, + V: Into, + { + let table_cstr = cstr(table); + let key_cstr = cstr(key); + let (fv_array, _keep_alive) = make_field_value_array(data); + + unsafe { + swss_try!(SWSSConfigDBConnector_set_entry( + self.ptr, + table_cstr.as_ptr(), + key_cstr.as_ptr(), + &fv_array + )) + } + } + + /// Modify an entry in a table (update existing fields, add new ones). + pub fn mod_entry(&self, table: &str, key: &str, data: I) -> Result<()> + where + I: IntoIterator, + F: AsRef<[u8]>, + V: Into, + { + let table_cstr = cstr(table); + let key_cstr = cstr(key); + let (fv_array, _keep_alive) = make_field_value_array(data); + + unsafe { + swss_try!(SWSSConfigDBConnector_mod_entry( + self.ptr, + table_cstr.as_ptr(), + key_cstr.as_ptr(), + &fv_array + )) + } + } + + /// Delete an entire table. + pub fn delete_table(&self, table: &str) -> Result<()> { + let table_cstr = cstr(table); + + unsafe { + swss_try!(SWSSConfigDBConnector_delete_table( + self.ptr, + table_cstr.as_ptr() + )) + } + } + + /// Get connection info. + pub fn use_unix_socket_path(&self) -> bool { + self.use_unix_socket_path + } + + /// Get netns. + pub fn netns(&self) -> &str { + &self.netns + } + + /// Get the Redis client for the specified database. + /// This allows direct Redis operations on the underlying database. + /// Returns a borrowed reference to the underlying Redis client. + pub fn get_redis_client(&self, db_name: &str) -> Result { + let db_name_cstr = cstr(db_name); + + unsafe { + let db_connector_ptr = swss_try!(p_db => SWSSConfigDBConnector_get_redis_client( + self.ptr, + db_name_cstr.as_ptr(), + p_db + ))?; + + // Create a borrowed wrapper around the returned pointer + // Note: This is a reference to an existing connector, so we don't own it + Ok(BorrowedDbConnector { ptr: db_connector_ptr }) + } + } + + /// Get the entire configuration as a nested map structure. + /// Returns a HashMap where keys are table names and values are table contents + /// (key -> field -> value mappings). + pub fn get_config(&self) -> Result>>> { + unsafe { + let config_map = swss_try!(p_config => SWSSConfigDBConnector_get_config( + self.ptr, + p_config + ))?; + + let mut result = HashMap::new(); + + // Process each table + for i in 0..config_map.len { + let i_usize = i as usize; + let table = &*config_map.data.add(i_usize); + let table_name = std::ffi::CStr::from_ptr(table.table_name).to_string_lossy().to_string(); + + let mut table_entries = HashMap::new(); + + // Process each key in the table + for j in 0..table.entries.len { + let j_usize = j as usize; + let entry = &*table.entries.data.add(j_usize); + let key = std::ffi::CStr::from_ptr(entry.key).to_string_lossy().to_string(); + + let mut field_values = HashMap::new(); + + // Process each field-value pair + for k in 0..entry.fieldValues.len { + let k_usize = k as usize; + let fv = &*entry.fieldValues.data.add(k_usize); + let field = std::ffi::CStr::from_ptr(fv.field).to_string_lossy().to_string(); + // Note: We need to take ownership of the string, but fv.value will be freed + // by SWSSConfigMap_free, so we need to clone the string content first + let value_str = std::ffi::CStr::from_ptr(SWSSStrRef_c_str(fv.value as SWSSStrRef)).to_string_lossy().to_string(); + let value = CxxString::new(value_str); + + field_values.insert(field, value); + } + + table_entries.insert(key, field_values); + } + + result.insert(table_name, table_entries); + } + + // Free the C structure + SWSSConfigMap_free(config_map); + + Ok(result) + } + } +} + +impl Drop for ConfigDBConnector { + fn drop(&mut self) { + // Ignore errors in Drop to avoid panic-during-panic scenarios + let _ = unsafe { swss_try!(SWSSConfigDBConnector_free(self.ptr)) }; + } +} + +unsafe impl Send for ConfigDBConnector {} + +#[cfg(feature = "async")] +impl ConfigDBConnector { + async_util::impl_basic_async_method!(new_async <= new(use_unix_socket_path: bool, netns: Option) -> Result); + async_util::impl_basic_async_method!(connect_async <= connect(&self, wait_for_init: bool, retry_on: bool) -> Result<()>); + async_util::impl_basic_async_method!(get_entry_async <= get_entry(&self, table: &str, key: &str) -> Result>); + async_util::impl_basic_async_method!(get_keys_async <= get_keys(&self, table: &str, split: bool) -> Result>); + async_util::impl_basic_async_method!(get_table_async <= get_table(&self, table: &str) -> Result>>); + async_util::impl_basic_async_method!(get_config_async <= get_config(&self) -> Result>>>); + async_util::impl_basic_async_method!(delete_table_async <= delete_table(&self, table: &str) -> Result<()>); +} + +/// A borrowed reference to a DbConnector obtained from ConfigDBConnector. +/// This does not own the underlying connection and does not implement Drop. +#[derive(Debug)] +pub struct BorrowedDbConnector { + ptr: SWSSDBConnector, +} + +impl BorrowedDbConnector { + /// Flush the current database, removing all keys. + /// Equivalent to Redis FLUSHDB command. + pub fn flush_db(&self) -> Result { + let status = unsafe { swss_try!(p_status => SWSSDBConnector_flushdb(self.ptr, p_status))? }; + Ok(status == 1) + } +} \ No newline at end of file diff --git a/crates/swss-common/src/types/events.rs b/crates/swss-common/src/types/events.rs new file mode 100644 index 0000000..be433a2 --- /dev/null +++ b/crates/swss-common/src/types/events.rs @@ -0,0 +1,111 @@ +use super::*; +use crate::bindings::*; +use std::collections::HashMap; + +/// Rust wrapper around `SWSSEventPublisher`. +#[derive(Debug)] +pub struct EventPublisher { + pub(crate) ptr: SWSSEventPublisher, + event_source: String, +} + +impl EventPublisher { + /// Create a new EventPublisher. + /// + /// # Arguments + /// * `event_source` - The YANG module name for the event source + pub fn new(event_source: &str) -> Result { + let source_cstr = cstr(event_source); + + let ptr = unsafe { + swss_try!(p_publisher => SWSSEventPublisher_new( + source_cstr.as_ptr(), + p_publisher + ))? + }; + + Ok(Self { + ptr, + event_source: event_source.to_string(), + }) + } + + /// Publish an event with the given tag and parameters. + /// + /// # Arguments + /// * `event_tag` - Name of the YANG container that defines this event + /// * `params` - Parameters associated with the event (optional) + pub fn publish(&self, event_tag: &str, params: Option<&HashMap>) -> Result<()> { + let tag_cstr = cstr(event_tag); + + unsafe { + if let Some(params_map) = params { + // Keep CStrings alive for the duration of the call + let keys_cstrs: Vec<_> = params_map.keys().map(|k| cstr(k)).collect(); + let values_cstrs: Vec<_> = params_map.values().map(|v| cstr(v)).collect(); + + // Convert HashMap to SWSSFieldValueArray + let mut field_values: Vec = keys_cstrs + .iter() + .zip(values_cstrs.iter()) + .map(|(key_cstr, value_cstr)| { + let value_str = SWSSString_new_c_str(value_cstr.as_ptr()); + SWSSFieldValueTuple { + field: key_cstr.as_ptr(), + value: value_str, + } + }) + .collect(); + + let params_array = SWSSFieldValueArray { + data: field_values.as_mut_ptr(), + len: field_values.len() as u64, + }; + + let result = swss_try!(SWSSEventPublisher_publish( + self.ptr, + tag_cstr.as_ptr(), + ¶ms_array + )); + + // Clean up SWSSStrings + for field_value in &field_values { + SWSSString_free(field_value.value); + } + + result + } else { + swss_try!(SWSSEventPublisher_publish( + self.ptr, + tag_cstr.as_ptr(), + std::ptr::null() + )) + } + } + } + + /// Deinitialize the event publisher - matches Python swsscommon.events_deinit_publisher() + pub fn deinit(&mut self) -> Result<()> { + unsafe { + swss_try!(SWSSEventPublisher_deinit(self.ptr)) + } + } + + /// Get the event source associated with this publisher. + pub fn event_source(&self) -> &str { + &self.event_source + } +} + +impl Drop for EventPublisher { + fn drop(&mut self) { + unsafe { + let _ = SWSSEventPublisher_free(self.ptr); + } + } +} + +// Safety: EventPublisher can be safely sent between threads +unsafe impl Send for EventPublisher {} +// Safety: EventPublisher can be safely shared between threads with proper synchronization +unsafe impl Sync for EventPublisher {} \ No newline at end of file diff --git a/crates/swss-common/tests/logger.rs b/crates/swss-common/tests/logger.rs index b27e1b2..1694726 100644 --- a/crates/swss-common/tests/logger.rs +++ b/crates/swss-common/tests/logger.rs @@ -4,6 +4,7 @@ use std::thread::sleep; use std::time::Duration; use swss_common::*; use swss_common_testing::*; +use serial_test::serial; lazy_static! { static ref LEVEL: Mutex = Mutex::new("INFO".to_string()); @@ -23,6 +24,7 @@ impl LoggerConfigChangeHandler for LoggerConfigHandlerForTest { } } #[test] +#[serial] fn logger_init_test() -> Result<(), Exception> { let redis = Redis::start_config_db(); // todo: change redis::db_connector to passing db_id as parameter diff --git a/crates/swss-common/tests/logger_fallback.rs b/crates/swss-common/tests/logger_fallback.rs index 5e3a761..e26ea86 100644 --- a/crates/swss-common/tests/logger_fallback.rs +++ b/crates/swss-common/tests/logger_fallback.rs @@ -1,4 +1,5 @@ use swss_common::*; +use std::process::Command; struct LoggerConfigHandlerForTest {} @@ -14,8 +15,17 @@ impl LoggerConfigChangeHandler for LoggerConfigHandlerForTest { // This test has to be run in a separate test suite because it can't run with other tests that use Redis. #[test] fn logger_init_without_redis() { + // Print current process list + let ps_output = Command::new("ps") + .args(&["aux"]) + .output(); + assert!(ps_output.is_ok(), "Failed to execute ps command"); + let ps_result = ps_output.unwrap(); + assert!(ps_result.status.success(), "ps command failed"); + println!("Process list:\n{}", String::from_utf8_lossy(&ps_result.stdout)); + let handler = LoggerConfigHandlerForTest {}; - // connect to swsscommon logger + // connect to swsscommon logger - should fail when Redis is not available let result = link_to_swsscommon_logger("test", handler); assert!(result.is_err()); diff --git a/crates/swss-common/tests/test_config_db_connector.rs b/crates/swss-common/tests/test_config_db_connector.rs new file mode 100644 index 0000000..3ff93ce --- /dev/null +++ b/crates/swss-common/tests/test_config_db_connector.rs @@ -0,0 +1,163 @@ +use std::collections::HashMap; +use std::io; +use std::thread; +use std::time::Duration; +use swss_common::{ConfigDBConnector, CxxString, DbConnector}; +use swss_common_testing::Redis; +use serial_test::serial; + +/// Test ConfigDBConnector functionality - Rust version of test_ConfigDBConnector() +/// +/// This test verifies: +/// - ConfigDBConnector creation and connection +/// - Setting and getting configuration entries +/// - Table operations (set_entry, get_table, delete_table) +/// - Configuration data integrity +#[cfg(test)] +#[test] +#[serial] +fn test_config_db_connector() -> Result<(), Box> { + let _redis = Redis::start_config_db(); + println!("Redis unix socket: {}", _redis.sock); + + // Create ConfigDBConnector instance + let config_db = ConfigDBConnector::new(true, None)?; + + // Verify initial state + assert_eq!(config_db.netns(), ""); + assert!(config_db.use_unix_socket_path()); + + // Connect to CONFIG_DB + config_db.connect(false, false)?; + + // Set up test data + let mut test_data = HashMap::new(); + test_data.insert("alias", CxxString::new("etp1x")); + config_db.set_entry("TEST_PORT", "Ethernet111", test_data)?; + + // Get entire table and verify entry was set correctly + let table_data = config_db.get_table("TEST_PORT")?; + assert!(table_data.contains_key("Ethernet111")); + let ethernet_entry = &table_data["Ethernet111"]; + assert!(ethernet_entry.contains_key("alias")); + assert_eq!(ethernet_entry["alias"].as_cxx_str(), "etp1x"); + + // Test entry update + let mut update_data = HashMap::new(); + update_data.insert("mtu", CxxString::new("12345")); + config_db.set_entry("TEST_PORT", "Ethernet111", update_data)?; + + // Verify update replaced previous data (alias should be gone, mtu should be present) + let updated_table = config_db.get_table("TEST_PORT")?; + assert!(updated_table.contains_key("Ethernet111")); + let updated_entry = &updated_table["Ethernet111"]; + + // Debug output to help diagnose the issue + println!("Updated entry keys: {:?}", updated_entry.keys().collect::>()); + + assert!(!updated_entry.contains_key("alias"), "Previous 'alias' field should be removed"); + assert!(updated_entry.contains_key("mtu"), "Entry should contain 'mtu' key. Available keys: {:?}", updated_entry.keys().collect::>()); + assert_eq!(updated_entry["mtu"].as_cxx_str(), "12345"); + + // Test table deletion + config_db.delete_table("TEST_PORT")?; + + // Verify table is empty after deletion + let empty_table = config_db.get_table("TEST_PORT")?; + assert!(empty_table.is_empty(), "Table should be empty after deletion"); + + Ok(()) +} + +/// Test ConfigDBConnector with table separator functionality +/// Rust version of test_ConfigDBConnectorSeparator() +#[test] +#[serial] +fn test_config_db_connector_separator() -> Result<(), Box> { + let _redis = Redis::start_config_db(); + + // NOTE: This test verifies table separator functionality. + // The ConfigDBConnector should only include properly separated keys. + let config_db = ConfigDBConnector::new(true, None)?; + config_db.connect(false, false)?; + + // Set an entry in TEST_PORT table + let mut test_data = HashMap::new(); + test_data.insert("alias", CxxString::new("etp2x")); + config_db.set_entry("TEST_PORT", "Ethernet222", test_data)?; + + // Verify the entry appears in get_config (which should only include properly separated keys) + let all_config = config_db.get_table("*")?; + + // Verify our properly formatted entry exists + assert!(all_config.contains_key("Ethernet222")); + + // Clean up + config_db.delete_table("TEST_PORT")?; + let final_config = config_db.get_table("*")?; + + // Note: This may not be empty if other tests have run, but TEST_PORT should be gone + assert!(!final_config.contains_key("Ethernet222")); + + Ok(()) +} + +/// Test ConfigDBConnector connect functionality +/// Rust version of test_ConfigDBConnect() +#[test] +#[serial] +fn test_config_db_connect() -> Result<(), Box> { + let _redis = Redis::start_config_db(); + let config_db = ConfigDBConnector::new(true, None)?; + + // Test connection + config_db.connect(false, false)?; + + // Clear the database + let redis_client = config_db.get_redis_client("CONFIG_DB")?; + let flush_success = redis_client.flush_db()?; + assert!(flush_success, "Database flush should succeed"); + + // Verify we can perform basic operations after connection + // This is equivalent to allconfig = config_db.get_config() followed by assert len(allconfig) == 0 + let all_config = config_db.get_config()?; + assert_eq!(all_config.len(), 0, "Config should be empty after flushdb"); + + Ok(()) +} + +/// Test ConfigDBConnector scan functionality with many entries +/// Rust version of test_ConfigDBScan() +#[test] +#[serial] +fn test_config_db_scan() -> Result<(), Box> { + let config_db = ConfigDBConnector::new(true, None)?; + config_db.connect(false, false)?; + + let n = 100; // Reduced from 1000 to keep test fast + + // Create many table entries to test scan functionality + for i in 0..n { + let table_name = format!("TEST_TYPE{}", i); + let key_name = format!("Ethernet{}", i); + let mut field_data = HashMap::new(); + field_data.insert(format!("alias{}", i), CxxString::new(format!("etp{}", i))); + + config_db.set_entry(&table_name, &key_name, field_data)?; + } + + // Verify all entries were created by checking each table + for i in 0..n { + let table_name = format!("TEST_TYPE{}", i); + let table_data = config_db.get_table(&table_name)?; + assert!(!table_data.is_empty(), "Table {} should contain data", table_name); + } + + // Clean up - delete all test tables + for i in 0..n { + let table_name = format!("TEST_TYPE{}", i); + config_db.delete_table(&table_name)?; + } + + Ok(()) +} diff --git a/tests/c_api_ut.cpp b/tests/c_api_ut.cpp index d8d7d6b..779b718 100644 --- a/tests/c_api_ut.cpp +++ b/tests/c_api_ut.cpp @@ -4,7 +4,9 @@ #include #include "common/c-api/consumerstatetable.h" +#include "common/c-api/configdbconnector.h" #include "common/c-api/dbconnector.h" +#include "common/c-api/events.h" #include "common/c-api/producerstatetable.h" #include "common/c-api/result.h" #include "common/c-api/subscriberstatetable.h" @@ -65,6 +67,10 @@ static void freeKeyOpFieldValuesArray(SWSSKeyOpFieldValuesArray arr) { SWSSKeyOpFieldValuesArray_free(arr); } +static void freeConfigMap(SWSSConfigMap config) { + SWSSConfigMap_free(config); +} + struct SWSSStringManager { vector m_strings; bool use_c_str = false; @@ -92,6 +98,124 @@ struct SWSSStringManager { } }; +TEST(c_api, ConfigDBConnector) { + clearDB(); + SWSSStringManager sm; + + // Test ConfigDBConnector creation + SWSSConfigDBConnector configDb; + SWSSConfigDBConnector_new(1, "", &configDb); + + // Test connect + SWSSConfigDBConnector_connect(configDb, 0, 0); + + // Test set_entry + SWSSFieldValueTuple data[2] = { + {.field = "field1", .value = sm.makeString("value1")}, + {.field = "field2", .value = sm.makeString("value2")} + }; + SWSSFieldValueArray fvs = {.len = 2, .data = data}; + SWSSConfigDBConnector_set_entry(configDb, "TEST_TABLE", "test_key", &fvs); + + // Test get_entry + SWSSFieldValueArray entry; + SWSSConfigDBConnector_get_entry(configDb, "TEST_TABLE", "test_key", &entry); + ASSERT_EQ(entry.len, 2); + // Verify the entries are correct (note: order may vary) + bool found_field1 = false, found_field2 = false; + for (uint64_t i = 0; i < entry.len; i++) { + if (strcmp(entry.data[i].field, "field1") == 0) { + EXPECT_STREQ(SWSSStrRef_c_str((SWSSStrRef)entry.data[i].value), "value1"); + found_field1 = true; + } else if (strcmp(entry.data[i].field, "field2") == 0) { + EXPECT_STREQ(SWSSStrRef_c_str((SWSSStrRef)entry.data[i].value), "value2"); + found_field2 = true; + } + } + EXPECT_TRUE(found_field1); + EXPECT_TRUE(found_field2); + freeFieldValuesArray(entry); + + // Test get_keys + SWSSStringArray keys; + SWSSConfigDBConnector_get_keys(configDb, "TEST_TABLE", 1, &keys); + ASSERT_EQ(keys.len, 1); + EXPECT_STREQ(keys.data[0], "test_key"); + for (uint64_t i = 0; i < keys.len; i++) { + free(keys.data[i]); + } + SWSSStringArray_free(keys); + + // Test get_table + SWSSKeyOpFieldValuesArray table; + SWSSConfigDBConnector_get_table(configDb, "TEST_TABLE", &table); + ASSERT_EQ(table.len, 1); + EXPECT_STREQ(table.data[0].key, "test_key"); + ASSERT_EQ(table.data[0].fieldValues.len, 2); + freeKeyOpFieldValuesArray(table); + + // Test mod_entry (add another field) + SWSSFieldValueTuple mod_data[1] = { + {.field = "field3", .value = sm.makeString("value3")} + }; + SWSSFieldValueArray mod_fvs = {.len = 1, .data = mod_data}; + SWSSConfigDBConnector_mod_entry(configDb, "TEST_TABLE", "test_key", &mod_fvs); + + // Verify mod_entry worked + SWSSConfigDBConnector_get_entry(configDb, "TEST_TABLE", "test_key", &entry); + EXPECT_EQ(entry.len, 3); // Should now have 3 fields + freeFieldValuesArray(entry); + + // Test get_redis_client and flush_db + SWSSDBConnector redisClient; + SWSSConfigDBConnector_get_redis_client(configDb, "CONFIG_DB", &redisClient); + int8_t flushStatus; + SWSSDBConnector_flushdb(redisClient, &flushStatus); + EXPECT_TRUE(flushStatus); + + // Add some test data for get_config + SWSSConfigDBConnector_set_entry(configDb, "TABLE1", "key1", &fvs); + SWSSFieldValueTuple single_data[1] = { + {.field = "single_field", .value = sm.makeString("single_value")} + }; + SWSSFieldValueArray single_fvs = {.len = 1, .data = single_data}; + SWSSConfigDBConnector_set_entry(configDb, "TABLE2", "key2", &single_fvs); + + // Test get_config + SWSSConfigMap config; + SWSSConfigDBConnector_get_config(configDb, &config); + + // Should have at least the tables we just created + EXPECT_GE(config.len, 2); + + bool found_table1 = false, found_table2 = false; + for (uint64_t i = 0; i < config.len; i++) { + if (strcmp(config.data[i].table_name, "TABLE1") == 0) { + found_table1 = true; + ASSERT_EQ(config.data[i].entries.len, 1); + EXPECT_STREQ(config.data[i].entries.data[0].key, "key1"); + EXPECT_EQ(config.data[i].entries.data[0].fieldValues.len, 2); + } else if (strcmp(config.data[i].table_name, "TABLE2") == 0) { + found_table2 = true; + ASSERT_EQ(config.data[i].entries.len, 1); + EXPECT_STREQ(config.data[i].entries.data[0].key, "key2"); + EXPECT_EQ(config.data[i].entries.data[0].fieldValues.len, 1); + } + } + EXPECT_TRUE(found_table1); + EXPECT_TRUE(found_table2); + freeConfigMap(config); + + // Test delete_table + SWSSConfigDBConnector_delete_table(configDb, "TABLE1"); + SWSSConfigDBConnector_get_keys(configDb, "TABLE1", 0, &keys); + EXPECT_EQ(keys.len, 0); // Should be empty after deletion + SWSSStringArray_free(keys); + + // Clean up + SWSSConfigDBConnector_free(configDb); +} + void logLevelNotify(const char* component, const char* prioStr) { if (strcmp(component, LOG_NAME_FOR_TEST) != 0) @@ -488,6 +612,56 @@ TEST(c_api, ZmqConsumerProducerStateTable) { SWSSDBConnector_free(db); } +TEST(c_api, EventPublisher) { + SWSSStringManager sm; + + // Test EventPublisher creation + SWSSEventPublisher publisher; + SWSSResult result = SWSSEventPublisher_new("test-module", &publisher); + EXPECT_EQ(result.exception, SWSSException_None); + EXPECT_NE(publisher, nullptr); + + // Test publishing event without parameters + result = SWSSEventPublisher_publish(publisher, "test-event", nullptr); + EXPECT_EQ(result.exception, SWSSException_None); + + // Test publishing event with parameters + SWSSFieldValueTuple params[2] = { + {.field = "param1", .value = sm.makeString("value1")}, + {.field = "param2", .value = sm.makeString("value2")} + }; + SWSSFieldValueArray param_array = {.len = 2, .data = params}; + result = SWSSEventPublisher_publish(publisher, "test-event-with-params", ¶m_array); + EXPECT_EQ(result.exception, SWSSException_None); + + // Test deinitialize + result = SWSSEventPublisher_deinit(publisher); + EXPECT_EQ(result.exception, SWSSException_None); + + // Test free + result = SWSSEventPublisher_free(publisher); + EXPECT_EQ(result.exception, SWSSException_None); + + // Test error handling - invalid publisher + result = SWSSEventPublisher_publish(nullptr, "test-event", nullptr); + EXPECT_NE(result.exception, SWSSException_None); + if (result.location) SWSSString_free(result.location); + if (result.message) SWSSString_free(result.message); + + // Test error handling - invalid event_tag + SWSSEventPublisher publisher2; + result = SWSSEventPublisher_new("test-module2", &publisher2); + EXPECT_EQ(result.exception, SWSSException_None); + + result = SWSSEventPublisher_publish(publisher2, nullptr, nullptr); + EXPECT_NE(result.exception, SWSSException_None); + if (result.location) SWSSString_free(result.location); + if (result.message) SWSSString_free(result.message); + + // Clean up + SWSSEventPublisher_free(publisher2); +} + TEST(c_api, exceptions) { SWSSDBConnector db = nullptr; SWSSResult result = SWSSDBConnector_new_tcp(0, "127.0.0.1", 1, 1000, &db);