#![deny(missing_docs)]
pub use ios_config_core::ir::{
NetworkConfig,
Interface, InterfaceName, InterfaceKind, InterfaceSpeed, Duplex,
IpAddress, L2Config, L2Mode, NatDirection,
HsrpGroup, HsrpTimers, HsrpTrack,
InterfaceOspf, OspfIfTimers, OspfNetworkType,
InterfaceStp, StormControl,
Vlan,
RoutingConfig, StaticRoute, NextHop,
OspfProcess, OspfAreaConfig, OspfArea, OspfNetwork, OspfAreaType,
OspfAuth, OspfDefaultOriginate, OspfRedistribute, RedistributeSource,
BgpConfig, BgpNeighbor, BgpNeighborAddr, BgpPeerGroup,
BgpAddressFamily, BgpAfi, BgpSafi, BgpAggregate,
EigrpProcess,
Acl, AclName, AclType, AclEntry, AclAction, AclProtocol, AclMatch, AclPort,
NatRule, NatType, NatPool, NatStaticEntry,
NtpServer, SnmpConfig, SnmpCommunity, SnmpAccess,
AaaConfig, AaaMethod,
GlobalStp, StpMode, StpVlanPriority,
LoggingConfig, LocalUser, PasswordType,
LineVty, SshConfig,
Confidence,
UnknownBlock,
};
mod tree;
mod semantic;
#[cfg(test)]
mod tests;
use tree::parse_raw_tree;
use semantic::SemanticParser;
#[derive(Debug, Clone, PartialEq)]
pub enum ParseError {
EmptyInput,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseError::EmptyInput => write!(f, "ios-config: input is empty"),
}
}
}
impl std::error::Error for ParseError {}
pub fn parse(input: &str) -> Result<NetworkConfig, ParseError> {
let has_content = input.lines()
.any(|l| {
let t = l.trim();
!t.is_empty() && !t.starts_with('!')
});
if !has_content {
return Err(ParseError::EmptyInput);
}
let tree = parse_raw_tree(input);
let config = SemanticParser.analyze(&tree);
Ok(config)
}
#[cfg(test)]
mod doctest_examples {
use super::*;
const SAMPLE: &str = r#"
hostname EDGE-RTR-01
ip domain-name corp.local
!
interface GigabitEthernet0/0
description WAN Uplink
ip address 203.0.113.2 255.255.255.252
ip nat outside
no shutdown
!
interface GigabitEthernet0/1
description LAN
ip address 10.0.0.1 255.255.255.0
ip nat inside
ip helper-address 10.0.0.254
no shutdown
!
interface Vlan10
description Management
ip address 10.10.10.1 255.255.255.0
no shutdown
!
router ospf 1
router-id 1.1.1.1
network 10.0.0.0 0.0.0.255 area 0
network 10.10.10.0 0.0.0.255 area 0
passive-interface GigabitEthernet0/1
!
ip access-list extended ACL-WAN-IN
permit tcp any any established
deny ip any any log
!
ip nat inside source list ACL-NAT interface GigabitEthernet0/0 overload
!
ntp server 216.239.35.0 prefer
ip name-server 8.8.8.8
ip name-server 8.8.4.4
!
snmp-server community public RO
snmp-server location DC-1 Rack-12
"#;
#[test]
fn test_parse_hostname() {
let cfg = parse(SAMPLE).unwrap();
assert_eq!(cfg.hostname.as_deref(), Some("EDGE-RTR-01"));
assert_eq!(cfg.domain_name.as_deref(), Some("corp.local"));
}
#[test]
fn test_parse_interfaces() {
let cfg = parse(SAMPLE).unwrap();
assert_eq!(cfg.interfaces.len(), 3);
let wan = cfg.interfaces.iter().find(|i| i.name.original == "GigabitEthernet0/0").unwrap();
assert_eq!(wan.description.as_deref(), Some("WAN Uplink"));
assert!(!wan.shutdown);
assert_eq!(wan.nat_direction, Some(NatDirection::Outside));
assert_eq!(wan.addresses.len(), 1);
}
#[test]
fn test_parse_ospf() {
let cfg = parse(SAMPLE).unwrap();
assert_eq!(cfg.routing.ospf.len(), 1);
let ospf = &cfg.routing.ospf[0];
assert_eq!(ospf.process_id, 1);
assert_eq!(ospf.router_id, Some("1.1.1.1".parse().unwrap()));
assert_eq!(ospf.passive_interfaces, vec!["GigabitEthernet0/1"]);
}
#[test]
fn test_parse_acl() {
let cfg = parse(SAMPLE).unwrap();
assert_eq!(cfg.acls.len(), 1);
let acl = &cfg.acls[0];
assert!(matches!(acl.name, AclName::Named(ref n) if n == "ACL-WAN-IN"));
assert_eq!(acl.acl_type, AclType::Extended);
assert_eq!(acl.entries.len(), 2);
}
#[test]
fn test_parse_ntp_dns_snmp() {
let cfg = parse(SAMPLE).unwrap();
assert_eq!(cfg.ntp.len(), 1);
assert!(cfg.ntp[0].prefer);
assert_eq!(cfg.dns.len(), 2);
let snmp = cfg.snmp.as_ref().unwrap();
assert_eq!(snmp.communities.len(), 1);
assert_eq!(snmp.location.as_deref(), Some("DC-1 Rack-12"));
}
#[test]
fn test_empty_input_error() {
assert!(matches!(parse(""), Err(ParseError::EmptyInput)));
assert!(matches!(parse("! just a comment\n!"), Err(ParseError::EmptyInput)));
}
#[test]
fn test_interface_name_normalization() {
let cfg = parse("interface Gi0/0\n ip address 192.168.1.1 255.255.255.0\n").unwrap();
assert_eq!(cfg.interfaces[0].name.kind, InterfaceKind::GigabitEthernet);
assert_eq!(cfg.interfaces[0].name.id, "0/0");
}
}