Introduces a higher-order topology that wraps two OPNSenseFirewall instances (primary + backup) and orchestrates score application across both. CARP VIPs get differentiated advskew values (primary=0, backup=configurable) while all other scores apply identically to both firewalls. Includes CarpVipScore, DhcpServer delegation, pair Score impls for all existing OPNsense scores, and opnsense_from_config() factory method. Also adds ROADMAP entries for generic firewall trait (10), delegation macro, integration tests, and named config instances (11). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3.7 KiB
Phase 11: Named Config Instances & Cross-Namespace Access
Goal
Allow multiple instances of the same config type within a single namespace, identified by name. Also allow explicit namespace specification when retrieving config items, enabling cross-deployment orchestration.
Context
The current harmony_config system identifies config items by type only (T::KEY from #[derive(Config)]). This works for singletons but breaks when you need multiple instances of the same type:
- Firewall pair: primary and backup need separate
OPNSenseApiCredentials(different API keys for different devices) - Worker nodes: each BMC has its own
IpmiCredentialswith different username/password - Firewall administrators: multiple
OPNSenseApiCredentialswith different permission levels - Multi-tenant: customer firewalls vs. NationTech infrastructure firewalls need separate credential sets
Using separate namespaces per device is not the answer — a firewall pair belongs to a single deployment, and forcing namespace switches for each device in a pair adds unnecessary friction.
Cross-namespace access is a separate but related need: the NT firewall pair and C1 customer firewall pair live in separate namespaces (the customer manages their own firewall), but NationTech needs read access to the C1 namespace for BINAT coordination.
Tasks
11.1 Named config instances within a namespace
Priority: HIGH Status: Not started
Extend the Config trait and ConfigManager to support an optional instance name:
// Current (singleton): gets "OPNSenseApiCredentials" from the active namespace
let creds = ConfigManager::get::<OPNSenseApiCredentials>().await?;
// New (named): gets "OPNSenseApiCredentials/fw-primary" from the active namespace
let primary_creds = ConfigManager::get_named::<OPNSenseApiCredentials>("fw-primary").await?;
let backup_creds = ConfigManager::get_named::<OPNSenseApiCredentials>("fw-backup").await?;
Storage key becomes {T::KEY}/{instance_name} (or similar). The unnamed get() remains unchanged for backward compatibility.
This needs to work across all config sources:
EnvSource:HARMONY_CONFIG_{KEY}_{NAME}(e.g.,HARMONY_CONFIG_OPNSENSE_API_CREDENTIALS_FW_PRIMARY)SqliteSource: composite key{key}/{name}StoreSource(OpenBao): path{namespace}/{key}/{name}PromptSource: prompt includes the instance name for clarity
11.2 Cross-namespace config access
Priority: MEDIUM Status: Not started
Allow specifying an explicit namespace when retrieving a config item:
// Get from the active namespace (current behavior)
let nt_creds = ConfigManager::get::<OPNSenseApiCredentials>().await?;
// Get from a specific namespace
let c1_creds = ConfigManager::get_from_namespace::<OPNSenseApiCredentials>("c1").await?;
This enables orchestration across deployments: the NT deployment can read C1's firewall credentials for BINAT coordination without switching the global namespace.
For the StoreSource (OpenBao), this maps to reading from a different KV path prefix. For SqliteSource, it maps to a different database file or a namespace column. For EnvSource, it could use a different prefix (HARMONY_CONFIG_C1_{KEY}).
11.3 Update FirewallPairTopology to use named configs
Priority: MEDIUM Status: Blocked by 11.1
Once named config instances are available, update FirewallPairTopology::opnsense_from_config() to use them:
let primary_creds = ConfigManager::get_named::<OPNSenseApiCredentials>("fw-primary").await?;
let backup_creds = ConfigManager::get_named::<OPNSenseApiCredentials>("fw-backup").await?;
This removes the current limitation of shared credentials between primary and backup.