1#![deny(missing_docs)]
7pub 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#[derive(Debug, Clone, PartialEq)]
81pub enum ParseError {
82 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
96pub 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}