use super::{CloseSequence, VendorProfile};
use crate::capability::Capabilities;
use crate::facts::{self, Facts};
const JUNOS_CAPABILITY: &str = "http://xml.juniper.net/netconf/junos/1.0";
#[derive(Debug, Default)]
pub struct JunosVendor {
is_cluster: bool,
}
impl JunosVendor {
pub fn detect(capabilities: &Capabilities) -> Option<Box<dyn VendorProfile>> {
if capabilities.supports(JUNOS_CAPABILITY) {
Some(Box::new(JunosVendor::default()))
} else {
None
}
}
pub fn is_cluster(&self) -> bool {
self.is_cluster
}
}
impl VendorProfile for JunosVendor {
fn name(&self) -> &str {
"junos"
}
fn wrap_config(&self, config: &str) -> String {
let trimmed = config.trim();
if trimmed.starts_with("<configuration") {
return config.to_string();
}
format!("<configuration>{trimmed}</configuration>")
}
fn unwrap_config(&self, response: &str) -> String {
let trimmed = response.trim();
let config_start = match trimmed.find("<configuration") {
Some(pos) => pos,
None => return response.to_string(),
};
let tag_end = match trimmed[config_start..].find('>') {
Some(pos) => config_start + pos + 1,
None => return response.to_string(),
};
let config_end = match trimmed.rfind("</configuration>") {
Some(pos) => pos,
None => return response.to_string(),
};
if tag_end >= config_end {
return response.to_string();
}
trimmed[tag_end..config_end].trim().to_string()
}
fn normalize_capability(&self, uri: &str) -> Option<String> {
const LEGACY_PREFIX: &str = "urn:ietf:params:xml:ns:netconf:";
const STANDARD_PREFIX: &str = "urn:ietf:params:netconf:";
uri.strip_prefix(LEGACY_PREFIX)
.map(|suffix| format!("{STANDARD_PREFIX}{suffix}"))
}
fn close_sequence(&self) -> CloseSequence {
CloseSequence::DiscardThenClose
}
fn facts_rpc(&self) -> Option<&str> {
Some("<get-system-information/>")
}
fn parse_facts(&self, response: &str) -> Facts {
facts::parse_junos_system_information(response)
}
fn post_facts_hook(&mut self, _facts: &Facts, raw_response: &str) {
if raw_response.contains("<multi-routing-engine-results>") {
self.is_cluster = true;
tracing::info!("Junos chassis cluster detected");
}
}
fn requires_open_configuration(&self) -> bool {
self.is_cluster
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn junos_capabilities() -> Capabilities {
let mut uris = HashSet::new();
uris.insert("urn:ietf:params:netconf:base:1.0".to_string());
uris.insert(JUNOS_CAPABILITY.to_string());
uris.insert("urn:ietf:params:netconf:capability:candidate:1.0".to_string());
Capabilities::new(uris, Some(42))
}
fn non_junos_capabilities() -> Capabilities {
let mut uris = HashSet::new();
uris.insert("urn:ietf:params:netconf:base:1.0".to_string());
Capabilities::new(uris, Some(1))
}
#[test]
fn test_detect_junos() {
let caps = junos_capabilities();
let vendor = JunosVendor::detect(&caps);
assert!(vendor.is_some());
assert_eq!(vendor.unwrap().name(), "junos");
}
#[test]
fn test_detect_non_junos() {
let caps = non_junos_capabilities();
assert!(JunosVendor::detect(&caps).is_none());
}
#[test]
fn test_wrap_config_bare_elements() {
let vendor = JunosVendor::default();
let config = "<system><host-name>test</host-name></system>";
let wrapped = vendor.wrap_config(config);
assert_eq!(
wrapped,
"<configuration><system><host-name>test</host-name></system></configuration>"
);
}
#[test]
fn test_wrap_config_already_wrapped() {
let vendor = JunosVendor::default();
let config = "<configuration><system><host-name>test</host-name></system></configuration>";
let wrapped = vendor.wrap_config(config);
assert_eq!(wrapped, config, "should not double-wrap");
}
#[test]
fn test_wrap_config_with_xmlns() {
let vendor = JunosVendor::default();
let config = r#"<configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm"><system/></configuration>"#;
let wrapped = vendor.wrap_config(config);
assert_eq!(wrapped, config, "should not wrap when <configuration already present");
}
#[test]
fn test_unwrap_config_strips_wrapper() {
let vendor = JunosVendor::default();
let response = r#"
<configuration junos:commit-seconds="1773949021" junos:commit-localtime="2026-03-19 19:37:01 UTC" junos:commit-user="root">
<system>
<host-name>vSRX-rustnetconf</host-name>
</system>
</configuration>"#;
let unwrapped = vendor.unwrap_config(response);
assert!(unwrapped.contains("<system>"));
assert!(unwrapped.contains("vSRX-rustnetconf"));
assert!(!unwrapped.contains("junos:commit-seconds"));
assert!(!unwrapped.starts_with("<configuration"));
}
#[test]
fn test_unwrap_config_no_wrapper() {
let vendor = JunosVendor::default();
let response = "<system><host-name>test</host-name></system>";
let unwrapped = vendor.unwrap_config(response);
assert_eq!(unwrapped, response, "should return as-is when no wrapper");
}
#[test]
fn test_normalize_legacy_capability() {
let vendor = JunosVendor::default();
let legacy = "urn:ietf:params:xml:ns:netconf:capability:candidate:1.0";
let normalized = vendor.normalize_capability(legacy);
assert_eq!(
normalized,
Some("urn:ietf:params:netconf:capability:candidate:1.0".to_string())
);
}
#[test]
fn test_normalize_standard_capability() {
let vendor = JunosVendor::default();
let standard = "urn:ietf:params:netconf:capability:candidate:1.0";
assert_eq!(vendor.normalize_capability(standard), None, "standard URIs need no normalization");
}
#[test]
fn test_close_sequence() {
assert_eq!(JunosVendor::default().close_sequence(), CloseSequence::DiscardThenClose);
}
#[test]
fn test_cluster_detection_from_multi_re() {
let mut vendor = JunosVendor::default();
assert!(!vendor.is_cluster());
assert!(!vendor.requires_open_configuration());
let response = r#"<multi-routing-engine-results>
<multi-routing-engine-item>
<re-name>node0</re-name>
<software-information>
<host-name>vsrx-node0</host-name>
</software-information>
</multi-routing-engine-item>
</multi-routing-engine-results>"#;
let facts = Facts::default();
vendor.post_facts_hook(&facts, response);
assert!(vendor.is_cluster());
assert!(vendor.requires_open_configuration());
}
#[test]
fn test_non_cluster_detection() {
let mut vendor = JunosVendor::default();
let response = r#"<software-information>
<host-name>vsrx1</host-name>
<product-model>vSRX</product-model>
</software-information>"#;
let facts = Facts::default();
vendor.post_facts_hook(&facts, response);
assert!(!vendor.is_cluster());
assert!(!vendor.requires_open_configuration());
}
}