Skip to main content

ios_config/
lib.rs

1//! # ios-config
2//!
3//! A Cisco IOS `show running-config` parser that produces a typed intermediate
4//! representation (IR) covering interfaces, routing (OSPF, BGP, static),
5//! ACLs, NAT, VLANs, SNMP, AAA, STP, and more.
6#![deny(missing_docs)]
7//!
8//! ## Quick start
9//!
10//! ```rust
11//! use ios_config::parse;
12//!
13//! let raw = r#"
14//! hostname CORE-RTR-01
15//! !
16//! interface GigabitEthernet0/0
17//!  description WAN
18//!  ip address 203.0.113.1 255.255.255.252
19//!  no shutdown
20//! !
21//! router ospf 1
22//!  router-id 1.1.1.1
23//!  network 203.0.113.0 0.0.0.3 area 0
24//! "#;
25//!
26//! let config = parse(raw).unwrap();
27//! assert_eq!(config.hostname.as_deref(), Some("CORE-RTR-01"));
28//! assert_eq!(config.interfaces.len(), 1);
29//! assert_eq!(config.routing.ospf.len(), 1);
30//! ```
31//!
32//! ## What gets parsed
33//!
34//! | Section | Types |
35//! |---------|-------|
36//! | Interfaces | GigabitEthernet, FastEthernet, TenGigabitEthernet, Loopback, Vlan, Tunnel, Serial |
37//! | L2 | switchport access/trunk, voice vlan, storm-control, STP per-interface |
38//! | Routing | static routes, OSPF (multi-process), BGP (neighbors, peer-groups, address-families), EIGRP |
39//! | ACL | standard and extended, named and numbered |
40//! | NAT | dynamic, PAT/overload, static |
41//! | Management | NTP, DNS, SNMP, AAA, SSH, line vty, local users, banner |
42//! | Global STP | mode, loopguard, portfast/bpduguard defaults, per-VLAN priority |
43//! | Unknown | unrecognized commands preserved in `unknown_blocks` |
44
45pub use ios_config_core::ir::{
46    NetworkConfig,
47    Interface, InterfaceName, InterfaceKind, InterfaceSpeed, Duplex,
48    IpAddress, L2Config, L2Mode, NatDirection,
49    HsrpGroup, HsrpTimers, HsrpTrack,
50    InterfaceOspf, OspfIfTimers, OspfNetworkType,
51    InterfaceStp, StormControl,
52    Vlan,
53    RoutingConfig, StaticRoute, NextHop,
54    OspfProcess, OspfAreaConfig, OspfArea, OspfNetwork, OspfAreaType,
55    OspfAuth, OspfDefaultOriginate, OspfRedistribute, RedistributeSource,
56    BgpConfig, BgpNeighbor, BgpNeighborAddr, BgpPeerGroup,
57    BgpAddressFamily, BgpAfi, BgpSafi, BgpAggregate,
58    EigrpProcess,
59    Acl, AclName, AclType, AclEntry, AclAction, AclProtocol, AclMatch, AclPort,
60    NatRule, NatType, NatPool, NatStaticEntry,
61    NtpServer, SnmpConfig, SnmpCommunity, SnmpAccess,
62    AaaConfig, AaaMethod,
63    GlobalStp, StpMode, StpVlanPriority,
64    LoggingConfig, LocalUser, PasswordType,
65    LineVty, SshConfig,
66    Confidence,
67    UnknownBlock,
68};
69
70mod tree;
71mod semantic;
72
73#[cfg(test)]
74mod tests;
75
76use tree::parse_raw_tree;
77use semantic::SemanticParser;
78
79/// Parse errors returned by [`parse`].
80#[derive(Debug, Clone, PartialEq)]
81pub enum ParseError {
82    /// Input was empty or contained no recognizable IOS commands.
83    EmptyInput,
84}
85
86impl std::fmt::Display for ParseError {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            ParseError::EmptyInput => write!(f, "ios-config: input is empty"),
90        }
91    }
92}
93
94impl std::error::Error for ParseError {}
95
96/// Parse a Cisco IOS `show running-config` string into a [`NetworkConfig`].
97///
98/// The parser is lenient: unrecognized commands are preserved in
99/// [`NetworkConfig::unknown_blocks`] rather than causing an error.
100///
101/// # Errors
102///
103/// Returns [`ParseError::EmptyInput`] if the input contains no non-whitespace,
104/// non-comment lines.
105pub fn parse(input: &str) -> Result<NetworkConfig, ParseError> {
106    let has_content = input.lines()
107        .any(|l| {
108            let t = l.trim();
109            !t.is_empty() && !t.starts_with('!')
110        });
111
112    if !has_content {
113        return Err(ParseError::EmptyInput);
114    }
115
116    let tree = parse_raw_tree(input);
117    let config = SemanticParser.analyze(&tree);
118    Ok(config)
119}
120
121#[cfg(test)]
122mod doctest_examples {
123    use super::*;
124
125    const SAMPLE: &str = r#"
126hostname EDGE-RTR-01
127ip domain-name corp.local
128!
129interface GigabitEthernet0/0
130 description WAN Uplink
131 ip address 203.0.113.2 255.255.255.252
132 ip nat outside
133 no shutdown
134!
135interface GigabitEthernet0/1
136 description LAN
137 ip address 10.0.0.1 255.255.255.0
138 ip nat inside
139 ip helper-address 10.0.0.254
140 no shutdown
141!
142interface Vlan10
143 description Management
144 ip address 10.10.10.1 255.255.255.0
145 no shutdown
146!
147router ospf 1
148 router-id 1.1.1.1
149 network 10.0.0.0 0.0.0.255 area 0
150 network 10.10.10.0 0.0.0.255 area 0
151 passive-interface GigabitEthernet0/1
152!
153ip access-list extended ACL-WAN-IN
154 permit tcp any any established
155 deny   ip any any log
156!
157ip nat inside source list ACL-NAT interface GigabitEthernet0/0 overload
158!
159ntp server 216.239.35.0 prefer
160ip name-server 8.8.8.8
161ip name-server 8.8.4.4
162!
163snmp-server community public RO
164snmp-server location DC-1 Rack-12
165"#;
166
167    #[test]
168    fn test_parse_hostname() {
169        let cfg = parse(SAMPLE).unwrap();
170        assert_eq!(cfg.hostname.as_deref(), Some("EDGE-RTR-01"));
171        assert_eq!(cfg.domain_name.as_deref(), Some("corp.local"));
172    }
173
174    #[test]
175    fn test_parse_interfaces() {
176        let cfg = parse(SAMPLE).unwrap();
177        assert_eq!(cfg.interfaces.len(), 3);
178
179        let wan = cfg.interfaces.iter().find(|i| i.name.original == "GigabitEthernet0/0").unwrap();
180        assert_eq!(wan.description.as_deref(), Some("WAN Uplink"));
181        assert!(!wan.shutdown);
182        assert_eq!(wan.nat_direction, Some(NatDirection::Outside));
183        assert_eq!(wan.addresses.len(), 1);
184    }
185
186    #[test]
187    fn test_parse_ospf() {
188        let cfg = parse(SAMPLE).unwrap();
189        assert_eq!(cfg.routing.ospf.len(), 1);
190        let ospf = &cfg.routing.ospf[0];
191        assert_eq!(ospf.process_id, 1);
192        assert_eq!(ospf.router_id, Some("1.1.1.1".parse().unwrap()));
193        assert_eq!(ospf.passive_interfaces, vec!["GigabitEthernet0/1"]);
194    }
195
196    #[test]
197    fn test_parse_acl() {
198        let cfg = parse(SAMPLE).unwrap();
199        assert_eq!(cfg.acls.len(), 1);
200        let acl = &cfg.acls[0];
201        assert!(matches!(acl.name, AclName::Named(ref n) if n == "ACL-WAN-IN"));
202        assert_eq!(acl.acl_type, AclType::Extended);
203        assert_eq!(acl.entries.len(), 2);
204    }
205
206    #[test]
207    fn test_parse_ntp_dns_snmp() {
208        let cfg = parse(SAMPLE).unwrap();
209        assert_eq!(cfg.ntp.len(), 1);
210        assert!(cfg.ntp[0].prefer);
211        assert_eq!(cfg.dns.len(), 2);
212        let snmp = cfg.snmp.as_ref().unwrap();
213        assert_eq!(snmp.communities.len(), 1);
214        assert_eq!(snmp.location.as_deref(), Some("DC-1 Rack-12"));
215    }
216
217    #[test]
218    fn test_empty_input_error() {
219        assert!(matches!(parse(""), Err(ParseError::EmptyInput)));
220        assert!(matches!(parse("! just a comment\n!"), Err(ParseError::EmptyInput)));
221    }
222
223    #[test]
224    fn test_interface_name_normalization() {
225        let cfg = parse("interface Gi0/0\n ip address 192.168.1.1 255.255.255.0\n").unwrap();
226        assert_eq!(cfg.interfaces[0].name.kind, InterfaceKind::GigabitEthernet);
227        assert_eq!(cfg.interfaces[0].name.id, "0/0");
228    }
229}