92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Tuple
|
|
|
|
TOP_KEYS = ["ui", "hmis", "plcs", "sensors", "actuators", "hils", "serial_networks", "ip_networks"]
|
|
|
|
|
|
def validate_basic(cfg: dict[str, Any]) -> List[str]:
|
|
errors: List[str] = []
|
|
|
|
if not isinstance(cfg, dict):
|
|
return ["Top-level JSON is not an object"]
|
|
|
|
for k in TOP_KEYS:
|
|
if k not in cfg:
|
|
errors.append(f"Missing top-level key: {k}")
|
|
if errors:
|
|
return errors
|
|
|
|
if not isinstance(cfg["ui"], dict):
|
|
errors.append("ui must be an object")
|
|
|
|
for k in ["hmis", "plcs", "sensors", "actuators", "hils", "serial_networks", "ip_networks"]:
|
|
if not isinstance(cfg[k], list):
|
|
errors.append(f"{k} must be an array")
|
|
if errors:
|
|
return errors
|
|
|
|
ui = cfg.get("ui", {})
|
|
if not isinstance(ui, dict):
|
|
errors.append("ui must be an object")
|
|
return errors
|
|
uinet = ui.get("network")
|
|
if not isinstance(uinet, dict):
|
|
errors.append("ui.network must be an object")
|
|
return errors
|
|
for req in ["ip", "port", "docker_network"]:
|
|
if req not in uinet:
|
|
errors.append(f"ui.network missing key: {req}")
|
|
|
|
names: List[str] = []
|
|
for section in ["hmis", "plcs", "sensors", "actuators", "hils"]:
|
|
for dev in cfg.get(section, []):
|
|
if isinstance(dev, dict) and isinstance(dev.get("name"), str):
|
|
names.append(dev["name"])
|
|
dup = {n for n in set(names) if names.count(n) > 1}
|
|
if dup:
|
|
errors.append(f"Duplicate device names: {sorted(list(dup))}")
|
|
|
|
seen: Dict[Tuple[str, str], str] = {}
|
|
|
|
def check_net(dev: dict[str, Any]) -> None:
|
|
net = dev.get("network") or {}
|
|
dn = net.get("docker_network")
|
|
ip = net.get("ip")
|
|
name = dev.get("name", "<unnamed>")
|
|
if not isinstance(dn, str) or not isinstance(ip, str):
|
|
return
|
|
key = (dn, ip)
|
|
if key in seen:
|
|
errors.append(f"Duplicate IP {ip} on docker_network {dn} (devices: {seen[key]} and {name})")
|
|
else:
|
|
seen[key] = str(name)
|
|
|
|
for section in ["hmis", "plcs", "sensors", "actuators"]:
|
|
for dev in cfg.get(section, []):
|
|
if isinstance(dev, dict):
|
|
check_net(dev)
|
|
|
|
def uses_rtu(dev: dict[str, Any]) -> bool:
|
|
for c in (dev.get("inbound_connections") or []):
|
|
if isinstance(c, dict) and c.get("type") == "rtu":
|
|
return True
|
|
for c in (dev.get("outbound_connections") or []):
|
|
if isinstance(c, dict) and c.get("type") == "rtu":
|
|
return True
|
|
return False
|
|
|
|
any_rtu = False
|
|
for section in ["hmis", "plcs", "sensors", "actuators"]:
|
|
for dev in cfg.get(section, []):
|
|
if isinstance(dev, dict) and uses_rtu(dev):
|
|
any_rtu = True
|
|
|
|
serial_nets = cfg.get("serial_networks", [])
|
|
if any_rtu and len(serial_nets) == 0:
|
|
errors.append("RTU used but serial_networks is empty")
|
|
if (not any_rtu) and len(serial_nets) != 0:
|
|
errors.append("serial_networks must be empty when RTU is not used")
|
|
|
|
return errors
|