annet 0.16.8__py3-none-any.whl → 0.16.10__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.
Potentially problematic release.
This version of annet might be problematic. Click here for more details.
- annet/adapters/fetchers/__init__.py +0 -0
- annet/adapters/fetchers/stub/__init__.py +0 -0
- annet/adapters/fetchers/stub/fetcher.py +19 -0
- annet/adapters/file/__init__.py +0 -0
- annet/adapters/file/provider.py +226 -0
- annet/adapters/netbox/common/manufacturer.py +2 -0
- annet/adapters/netbox/common/models.py +109 -4
- annet/adapters/netbox/v37/storage.py +31 -3
- annet/annlib/netdev/views/hardware.py +31 -0
- annet/bgp_models.py +266 -0
- annet/configs/context.yml +2 -12
- annet/configs/logging.yaml +1 -0
- annet/generators/__init__.py +18 -4
- annet/generators/jsonfragment.py +13 -12
- annet/mesh/__init__.py +16 -0
- annet/mesh/basemodel.py +180 -0
- annet/mesh/device_models.py +62 -0
- annet/mesh/executor.py +248 -0
- annet/mesh/match_args.py +165 -0
- annet/mesh/models_converter.py +84 -0
- annet/mesh/peer_models.py +98 -0
- annet/mesh/registry.py +212 -0
- annet/rulebook/routeros/__init__.py +0 -0
- annet/rulebook/routeros/file.py +5 -0
- annet/storage.py +41 -2
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/METADATA +3 -1
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/RECORD +36 -17
- annet_generators/example/__init__.py +1 -3
- annet_generators/mesh_example/__init__.py +9 -0
- annet_generators/mesh_example/bgp.py +43 -0
- annet_generators/mesh_example/mesh_logic.py +28 -0
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/AUTHORS +0 -0
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/LICENSE +0 -0
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/WHEEL +0 -0
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/entry_points.txt +0 -0
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/top_level.txt +0 -0
annet/mesh/executor.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from typing import Annotated, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from annet.bgp_models import Peer, GlobalOptions
|
|
6
|
+
from annet.storage import Device, Storage
|
|
7
|
+
from .basemodel import merge, BaseMeshModel, Merge, UseLast, MergeForbiddenError
|
|
8
|
+
from .device_models import GlobalOptionsDTO
|
|
9
|
+
from .models_converter import to_bgp_global_options, to_bgp_peer, InterfaceChanges, to_interface_changes
|
|
10
|
+
from .peer_models import PeerDTO
|
|
11
|
+
from .registry import MeshRulesRegistry, GlobalOptions as MeshGlobalOptions, DirectPeer, MeshSession, IndirectPeer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class MeshExecutionResult:
|
|
19
|
+
global_options: GlobalOptions
|
|
20
|
+
peers: list[Peer]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class PeerKey:
|
|
25
|
+
fqdn: str
|
|
26
|
+
addr: str
|
|
27
|
+
vrf: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Pair(BaseMeshModel):
|
|
31
|
+
local: Annotated[PeerDTO, Merge()]
|
|
32
|
+
connected: Annotated[PeerDTO, Merge()]
|
|
33
|
+
device: Annotated[Device, UseLast()]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MeshExecutor:
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
registry: MeshRulesRegistry,
|
|
40
|
+
storage: Storage,
|
|
41
|
+
):
|
|
42
|
+
self._registry = registry
|
|
43
|
+
self._storage = storage
|
|
44
|
+
|
|
45
|
+
def _execute_globals(self, device: Device) -> GlobalOptionsDTO:
|
|
46
|
+
global_opts = GlobalOptionsDTO()
|
|
47
|
+
for rule in self._registry.lookup_global(device.fqdn):
|
|
48
|
+
handler_name = self._handler_name(rule.handler)
|
|
49
|
+
rule_global_opts = MeshGlobalOptions(rule.match, device)
|
|
50
|
+
logger.debug("Running device handler: %s", handler_name)
|
|
51
|
+
rule.handler(rule_global_opts)
|
|
52
|
+
try:
|
|
53
|
+
global_opts = merge(global_opts, rule_global_opts)
|
|
54
|
+
except MergeForbiddenError as e:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Handler `{handler_name}` global options conflicting with "
|
|
57
|
+
f"previously loaded for device `{device.fqdn}`:\n" + str(e)
|
|
58
|
+
) from e
|
|
59
|
+
return global_opts
|
|
60
|
+
|
|
61
|
+
def _handler_name(self, handler: Callable) -> str:
|
|
62
|
+
try:
|
|
63
|
+
return f"{handler.__module__}.{handler.__qualname__}"
|
|
64
|
+
except AttributeError:
|
|
65
|
+
return str(handler)
|
|
66
|
+
|
|
67
|
+
def _execute_direct(self, device: Device) -> list[Pair]:
|
|
68
|
+
# we can have multiple rules for the same pair
|
|
69
|
+
# we merge them according to remote fqdn
|
|
70
|
+
neighbor_peers: dict[PeerKey, Pair] = {}
|
|
71
|
+
# TODO batch resolve
|
|
72
|
+
for rule in self._registry.lookup_direct(device.fqdn, device.neighbours_fqdns):
|
|
73
|
+
session = MeshSession()
|
|
74
|
+
handler_name = self._handler_name(rule.handler)
|
|
75
|
+
logger.debug("Running direct handler: %s", handler_name)
|
|
76
|
+
if rule.direct_order:
|
|
77
|
+
neighbor_device = self._storage.make_devices([rule.name_right])[0]
|
|
78
|
+
peer_device = DirectPeer(rule.match_left, device, [])
|
|
79
|
+
peer_neighbor = DirectPeer(rule.match_right, neighbor_device, [])
|
|
80
|
+
else:
|
|
81
|
+
neighbor_device = self._storage.make_devices([rule.name_left])[0]
|
|
82
|
+
peer_neighbor = DirectPeer(rule.match_left, neighbor_device, [])
|
|
83
|
+
peer_device = DirectPeer(rule.match_right, device, [])
|
|
84
|
+
|
|
85
|
+
interfaces = self._storage.search_connections(device, neighbor_device)
|
|
86
|
+
for local_port, remote_port in interfaces:
|
|
87
|
+
peer_device.ports.append(local_port.name)
|
|
88
|
+
peer_neighbor.ports.append(remote_port.name)
|
|
89
|
+
|
|
90
|
+
if rule.direct_order:
|
|
91
|
+
rule.handler(peer_device, peer_neighbor, session)
|
|
92
|
+
else:
|
|
93
|
+
rule.handler(peer_neighbor, peer_device, session)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
neighbor_dto = merge(PeerDTO(), peer_neighbor, session)
|
|
97
|
+
except MergeForbiddenError as e:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Handler `{handler_name}` provided session data conflicting with "
|
|
100
|
+
f"peer data for device `{neighbor_device.fqdn}`:\n" + str(e)
|
|
101
|
+
) from e
|
|
102
|
+
try:
|
|
103
|
+
device_dto = merge(PeerDTO(), peer_device, session)
|
|
104
|
+
except MergeForbiddenError as e:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Handler `{handler_name}` provided session data conflicting with "
|
|
107
|
+
f"peer data for device `{device.fqdn}`:\n" + str(e)
|
|
108
|
+
) from e
|
|
109
|
+
|
|
110
|
+
pair = Pair(local=device_dto, connected=neighbor_dto, device=neighbor_device)
|
|
111
|
+
addr = getattr(neighbor_dto, "addr", None)
|
|
112
|
+
if addr is None:
|
|
113
|
+
raise ValueError(f"Handler `{handler_name}` returned no peer addr")
|
|
114
|
+
peer_key = PeerKey(
|
|
115
|
+
fqdn=neighbor_device.fqdn,
|
|
116
|
+
addr=addr,
|
|
117
|
+
vrf=getattr(neighbor_dto, "vrf", "")
|
|
118
|
+
)
|
|
119
|
+
try:
|
|
120
|
+
if peer_key in neighbor_peers:
|
|
121
|
+
pair = merge(neighbor_peers[peer_key], pair)
|
|
122
|
+
except MergeForbiddenError as e:
|
|
123
|
+
if rule.direct_order:
|
|
124
|
+
pair_names = device.fqdn, neighbor_device.fqdn
|
|
125
|
+
else:
|
|
126
|
+
pair_names = neighbor_device.fqdn, device.fqdn
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"Handler `{handler_name}` provides data conflicting with "
|
|
129
|
+
f"previously loaded for device pair {pair_names} "
|
|
130
|
+
f"with addr={peer_key.addr}, vrf{peer_key.vrf}:\n" + str(e)
|
|
131
|
+
) from e
|
|
132
|
+
neighbor_peers[peer_key] = pair
|
|
133
|
+
return list(neighbor_peers.values())
|
|
134
|
+
|
|
135
|
+
def _execute_indirect(self, device: Device, all_fqdns: list[str]) -> list[Pair]:
|
|
136
|
+
# we can have multiple rules for the same pair
|
|
137
|
+
# we merge them according to remote fqdn
|
|
138
|
+
connected_peers: dict[PeerKey, Pair] = {}
|
|
139
|
+
for rule in self._registry.lookup_indirect(device.fqdn, all_fqdns):
|
|
140
|
+
session = MeshSession()
|
|
141
|
+
handler_name = self._handler_name(rule.handler)
|
|
142
|
+
logger.debug("Running indirect handler: %s", handler_name)
|
|
143
|
+
if rule.direct_order:
|
|
144
|
+
connected_device = self._storage.make_devices([rule.name_right])[0]
|
|
145
|
+
peer_device = IndirectPeer(rule.match_left, device)
|
|
146
|
+
peer_connected = IndirectPeer(rule.match_right, connected_device)
|
|
147
|
+
rule.handler(peer_device, peer_connected, session)
|
|
148
|
+
else:
|
|
149
|
+
connected_device = self._storage.make_devices([rule.name_left])[0]
|
|
150
|
+
peer_connected = IndirectPeer(rule.match_left, connected_device)
|
|
151
|
+
peer_device = IndirectPeer(rule.match_right, device)
|
|
152
|
+
rule.handler(peer_connected, peer_device, session)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
connected_dto = merge(PeerDTO(), peer_connected, session)
|
|
156
|
+
except MergeForbiddenError as e:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"Handler `{handler_name}` provided session data conflicting with "
|
|
159
|
+
f"peer data for device `{connected_device.fqdn}`:\n" + str(e)
|
|
160
|
+
) from e
|
|
161
|
+
try:
|
|
162
|
+
device_dto = merge(PeerDTO(), peer_device, session)
|
|
163
|
+
except MergeForbiddenError as e:
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f"Handler `{handler_name}` provided session data conflicting with "
|
|
166
|
+
f"peer data for device `{device.fqdn}`:\n" + str(e)
|
|
167
|
+
) from e
|
|
168
|
+
|
|
169
|
+
pair = Pair(local=device_dto, connected=connected_dto, device=connected_device)
|
|
170
|
+
addr = getattr(connected_dto, "addr", None)
|
|
171
|
+
if addr is None:
|
|
172
|
+
raise ValueError(f"Handler `{handler_name}` returned no peer addr")
|
|
173
|
+
peer_key = PeerKey(
|
|
174
|
+
fqdn=connected_device.fqdn,
|
|
175
|
+
addr=addr,
|
|
176
|
+
vrf=getattr(connected_dto, "vrf", "")
|
|
177
|
+
)
|
|
178
|
+
try:
|
|
179
|
+
if peer_key in connected_peers:
|
|
180
|
+
pair = merge(connected_peers[peer_key], pair)
|
|
181
|
+
except MergeForbiddenError as e:
|
|
182
|
+
if rule.direct_order:
|
|
183
|
+
pair_names = device.fqdn, connected_device.fqdn
|
|
184
|
+
else:
|
|
185
|
+
pair_names = connected_device.fqdn, device.fqdn
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"Handler `{handler_name}` provides data conflicting with "
|
|
188
|
+
f"previously loaded for device pair {pair_names} "
|
|
189
|
+
f"with addr={peer_key.addr}, vrf{peer_key.vrf}:\n" + str(e)
|
|
190
|
+
) from e
|
|
191
|
+
connected_peers[peer_key] = pair
|
|
192
|
+
|
|
193
|
+
return list(connected_peers.values()) # FIXME
|
|
194
|
+
|
|
195
|
+
def _to_bgp_peer(self, pair: Pair, interface: Optional[str]) -> Peer:
|
|
196
|
+
return to_bgp_peer(pair.local, pair.connected, pair.device, interface)
|
|
197
|
+
|
|
198
|
+
def _to_bgp_global(self, global_options: GlobalOptionsDTO) -> GlobalOptions:
|
|
199
|
+
return to_bgp_global_options(global_options)
|
|
200
|
+
|
|
201
|
+
def _apply_interface_changes(self, device: Device, neighbor: Device, changes: InterfaceChanges) -> str:
|
|
202
|
+
port_pairs = self._storage.search_connections(device, neighbor)
|
|
203
|
+
if len(port_pairs) > 1:
|
|
204
|
+
if changes.lag is changes.svi is None:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"Multiple connections found between {device.fqdn} and {neighbor.fqdn}."
|
|
207
|
+
"Specify LAG or SVI"
|
|
208
|
+
)
|
|
209
|
+
if changes.lag is not None:
|
|
210
|
+
target_interface = device.make_lag(
|
|
211
|
+
lag=changes.lag,
|
|
212
|
+
ports=[local_port.name for local_port, remote_port in port_pairs],
|
|
213
|
+
lag_min_links=changes.lag_links_min,
|
|
214
|
+
)
|
|
215
|
+
if changes.subif is not None:
|
|
216
|
+
target_interface = device.add_subif(target_interface.name, changes.subif)
|
|
217
|
+
elif changes.subif is not None:
|
|
218
|
+
# single connection
|
|
219
|
+
local_port, remote_port = port_pairs[0]
|
|
220
|
+
target_interface = device.add_subif(local_port.name, changes.subif)
|
|
221
|
+
elif changes.svi is not None:
|
|
222
|
+
target_interface = device.add_svi(changes.svi)
|
|
223
|
+
else:
|
|
224
|
+
target_interface, _ = port_pairs[0]
|
|
225
|
+
target_interface.add_addr(changes.addr, changes.vrf)
|
|
226
|
+
return target_interface.name
|
|
227
|
+
|
|
228
|
+
def execute_for(self, device: Device) -> MeshExecutionResult:
|
|
229
|
+
all_fqdns = self._storage.resolve_all_fdnds()
|
|
230
|
+
|
|
231
|
+
global_options = self._to_bgp_global(self._execute_globals(device))
|
|
232
|
+
|
|
233
|
+
peers = []
|
|
234
|
+
for direct_pair in self._execute_direct(device):
|
|
235
|
+
target_interface = self._apply_interface_changes(
|
|
236
|
+
device,
|
|
237
|
+
direct_pair.device,
|
|
238
|
+
to_interface_changes(direct_pair.local),
|
|
239
|
+
)
|
|
240
|
+
peers.append(self._to_bgp_peer(direct_pair, target_interface))
|
|
241
|
+
|
|
242
|
+
for connected_pair in self._execute_indirect(device, all_fqdns):
|
|
243
|
+
peers.append(self._to_bgp_peer(connected_pair, None))
|
|
244
|
+
|
|
245
|
+
return MeshExecutionResult(
|
|
246
|
+
global_options=global_options,
|
|
247
|
+
peers=peers,
|
|
248
|
+
)
|
annet/mesh/match_args.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Sequence, Optional
|
|
6
|
+
|
|
7
|
+
MatchedArgs = SimpleNamespace
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def identity(x: Any) -> Any:
|
|
11
|
+
return x
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MatchExpr:
|
|
15
|
+
def __init__(self, expr: Callable[[Any], Any] = identity):
|
|
16
|
+
self.expr = expr
|
|
17
|
+
|
|
18
|
+
def __getattr__(self, item: str):
|
|
19
|
+
return MatchExpr(lambda x: getattr(self.expr(x), item))
|
|
20
|
+
|
|
21
|
+
def __getitem__(self, item: Any):
|
|
22
|
+
return MatchExpr(lambda x: self.expr(x)[item])
|
|
23
|
+
|
|
24
|
+
def __eq__(self, other) -> "MatchExpr": # type: ignore[override] # https://github.com/python/mypy/issues/5951
|
|
25
|
+
if isinstance(other, MatchExpr):
|
|
26
|
+
return MatchExpr(lambda x: self.expr(x) == other.expr(x))
|
|
27
|
+
else:
|
|
28
|
+
return MatchExpr(lambda x: self.expr(x) == other)
|
|
29
|
+
|
|
30
|
+
def __ne__(self, other) -> "MatchExpr": # type: ignore[override] # https://github.com/python/mypy/issues/5951
|
|
31
|
+
if isinstance(other, MatchExpr):
|
|
32
|
+
return MatchExpr(lambda x: self.expr(x) != other.expr(x))
|
|
33
|
+
else:
|
|
34
|
+
return MatchExpr(lambda x: self.expr(x) != other)
|
|
35
|
+
|
|
36
|
+
def __lt__(self, other) -> "MatchExpr":
|
|
37
|
+
if isinstance(other, MatchExpr):
|
|
38
|
+
return MatchExpr(lambda x: self.expr(x) < other.expr(x))
|
|
39
|
+
else:
|
|
40
|
+
return MatchExpr(lambda x: self.expr(x) < other)
|
|
41
|
+
|
|
42
|
+
def __gt__(self, other) -> "MatchExpr":
|
|
43
|
+
if isinstance(other, MatchExpr):
|
|
44
|
+
return MatchExpr(lambda x: self.expr(x) > other.expr(x))
|
|
45
|
+
else:
|
|
46
|
+
return MatchExpr(lambda x: self.expr(x) > other)
|
|
47
|
+
|
|
48
|
+
def __le__(self, other) -> "MatchExpr":
|
|
49
|
+
if isinstance(other, MatchExpr):
|
|
50
|
+
return MatchExpr(lambda x: self.expr(x) <= other.expr(x))
|
|
51
|
+
else:
|
|
52
|
+
return MatchExpr(lambda x: self.expr(x) <= other)
|
|
53
|
+
|
|
54
|
+
def __ge__(self, other) -> "MatchExpr":
|
|
55
|
+
if isinstance(other, MatchExpr):
|
|
56
|
+
return MatchExpr(lambda x: self.expr(x) >= other.expr(x))
|
|
57
|
+
else:
|
|
58
|
+
return MatchExpr(lambda x: self.expr(x) >= other)
|
|
59
|
+
|
|
60
|
+
def __or__(self, other: "MatchExpr") -> "MatchExpr":
|
|
61
|
+
return MatchExpr(lambda x: self.expr(x) or other.expr(x))
|
|
62
|
+
|
|
63
|
+
def __and__(self, other: "MatchExpr") -> "MatchExpr":
|
|
64
|
+
return MatchExpr(lambda x: self.expr(x) and other.expr(x))
|
|
65
|
+
|
|
66
|
+
def cast_(self, type_: Callable[[Any], Any]) -> "MatchExpr":
|
|
67
|
+
return MatchExpr(lambda x: type_(self.expr(x)))
|
|
68
|
+
|
|
69
|
+
def in_(self, value: Any) -> "MatchExpr":
|
|
70
|
+
if isinstance(value, MatchExpr):
|
|
71
|
+
return MatchExpr(lambda x: self.expr(x) in value.expr(x))
|
|
72
|
+
else:
|
|
73
|
+
return MatchExpr(lambda x: self.expr(x) in value)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
Match = MatchExpr()[0]
|
|
77
|
+
Left = MatchExpr()[0]
|
|
78
|
+
Right = MatchExpr()[1]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PeerNameTemplate:
|
|
82
|
+
def __init__(self, raw_str):
|
|
83
|
+
self._str = str(raw_str)
|
|
84
|
+
self._regex, self._types = self._compile(self._str)
|
|
85
|
+
|
|
86
|
+
def __str__(self):
|
|
87
|
+
return self._str
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _compile(value: str) -> tuple[re.Pattern[str], dict[str, type]]:
|
|
91
|
+
int_groups = re.findall(r"{(?P<group_name>\w+)}", value)
|
|
92
|
+
# '{name}' -> (?P<name>\d+)
|
|
93
|
+
regex_string = re.sub(r"{(?P<group_name>\w+)}", r"(?P<\g<group_name>>\\d+)", value)
|
|
94
|
+
# '{name:regex}' -> (?P<name>regex)
|
|
95
|
+
regex_string = re.sub(
|
|
96
|
+
r"{(?P<group_name>\w+):(?P<custom_regex>.*?)}", r"(?P<\g<group_name>>\g<custom_regex>)",
|
|
97
|
+
regex_string
|
|
98
|
+
)
|
|
99
|
+
pattern = re.compile(regex_string)
|
|
100
|
+
types: dict[str, type] = {
|
|
101
|
+
name: (int if name in int_groups else str)
|
|
102
|
+
for name in pattern.groupindex
|
|
103
|
+
}
|
|
104
|
+
return pattern, types
|
|
105
|
+
|
|
106
|
+
def match(self, hostname: str) -> Optional[dict[str, str]]:
|
|
107
|
+
reg_match = self._regex.fullmatch(hostname)
|
|
108
|
+
if reg_match:
|
|
109
|
+
return {
|
|
110
|
+
key: self._types[key](value)
|
|
111
|
+
for key, value in reg_match.groupdict().items()
|
|
112
|
+
}
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def match_safe(match_expressions: Sequence[MatchExpr], value: Any) -> bool:
|
|
117
|
+
for matcher in match_expressions:
|
|
118
|
+
try:
|
|
119
|
+
res = matcher.expr(value)
|
|
120
|
+
if not bool(res):
|
|
121
|
+
return False
|
|
122
|
+
except (TypeError, ValueError, AttributeError, KeyError, IndexError):
|
|
123
|
+
return False
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class SingleMatcher:
|
|
129
|
+
def __init__(self, rule: str, match_expressions: Sequence[MatchExpr]):
|
|
130
|
+
self.rule = PeerNameTemplate(rule)
|
|
131
|
+
self.match_expressions = match_expressions
|
|
132
|
+
|
|
133
|
+
def match_one(self, host: str) -> Optional[MatchedArgs]:
|
|
134
|
+
data = self.rule.match(host)
|
|
135
|
+
if data is None:
|
|
136
|
+
return None
|
|
137
|
+
args = MatchedArgs(**data)
|
|
138
|
+
if not match_safe(self.match_expressions, (args,)):
|
|
139
|
+
return None
|
|
140
|
+
return args
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class PairMatcher:
|
|
145
|
+
def __init__(self, left_rule: str, right_rule: str, match_expressions: Sequence[MatchExpr]):
|
|
146
|
+
self.left_rule = PeerNameTemplate(left_rule)
|
|
147
|
+
self.right_rule = PeerNameTemplate(right_rule)
|
|
148
|
+
self.match_expressions = match_expressions
|
|
149
|
+
|
|
150
|
+
def match_pair(self, left: str, right: str) -> Optional[tuple[MatchedArgs, MatchedArgs]]:
|
|
151
|
+
left_args = self._match_host(self.left_rule, left)
|
|
152
|
+
if left_args is None:
|
|
153
|
+
return None
|
|
154
|
+
right_args = self._match_host(self.right_rule, right)
|
|
155
|
+
if right_args is None:
|
|
156
|
+
return None
|
|
157
|
+
if not match_safe(self.match_expressions, (left_args, right_args)):
|
|
158
|
+
return None
|
|
159
|
+
return left_args, right_args
|
|
160
|
+
|
|
161
|
+
def _match_host(self, rule: PeerNameTemplate, host: str) -> Optional[MatchedArgs]:
|
|
162
|
+
data = rule.match(host)
|
|
163
|
+
if data is None:
|
|
164
|
+
return None
|
|
165
|
+
return MatchedArgs(**data)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from ipaddress import ip_interface
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from adaptix import Retort, loader, Chain, name_mapping
|
|
6
|
+
|
|
7
|
+
from .peer_models import PeerDTO
|
|
8
|
+
from ..bgp_models import GlobalOptions, VrfOptions, FamilyOptions, Peer, PeerGroup, ASN, PeerOptions
|
|
9
|
+
from ..storage import Device
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class InterfaceChanges:
|
|
14
|
+
addr: str
|
|
15
|
+
lag: Optional[int] = None
|
|
16
|
+
lag_links_min: Optional[int] = None
|
|
17
|
+
svi: Optional[int] = None
|
|
18
|
+
subif: Optional[int] = None
|
|
19
|
+
vrf: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
def __post_init__(self):
|
|
22
|
+
if self.lag is not None and self.svi is not None:
|
|
23
|
+
raise ValueError("Cannot use LAG and SVI together")
|
|
24
|
+
if self.svi is not None and self.subif is not None:
|
|
25
|
+
raise ValueError("Cannot use Subif and SVI together")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ObjMapping:
|
|
29
|
+
def __init__(self, obj):
|
|
30
|
+
self.obj = obj
|
|
31
|
+
|
|
32
|
+
def __contains__(self, item):
|
|
33
|
+
return hasattr(self.obj, item)
|
|
34
|
+
|
|
35
|
+
def __getitem__(self, name):
|
|
36
|
+
return getattr(self.obj, name)
|
|
37
|
+
|
|
38
|
+
def get(self, name, default=None):
|
|
39
|
+
return getattr(self.obj, name, default)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
retort = Retort(
|
|
43
|
+
recipe=[
|
|
44
|
+
loader(InterfaceChanges, ObjMapping, Chain.FIRST),
|
|
45
|
+
loader(ASN, ASN),
|
|
46
|
+
loader(GlobalOptions, ObjMapping, Chain.FIRST),
|
|
47
|
+
loader(VrfOptions, ObjMapping, Chain.FIRST),
|
|
48
|
+
loader(FamilyOptions, ObjMapping, Chain.FIRST),
|
|
49
|
+
loader(PeerOptions, ObjMapping, Chain.FIRST),
|
|
50
|
+
name_mapping(PeerOptions, map={
|
|
51
|
+
"local_as": "asnum",
|
|
52
|
+
}),
|
|
53
|
+
loader(list[PeerGroup], lambda x: list(x.values()), Chain.FIRST),
|
|
54
|
+
loader(PeerGroup, ObjMapping, Chain.FIRST),
|
|
55
|
+
name_mapping(PeerGroup, map={
|
|
56
|
+
"local_as": "asnum",
|
|
57
|
+
}),
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
to_bgp_global_options = retort.get_loader(GlobalOptions)
|
|
62
|
+
to_interface_changes = retort.get_loader(InterfaceChanges)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def to_bgp_peer(local: PeerDTO, connected: PeerDTO, connected_device: Device, interface: Optional[str]) -> Peer:
|
|
66
|
+
options = retort.load(local, PeerOptions)
|
|
67
|
+
# TODO validate `lagg_links` before conversion
|
|
68
|
+
result = Peer(
|
|
69
|
+
addr=str(ip_interface(connected.addr).ip),
|
|
70
|
+
interface=interface,
|
|
71
|
+
remote_as=ASN(connected.asnum),
|
|
72
|
+
hostname=connected_device.hostname,
|
|
73
|
+
options=options,
|
|
74
|
+
)
|
|
75
|
+
# connected
|
|
76
|
+
result.vrf_name = getattr(connected, "vrf", result.vrf_name)
|
|
77
|
+
result.group_name = getattr(connected, "group_name", result.group_name)
|
|
78
|
+
result.description = getattr(connected, "description", result.description)
|
|
79
|
+
result.families = getattr(connected, "families", result.families)
|
|
80
|
+
# local
|
|
81
|
+
result.import_policy = getattr(connected, "import_policy", result.import_policy)
|
|
82
|
+
result.export_policy = getattr(connected, "export_policy", result.export_policy)
|
|
83
|
+
result.update_source = getattr(connected, "update_source", result.update_source)
|
|
84
|
+
return result
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from typing import Literal, Annotated, Union, Optional
|
|
2
|
+
|
|
3
|
+
from .basemodel import BaseMeshModel, Concat
|
|
4
|
+
from ..bgp_models import BFDTimers
|
|
5
|
+
|
|
6
|
+
FamilyName = Literal["ipv4_unicast", "ipv6_unicast", "ipv4_labeled", "ipv6_labeled"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _SharedOptionsDTO(BaseMeshModel):
|
|
10
|
+
"""
|
|
11
|
+
Options which can be set on connected pair or group of peers
|
|
12
|
+
"""
|
|
13
|
+
add_path: bool
|
|
14
|
+
multipath: bool
|
|
15
|
+
advertise_irb: bool
|
|
16
|
+
send_labeled: bool
|
|
17
|
+
send_community: bool
|
|
18
|
+
bfd: bool
|
|
19
|
+
bfd_timers: BFDTimers
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MeshSession(_SharedOptionsDTO):
|
|
23
|
+
"""
|
|
24
|
+
Options which are set on connected pair
|
|
25
|
+
"""
|
|
26
|
+
asnum: Union[int, str]
|
|
27
|
+
vrf: str
|
|
28
|
+
families: Annotated[set[FamilyName], Concat()]
|
|
29
|
+
group_name: str
|
|
30
|
+
|
|
31
|
+
bmp_monitor: bool
|
|
32
|
+
|
|
33
|
+
import_policy: str
|
|
34
|
+
export_policy: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _OptionsDTO(_SharedOptionsDTO):
|
|
38
|
+
"""
|
|
39
|
+
Options which can be set on group of peers or peer itself
|
|
40
|
+
"""
|
|
41
|
+
unnumbered: bool
|
|
42
|
+
rr_client: bool
|
|
43
|
+
next_hop_self: bool
|
|
44
|
+
extended_next_hop: bool
|
|
45
|
+
send_lcommunity: bool
|
|
46
|
+
send_extcommunity: bool
|
|
47
|
+
import_limit: bool
|
|
48
|
+
teardown_timeout: bool
|
|
49
|
+
redistribute: bool
|
|
50
|
+
passive: bool
|
|
51
|
+
mtu_discovery: bool
|
|
52
|
+
advertise_inactive: bool
|
|
53
|
+
advertise_bgp_static: bool
|
|
54
|
+
allowas_in: bool
|
|
55
|
+
auth_key: bool
|
|
56
|
+
multihop: bool
|
|
57
|
+
multihop_no_nexthop_change: bool
|
|
58
|
+
af_no_install: bool
|
|
59
|
+
rib: bool
|
|
60
|
+
resolve_vpn: bool
|
|
61
|
+
af_rib_group: Optional[str]
|
|
62
|
+
af_loops: int
|
|
63
|
+
hold_time: int
|
|
64
|
+
listen_network: bool
|
|
65
|
+
remove_private: bool
|
|
66
|
+
as_override: bool
|
|
67
|
+
aigp: bool
|
|
68
|
+
no_prepend: bool
|
|
69
|
+
no_explicit_null: bool
|
|
70
|
+
uniq_iface: bool
|
|
71
|
+
advertise_peer_as: bool
|
|
72
|
+
connect_retry: bool
|
|
73
|
+
advertise_external: bool
|
|
74
|
+
listen_only: bool
|
|
75
|
+
soft_reconfiguration_inbound: bool
|
|
76
|
+
not_active: bool
|
|
77
|
+
mtu: int
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class PeerDTO(MeshSession, _OptionsDTO):
|
|
81
|
+
pod: int
|
|
82
|
+
addr: str
|
|
83
|
+
description: str
|
|
84
|
+
|
|
85
|
+
subif: int
|
|
86
|
+
lag: Optional[int]
|
|
87
|
+
lag_links_min: Optional[int]
|
|
88
|
+
svi: Optional[int]
|
|
89
|
+
|
|
90
|
+
group_name: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MeshPeerGroup(_OptionsDTO):
|
|
94
|
+
name: str
|
|
95
|
+
remote_as: Union[int, str]
|
|
96
|
+
internal_name: str
|
|
97
|
+
update_source: str
|
|
98
|
+
description: str
|