bytesim 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bytesim/__init__.py +14 -0
- bytesim/__main__.py +3 -0
- bytesim/cli.py +45 -0
- bytesim/constants.py +48 -0
- bytesim/demos.py +74 -0
- bytesim/model/__init__.py +5 -0
- bytesim/model/addressing.py +160 -0
- bytesim/model/base.py +57 -0
- bytesim/model/cloning.py +249 -0
- bytesim/model/equivalence.py +86 -0
- bytesim/model/groups.py +133 -0
- bytesim/model/ha.py +93 -0
- bytesim/model/layer2.py +106 -0
- bytesim/model/layout.py +203 -0
- bytesim/model/naming.py +114 -0
- bytesim/model/network.py +138 -0
- bytesim/model/routing.py +117 -0
- bytesim/model/stp.py +315 -0
- bytesim/model/types.py +107 -0
- bytesim/py.typed +0 -0
- bytesim/scripting.py +108 -0
- bytesim/synthesis.py +644 -0
- bytesim/topology.py +517 -0
- bytesim/tui/__init__.py +4 -0
- bytesim/tui/app.py +161 -0
- bytesim/tui/cables.py +58 -0
- bytesim/tui/canvas.py +489 -0
- bytesim/tui/command_input.py +29 -0
- bytesim/tui/commands/__init__.py +1 -0
- bytesim/tui/commands/addressing.py +235 -0
- bytesim/tui/commands/base.py +43 -0
- bytesim/tui/commands/groups.py +215 -0
- bytesim/tui/commands/history.py +134 -0
- bytesim/tui/commands/interpreter.py +85 -0
- bytesim/tui/commands/naming.py +68 -0
- bytesim/tui/commands/registry.py +28 -0
- bytesim/tui/commands/scripting.py +54 -0
- bytesim/tui/commands/switching.py +192 -0
- bytesim/tui/commands/topology.py +255 -0
- bytesim/tui/commands/traffic.py +312 -0
- bytesim/tui/help.py +50 -0
- bytesim/tui/inspector.py +115 -0
- bytesim/tui/theme.py +12 -0
- bytesim-0.1.0.dist-info/METADATA +178 -0
- bytesim-0.1.0.dist-info/RECORD +48 -0
- bytesim-0.1.0.dist-info/WHEEL +4 -0
- bytesim-0.1.0.dist-info/entry_points.txt +2 -0
- bytesim-0.1.0.dist-info/licenses/LICENSE +21 -0
bytesim/model/types.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Plain data: devices, cables, interface addresses."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from ..constants import BOX_H, KINDS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NetError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fmt(addr: tuple[int, int] | None) -> str:
|
|
14
|
+
return f"({addr[0]},{addr[1]})" if addr else "—"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Iface:
|
|
19
|
+
"""Logical address of one port/NIC: (network id, 8-bit host).
|
|
20
|
+
|
|
21
|
+
mode: "dhcp" (computers' default), "adhoc" (pick a free host, no server),
|
|
22
|
+
"static", "auto" (router/gateway ports), or "none".
|
|
23
|
+
via: how the current address was actually obtained.
|
|
24
|
+
"""
|
|
25
|
+
mode: str
|
|
26
|
+
net: int | None = None
|
|
27
|
+
host: int | None = None
|
|
28
|
+
gw: int | None = None # gateway's host number, on the same network
|
|
29
|
+
via: str = ""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def addr(self) -> tuple[int, int] | None:
|
|
33
|
+
return (self.net, self.host) if self.host is not None and self.net is not None else None
|
|
34
|
+
|
|
35
|
+
def clear(self) -> None:
|
|
36
|
+
self.net = self.host = self.gw = None
|
|
37
|
+
self.via = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
Leg = tuple[tuple[str, int], tuple[str, int], list[tuple[str, int]]]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Device:
|
|
45
|
+
kind: str
|
|
46
|
+
num: int
|
|
47
|
+
nports: int
|
|
48
|
+
x: int = 0
|
|
49
|
+
y: int = 0
|
|
50
|
+
up: bool = True
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def id(self) -> str:
|
|
54
|
+
return f"{self.kind}{self.num}"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def name(self) -> str:
|
|
58
|
+
return KINDS[self.kind][0]
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def color(self) -> str:
|
|
62
|
+
return KINDS[self.kind][2]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def width(self) -> int:
|
|
66
|
+
return max(len(self.id) + 2, self.nports + 2) + 2
|
|
67
|
+
|
|
68
|
+
def port_x(self, port: int) -> int:
|
|
69
|
+
inner = self.width - 2
|
|
70
|
+
return self.x + 1 + (inner - self.nports) // 2 + (port - 1)
|
|
71
|
+
|
|
72
|
+
def port_name(self, port: int) -> str:
|
|
73
|
+
return f"nic{port}" if self.kind == "c" else f"p{port}"
|
|
74
|
+
|
|
75
|
+
def mac(self, port: int) -> str:
|
|
76
|
+
"""Device id plus a port letter: R1A, R1B, ... Z, AA, AB ..."""
|
|
77
|
+
letters, n = "", port
|
|
78
|
+
while n:
|
|
79
|
+
n, r = divmod(n - 1, 26)
|
|
80
|
+
letters = chr(65 + r) + letters
|
|
81
|
+
return self.id.upper() + letters
|
|
82
|
+
|
|
83
|
+
def contains(self, x: int, y: int) -> bool:
|
|
84
|
+
return self.x <= x < self.x + self.width and self.y <= y < self.y + BOX_H
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class Link:
|
|
89
|
+
a: str
|
|
90
|
+
pa: int
|
|
91
|
+
b: str
|
|
92
|
+
pb: int
|
|
93
|
+
role: str = "data" # data | control | control-backup | fabric
|
|
94
|
+
up: bool = True
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def control(self) -> bool:
|
|
98
|
+
return self.role.startswith("control")
|
|
99
|
+
|
|
100
|
+
def touches(self, dev_id: str) -> bool:
|
|
101
|
+
return dev_id in (self.a, self.b)
|
|
102
|
+
|
|
103
|
+
def other(self, dev_id: str) -> tuple[str, int]:
|
|
104
|
+
return (self.b, self.pb) if dev_id == self.a else (self.a, self.pa)
|
|
105
|
+
|
|
106
|
+
def port_of(self, dev_id: str) -> int:
|
|
107
|
+
return self.pa if dev_id == self.a else self.pb
|
bytesim/py.typed
ADDED
|
File without changes
|
bytesim/scripting.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Run ByteSim command scripts without the UI.
|
|
2
|
+
|
|
3
|
+
The runner uses the *same* command handlers and interpreter as the app; only the
|
|
4
|
+
screen is replaced by a stub. It builds structure (devices, groups, cables,
|
|
5
|
+
settings) and skips what the screen and the address model would do after each
|
|
6
|
+
command, which a script doesn't need to reproduce its structure.
|
|
7
|
+
|
|
8
|
+
from bytesim.scripting import replay
|
|
9
|
+
net = replay(open("site.txt").read())
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections import Counter
|
|
14
|
+
from types import SimpleNamespace
|
|
15
|
+
|
|
16
|
+
from .model import NetError, Network
|
|
17
|
+
from .tui.commands.addressing import AddressCommands
|
|
18
|
+
from .tui.commands.groups import GroupCommands
|
|
19
|
+
from .tui.commands.interpreter import Interpreter
|
|
20
|
+
from .tui.commands.naming import NamingCommands
|
|
21
|
+
from .tui.commands.registry import collect
|
|
22
|
+
from .tui.commands.switching import SwitchingCommands
|
|
23
|
+
from .tui.commands.topology import TopologyCommands
|
|
24
|
+
from .tui.commands.traffic import TrafficCommands
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _Screen:
|
|
28
|
+
"""Stands in for the canvas: remembers the selection, finishes animations at once."""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self.selected: str | None = None
|
|
32
|
+
self.selected_group: str | None = None
|
|
33
|
+
self.pan = (0, 0)
|
|
34
|
+
self.packets: list = []
|
|
35
|
+
self.flash: dict = {}
|
|
36
|
+
self.hot_links: Counter = Counter()
|
|
37
|
+
self.size = SimpleNamespace(width=120, height=40)
|
|
38
|
+
self.busy = False
|
|
39
|
+
|
|
40
|
+
def select(self, dev_id: str | None) -> None:
|
|
41
|
+
self.selected, self.selected_group = dev_id, None
|
|
42
|
+
|
|
43
|
+
def select_group(self, name: str) -> None:
|
|
44
|
+
self.selected, self.selected_group = None, name
|
|
45
|
+
|
|
46
|
+
def send_packet(self, cells, color, on_done=None) -> None:
|
|
47
|
+
if on_done:
|
|
48
|
+
on_done()
|
|
49
|
+
|
|
50
|
+
def focus(self, *a) -> None: ...
|
|
51
|
+
def animate(self) -> None: ...
|
|
52
|
+
def refresh(self, *a, **k) -> None: ...
|
|
53
|
+
def reset(self) -> None: ...
|
|
54
|
+
def heat(self, links) -> None: ...
|
|
55
|
+
def cool(self, links) -> None: ...
|
|
56
|
+
def flash_device(self, *a, **k) -> None: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ScriptRunner(TopologyCommands, GroupCommands, AddressCommands, TrafficCommands, NamingCommands,
|
|
60
|
+
SwitchingCommands, Interpreter):
|
|
61
|
+
"""A headless ByteSim: feed it commands, read back ``net``."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, network: Network | None = None) -> None:
|
|
64
|
+
self.net = network or Network()
|
|
65
|
+
self.auto = False
|
|
66
|
+
self.graph = False
|
|
67
|
+
self.replaying = 1 # always "in a script": no per-command side effects
|
|
68
|
+
self.naming = None
|
|
69
|
+
self.log: list[str] = []
|
|
70
|
+
self._ha_seen: dict = {}
|
|
71
|
+
self._screen = _Screen()
|
|
72
|
+
self._commands = collect(type(self))
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def canvas(self): # type: ignore[override]
|
|
76
|
+
return self._screen
|
|
77
|
+
|
|
78
|
+
def log_msg(self, msg: str) -> None:
|
|
79
|
+
self.log.append(msg)
|
|
80
|
+
|
|
81
|
+
def changed(self, skip=frozenset()) -> None: # structure only: no layout, no re-addressing
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def refresh_sidebar(self) -> None:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def replace_network(self, net: Network, auto: bool) -> None:
|
|
88
|
+
self.net = net
|
|
89
|
+
|
|
90
|
+
def arrange(self, announce: bool = False) -> None:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
def exit(self) -> None:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def run(self, text: str, origin: str = "script") -> Network:
|
|
97
|
+
"""Run every command; raises NetError naming the line that failed."""
|
|
98
|
+
for n, line in self.script_lines(text):
|
|
99
|
+
try:
|
|
100
|
+
self.dispatch(self.tokens(line))
|
|
101
|
+
except NetError as e:
|
|
102
|
+
raise NetError(f"{origin}, line {n} ({line}): {e}") from None
|
|
103
|
+
return self.net
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def replay(text: str, network: Network | None = None) -> Network:
|
|
107
|
+
"""Build a network by running a script (into a fresh network unless one is given)."""
|
|
108
|
+
return ScriptRunner(network).run(text)
|