some refactor
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
tmp
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
, prop1 , prop2 , prop3
|
||||
ob1 , val1 , , val2
|
||||
obj2 , , val3 , val4
|
||||
|
@@ -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
|
||||
@@ -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 .*)$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[project]
|
||||
name = "netdiag"
|
||||
version = "0.2.0"
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = {"" = "src"}
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
@@ -0,0 +1,3 @@
|
||||
from .base import run
|
||||
|
||||
raise SystemExit(run())
|
||||
@@ -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()
|
||||
@@ -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})"
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user