Skip to main content

netconv_core/
profile.rs

1use serde::{Deserialize, Serialize};
2
3use crate::ir::NetworkConfig;
4
5// ---------------------------------------------------------------------------
6// DeviceProfile — явный выбор класса устройства
7// ---------------------------------------------------------------------------
8//
9// Профиль отражает реальную роль железа, а не синтаксис вендора: коммутатор
10// не становится маршрутизатором оттого, что его конфиг успешно распарсился.
11// Профиль всегда выбирается пользователем явно (вкладка в UI, --profile в
12// CLI) — конвертер намеренно не пытается угадать его эвристикой по
13// содержимому конфига, чтобы не подменять решение пользователя догадкой.
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum DeviceProfile {
17    /// L2: коммутатор — VLAN, access/trunk, STP, port-security, voice VLAN
18    L2Switch,
19    /// L3: маршрутизатор/firewall — routing, ACL, NAT, HSRP/VRRP
20    L3Router,
21}
22
23impl DeviceProfile {
24    pub fn label(&self) -> &'static str {
25        match self {
26            DeviceProfile::L2Switch => "L2 (switching)",
27            DeviceProfile::L3Router => "L3 (routing)",
28        }
29    }
30}
31
32// ---------------------------------------------------------------------------
33// DomainMismatch — найденная в IR сущность чужого домена
34// ---------------------------------------------------------------------------
35//
36// Рендереры пока не фильтруют IR по профилю (это следующий шаг), поэтому
37// на данном этапе detect_domain_mismatches только формирует список того,
38// что обнаружено не по адресу — вместо того, чтобы рендерер молча дропал
39// это с комментарием в самом выходном конфиге, как раньше делал
40// EltexRenderer для L2-команд.
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct DomainMismatch {
44    /// Короткий идентификатор домена/фичи, например "L3/ospf"
45    pub domain: String,
46    pub detail: String,
47}
48
49/// Сверяет NetworkConfig с выбранным профилем и возвращает список
50/// найденных сущностей чужого домена. IR не изменяется.
51pub fn detect_domain_mismatches(
52    config: &NetworkConfig,
53    profile: DeviceProfile,
54) -> Vec<DomainMismatch> {
55    let mut out = Vec::new();
56
57    match profile {
58        DeviceProfile::L2Switch => {
59            if !config.routing.static_routes.is_empty() {
60                out.push(DomainMismatch {
61                    domain: "L3/static".to_string(),
62                    detail: format!(
63                        "{} статических маршрутов — не относится к L2-профилю",
64                        config.routing.static_routes.len()
65                    ),
66                });
67            }
68            if !config.routing.ospf.is_empty() {
69                out.push(DomainMismatch {
70                    domain: "L3/ospf".to_string(),
71                    detail: format!("{} процессов OSPF", config.routing.ospf.len()),
72                });
73            }
74            if config.routing.bgp.is_some() {
75                out.push(DomainMismatch {
76                    domain: "L3/bgp".to_string(),
77                    detail: "конфигурация BGP".to_string(),
78                });
79            }
80            if !config.routing.eigrp.is_empty() {
81                out.push(DomainMismatch {
82                    domain: "L3/eigrp".to_string(),
83                    detail: format!("{} процессов EIGRP", config.routing.eigrp.len()),
84                });
85            }
86            if !config.acls.is_empty() {
87                out.push(DomainMismatch {
88                    domain: "L3/acl".to_string(),
89                    detail: format!("{} ACL", config.acls.len()),
90                });
91            }
92            if !config.nat.is_empty() {
93                out.push(DomainMismatch {
94                    domain: "L3/nat".to_string(),
95                    detail: format!("{} правил NAT", config.nat.len()),
96                });
97            }
98            let hsrp_count: usize = config.interfaces.iter().map(|i| i.hsrp.len()).sum();
99            if hsrp_count > 0 {
100                out.push(DomainMismatch {
101                    domain: "L3/hsrp".to_string(),
102                    detail: format!("{} групп HSRP", hsrp_count),
103                });
104            }
105        }
106        DeviceProfile::L3Router => {
107            if !config.vlans.is_empty() {
108                out.push(DomainMismatch {
109                    domain: "L2/vlan".to_string(),
110                    detail: format!("{} VLAN в базе", config.vlans.len()),
111                });
112            }
113            if config.stp.is_some() {
114                out.push(DomainMismatch {
115                    domain: "L2/stp".to_string(),
116                    detail: "глобальная конфигурация STP".to_string(),
117                });
118            }
119            let l2_iface_count = config.interfaces.iter().filter(|i| i.l2.is_some()).count();
120            if l2_iface_count > 0 {
121                out.push(DomainMismatch {
122                    domain: "L2/switchport".to_string(),
123                    detail: format!(
124                        "{} интерфейсов в режиме switchport (access/trunk)",
125                        l2_iface_count
126                    ),
127                });
128            }
129            let voice_vlan_count = config
130                .interfaces
131                .iter()
132                .filter(|i| i.voice_vlan.is_some())
133                .count();
134            if voice_vlan_count > 0 {
135                out.push(DomainMismatch {
136                    domain: "L2/voice-vlan".to_string(),
137                    detail: format!("{} интерфейсов с voice VLAN", voice_vlan_count),
138                });
139            }
140        }
141    }
142
143    out
144}