remove old files
This commit is contained in:
+1
-1
@@ -188,7 +188,7 @@ cython_debug/
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
.vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"nixEnvSelector.nixFile": "${workspaceFolder}/flake.nix"
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 186 KiB |
@@ -1,11 +0,0 @@
|
||||
Name,Role,Interface,Network,VLAN,Network IP,Mask,Device IP,Default Gateway
|
||||
PC1,Host,eth1,A,4,10.0.0.0,/24,10.0.0.1,0.0.0.0
|
||||
PC2,Host,eth1,B,2,10.0.0.0,/24,10.0.0.2,0.0.0.0
|
||||
PC3,Host,eth1,C,3,10.0.0.0,/24,10.0.0.3,0.0.0.0
|
||||
PC4,Host,eth1,D,4,10.0.0.0,/24,10.0.0.4,0.0.0.0
|
||||
com_left,Switch,eth1,tr,trunk,,,,
|
||||
com_left,Switch,eth2,A,4,,,,
|
||||
com_left,Switch,eth3,B,2,,,,
|
||||
com_right,Switch,eth1,tr,trunk,,,,
|
||||
com_right,Switch,eth2,D,4,,,,
|
||||
com_right,Switch,eth3,C,3,,,,
|
||||
|
@@ -1,6 +0,0 @@
|
||||
name,"type","interfaces","ip","endpoints"
|
||||
PC1,"host","int1","",""
|
||||
PC2,"host","int2","",""
|
||||
int1,"interface","","10.0.12.1/24",""
|
||||
int2,"interface","","10.0.12.2/24",""
|
||||
link,"wire","","","int1,int2"
|
||||
|
@@ -1,11 +0,0 @@
|
||||
Name,Role,Interface,Network,VLAN,Network IP,Mask,Device IP,Default Gateway
|
||||
PC1,Host,eth1,A,4,10.0.0.0,/24,10.0.0.1,0.0.0.0
|
||||
PC2,Host,eth1,B,2,10.0.0.0,/24,10.0.0.2,0.0.0.0
|
||||
PC3,Host,eth1,C,3,10.0.0.0,/24,10.0.0.3,0.0.0.0
|
||||
PC4,Host,eth1,D,4,10.0.0.0,/24,10.0.0.4,0.0.0.0
|
||||
com_left,Switch,eth1,tr,trunk,,,,
|
||||
com_left,Switch,eth2,A,4,,,,
|
||||
com_left,Switch,eth3,B,2,,,,
|
||||
com_right,Switch,eth1,tr,trunk,,,,
|
||||
com_right,Switch,eth2,D,4,,,,
|
||||
com_right,Switch,eth3,C,3,,,,
|
||||
|
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,19 +0,0 @@
|
||||
links:
|
||||
networks:
|
||||
- A: [PC1.eth0, PC2.eth0]
|
||||
meta:
|
||||
id: host-host.csv
|
||||
name: host-host.csv
|
||||
nodes:
|
||||
- role: host
|
||||
name: PC1
|
||||
interfaces:
|
||||
- eth0:
|
||||
- ip: 10.0.12.1/24
|
||||
network: A
|
||||
- role: host
|
||||
name: PC2
|
||||
interfaces:
|
||||
- eth0:
|
||||
- ip: 10.0.12.2/24
|
||||
network: A
|
||||
@@ -1,21 +0,0 @@
|
||||
meta:
|
||||
id: "lab-02-basic-utils"
|
||||
name: "Basic Network Utilities & Traffic Monitoring"
|
||||
description: "Two hosts connected directly to each other."
|
||||
|
||||
links:
|
||||
- endpoints: ["PC1", "PC2"]
|
||||
|
||||
nodes:
|
||||
- name: PC1
|
||||
role: host
|
||||
interfaces:
|
||||
- ip: "10.0.12.1/24"
|
||||
|
||||
- name: PC2
|
||||
role: host
|
||||
interfaces:
|
||||
- ip: "10.0.12.2/24"
|
||||
|
||||
# Optional:
|
||||
# - "ip link set dev eth1 down"
|
||||
@@ -1,81 +0,0 @@
|
||||
links: []
|
||||
networks:
|
||||
- A:
|
||||
members:
|
||||
- PC1.eth1
|
||||
- com_left.eth2
|
||||
network_ip: 10.0.0.0
|
||||
mask: /24
|
||||
vlan: 4
|
||||
- B:
|
||||
members:
|
||||
- PC2.eth1
|
||||
- com_left.eth3
|
||||
network_ip: 10.0.0.0
|
||||
mask: /24
|
||||
vlan: 2
|
||||
- C:
|
||||
members:
|
||||
- PC3.eth1
|
||||
- com_right.eth3
|
||||
network_ip: 10.0.0.0
|
||||
mask: /24
|
||||
vlan: 3
|
||||
- D:
|
||||
members:
|
||||
- PC4.eth1
|
||||
- com_right.eth2
|
||||
network_ip: 10.0.0.0
|
||||
mask: /24
|
||||
vlan: 4
|
||||
- tr:
|
||||
members:
|
||||
- com_left.eth1
|
||||
- com_right.eth1
|
||||
vlan: trunk
|
||||
meta:
|
||||
id: table.csv
|
||||
name: table.csv
|
||||
nodes:
|
||||
- role: host
|
||||
name: PC1
|
||||
interfaces:
|
||||
- eth1:
|
||||
- ip: 10.0.0.1/24
|
||||
network: A
|
||||
- role: host
|
||||
name: PC2
|
||||
interfaces:
|
||||
- eth1:
|
||||
- ip: 10.0.0.2/24
|
||||
network: B
|
||||
- role: host
|
||||
name: PC3
|
||||
interfaces:
|
||||
- eth1:
|
||||
- ip: 10.0.0.3/24
|
||||
network: C
|
||||
- role: host
|
||||
name: PC4
|
||||
interfaces:
|
||||
- eth1:
|
||||
- ip: 10.0.0.4/24
|
||||
network: D
|
||||
- role: switch
|
||||
name: com_left
|
||||
interfaces:
|
||||
- eth1:
|
||||
- network: tr
|
||||
- eth2:
|
||||
- network: A
|
||||
- eth3:
|
||||
- network: B
|
||||
- role: switch
|
||||
name: com_right
|
||||
interfaces:
|
||||
- eth1:
|
||||
- network: tr
|
||||
- eth2:
|
||||
- network: D
|
||||
- eth3:
|
||||
- network: C
|
||||
@@ -1,22 +0,0 @@
|
||||
from parse import parse_csv_to_structure
|
||||
import sys
|
||||
from structures import Host, Wire
|
||||
|
||||
|
||||
def check_correct(data):
|
||||
assert isinstance(data, dict)
|
||||
|
||||
for key, value in data.items():
|
||||
assert isinstance(key, str)
|
||||
assert isinstance(value, dict)
|
||||
|
||||
# for subkey, subvalue in value.items():
|
||||
# assert isinstance(subkey, str)
|
||||
# assert isinstance(subvalue, str)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = parse_csv_to_structure(sys.argv[1])
|
||||
if check_correct(data):
|
||||
print("yay!")
|
||||
print(data)
|
||||
@@ -1,251 +0,0 @@
|
||||
# export_yaml.py
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from parse import parse_csv_topology
|
||||
|
||||
|
||||
def _nonempty(v: Any) -> bool:
|
||||
if v is None:
|
||||
return False
|
||||
if isinstance(v, str) and not v.strip():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _mask_to_prefix(mask: Optional[str]) -> str:
|
||||
"""
|
||||
Маска в CSV может быть '/24' или '24' или None.
|
||||
Для записи ip/prefix нужно вернуть '24' (без слэша) либо ''.
|
||||
"""
|
||||
if not mask:
|
||||
return ""
|
||||
m = str(mask).strip()
|
||||
if not m:
|
||||
return ""
|
||||
return m[1:] if m.startswith("/") else m
|
||||
|
||||
|
||||
def _ip_with_prefix(device_ip: str, mask: Optional[str]) -> str:
|
||||
ip = (device_ip or "").strip()
|
||||
if not ip:
|
||||
return ""
|
||||
pfx = _mask_to_prefix(mask)
|
||||
return f"{ip}/{pfx}" if pfx else ip
|
||||
|
||||
|
||||
def _role_from_raw(
|
||||
devices_raw: Dict[str, Dict[str, Any]], device_name: str, fallback: str
|
||||
) -> str:
|
||||
raw = devices_raw.get(device_name, {})
|
||||
role = str(raw.get("role", "") or "").strip().lower()
|
||||
return role if role else fallback
|
||||
|
||||
|
||||
def _network_vlan_value(
|
||||
result_devices_raw: Dict[str, Dict[str, Any]], net_name: str
|
||||
) -> Optional[Union[int, str]]:
|
||||
"""
|
||||
VLAN — свойство сети. Берём:
|
||||
1) если у Network-объекта есть vlan_id — используем int (обрабатывается выше)
|
||||
2) иначе пытаемся найти vlan_raw (строковый VLAN, например 'trunk') в devices_raw
|
||||
"""
|
||||
found_vlan_raw: Optional[str] = None
|
||||
for d in result_devices_raw.values():
|
||||
for iface in d.get("interfaces", []):
|
||||
nf = iface.get("network") or {}
|
||||
if nf.get("name") == net_name and _nonempty(nf.get("vlan_raw")):
|
||||
found_vlan_raw = str(nf.get("vlan_raw")).strip()
|
||||
break
|
||||
if found_vlan_raw:
|
||||
break
|
||||
return found_vlan_raw if _nonempty(found_vlan_raw) else None
|
||||
|
||||
|
||||
# --- YAML dump (PyYAML preferred, fallback if not installed) ---
|
||||
def _dump_yaml_fallback(obj: Any, indent: int = 0) -> str:
|
||||
sp = " " * indent
|
||||
|
||||
if obj is None:
|
||||
return "null"
|
||||
if isinstance(obj, bool):
|
||||
return "true" if obj else "false"
|
||||
if isinstance(obj, (int, float)):
|
||||
return str(obj)
|
||||
if isinstance(obj, str):
|
||||
s = obj
|
||||
if (
|
||||
s == ""
|
||||
or s.strip() != s
|
||||
or any(c in s for c in [":", "#", "{", "}", "[", "]"])
|
||||
):
|
||||
return f'"{s.replace(chr(34), r"\"")}"'
|
||||
return s
|
||||
|
||||
if isinstance(obj, list):
|
||||
if not obj:
|
||||
return "[]"
|
||||
lines: List[str] = []
|
||||
for item in obj:
|
||||
val = _dump_yaml_fallback(item, indent + 1)
|
||||
if "\n" in val:
|
||||
first, *rest = val.splitlines()
|
||||
lines.append(f"{sp}- {first}")
|
||||
for r in rest:
|
||||
lines.append(f"{' ' * (indent + 1)}{r}")
|
||||
else:
|
||||
lines.append(f"{sp}- {val}")
|
||||
return "\n".join(lines)
|
||||
|
||||
if isinstance(obj, dict):
|
||||
if not obj:
|
||||
return "{}"
|
||||
lines: List[str] = []
|
||||
for k, v in obj.items():
|
||||
key = str(k)
|
||||
if isinstance(v, (dict, list)) and v:
|
||||
lines.append(f"{sp}{key}:")
|
||||
lines.append(_dump_yaml_fallback(v, indent + 1))
|
||||
else:
|
||||
lines.append(f"{sp}{key}: {_dump_yaml_fallback(v, indent + 1)}")
|
||||
return "\n".join(lines)
|
||||
|
||||
return _dump_yaml_fallback(str(obj), indent)
|
||||
|
||||
|
||||
def dump_yaml(obj: Any) -> str:
|
||||
try:
|
||||
import yaml # type: ignore
|
||||
|
||||
return yaml.safe_dump(obj, sort_keys=False, allow_unicode=True)
|
||||
except Exception:
|
||||
return _dump_yaml_fallback(obj) + "\n"
|
||||
|
||||
|
||||
def build_yaml_document(csv_path: Union[str, Path]) -> Dict[str, Any]:
|
||||
csv_path = Path(csv_path)
|
||||
result = parse_csv_topology(csv_path)
|
||||
|
||||
doc: Dict[str, Any] = {
|
||||
"links": [],
|
||||
"networks": [],
|
||||
"meta": {"id": csv_path.name, "name": csv_path.name},
|
||||
"nodes": [],
|
||||
}
|
||||
|
||||
# -----------------
|
||||
# NETWORKS (только тут: vlan, network_ip, mask)
|
||||
# -----------------
|
||||
networks_block: List[Dict[str, Any]] = []
|
||||
for net_name in sorted(result.network_interfaces.keys()):
|
||||
members = [
|
||||
f"{dev}.{iface}" for dev, iface in result.network_interfaces[net_name]
|
||||
]
|
||||
|
||||
net_entry: Dict[str, Any] = {"members": members}
|
||||
|
||||
net_obj = result.networks.get(net_name)
|
||||
if net_obj is not None:
|
||||
# network_ip, mask
|
||||
if _nonempty(net_obj.ip):
|
||||
net_entry["network_ip"] = net_obj.ip
|
||||
if _nonempty(net_obj.subnet_mask):
|
||||
net_entry["mask"] = net_obj.subnet_mask
|
||||
|
||||
# vlan (int)
|
||||
if _nonempty(net_obj.vlan_id):
|
||||
net_entry["vlan"] = net_obj.vlan_id
|
||||
|
||||
# vlan (string), если vlan_id отсутствует, но в raw есть vlan_raw
|
||||
if "vlan" not in net_entry:
|
||||
vlan_raw = _network_vlan_value(result.devices_raw, net_name)
|
||||
if _nonempty(vlan_raw):
|
||||
net_entry["vlan"] = vlan_raw
|
||||
|
||||
networks_block.append({net_name: net_entry})
|
||||
|
||||
doc["networks"] = networks_block
|
||||
|
||||
# -----------------
|
||||
# NODES (интерфейсы: без vlan/network_ip/mask)
|
||||
# -----------------
|
||||
nodes_block: List[Dict[str, Any]] = []
|
||||
for dev_name in sorted(result.devices.keys()):
|
||||
device = result.devices[dev_name]
|
||||
role_fallback = type(device).__name__.lower()
|
||||
role = _role_from_raw(result.devices_raw, dev_name, role_fallback)
|
||||
|
||||
node: Dict[str, Any] = {
|
||||
"role": role,
|
||||
"name": device.name,
|
||||
"interfaces": [],
|
||||
}
|
||||
|
||||
for iface in device.interfaces:
|
||||
iface_items: List[Dict[str, Any]] = []
|
||||
|
||||
network_name = iface.network.name if iface.network else None
|
||||
mask = iface.network.subnet_mask if iface.network else None
|
||||
|
||||
# Если есть IP-адреса — пишем ip/prefix + default_gateway + network (+ mode при наличии)
|
||||
if iface.ips:
|
||||
for ip_obj in iface.ips:
|
||||
entry: Dict[str, Any] = {}
|
||||
|
||||
ip_comp = _ip_with_prefix(ip_obj.ip_str, mask)
|
||||
if _nonempty(ip_comp):
|
||||
entry["ip"] = ip_comp
|
||||
|
||||
if _nonempty(network_name):
|
||||
entry["network"] = network_name
|
||||
|
||||
dg = getattr(ip_obj, "default_gateway", None)
|
||||
if _nonempty(dg):
|
||||
entry["default_gateway"] = dg
|
||||
|
||||
if _nonempty(iface.mode):
|
||||
entry["mode"] = iface.mode
|
||||
|
||||
# добавляем только если есть хоть что-то
|
||||
if entry:
|
||||
iface_items.append(entry)
|
||||
|
||||
else:
|
||||
# Нет IP — оставим network/mode (например для switch портов)
|
||||
entry: Dict[str, Any] = {}
|
||||
if _nonempty(network_name):
|
||||
entry["network"] = network_name
|
||||
if _nonempty(iface.mode):
|
||||
entry["mode"] = iface.mode
|
||||
if entry:
|
||||
iface_items.append(entry)
|
||||
|
||||
node["interfaces"].append({iface.name: iface_items})
|
||||
|
||||
nodes_block.append(node)
|
||||
|
||||
doc["nodes"] = nodes_block
|
||||
return doc
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Export topology parsed from CSV to YAML (network props only under networks)"
|
||||
)
|
||||
ap.add_argument("csv", help="Path to input CSV")
|
||||
ap.add_argument("-o", "--out", help="Path to output YAML (default: <csv>.yaml)")
|
||||
args = ap.parse_args()
|
||||
|
||||
csv_path = Path(args.csv)
|
||||
out_path = Path(args.out) if args.out else csv_path.with_suffix(".yaml")
|
||||
|
||||
doc = build_yaml_document(csv_path)
|
||||
out_path.write_text(dump_yaml(doc), encoding="utf-8")
|
||||
print(f"Saved: {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,61 +0,0 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device:
|
||||
name: str
|
||||
model: str
|
||||
properties: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.name:
|
||||
raise ValueError("Device name cannot be empty")
|
||||
if not self.model:
|
||||
raise ValueError("Device model cannot be empty")
|
||||
if not isinstance(self.properties, dict):
|
||||
raise TypeError("properties must be a dict")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Switch(Device):
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
if "ports" not in self.properties:
|
||||
raise ValueError(f"Switch '{self.name}' must define 'ports'")
|
||||
if (
|
||||
not isinstance(self.properties["ports"], int)
|
||||
or self.properties["ports"] <= 0
|
||||
):
|
||||
raise ValueError("'ports' must be a positive integer")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Router(Device):
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
if "interfaces" not in self.properties:
|
||||
raise ValueError(f"Router '{self.name}' must define 'interfaces'")
|
||||
if (
|
||||
not isinstance(self.properties["interfaces"], int)
|
||||
or self.properties["interfaces"] <= 0
|
||||
):
|
||||
raise ValueError("'interfaces' must be a positive integer")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Host(Device):
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
ip = self.properties.get("ip")
|
||||
if not ip or not isinstance(ip, str):
|
||||
raise ValueError(f"Host '{self.name}' must contain a valid 'ip' string")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Wire:
|
||||
from_device: Device
|
||||
to_device: Device
|
||||
@@ -1,182 +0,0 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict
|
||||
|
||||
|
||||
# ===========================
|
||||
# Interface Classes
|
||||
# ===========================
|
||||
|
||||
|
||||
@dataclass
|
||||
class Interface:
|
||||
name: str
|
||||
ip_address: Optional[str] = None
|
||||
mac_address: Optional[str] = None
|
||||
speed: Optional[str] = None
|
||||
duplex: Optional[str] = None
|
||||
|
||||
def __init__(self, fields):
|
||||
self.name = fields.get("name")
|
||||
self.ip_address = fields.get("ip")
|
||||
self.mac_address = fields.get("mac")
|
||||
self.speed = fields.get("speed")
|
||||
self.duplex = fields.get("duplex")
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.name, str) or not self.name:
|
||||
raise ValueError("Interface 'name' must be a non-empty string")
|
||||
|
||||
|
||||
@dataclass
|
||||
class VirtualInterface(Interface):
|
||||
vlan_id: int = 0
|
||||
parent_physical: Optional[Interface] = None
|
||||
|
||||
def __init__(self, fields):
|
||||
super().__init__(fields)
|
||||
self.vlan_id = fields.get("vlanID", fields.get("vlan_id", 0))
|
||||
self.parent_physical = fields.get(
|
||||
"parent_physical", fields.get("parentPhysical", fields.get("parent"))
|
||||
)
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if not isinstance(self.vlan_id, int) or self.vlan_id < 0:
|
||||
raise ValueError(
|
||||
"VirtualInterface 'vlan_id' must be a non-negative integer"
|
||||
)
|
||||
if not isinstance(self.parent_physical, Interface):
|
||||
raise TypeError(
|
||||
"VirtualInterface 'parent_physical' must reference an Interface"
|
||||
)
|
||||
|
||||
|
||||
# ===========================
|
||||
# Network / Subnet
|
||||
# ===========================
|
||||
|
||||
|
||||
@dataclass
|
||||
class Network:
|
||||
cidr: str
|
||||
gateway: Optional[str] = None
|
||||
interfaces: List[Interface] = field(default_factory=list)
|
||||
|
||||
def __init__(self, fields):
|
||||
self.cidr = fields.get("cidr")
|
||||
self.gateway = fields.get("gateway")
|
||||
interfaces = fields.get("interfaces", [])
|
||||
if isinstance(interfaces, str):
|
||||
interfaces = [iface for iface in interfaces.split(",") if iface]
|
||||
self.interfaces = interfaces
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.cidr, str) or not self.cidr:
|
||||
raise ValueError("Network 'cidr' must be a non-empty string")
|
||||
if self.gateway is not None and not isinstance(self.gateway, str):
|
||||
raise ValueError("Network 'gateway' must be a string")
|
||||
|
||||
|
||||
# ===========================
|
||||
# Device Base Class (Abstract)
|
||||
# ===========================
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device:
|
||||
name: str
|
||||
mgmt_ip: Optional[str] = None
|
||||
interfaces: List[Interface] = field(default_factory=list)
|
||||
|
||||
def __init__(self, fields):
|
||||
self.name = fields.get("name")
|
||||
self.mgmt_ip = fields.get("mgmtIP")
|
||||
self.interfaces = [f for f in fields.get("interfaces").split(",") if f]
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.name, str) or not self.name:
|
||||
raise ValueError("Device 'name' must be a non-empty string")
|
||||
if self.mgmt_ip is not None and not isinstance(self.mgmt_ip, str):
|
||||
raise ValueError("Device 'mgmtIP' must be a string")
|
||||
|
||||
|
||||
# ===========================
|
||||
# Host / Router / Switch
|
||||
# ===========================
|
||||
|
||||
|
||||
@dataclass
|
||||
class Host(Device):
|
||||
operating_system: Optional[str] = None
|
||||
|
||||
def __init__(self, fields):
|
||||
super().__init__(fields)
|
||||
self.operating_system = fields.get("operatingSystem")
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if not isinstance(self.operating_system, str):
|
||||
raise ValueError("Host 'operatingSystem' must be a string")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Router(Device):
|
||||
routing_table: Dict[str, str] = field(default_factory=dict)
|
||||
routing_protocols: List[str] = field(default_factory=list)
|
||||
|
||||
def __init__(self, fields):
|
||||
super().__init__(fields)
|
||||
self.routing_table = fields.get("routingTable", {})
|
||||
self.routing_protocols = fields.get("routingProtocols", [])
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Switch(Device):
|
||||
mac_table: Dict[str, str] = field(default_factory=dict)
|
||||
vlan_database: Dict[int, str] = field(default_factory=dict)
|
||||
|
||||
def __init__(self, fields):
|
||||
super().__init__(fields)
|
||||
self.mac_table = fields.get("macTable", {})
|
||||
self.vlan_database = fields.get("vlanDatabase", {})
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
# ===========================
|
||||
# Physical Link (Wire)
|
||||
# ===========================
|
||||
|
||||
|
||||
@dataclass
|
||||
class Wire:
|
||||
id: str
|
||||
endpoints: List[Interface] # Must contain exactly 2 interfaces
|
||||
bandwidth: Optional[str] = None
|
||||
|
||||
def __init__(self, fields):
|
||||
self.id = fields.get("name")
|
||||
self.endpoints = [ep for ep in fields.get("endpoints").split(",") if ep]
|
||||
self.bandwidth = fields.get("bandwidth")
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.id, str) or not self.id:
|
||||
raise ValueError("Wire 'id' must be a non-empty string")
|
||||
if len(self.endpoints) != 2:
|
||||
raise ValueError(
|
||||
"Wire 'endpoints' must contain exactly 2 Interface objects"
|
||||
)
|
||||
if not all(isinstance(i, Interface) for i in self.endpoints):
|
||||
raise TypeError("Wire 'endpoints' must be Interface objects")
|
||||
@@ -1,211 +0,0 @@
|
||||
# parse.py
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from structures import Device, Host, Router, Switch, Network, Interface
|
||||
|
||||
|
||||
# ===========================
|
||||
# CSV -> canonical fields
|
||||
# ===========================
|
||||
COLUMN_MAP: Dict[str, str] = {
|
||||
"Name": "device_name",
|
||||
"Role": "role",
|
||||
"Interface": "interface_name",
|
||||
"Network": "network_name",
|
||||
"VLAN": "vlan",
|
||||
"Network IP": "network_ip",
|
||||
"Mask": "mask",
|
||||
"Device IP": "device_ip",
|
||||
"Default Gateway": "default_gateway",
|
||||
}
|
||||
|
||||
ROLE_MAP = {
|
||||
"host": Host,
|
||||
"switch": Switch,
|
||||
"router": Router,
|
||||
}
|
||||
|
||||
|
||||
def _clean(v: Any) -> str:
|
||||
return str(v).strip() if v is not None else ""
|
||||
|
||||
|
||||
def _to_int(v: str) -> Optional[int]:
|
||||
v = _clean(v)
|
||||
if v.isdigit():
|
||||
return int(v)
|
||||
return None
|
||||
|
||||
|
||||
def _gateway_or_none(v: str) -> Optional[str]:
|
||||
v = _clean(v)
|
||||
if not v or v == "0.0.0.0":
|
||||
return None
|
||||
return v
|
||||
|
||||
|
||||
def _infer_mode(network_name: str, vlan: str) -> str:
|
||||
"""
|
||||
Принятое допущение под ваш пример:
|
||||
- если VLAN == 'trunk' или Network == 'tr'/'trunk' => mode='trunk'
|
||||
- иначе => mode='access'
|
||||
"""
|
||||
n = _clean(network_name).lower()
|
||||
v = _clean(vlan).lower()
|
||||
if v == "trunk" or n in {"tr", "trunk"}:
|
||||
return "trunk"
|
||||
return "access"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParseResult:
|
||||
# словарь, пригодный для сериализации/отладки и последующего создания структур
|
||||
devices_raw: Dict[str, Dict[str, Any]]
|
||||
# готовые python-объекты ваших классов Host/Switch/Router
|
||||
devices: Dict[str, Device]
|
||||
# агрегированные сети (по имени сети)
|
||||
networks: Dict[str, Network]
|
||||
# Network -> список (device_name, interface_name)
|
||||
network_interfaces: Dict[str, List[Tuple[str, str]]]
|
||||
|
||||
|
||||
def parse_csv_topology(path: Union[str, Path], encoding: str = "utf-8") -> ParseResult:
|
||||
path = Path(path)
|
||||
|
||||
devices_raw: Dict[str, Dict[str, Any]] = {}
|
||||
network_interfaces: Dict[str, List[Tuple[str, str]]] = {}
|
||||
network_agg: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
with path.open(newline="", encoding=encoding) as f:
|
||||
reader = csv.DictReader(f)
|
||||
if not reader.fieldnames:
|
||||
raise ValueError("CSV is empty or has no header row")
|
||||
|
||||
for row in reader:
|
||||
# 1) rename columns -> canonical keys
|
||||
canon: Dict[str, str] = {}
|
||||
for k, v in row.items():
|
||||
kk = COLUMN_MAP.get(
|
||||
k, k
|
||||
) # если встретили неизвестную колонку — оставим как есть
|
||||
canon[kk] = _clean(v)
|
||||
|
||||
device_name = canon.get("device_name", "")
|
||||
role = canon.get("role", "")
|
||||
iface_name = canon.get("interface_name", "")
|
||||
network_name = canon.get("network_name", "")
|
||||
vlan_raw = canon.get("vlan", "")
|
||||
net_ip = canon.get("network_ip", "")
|
||||
mask = canon.get("mask", "")
|
||||
dev_ip = canon.get("device_ip", "")
|
||||
gw = canon.get("default_gateway", "")
|
||||
|
||||
if not device_name:
|
||||
continue
|
||||
|
||||
# 2) init device record
|
||||
dev_rec = devices_raw.setdefault(
|
||||
device_name,
|
||||
{"name": device_name, "role": role, "interfaces": []},
|
||||
)
|
||||
|
||||
# если роль у устройства не была проставлена ранее, но появилась сейчас — заполним
|
||||
if role and not dev_rec.get("role"):
|
||||
dev_rec["role"] = role
|
||||
|
||||
# если интерфейс не указан — дальше нечего собирать
|
||||
if not iface_name:
|
||||
continue
|
||||
|
||||
mode = _infer_mode(network_name, vlan_raw)
|
||||
|
||||
# 3) network fields (для интерфейса) + агрегирование сетей
|
||||
network_fields: Optional[Dict[str, Any]] = None
|
||||
|
||||
if network_name:
|
||||
vlan_id = _to_int(vlan_raw) # trunk -> None
|
||||
network_fields = {
|
||||
"name": network_name,
|
||||
"vlan_id": vlan_id, # для структур (Network берёт vlan_id)
|
||||
"vlan_raw": vlan_raw or None, # для YAML/отладки (как в CSV)
|
||||
"ip": net_ip or None, # Network IP
|
||||
"mask": mask or None, # Mask
|
||||
}
|
||||
|
||||
# агрегируем сеть: берём первое непустое значение
|
||||
agg = network_agg.setdefault(network_name, {"name": network_name})
|
||||
if vlan_id is not None and agg.get("vlan_id") is None:
|
||||
agg["vlan_id"] = vlan_id
|
||||
if vlan_raw and not agg.get("vlan_raw"):
|
||||
agg["vlan_raw"] = vlan_raw
|
||||
if net_ip and not agg.get("ip"):
|
||||
agg["ip"] = net_ip
|
||||
if mask and not agg.get("mask"):
|
||||
agg["mask"] = mask
|
||||
|
||||
network_interfaces.setdefault(network_name, []).append(
|
||||
(device_name, iface_name)
|
||||
)
|
||||
|
||||
# 5) ips list
|
||||
ips: List[Interface.IP] = []
|
||||
if dev_ip:
|
||||
ips.append(
|
||||
Interface.IP(ip_str=dev_ip, default_gateway=_gateway_or_none(gw))
|
||||
)
|
||||
|
||||
iface_fields: Dict[str, Any] = {
|
||||
"name": iface_name,
|
||||
"mode": mode,
|
||||
"network": network_fields,
|
||||
"ips": ips,
|
||||
}
|
||||
|
||||
dev_rec["interfaces"].append(iface_fields)
|
||||
|
||||
# 6) Instantiate Network objects
|
||||
networks: Dict[str, Network] = {}
|
||||
for n_name, fields in network_agg.items():
|
||||
# Network.__init__ у вас принимает dict-like "fields"
|
||||
networks[n_name] = Network(fields)
|
||||
|
||||
# 7) Instantiate Device objects (Host/Switch/Router)
|
||||
devices: Dict[str, Device] = {}
|
||||
for d_name, fields in devices_raw.items():
|
||||
role = _clean(fields.get("role", "")).lower()
|
||||
cls = ROLE_MAP.get(
|
||||
role, Device
|
||||
) # если роль неизвестна — создадим базовый Device
|
||||
devices[d_name] = cls(fields)
|
||||
|
||||
return ParseResult(
|
||||
devices_raw=devices_raw,
|
||||
devices=devices,
|
||||
networks=networks,
|
||||
network_interfaces=network_interfaces,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = parse_csv_topology("table.csv")
|
||||
|
||||
print("=== devices_raw (dict for debugging/serialization) ===")
|
||||
for k, v in result.devices_raw.items():
|
||||
print(k, "=>", v)
|
||||
|
||||
print("\n=== networks (Network objects) ===")
|
||||
for k, v in result.networks.items():
|
||||
print(k, "=>", v)
|
||||
|
||||
print("\n=== network_interfaces (Network -> [(device, iface), ...]) ===")
|
||||
for net, items in result.network_interfaces.items():
|
||||
print(net, "=>", items)
|
||||
|
||||
print("\n=== devices (Device objects) ===")
|
||||
for k, v in result.devices.items():
|
||||
print(k, "=>", v)
|
||||
@@ -1,134 +0,0 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict
|
||||
|
||||
# ===========================
|
||||
# Network / Subnet
|
||||
# ===========================
|
||||
|
||||
@dataclass
|
||||
class Network:
|
||||
name: str
|
||||
vlan_id : Optional[int] = None
|
||||
ip: Optional[str] = None
|
||||
subnet_mask: Optional[str] = None
|
||||
|
||||
def __init__(self, fields):
|
||||
self.name = fields.get("name")
|
||||
self.vlan_id = fields.get("vlan", fields.get("vlan_id"))
|
||||
self.ip = fields.get("ip")
|
||||
self.subnet_mask = fields.get("mask", fields.get("subnet_mask"))
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.name, str) or not self.name:
|
||||
raise ValueError("Network 'name' must be a non-empty string")
|
||||
|
||||
# ===========================
|
||||
# Interface Classes
|
||||
# ===========================
|
||||
|
||||
|
||||
@dataclass
|
||||
class Interface:
|
||||
class IP:
|
||||
def __init__(self, ip_str: str, default_gateway: Optional[str] = None):
|
||||
self.ip_str = ip_str
|
||||
self.default_gateway = default_gateway
|
||||
|
||||
def __str__(self):
|
||||
return self.ip_str
|
||||
|
||||
name: str
|
||||
mode: Optional[str] = None
|
||||
network: Optional[Network] = None
|
||||
ips: Optional[List[IP]] = None
|
||||
mac_address: Optional[str] = None
|
||||
speed: Optional[str] = None
|
||||
|
||||
def __init__(self, fields):
|
||||
self.name = fields.get("name")
|
||||
ips = fields.get("ips", [])
|
||||
if isinstance(ips, str):
|
||||
ips = [self.IP(ip_str=ip.strip()) for ip in ips.split(",") if ip.strip()]
|
||||
self.ips = ips
|
||||
|
||||
# возможно передаём просто field
|
||||
self.network = Network(fields.get("network", {})) if fields.get("network") else None
|
||||
|
||||
self.mac_address = fields.get("mac")
|
||||
self.speed = fields.get("speed")
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.name, str) or not self.name:
|
||||
raise ValueError("Interface 'name' must be a non-empty string")
|
||||
|
||||
|
||||
# ===========================
|
||||
# Device Base Class (Abstract)
|
||||
# ===========================
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device:
|
||||
name: str
|
||||
interfaces: List[Interface] = field(default_factory=list)
|
||||
|
||||
def __init__(self, fields):
|
||||
self.name = fields.get("name")
|
||||
# тут точно понять, какие поля передаются
|
||||
self.interfaces = [Interface(iface) for iface in fields.get("interfaces", [])]
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.name, str) or not self.name:
|
||||
raise ValueError("Device 'name' must be a non-empty string")
|
||||
|
||||
|
||||
# ===========================
|
||||
# Host / Router / Switch
|
||||
# ===========================
|
||||
|
||||
|
||||
@dataclass
|
||||
class Host(Device):
|
||||
# operating_system: Optional[str] = None
|
||||
|
||||
def __init__(self, fields):
|
||||
super().__init__(fields)
|
||||
# self.operating_system = fields.get("operatingSystem")
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Router(Device):
|
||||
# routing_table: Dict[str, str] = field(default_factory=dict)
|
||||
# routing_protocols: List[str] = field(default_factory=list)
|
||||
|
||||
def __init__(self, fields):
|
||||
super().__init__(fields)
|
||||
# self.routing_table = fields.get("routingTable", {})
|
||||
# self.routing_protocols = fields.get("routingProtocols", [])
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Switch(Device):
|
||||
# mac_table: Dict[str, str] = field(default_factory=dict)
|
||||
# vlan_database: Dict[int, str] = field(default_factory=dict)
|
||||
|
||||
def __init__(self, fields):
|
||||
super().__init__(fields)
|
||||
# self.mac_table = fields.get("macTable", {})
|
||||
# self.vlan_database = fields.get("vlanDatabase", {})
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
Reference in New Issue
Block a user