From 76597cc7f14f9bea55aa25e1add1c5bffa63df0d Mon Sep 17 00:00:00 2001 From: Dmitrii Gudov Date: Mon, 16 Feb 2026 01:35:03 +0300 Subject: [PATCH] some refactor --- .gitignore | 2 + .../input/1НФ VLAN - 1НФ.csv | 0 host-host.csv => data/input/host-host.csv | 0 table.csv => data/input/table.csv | 0 output.yaml => data/output/output.yaml | 0 schema.yaml => data/output/schema.yaml | 0 table.yaml => data/output/table.yaml | 0 examples/input.csv | 3 - examples/template.yaml | 160 ----- examples/topology-schema.json | 607 ------------------ pyproject.toml | 9 + src/netdiag/__init__.py | 0 src/netdiag/__main__.py | 3 + src/netdiag/base.py | 30 + src/netdiag/domain/__init__.py | 0 src/netdiag/domain/models.py | 194 ++++++ src/netdiag/domain/validation.py | 0 src/netdiag/parse/__init__.py | 32 + src/netdiag/parse/convert_raw.py | 151 +++++ {src => src_old}/check_correct.py | 0 {src => src_old}/make_res.py | 0 {src => src_old}/old/structures copy.py | 0 {src => src_old}/old/structures.py | 0 {src => src_old}/parse.py | 0 {src => src_old}/structures.py | 0 25 files changed, 421 insertions(+), 770 deletions(-) rename 1НФ VLAN - 1НФ.csv => data/input/1НФ VLAN - 1НФ.csv (100%) rename host-host.csv => data/input/host-host.csv (100%) rename table.csv => data/input/table.csv (100%) rename output.yaml => data/output/output.yaml (100%) rename schema.yaml => data/output/schema.yaml (100%) rename table.yaml => data/output/table.yaml (100%) delete mode 100644 examples/input.csv delete mode 100644 examples/template.yaml delete mode 100644 examples/topology-schema.json create mode 100644 pyproject.toml create mode 100644 src/netdiag/__init__.py create mode 100644 src/netdiag/__main__.py create mode 100644 src/netdiag/base.py create mode 100644 src/netdiag/domain/__init__.py create mode 100644 src/netdiag/domain/models.py create mode 100644 src/netdiag/domain/validation.py create mode 100644 src/netdiag/parse/__init__.py create mode 100644 src/netdiag/parse/convert_raw.py rename {src => src_old}/check_correct.py (100%) rename {src => src_old}/make_res.py (100%) rename {src => src_old}/old/structures copy.py (100%) rename {src => src_old}/old/structures.py (100%) rename {src => src_old}/parse.py (100%) rename {src => src_old}/structures.py (100%) diff --git a/.gitignore b/.gitignore index b7faf40..648167a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +tmp + # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] diff --git a/1НФ VLAN - 1НФ.csv b/data/input/1НФ VLAN - 1НФ.csv similarity index 100% rename from 1НФ VLAN - 1НФ.csv rename to data/input/1НФ VLAN - 1НФ.csv diff --git a/host-host.csv b/data/input/host-host.csv similarity index 100% rename from host-host.csv rename to data/input/host-host.csv diff --git a/table.csv b/data/input/table.csv similarity index 100% rename from table.csv rename to data/input/table.csv diff --git a/output.yaml b/data/output/output.yaml similarity index 100% rename from output.yaml rename to data/output/output.yaml diff --git a/schema.yaml b/data/output/schema.yaml similarity index 100% rename from schema.yaml rename to data/output/schema.yaml diff --git a/table.yaml b/data/output/table.yaml similarity index 100% rename from table.yaml rename to data/output/table.yaml diff --git a/examples/input.csv b/examples/input.csv deleted file mode 100644 index 21a7b21..0000000 --- a/examples/input.csv +++ /dev/null @@ -1,3 +0,0 @@ - , prop1 , prop2 , prop3 -ob1 , val1 , , val2 -obj2 , , val3 , val4 \ No newline at end of file diff --git a/examples/template.yaml b/examples/template.yaml deleted file mode 100644 index e08905b..0000000 --- a/examples/template.yaml +++ /dev/null @@ -1,160 +0,0 @@ -# yaml-language-server: $schema=./topology-schema.json -schema: asvk.topology/1.0 -meta: - id: "string" - title: "string" - description: "string" - -provider: - name: virtualbox # virtualbox | kvm | container - defaults: - cpu: 1 - ram_mb: 512 - disk_gb: 8 - os_image: "debian-12-cloud.qcow2" - nic_model: virtio - -defaults: - routing: - stack: bird # bird | frr | linux - protocols_enabled: [] - switch: - impl: linux-bridge # linux-bridge | ovs - firewall: - impl: nftables # nftables | none - mgmt: - ssh_user: "lab" - ssh_key: "~/.ssh/id_rsa.pub" - -variables: # for templates - asn: 65000 - domain: "lab.local" - -networks: # L2 / L3 / VLAN trunks - - id: lan1 - type: l2 # l2 | l3 - mtu: 1500 - - id: lan13 - type: l2 - - id: trunk1 - type: l2 - vlan: - mode: trunk - allowed: [4, 8] - - id: mgmt - type: l3 - cidr: 192.168.100.0/24 - dhcp: false - -nodes: # nodes (VM/containers) - - name: R1 - role: router # router | switch | host - resources: - cpu: 1 - ram_mb: 512 - mgmt: - ip: 192.168.100.11/24 - gw: 192.168.100.1 - net: mgmt - interfaces: # order matters: eth1 ↔ adapter2 and so on - - name: eth1 - network: lan13 - addresses: ["10.0.13.1/24"] - - name: eth0 # MGMT - network: mgmt - routing: - stack: bird # inherited from defaults, can be omitted - protocols: - static: - routes: - - to: 0.0.0.0/0 - via: 10.0.13.3 - ospf: # if needed - enabled: false - rip: - enabled: false - services: # servers/daemons of the host - - name: sshd - enabled: true - -switching: # L2 config for switch nodes - - node: S1 - impl: linux-bridge # linux-bridge | ovs - bridges: - - name: br0 - ports: - - if: eth1 # to trunk1 - vlan: - mode: trunk - allowed: [4, 8] - - if: eth2 # access → VLAN 4 - vlan: - mode: access - vid: 4 - - if: eth3 # access → VLAN 8 - vlan: - mode: access - vid: 8 - svis: # VLAN management interfaces (SVI) - - vid: 4 - ifname: br0.4 - addresses: ["10.0.4.254/24"] - -firewall: # High-level ACL-> to nftables - - node: R2 - rules: - - id: allow-ospf - match: - in_if: eth1 - ip_proto: ospf - action: accept - - id: telnet-from-R1-to-S1 - match: - src: 10.0.13.1/32 - dst: 10.0.4.254/32 - l4: { proto: tcp, dport: 23 } - action: accept - - id: ftp-from-R3-to-S2 - match: - src: 10.0.13.3/32 - dst: 10.0.6.254/32 - l4: { proto: tcp, dports: [20, 21] } - action: accept - - id: drop-rest - action: drop - -profiles: # overlays - patches on top of the base description - - name: "all-frr" - description: "Switch all control-plane to FRR" - patches: - - op: replace - path: /defaults/routing/stack - value: frr - - name: "mix-bird-frr-ospf" - description: "BIRD by default, but OSPF on R2 via FRR" - patches: - - op: add - path: /nodes - where: - name: R2 - patch: - routing: - protocols: - ospf: - enabled: true - impl: frr - - name: "switch-ovs" - patches: - - op: replace - path: /defaults/switch/impl - value: ovs - -tests: # tests (expected result) - - name: ping-R1-to-R3 - from: R1 - to: 10.0.13.3 - expect: success - - name: ping-VLAN-4-R1-to-R3 - from: R1 - to: 10.0.4.1 - expect: success diff --git a/examples/topology-schema.json b/examples/topology-schema.json deleted file mode 100644 index 4baf645..0000000 --- a/examples/topology-schema.json +++ /dev/null @@ -1,607 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://github.com/asvk-topology/schema/1.0", - "title": "ASVK Topology Configuration Schema", - "description": "JSON Schema for ASVK topology configuration files", - "type": "object", - "required": ["schema", "meta", "provider", "nodes"], - "properties": { - "schema": { - "type": "string", - "description": "Schema identifier", - "pattern": "^asvk\\.topology/[0-9]+\\.[0-9]+$", - "examples": ["asvk.topology/1.0"] - }, - "meta": { - "type": "object", - "description": "Metadata for the topology configuration", - "required": ["id", "title"], - "properties": { - "id": { - "type": "string", - "description": "Unique identifier for the topology", - "pattern": "^[a-zA-Z0-9_-]+$" - }, - "title": { - "type": "string", - "description": "Human-readable name for the topology" - }, - "description": { - "type": "string", - "description": "Detailed description of the topology" - } - } - }, - "provider": { - "type": "object", - "description": "Provider configuration for VMs/containers", - "required": ["name"], - "properties": { - "name": { - "type": "string", - "description": "Provider name", - "enum": ["virtualbox", "kvm", "container"] - }, - "defaults": { - "type": "object", - "description": "Default resource allocation", - "properties": { - "cpu": { - "type": "integer", - "description": "Default CPU cores", - "minimum": 1, - "default": 1 - }, - "ram_mb": { - "type": "integer", - "description": "Default RAM in MB", - "minimum": 128, - "default": 512 - }, - "disk_gb": { - "type": "integer", - "description": "Default disk size in GB", - "minimum": 1, - "default": 8 - }, - "os_image": { - "type": "string", - "description": "Default OS image" - }, - "nic_model": { - "type": "string", - "description": "Default NIC model", - "enum": ["virtio", "e1000", "rtl8139"] - }, - "promiscuous": { - "type": "string", - "description": "Promiscuous mode setting", - "enum": ["deny", "allow-vms", "allow-all"] - } - } - }, - "images": { - "type": "object", - "description": "Custom images for different node types", - "properties": { - "router": { "type": "string" }, - "switch": { "type": "string" }, - "host": { "type": "string" } - } - } - } - }, - "defaults": { - "type": "object", - "description": "Default behavior and software configuration", - "properties": { - "routing": { - "type": "object", - "properties": { - "stack": { - "type": "string", - "enum": ["bird", "frr", "linux"], - "default": "bird" - }, - "protocols_enabled": { - "type": "array", - "items": { - "type": "string", - "enum": ["ospf", "rip", "bgp", "static"] - }, - "default": [] - } - } - }, - "switch": { - "type": "object", - "properties": { - "impl": { - "type": "string", - "enum": ["linux-bridge", "ovs"], - "default": "linux-bridge" - } - } - }, - "firewall": { - "type": "object", - "properties": { - "impl": { - "type": "string", - "enum": ["nftables", "none"], - "default": "nftables" - } - } - }, - "mgmt": { - "type": "object", - "properties": { - "ssh_user": { - "type": "string", - "default": "lab" - }, - "ssh_key": { - "type": "string", - "description": "Path to SSH public key" - } - } - } - } - }, - "variables": { - "type": "object", - "description": "Global variables that can be referenced in templates", - "additionalProperties": true - }, - "networks": { - "type": "array", - "description": "Network definitions (L2 domains, L3 networks, VLAN trunks)", - "items": { - "type": "object", - "required": ["id", "type"], - "properties": { - "id": { - "type": "string", - "description": "Unique network identifier", - "pattern": "^[a-zA-Z0-9_-]+$" - }, - "type": { - "type": "string", - "enum": ["l2", "l3"], - "description": "Network type" - }, - "mtu": { - "type": "integer", - "description": "MTU for the network", - "minimum": 68, - "maximum": 9000, - "default": 1500 - }, - "cidr": { - "type": "string", - "description": "CIDR notation for L3 networks", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$" - }, - "dhcp": { - "type": "boolean", - "description": "Enable DHCP on this network", - "default": false - }, - "vlan": { - "type": "object", - "description": "VLAN configuration for trunk ports", - "properties": { - "mode": { - "type": "string", - "enum": ["access", "trunk"] - }, - "allowed": { - "type": "array", - "items": { - "type": "integer", - "minimum": 1, - "maximum": 4094 - }, - "description": "Allowed VLAN IDs on trunk" - }, - "vid": { - "type": "integer", - "description": "Native VLAN ID for access ports", - "minimum": 1, - "maximum": 4094 - } - } - } - } - } - }, - "nodes": { - "type": "array", - "description": "Node definitions (VMs/containers)", - "items": { - "type": "object", - "required": ["name", "role"], - "properties": { - "name": { - "type": "string", - "description": "Node name", - "pattern": "^[a-zA-Z0-9_-]+$" - }, - "role": { - "type": "string", - "enum": ["router", "switch", "host"], - "description": "Node role/type" - }, - "image": { - "type": "string", - "description": "OS image for the node (can reference provider.images)" - }, - "resources": { - "type": "object", - "properties": { - "cpu": { - "type": "integer", - "minimum": 1 - }, - "ram_mb": { - "type": "integer", - "minimum": 128 - }, - "disk_gb": { - "type": "integer", - "minimum": 1 - } - } - }, - "mgmt": { - "type": "object", - "description": "Management network configuration", - "properties": { - "ip": { - "type": "string", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$" - }, - "gw": { - "type": "string", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}$" - }, - "net": { - "type": "string" - } - } - }, - "interfaces": { - "type": "array", - "description": "Network interfaces (order matters)", - "items": { - "type": "object", - "required": ["name"], - "properties": { - "name": { - "type": "string", - "pattern": "^(eth[0-9]+|lo)$" - }, - "network": { - "type": "string" - }, - "loopback": { - "type": "boolean", - "description": "Indicates if this is a loopback interface" - }, - "addresses": { - "type": "array", - "items": { - "type": "string", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$" - } - } - }, - "anyOf": [ - { - "required": ["network"] - }, - { - "required": ["loopback"], - "properties": { - "loopback": { "const": true } - } - } - ] - } - }, - "routing": { - "type": "object", - "properties": { - "stack": { - "type": "string", - "enum": ["bird", "frr", "linux"] - }, - "protocols": { - "type": "object", - "properties": { - "static": { - "type": "object", - "properties": { - "routes": { - "type": "array", - "items": { - "type": "object", - "required": ["to", "via"], - "properties": { - "to": { - "type": "string", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])|0\\.0\\.0\\.0/0$" - }, - "via": { - "type": "string", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}$" - } - } - } - } - } - }, - "ospf": { - "type": "object", - "properties": { - "enabled": { "type": "boolean", "default": false }, - "impl": { - "type": "string", - "enum": ["bird", "frr"] - } - } - }, - "rip": { - "type": "object", - "properties": { - "enabled": { "type": "boolean", "default": false } - } - }, - "bgp": { - "type": "object", - "properties": { - "enabled": { "type": "boolean", "default": false }, - "asn": { "type": "integer" } - } - } - } - } - } - }, - "services": { - "type": "array", - "items": { - "type": "object", - "required": ["name"], - "properties": { - "name": { "type": "string" }, - "enabled": { "type": "boolean", "default": true } - } - } - } - } - } - }, - "switching": { - "type": "array", - "description": "L2 switching configuration", - "items": { - "type": "object", - "required": ["node", "impl"], - "properties": { - "node": { - "type": "string", - "description": "Switch node name" - }, - "impl": { - "type": "string", - "enum": ["linux-bridge", "ovs"] - }, - "bridges": { - "type": "array", - "items": { - "type": "object", - "required": ["name"], - "properties": { - "name": { "type": "string" }, - "ports": { - "type": "array", - "items": { - "type": "object", - "required": ["if"], - "properties": { - "if": { - "type": "string", - "pattern": "^eth[0-9]+$" - }, - "vlan": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["access", "trunk"] - }, - "allowed": { - "type": "array", - "items": { - "type": "integer", - "minimum": 1, - "maximum": 4094 - } - }, - "vid": { - "type": "integer", - "minimum": 1, - "maximum": 4094 - } - } - } - } - } - }, - "svis": { - "type": "array", - "description": "Switched Virtual Interfaces (SVI)", - "items": { - "type": "object", - "required": ["vid", "ifname"], - "properties": { - "vid": { - "type": "integer", - "minimum": 1, - "maximum": 4094 - }, - "ifname": { - "type": "string", - "pattern": "^[a-zA-Z0-9._-]+$" - }, - "addresses": { - "type": "array", - "items": { - "type": "string", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$" - } - } - } - } - } - } - } - } - } - } - }, - "firewall": { - "type": "array", - "description": "Firewall configuration", - "items": { - "type": "object", - "required": ["node", "rules"], - "properties": { - "node": { "type": "string" }, - "rules": { - "type": "array", - "items": { - "type": "object", - "required": ["id", "action"], - "properties": { - "id": { - "type": "string", - "pattern": "^[a-zA-Z0-9_-]+$" - }, - "match": { - "type": "object", - "properties": { - "in_if": { "type": "string" }, - "out_if": { "type": "string" }, - "src": { - "type": "string", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$" - }, - "dst": { - "type": "string", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$" - }, - "ip_proto": { - "type": "string", - "enum": ["tcp", "udp", "icmp", "ospf", "all"] - }, - "l4": { - "type": "object", - "properties": { - "proto": { - "type": "string", - "enum": ["tcp", "udp"] - }, - "sport": { - "type": ["integer", "array"], - "items": { "type": "integer" }, - "minimum": 1, - "maximum": 65535 - }, - "dport": { - "type": ["integer", "array"], - "items": { "type": "integer" }, - "minimum": 1, - "maximum": 65535 - }, - "dports": { - "type": ["integer", "array"], - "items": { "type": "integer" }, - "minimum": 1, - "maximum": 65535 - } - } - } - } - }, - "action": { - "type": "string", - "enum": ["accept", "drop", "reject"] - } - } - } - } - } - } - }, - "profiles": { - "type": "array", - "description": "Configuration profiles/overlays", - "items": { - "type": "object", - "required": ["name", "patches"], - "properties": { - "name": { "type": "string" }, - "description": { "type": "string" }, - "patches": { - "type": "array", - "items": { - "type": "object", - "required": ["op", "path"], - "properties": { - "op": { - "type": "string", - "enum": ["add", "replace", "remove"] - }, - "path": { "type": "string" }, - "value": true, - "where": { - "type": "object", - "description": "Conditional matching for patches" - }, - "patch": true - } - } - } - } - } - }, - "tests": { - "type": "array", - "description": "Validation tests", - "items": { - "type": "object", - "required": ["name", "from", "to", "expect"], - "properties": { - "name": { "type": "string" }, - "from": { "type": "string" }, - "to": { - "type": ["string", "object"], - "properties": { - "ip": { - "type": "string", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}$" - }, - "node": { "type": "string" } - } - }, - "expect": { - "type": "string", - "pattern": "^(success|failure|timeout|via .*)$" - } - } - } - } - } -} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7e77bf5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "netdiag" +version = "0.2.0" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/netdiag/__init__.py b/src/netdiag/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/netdiag/__main__.py b/src/netdiag/__main__.py new file mode 100644 index 0000000..8654f3f --- /dev/null +++ b/src/netdiag/__main__.py @@ -0,0 +1,3 @@ +from .base import run + +raise SystemExit(run()) diff --git a/src/netdiag/base.py b/src/netdiag/base.py new file mode 100644 index 0000000..1def400 --- /dev/null +++ b/src/netdiag/base.py @@ -0,0 +1,30 @@ +from .parse import parse_csv +from .domain.models import Topology +from .parse.convert_raw import ( + convert_raw_topology, +) +import logging + +# from .make_res import make_result +# from .structures import Result + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) + + +def run(): + raw_devices = parse_csv("data/input/table.csv") + # print(raw_devices) + topology = convert_raw_topology(raw_devices) + print(topology) + + # 2) make structured result + # result = make_result(devices_raw, network_agg, network_interfaces) + + # # 3) output (for debug) + # print(result) + + +if __name__ == "__main__": + run() diff --git a/src/netdiag/domain/__init__.py b/src/netdiag/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/netdiag/domain/models.py b/src/netdiag/domain/models.py new file mode 100644 index 0000000..ed4788a --- /dev/null +++ b/src/netdiag/domain/models.py @@ -0,0 +1,194 @@ +from typing import List, Dict, Any, Optional + + +# =========================== + + +class Interface: + name: str + ip_address: Optional[str] + network: Optional[str] + default_gateway: Optional[str] + # --- + device: Optional[ + "Device" + ] # set by Device.add_interface() when the interface is added to a device + + def __init__( + self, + name: str, + ip_address: Optional[str] = None, + network: Optional[str] = None, + default_gateway: Optional[str] = None, + ): + if not isinstance(name, str): + raise ValueError("Interface 'name' must be a string") + if ip_address is not None and not isinstance(ip_address, str): + raise ValueError("Interface 'ip_address' must be a string or None") + if network is not None and not isinstance(network, str): + raise ValueError("Interface 'network' must be a string or None") + if default_gateway is not None and not isinstance(default_gateway, str): + raise ValueError("Interface 'default_gateway' must be a string or None") + + self.name = name + self.ip_address = ip_address + self.network = network + self.default_gateway = default_gateway + + def __repr__(self) -> str: + return f"Interface(name={self.name}, ip_address={self.ip_address}, network={self.network}, default_gateway={self.default_gateway})" + + +class VirtualInterface(Interface): + pass + + +# =========================== + + +class Device: + name: str + interfaces: Dict[str, Interface] + + def __init__(self, name: str): + if not isinstance(name, str): + raise ValueError("Device 'name' must be a string") + + self.name = name + self.interfaces = dict() + + def add_interface(self, interface: Interface): + interface.device = ( + self # set the device attribute of the interface to this device + ) + self.interfaces[interface.name] = interface + + def rm_interface(self, interface: Interface): + if interface.name in self.interfaces: + del self.interfaces[interface.name] + interface.device = None # clear the device attribute of the interface + else: + raise ValueError( + f"Interface '{interface.name}' not found in device '{self.name}'" + ) + + def __repr__(self) -> str: + return f"Device(name={self.name}, interfaces={self.interfaces})" + + +class Host(Device): + pass + + +class Router(Device): + pass + + +class Switch(Device): + pass + + +# =========================== + + +class Network: + name: str + interfaces: List[Interface] + vlan: Optional[str] # need a proper parse + network_ip: Optional[str] + subnet_mask: Optional[str] + + def __init__( + self, + name: str, + vlan: Optional[str] = None, + network_ip: Optional[str] = None, + subnet_mask: Optional[str] = None, + ): + if not isinstance(name, str): + raise ValueError("Network 'name' must be a string") + if vlan is not None and not isinstance(vlan, str): + raise ValueError("Network 'vlan' must be a string or None") + if network_ip is not None and not isinstance(network_ip, str): + raise ValueError("Network 'network_ip' must be a string or None") + if subnet_mask is not None and not isinstance(subnet_mask, str): + raise ValueError("Network 'subnet_mask' must be a string or None") + + self.name = name + self.interfaces = [] + self.vlan = vlan + self.network_ip = network_ip + self.subnet_mask = subnet_mask + + def add_interface(self, interface: Interface): + if not isinstance(interface, Interface): + raise ValueError("Argument must be an instance of Interface") + if interface in self.interfaces: + raise ValueError( + f"Interface '{interface.name}' already exists in network '{self.name}'" + ) + + interface.network = ( + self.name # set the network attribute of the interface to this network + ) + self.interfaces.append(interface) + + def rm_interface(self, interface: Interface): + if interface in self.interfaces: + self.interfaces.remove(interface) + interface.network = None # clear the network attribute of the interface + else: + raise ValueError( + f"Interface '{interface.name}' not found in network '{self.name}'" + ) + + def __repr__(self) -> str: + return f"Network(name={self.name}, interfaces={self.interfaces}), vlan={self.vlan}, network_ip={self.network_ip}, subnet_mask={self.subnet_mask})" + + +class Topology: + devices: Dict[str, Device] + networks: Dict[str, Network] + + def __init__(self): + self.devices = dict() + self.networks = dict() + + def add_device(self, device: Device): + if not isinstance(device, Device): + raise ValueError("Argument must be an instance of Device") + + if device.name in self.devices: + raise ValueError( + f"Device with name '{device.name}' already exists in topology" + ) + + self.devices[device.name] = device + + def rm_device(self, device: Device): + if device.name in self.devices: + del self.devices[device.name] + else: + raise ValueError(f"Device with name '{device.name}' not found in topology") + + def add_network(self, network: Network): + if not isinstance(network, Network): + raise ValueError("Argument must be an instance of Network") + + if network.name in self.networks: + raise ValueError( + f"Network with name '{network.name}' already exists in topology" + ) + + self.networks[network.name] = network + + def rm_network(self, network: Network): + if network.name in self.networks: + del self.networks[network.name] + else: + raise ValueError( + f"Network with name '{network.name}' not found in topology" + ) + + def __repr__(self) -> str: + return f"Topology(devices={self.devices}, networks={self.networks})" diff --git a/src/netdiag/domain/validation.py b/src/netdiag/domain/validation.py new file mode 100644 index 0000000..e69de29 diff --git a/src/netdiag/parse/__init__.py b/src/netdiag/parse/__init__.py new file mode 100644 index 0000000..6d95e00 --- /dev/null +++ b/src/netdiag/parse/__init__.py @@ -0,0 +1,32 @@ +from typing import Dict, Any, List +import csv +import logging + + +class RawDevices: + id: int + fields: Dict[str, Any] + + def __init__(self, id: int, fields: Dict[str, Any]): + if not isinstance(id, int): + raise ValueError("RawDevices 'id' must be an integer") + if not isinstance(fields, dict): + raise ValueError("RawDevices 'fields' must be a dictionary") + if any(not isinstance(k, str) for k in fields.keys()): + raise ValueError("RawDevices 'fields' keys must be strings") + + self.id = id + self.fields = fields + + def __repr__(self) -> str: + return f"RawDevices(id={self.id}, fields={self.fields})" + + +def parse_csv(file_path: str, delimiter: str = ",") -> List[RawDevices]: + logging.info(f"Parsing CSV file: {file_path}") + + with open(file_path, mode="r", encoding="utf-8") as csvfile: + reader = csv.DictReader(csvfile, delimiter=delimiter) + devices = [RawDevices(idx + 1, row) for idx, row in enumerate(reader)] + + return devices diff --git a/src/netdiag/parse/convert_raw.py b/src/netdiag/parse/convert_raw.py new file mode 100644 index 0000000..1c37d48 --- /dev/null +++ b/src/netdiag/parse/convert_raw.py @@ -0,0 +1,151 @@ +from ..domain.models import ( + Interface, + VirtualInterface, + Device, + Host, + Router, + Switch, + Network, + Topology, +) + +from . import RawDevices + +name_matching = { + "DEVICE_TYPE": "Role", + "DEVICE_NAME": "Name", + "INTERFACE_NAME": "Interface", + "NETWORK_NAME": "Network", + "VLAN": "VLAN", + "NETWORK_IP": "Network IP", + "SUBNET_MASK": "Mask", + "IP_ADDRESS": "Device IP", + "DEFAULT_GATEWAY": "Default Gateway", + # --- In-cell fields --- + "TRUNK": "trunk", + "HOST": "Host", + "ROUTER": "Router", + "SWITCH": "Switch", +} + + +def parse_devices(raw_devices: list[RawDevices]) -> list[Device]: + devices = [] + + for raw_device in raw_devices: + device_type = raw_device.fields.get(name_matching["DEVICE_TYPE"], "").strip() + device_name = raw_device.fields.get(name_matching["DEVICE_NAME"], "").strip() + + if not device_type or not device_name: + raise ValueError( + f"Device with ID {raw_device.id} is missing required fields 'DEVICE_TYPE' and 'DEVICE_NAME'" + ) + + if device_name in [d.name for d in devices]: + continue + + if device_type == name_matching["HOST"]: + device = Host(name=device_name) + elif device_type == name_matching["ROUTER"]: + device = Router(name=device_name) + elif device_type == name_matching["SWITCH"]: + device = Switch(name=device_name) + else: + raise ValueError( + f"Device with ID {raw_device.id} has unrecognized DEVICE_TYPE '{device_type}'" + ) + + devices.append(device) + + return devices + + +def add_interfaces(devices: list[Device], raw_devices: list[RawDevices]) -> None: + for raw_device in raw_devices: + device_name = raw_device.fields.get(name_matching["DEVICE_NAME"], "").strip() + device = next( + (d for d in devices if d.name == device_name), None + ) # some other way? + + if not device: + raise ValueError( + f"Device with name '{device_name}' not found for interface parsing" + ) + + interface = Interface( + name=raw_device.fields.get(name_matching["INTERFACE_NAME"], "").strip(), + ip_address=raw_device.fields.get(name_matching["IP_ADDRESS"], "").strip(), + network=raw_device.fields.get(name_matching["NETWORK_NAME"], "").strip(), + default_gateway=raw_device.fields.get( + name_matching["DEFAULT_GATEWAY"], "" + ).strip(), + ) + device.add_interface(interface) + + return + + +def parse_networks( + devices: list[Device], raw_devices: list[RawDevices] +) -> list[Network]: + networks = [] + + for raw_device in raw_devices: + network_name = raw_device.fields.get(name_matching["NETWORK_NAME"], "").strip() + if not network_name: + continue + + vlan = (raw_device.fields.get(name_matching["VLAN"], "").strip(),) + network_ip = (raw_device.fields.get(name_matching["NETWORK_IP"], "").strip(),) + subnet_mask = (raw_device.fields.get(name_matching["SUBNET_MASK"], "").strip(),) + + if network_name in [n.name for n in networks]: # update if already exists + network = next(n for n in networks if n.name == network_name) + + if not network.vlan and vlan: + network.vlan = vlan + if not network.network_ip and network_ip: + network.network_ip = network_ip + if not network.subnet_mask and subnet_mask: + network.subnet_mask = subnet_mask + continue + + network = Network( + name=network_name, + vlan=raw_device.fields.get(name_matching["VLAN"], "").strip(), + network_ip=raw_device.fields.get(name_matching["NETWORK_IP"], "").strip(), + subnet_mask=raw_device.fields.get(name_matching["SUBNET_MASK"], "").strip(), + ) + networks.append(network) + + return networks + + +def assign_interfaces_to_networks( + networks: list[Network], devices: list[Device] +) -> None: + for network in networks: + for device in devices: + for _, interface in device.interfaces.items(): + if interface.network == network.name: + network.add_interface(interface) + + return + + +def convert_raw_topology(raw_devices: list[RawDevices]) -> Topology: + topology = Topology() + + devices = parse_devices(raw_devices) + add_interfaces(devices, raw_devices) + + for device in devices: + topology.add_device(device) + + networks = parse_networks(devices, raw_devices) + assign_interfaces_to_networks(networks, devices) + + for network in networks: + topology.networks[network.name] = network + + return topology diff --git a/src/check_correct.py b/src_old/check_correct.py similarity index 100% rename from src/check_correct.py rename to src_old/check_correct.py diff --git a/src/make_res.py b/src_old/make_res.py similarity index 100% rename from src/make_res.py rename to src_old/make_res.py diff --git a/src/old/structures copy.py b/src_old/old/structures copy.py similarity index 100% rename from src/old/structures copy.py rename to src_old/old/structures copy.py diff --git a/src/old/structures.py b/src_old/old/structures.py similarity index 100% rename from src/old/structures.py rename to src_old/old/structures.py diff --git a/src/parse.py b/src_old/parse.py similarity index 100% rename from src/parse.py rename to src_old/parse.py diff --git a/src/structures.py b/src_old/structures.py similarity index 100% rename from src/structures.py rename to src_old/structures.py