use crate::command_types::CommandType;
use crate::error::{Error, Result};
use crate::version::{FirmwareVersion, VersionRange};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::path::Path;
const BUNDLED: &[(&str, &str)] = &[
(
"arista-eos.yaml",
include_str!("../catalog/arista-eos.yaml"),
),
(
"aruba-aoscx.yaml",
include_str!("../catalog/aruba-aoscx.yaml"),
),
(
"cisco-ios-xe.yaml",
include_str!("../catalog/cisco-ios-xe.yaml"),
),
(
"cisco-nxos.yaml",
include_str!("../catalog/cisco-nxos.yaml"),
),
(
"hpe-procurve.yaml",
include_str!("../catalog/hpe-procurve.yaml"),
),
(
"juniper-junos.yaml",
include_str!("../catalog/juniper-junos.yaml"),
),
(
"meraki-mx-ms.yaml",
include_str!("../catalog/meraki-mx-ms.yaml"),
),
];
#[derive(Debug, Clone, Default)]
pub struct Catalog {
vendors: IndexMap<String, VendorFile>,
}
impl Catalog {
pub fn load_bundled() -> Result<Self> {
let mut cat = Catalog::default();
for (name, body) in BUNDLED {
let parsed: VendorFile =
serde_yaml::from_str(body).map_err(|source| Error::CatalogParse {
file: (*name).to_owned(),
source,
})?;
cat.vendors.insert(parsed.vendor.clone(), parsed);
}
Ok(cat)
}
pub fn load_dir(dir: impl AsRef<Path>) -> Result<Self> {
let mut cat = Catalog::default();
for entry in std::fs::read_dir(dir.as_ref())? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("yaml") {
continue;
}
let body = std::fs::read_to_string(&path)?;
let name = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("?")
.to_owned();
let parsed: VendorFile = serde_yaml::from_str(&body)
.map_err(|source| Error::CatalogParse { file: name, source })?;
cat.vendors.insert(parsed.vendor.clone(), parsed);
}
Ok(cat)
}
pub fn vendors(&self) -> impl Iterator<Item = &str> {
self.vendors.keys().map(String::as_str)
}
pub fn vendor(&self, id: &str) -> Option<&VendorFile> {
self.vendors.get(id)
}
pub fn lookup(
&self,
vendor: &str,
firmware: &str,
command: CommandType,
) -> Result<Option<&CommandEntry>> {
let vf = self
.vendors
.get(vendor)
.ok_or_else(|| Error::UnknownVendor(vendor.to_owned()))?;
let fw = FirmwareVersion::parse(firmware)?;
let Some(cmd) = vf.commands.iter().find(|c| c.command_type == command) else {
return Ok(None);
};
let mut best: Option<(&CommandEntry, usize)> = None;
for entry in &cmd.versions {
let range = VersionRange::parse(&entry.applies_to)?;
if !range.matches(&fw) {
continue;
}
let score = range.specificity();
if best.is_none_or(|(_, s)| score > s) {
best = Some((entry, score));
}
}
match best {
Some((entry, _)) => {
if entry.cli == "NOT_SUPPORTED" {
return Err(Error::NotSupported {
vendor: vendor.to_owned(),
command,
reason: entry.notes.clone().unwrap_or_default(),
});
}
Ok(Some(entry))
}
None => Err(Error::NoMatchingEntry {
vendor: vendor.to_owned(),
firmware: Some(firmware.to_owned()),
command,
}),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VendorFile {
pub vendor: String,
pub display_name: String,
pub manufacturer: String,
pub product_family: String,
#[serde(default)]
pub notes: Option<String>,
#[serde(default)]
pub sources: Vec<Source>,
#[serde(default)]
pub protocol_capabilities: ProtocolCapabilities,
pub commands: Vec<CommandBlock>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Source {
pub title: String,
pub url: String,
pub accessed: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProtocolCapabilities {
#[serde(default)]
pub netconf: Option<ProtocolCapability>,
#[serde(default)]
pub restconf: Option<ProtocolCapability>,
#[serde(default)]
pub gnmi: Option<ProtocolCapability>,
#[serde(default)]
pub rest_api: Option<ProtocolCapability>,
#[serde(default)]
pub snmp: Option<ProtocolCapability>,
#[serde(default)]
pub dashboard_api: Option<ProtocolCapability>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtocolCapability {
#[serde(default)]
pub introduced_in: Option<String>,
#[serde(default)]
pub notes: Option<String>,
#[serde(flatten)]
pub extras: IndexMap<String, serde_yaml::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandBlock {
#[serde(rename = "type")]
pub command_type: CommandType,
#[serde(default)]
pub description: Option<String>,
pub versions: Vec<CommandEntry>,
#[serde(default)]
pub protocol_alternatives: ProtocolAlternatives,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandEntry {
pub applies_to: String,
pub cli: String,
#[serde(default)]
pub sample_output: Option<String>,
#[serde(default)]
pub parser_notes: Option<String>,
#[serde(default)]
pub config_required: Option<String>,
#[serde(default)]
pub notes: Option<String>,
#[serde(default)]
pub unverified: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProtocolAlternatives {
#[serde(default)]
pub netconf: Option<NetconfMapping>,
#[serde(default)]
pub restconf: Option<RestconfMapping>,
#[serde(default)]
pub gnmi: Option<GnmiMapping>,
#[serde(default)]
pub eapi: Option<EapiMapping>,
#[serde(default)]
pub rest_api: Option<RestApiMapping>,
#[serde(default)]
pub snmp: Option<SnmpMapping>,
#[serde(default)]
pub dashboard_api: Option<DashboardApiMapping>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetconfMapping {
pub yang_model: String,
pub data_path: String,
#[serde(default)]
pub firmware_required: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestconfMapping {
pub url_path: String,
#[serde(default)]
pub firmware_required: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GnmiMapping {
pub path: String,
#[serde(default)]
pub firmware_required: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EapiMapping {
pub method: String,
#[serde(default)]
pub cli: Option<String>,
#[serde(default)]
pub commands: Option<Vec<String>>,
#[serde(default)]
pub format: Option<String>,
#[serde(default)]
pub firmware_required: Option<String>,
}
impl EapiMapping {
pub fn command_list(&self) -> Vec<&str> {
if let Some(commands) = &self.commands {
commands.iter().map(String::as_str).collect()
} else if let Some(cli) = &self.cli {
vec![cli.as_str()]
} else {
Vec::new()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestApiMapping {
pub method: String,
pub path: String,
#[serde(default)]
pub firmware_required: Option<String>,
#[serde(flatten)]
pub extras: IndexMap<String, serde_yaml::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnmpMapping {
pub oid: String,
#[serde(default)]
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardApiMapping {
pub endpoint: String,
#[serde(flatten)]
pub extras: IndexMap<String, serde_yaml::Value>,
}