annet 0.16.26__py3-none-any.whl → 0.16.28__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/file/provider.py +28 -10
- annet/adapters/netbox/v37/storage.py +1 -1
- annet/annlib/netdev/devdb/data/devdb.json +3 -2
- annet/bgp_models.py +28 -0
- annet/implicit.py +23 -11
- annet/mesh/__init__.py +4 -0
- annet/mesh/basemodel.py +5 -0
- annet/mesh/device_models.py +2 -0
- annet/mesh/executor.py +90 -66
- annet/mesh/peer_models.py +3 -3
- annet/mesh/port_processor.py +18 -0
- annet/mesh/registry.py +12 -4
- annet/rpl/match_builder.py +30 -9
- annet/rpl/routemap.py +5 -3
- annet/rpl/statement_builder.py +31 -7
- annet/rpl_generators/__init__.py +24 -0
- annet/rpl_generators/aspath.py +57 -0
- annet/rpl_generators/community.py +242 -0
- annet/rpl_generators/cumulus_frr.py +458 -0
- annet/rpl_generators/entities.py +70 -0
- annet/rpl_generators/execute.py +12 -0
- annet/rpl_generators/policy.py +676 -0
- annet/rpl_generators/prefix_lists.py +158 -0
- annet/rpl_generators/rd.py +40 -0
- {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/METADATA +2 -2
- {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/RECORD +36 -25
- annet_generators/rpl_example/__init__.py +3 -5
- annet_generators/rpl_example/generator.py +127 -0
- annet_generators/rpl_example/items.py +21 -31
- annet_generators/rpl_example/mesh.py +9 -0
- annet_generators/rpl_example/route_policy.py +43 -9
- annet_generators/rpl_example/policy_generator.py +0 -233
- {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/AUTHORS +0 -0
- {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/LICENSE +0 -0
- {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/WHEEL +0 -0
- {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/entry_points.txt +0 -0
- {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/top_level.txt +0 -0
annet/adapters/file/provider.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from annet.annlib.netdev.views.dump import DumpableView
|
|
2
2
|
from annet.storage import Query
|
|
3
3
|
from dataclasses import dataclass, fields
|
|
4
|
-
from typing import List, Iterable, Optional, Any
|
|
4
|
+
from typing import List, Iterable, Optional, Any, Sequence
|
|
5
5
|
from annet.storage import StorageProvider, Storage
|
|
6
6
|
from annet.connectors import AdapterWithName
|
|
7
7
|
from annet.storage import Device as DeviceCls
|
|
@@ -56,6 +56,15 @@ class DeviceStorage:
|
|
|
56
56
|
class Device(DeviceCls, DumpableView):
|
|
57
57
|
dev: DeviceStorage
|
|
58
58
|
|
|
59
|
+
def __hash__(self):
|
|
60
|
+
return hash((self.id, type(self)))
|
|
61
|
+
|
|
62
|
+
def __eq__(self, other):
|
|
63
|
+
return type(self) is type(other) and self.fqdn == other.fqdn and self.vendor == other.vendor
|
|
64
|
+
|
|
65
|
+
def is_pc(self) -> bool:
|
|
66
|
+
return False
|
|
67
|
+
|
|
59
68
|
@property
|
|
60
69
|
def hostname(self) -> str:
|
|
61
70
|
return self.dev.hostname
|
|
@@ -68,15 +77,6 @@ class Device(DeviceCls, DumpableView):
|
|
|
68
77
|
def id(self):
|
|
69
78
|
return self.dev.id
|
|
70
79
|
|
|
71
|
-
def __hash__(self):
|
|
72
|
-
return hash((self.id, type(self)))
|
|
73
|
-
|
|
74
|
-
def __eq__(self, other):
|
|
75
|
-
return type(self) is type(other) and self.fqdn == other.fqdn and self.vendor == other.vendor
|
|
76
|
-
|
|
77
|
-
def is_pc(self) -> bool:
|
|
78
|
-
return False
|
|
79
|
-
|
|
80
80
|
@property
|
|
81
81
|
def storage(self) -> Storage:
|
|
82
82
|
return self
|
|
@@ -93,6 +93,18 @@ class Device(DeviceCls, DumpableView):
|
|
|
93
93
|
def neighbours_ids(self):
|
|
94
94
|
pass
|
|
95
95
|
|
|
96
|
+
def make_lag(self, lag: int, ports: Sequence[str], lag_min_links: Optional[int]) -> Interface:
|
|
97
|
+
raise NotImplementedError
|
|
98
|
+
|
|
99
|
+
def add_svi(self, svi: int) -> Interface:
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
|
|
102
|
+
def add_subif(self, interface: str, subif: int) -> Interface:
|
|
103
|
+
raise NotImplementedError
|
|
104
|
+
|
|
105
|
+
def neighbours_fqdns(self) -> list[str]:
|
|
106
|
+
return []
|
|
107
|
+
|
|
96
108
|
|
|
97
109
|
@dataclass
|
|
98
110
|
class Devices:
|
|
@@ -199,6 +211,12 @@ class FS(Storage):
|
|
|
199
211
|
def flush_perf(self):
|
|
200
212
|
pass
|
|
201
213
|
|
|
214
|
+
def resolve_all_fdnds(self) -> list[str]:
|
|
215
|
+
return [d.fqdn for d in self.inventory.devices]
|
|
216
|
+
|
|
217
|
+
def search_connections(self, device: "Device", neighbor: "Device") -> list[tuple["Interface", "Interface"]]:
|
|
218
|
+
return []
|
|
219
|
+
|
|
202
220
|
|
|
203
221
|
def filter_query(devices: list[Device], query: Query) -> list[Device]:
|
|
204
222
|
result: list[Device] = []
|
|
@@ -104,9 +104,9 @@
|
|
|
104
104
|
"Nokia.NS7750": " 7750",
|
|
105
105
|
"Nokia.SR_1s": "SR-1s",
|
|
106
106
|
|
|
107
|
-
"PC": "^(PC|pc|[Mm]ellanox SN|[Ee]dge-?[Cc]ore|[Mm]oxa|[Aa]sterfusion CX|[Uu]fi[Ss]pace)",
|
|
107
|
+
"PC": "^(PC|pc|[Mm]ellanox SN|[Ee]dge-?[Cc]ore|[Mm]oxa|NVIDIA|[Aa]sterfusion CX|[Uu]fi[Ss]pace)",
|
|
108
108
|
|
|
109
|
-
"PC.Whitebox": "([Mm]ellanox SN|[Ee]dge-?[Cc]ore|[Aa]sterfusion CX|[Uu]fi[Ss]pace)",
|
|
109
|
+
"PC.Whitebox": "([Mm]ellanox SN|[Ee]dge-?[Cc]ore|NVIDIA|[Aa]sterfusion CX|[Uu]fi[Ss]pace)",
|
|
110
110
|
"PC.Whitebox.Mellanox": "[Mm]ellanox",
|
|
111
111
|
"PC.Whitebox.Mellanox.SN": " SN",
|
|
112
112
|
"PC.Whitebox.Mellanox.SN.SN2100": " SN2100",
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
"PC.Whitebox.Edgecore.AS9736": "AS9736",
|
|
119
119
|
"PC.Whitebox.NVIDIA": "NVIDIA",
|
|
120
120
|
"PC.Whitebox.NVIDIA.SN": " SN",
|
|
121
|
+
"PC.Whitebox.NVIDIA.SN.SN5400": " SN5400",
|
|
121
122
|
"PC.Whitebox.NVIDIA.SN.SN5600": " SN5600",
|
|
122
123
|
"PC.Moxa": "[Mm]oxa",
|
|
123
124
|
"PC.Moxa.NPort": " [Nn][Pp]ort",
|
annet/bgp_models.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections.abc import Sequence, Iterable
|
|
1
2
|
from dataclasses import dataclass, field
|
|
2
3
|
from typing import Literal, Union, Optional
|
|
3
4
|
|
|
@@ -244,6 +245,7 @@ class VrfOptions:
|
|
|
244
245
|
ipv6_labeled_unicast: FamilyOptions
|
|
245
246
|
|
|
246
247
|
vrf_name_global: Optional[str] = None
|
|
248
|
+
as_path_relax: bool = False
|
|
247
249
|
rt_import: list[str] = field(default_factory=list)
|
|
248
250
|
rt_export: list[str] = field(default_factory=list)
|
|
249
251
|
rt_import_v4: list[str] = field(default_factory=list)
|
|
@@ -261,6 +263,7 @@ class GlobalOptions:
|
|
|
261
263
|
ipv4_labeled_unicast: FamilyOptions
|
|
262
264
|
ipv6_labeled_unicast: FamilyOptions
|
|
263
265
|
|
|
266
|
+
as_path_relax: bool = False
|
|
264
267
|
local_as: ASN = ASN(None)
|
|
265
268
|
loops: int = 0
|
|
266
269
|
multipath: int = 0
|
|
@@ -268,3 +271,28 @@ class GlobalOptions:
|
|
|
268
271
|
vrf: dict[str, VrfOptions] = field(default_factory=dict)
|
|
269
272
|
|
|
270
273
|
groups: list[PeerGroup] = field(default_factory=list)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@dataclass
|
|
277
|
+
class BgpConfig:
|
|
278
|
+
global_options: GlobalOptions
|
|
279
|
+
peers: list[Peer]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _used_policies(peer: Union[Peer, PeerGroup]) -> Iterable[str]:
|
|
283
|
+
if peer.import_policy:
|
|
284
|
+
yield peer.import_policy
|
|
285
|
+
if peer.export_policy:
|
|
286
|
+
yield peer.export_policy
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def extract_policies(config: BgpConfig) -> Sequence[str]:
|
|
290
|
+
result: list[str] = []
|
|
291
|
+
for vrf in config.global_options.vrf.values():
|
|
292
|
+
for group in vrf.groups:
|
|
293
|
+
result.extend(_used_policies(group))
|
|
294
|
+
for group in config.global_options.groups:
|
|
295
|
+
result.extend(_used_policies(group))
|
|
296
|
+
for peer in config.peers:
|
|
297
|
+
result.extend(_used_policies(peer))
|
|
298
|
+
return result
|
annet/implicit.py
CHANGED
|
@@ -133,17 +133,29 @@ def _implicit_tree(device):
|
|
|
133
133
|
no shutdown
|
|
134
134
|
"""
|
|
135
135
|
elif device.hw.Cisco:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
136
|
+
# C2900/C3500/C3600 does not support the MTU on a per-interface basis
|
|
137
|
+
if device.hw.Cisco.Catalyst.C2900 or device.hw.Cisco.Catalyst.C3500 \
|
|
138
|
+
or device.hw.Cisco.Catalyst.C3600:
|
|
139
|
+
text += r"""
|
|
140
|
+
!interface */\S*Ethernet\S+/
|
|
141
|
+
no shutdown
|
|
142
|
+
!interface */Loopback[0-9.]+/
|
|
143
|
+
no shutdown
|
|
144
|
+
!interface */port-channel[0-9.]+/
|
|
145
|
+
no shutdown
|
|
146
|
+
"""
|
|
147
|
+
else:
|
|
148
|
+
text += r"""
|
|
149
|
+
!interface */\S*Ethernet\S+/
|
|
150
|
+
mtu 1500
|
|
151
|
+
no shutdown
|
|
152
|
+
!interface */Loopback[0-9.]+/
|
|
153
|
+
mtu 1500
|
|
154
|
+
no shutdown
|
|
155
|
+
!interface */port-channel[0-9.]+/
|
|
156
|
+
mtu 1500
|
|
157
|
+
no shutdown
|
|
158
|
+
"""
|
|
147
159
|
if device.hw.Cisco.Catalyst:
|
|
148
160
|
# this configuration is not visible in running-config when enabled
|
|
149
161
|
text += r"""
|
annet/mesh/__init__.py
CHANGED
|
@@ -10,6 +10,9 @@ __all__ = [
|
|
|
10
10
|
"Match",
|
|
11
11
|
"VirtualLocal",
|
|
12
12
|
"VirtualPeer",
|
|
13
|
+
"PortProcessor",
|
|
14
|
+
"separate_ports",
|
|
15
|
+
"united_ports"
|
|
13
16
|
]
|
|
14
17
|
|
|
15
18
|
from .executor import MeshExecutor
|
|
@@ -17,3 +20,4 @@ from .match_args import Left, Right, Match
|
|
|
17
20
|
from .registry import (
|
|
18
21
|
DirectPeer, IndirectPeer, MeshSession, GlobalOptions, MeshRulesRegistry, VirtualLocal, VirtualPeer,
|
|
19
22
|
)
|
|
23
|
+
from .port_processor import PortProcessor, united_ports, separate_ports
|
annet/mesh/basemodel.py
CHANGED
|
@@ -60,6 +60,11 @@ class Concat(Merger):
|
|
|
60
60
|
return x + y # type: ignore[operator]
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
class Unite(Merger):
|
|
64
|
+
def _merge(self, name: str, x: T, y: T) -> T:
|
|
65
|
+
return x | y # type: ignore[operator]
|
|
66
|
+
|
|
67
|
+
|
|
63
68
|
class Merge(Merger):
|
|
64
69
|
def _merge(self, name: str, x: "ModelT", y: "ModelT") -> "ModelT": # type: ignore[override]
|
|
65
70
|
return merge(x, y)
|
annet/mesh/device_models.py
CHANGED
|
@@ -61,6 +61,7 @@ class VrfOptions(_FamiliesMixin, BaseMeshModel):
|
|
|
61
61
|
|
|
62
62
|
vrf_name: str
|
|
63
63
|
vrf_name_global: Optional[str]
|
|
64
|
+
as_path_relax: bool
|
|
64
65
|
import_policy: Optional[str]
|
|
65
66
|
export_policy: Optional[str]
|
|
66
67
|
rt_import: Annotated[tuple[str, ...], Concat()]
|
|
@@ -78,6 +79,7 @@ class GlobalOptionsDTO(_FamiliesMixin, BaseMeshModel):
|
|
|
78
79
|
kwargs.setdefault("vrf", KeyDefaultDict(lambda x: VrfOptions(vrf_name=x)))
|
|
79
80
|
super().__init__(**kwargs)
|
|
80
81
|
|
|
82
|
+
as_path_relax: bool
|
|
81
83
|
local_as: Union[int, str]
|
|
82
84
|
loops: int
|
|
83
85
|
multipath: int
|
annet/mesh/executor.py
CHANGED
|
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from logging import getLogger
|
|
3
3
|
from typing import Annotated, Callable, Optional, Union
|
|
4
4
|
|
|
5
|
-
from annet.bgp_models import Peer, GlobalOptions
|
|
5
|
+
from annet.bgp_models import Peer, GlobalOptions, BgpConfig
|
|
6
6
|
from annet.storage import Device, Storage
|
|
7
7
|
from .basemodel import merge, BaseMeshModel, Merge, UseLast, MergeForbiddenError
|
|
8
8
|
from .device_models import GlobalOptionsDTO
|
|
@@ -12,6 +12,7 @@ from .registry import (
|
|
|
12
12
|
DirectPeer,
|
|
13
13
|
GlobalOptions as MeshGlobalOptions,
|
|
14
14
|
IndirectPeer,
|
|
15
|
+
MatchedDirectPair,
|
|
15
16
|
MeshRulesRegistry,
|
|
16
17
|
MeshSession,
|
|
17
18
|
VirtualLocal,
|
|
@@ -21,12 +22,6 @@ from .registry import (
|
|
|
21
22
|
logger = getLogger(__name__)
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
@dataclass
|
|
25
|
-
class MeshExecutionResult:
|
|
26
|
-
global_options: GlobalOptions
|
|
27
|
-
peers: list[Peer]
|
|
28
|
-
|
|
29
|
-
|
|
30
25
|
@dataclass(frozen=True)
|
|
31
26
|
class PeerKey:
|
|
32
27
|
fqdn: str
|
|
@@ -38,6 +33,7 @@ class Pair(BaseMeshModel):
|
|
|
38
33
|
local: Annotated[Union[DirectPeerDTO, IndirectPeerDTO], Merge()]
|
|
39
34
|
connected: Annotated[Union[DirectPeerDTO, IndirectPeerDTO], Merge()]
|
|
40
35
|
device: Annotated[Device, UseLast()]
|
|
36
|
+
ports: list[str]
|
|
41
37
|
|
|
42
38
|
|
|
43
39
|
class VirtualPair(BaseMeshModel):
|
|
@@ -76,72 +72,92 @@ class MeshExecutor:
|
|
|
76
72
|
except AttributeError:
|
|
77
73
|
return str(handler)
|
|
78
74
|
|
|
75
|
+
def _execute_direct_pair(
|
|
76
|
+
self,
|
|
77
|
+
device: Device,
|
|
78
|
+
neighbor_device: Device,
|
|
79
|
+
rule: MatchedDirectPair,
|
|
80
|
+
ports: list[tuple[str, str]],
|
|
81
|
+
all_connected_ports: list[tuple[str, str]],
|
|
82
|
+
) -> Pair:
|
|
83
|
+
session = MeshSession()
|
|
84
|
+
handler_name = self._handler_name(rule.handler)
|
|
85
|
+
logger.debug("Running direct handler: %s", handler_name)
|
|
86
|
+
if rule.direct_order:
|
|
87
|
+
peer_device = DirectPeer(rule.match_left, device, [], [])
|
|
88
|
+
peer_neighbor = DirectPeer(rule.match_right, neighbor_device, [], [])
|
|
89
|
+
else:
|
|
90
|
+
peer_neighbor = DirectPeer(rule.match_left, neighbor_device, [], [])
|
|
91
|
+
peer_device = DirectPeer(rule.match_right, device, [], [])
|
|
92
|
+
|
|
93
|
+
for local_port, remote_port in ports:
|
|
94
|
+
peer_device.ports.append(local_port)
|
|
95
|
+
peer_neighbor.ports.append(remote_port)
|
|
96
|
+
for local_port, remote_port in all_connected_ports:
|
|
97
|
+
peer_device.all_connected_ports.append(local_port)
|
|
98
|
+
peer_neighbor.all_connected_ports.append(remote_port)
|
|
99
|
+
|
|
100
|
+
if rule.direct_order:
|
|
101
|
+
rule.handler(peer_device, peer_neighbor, session)
|
|
102
|
+
else:
|
|
103
|
+
rule.handler(peer_neighbor, peer_device, session)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
neighbor_dto = merge(DirectPeerDTO(), peer_neighbor, session)
|
|
107
|
+
except MergeForbiddenError as e:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Handler `{handler_name}` provided session data conflicting with "
|
|
110
|
+
f"peer data for device `{neighbor_device.fqdn}`:\n" + str(e)
|
|
111
|
+
) from e
|
|
112
|
+
try:
|
|
113
|
+
device_dto = merge(DirectPeerDTO(), peer_device, session)
|
|
114
|
+
except MergeForbiddenError as e:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"Handler `{handler_name}` provided session data conflicting with "
|
|
117
|
+
f"peer data for device `{device.fqdn}`:\n" + str(e)
|
|
118
|
+
) from e
|
|
119
|
+
|
|
120
|
+
return Pair(local=device_dto, connected=neighbor_dto, device=neighbor_device, ports=peer_device.ports)
|
|
121
|
+
|
|
79
122
|
def _execute_direct(self, device: Device) -> list[Pair]:
|
|
80
123
|
# we can have multiple rules for the same pair
|
|
81
124
|
# we merge them according to remote fqdn
|
|
82
125
|
neighbor_peers: dict[PeerKey, Pair] = {}
|
|
83
126
|
# TODO batch resolve
|
|
84
127
|
for rule in self._registry.lookup_direct(device.fqdn, device.neighbours_fqdns):
|
|
85
|
-
session = MeshSession()
|
|
86
128
|
handler_name = self._handler_name(rule.handler)
|
|
87
|
-
logger.debug("Running direct handler: %s", handler_name)
|
|
88
129
|
if rule.direct_order:
|
|
89
130
|
neighbor_device = self._storage.make_devices([rule.name_right])[0]
|
|
90
|
-
peer_device = DirectPeer(rule.match_left, device, [])
|
|
91
|
-
peer_neighbor = DirectPeer(rule.match_right, neighbor_device, [])
|
|
92
131
|
else:
|
|
93
132
|
neighbor_device = self._storage.make_devices([rule.name_left])[0]
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
pair = Pair(local=device_dto, connected=neighbor_dto, device=neighbor_device)
|
|
123
|
-
addr = getattr(neighbor_dto, "addr", None)
|
|
124
|
-
if addr is None:
|
|
125
|
-
raise ValueError(f"Handler `{handler_name}` returned no peer addr")
|
|
126
|
-
peer_key = PeerKey(
|
|
127
|
-
fqdn=neighbor_device.fqdn,
|
|
128
|
-
addr=addr,
|
|
129
|
-
vrf=getattr(neighbor_dto, "vrf", "")
|
|
130
|
-
)
|
|
131
|
-
try:
|
|
132
|
-
if peer_key in neighbor_peers:
|
|
133
|
-
pair = merge(neighbor_peers[peer_key], pair)
|
|
134
|
-
except MergeForbiddenError as e:
|
|
135
|
-
if rule.direct_order:
|
|
136
|
-
pair_names = device.fqdn, neighbor_device.fqdn
|
|
137
|
-
else:
|
|
138
|
-
pair_names = neighbor_device.fqdn, device.fqdn
|
|
139
|
-
raise ValueError(
|
|
140
|
-
f"Handler `{handler_name}` provides data conflicting with "
|
|
141
|
-
f"previously loaded for device pair {pair_names} "
|
|
142
|
-
f"with addr={peer_key.addr}, vrf{peer_key.vrf}:\n" + str(e)
|
|
143
|
-
) from e
|
|
144
|
-
neighbor_peers[peer_key] = pair
|
|
133
|
+
all_connected_ports = [
|
|
134
|
+
(p1.name, p2.name)
|
|
135
|
+
for p1, p2 in self._storage.search_connections(device, neighbor_device)
|
|
136
|
+
]
|
|
137
|
+
for ports in rule.port_processor(all_connected_ports):
|
|
138
|
+
pair = self._execute_direct_pair(device, neighbor_device, rule, ports, all_connected_ports)
|
|
139
|
+
addr = getattr(pair.connected, "addr", None)
|
|
140
|
+
if addr is None:
|
|
141
|
+
raise ValueError(f"Handler `{handler_name}` returned no peer addr")
|
|
142
|
+
peer_key = PeerKey(
|
|
143
|
+
fqdn=pair.device.fqdn,
|
|
144
|
+
addr=addr,
|
|
145
|
+
vrf=getattr(pair.connected, "vrf", "")
|
|
146
|
+
)
|
|
147
|
+
try:
|
|
148
|
+
if peer_key in neighbor_peers:
|
|
149
|
+
pair = merge(neighbor_peers[peer_key], pair)
|
|
150
|
+
except MergeForbiddenError as e:
|
|
151
|
+
if rule.direct_order:
|
|
152
|
+
pair_names = device.fqdn, pair.device.fqdn
|
|
153
|
+
else:
|
|
154
|
+
pair_names = pair.device.fqdn, device.fqdn
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Handler `{handler_name}` provides data conflicting with "
|
|
157
|
+
f"previously loaded for device pair {pair_names} "
|
|
158
|
+
f"with addr={peer_key.addr}, vrf{peer_key.vrf}:\n" + str(e)
|
|
159
|
+
) from e
|
|
160
|
+
neighbor_peers[peer_key] = pair
|
|
145
161
|
return list(neighbor_peers.values())
|
|
146
162
|
|
|
147
163
|
def _execute_virtual(self, device: Device) -> list[VirtualPair]:
|
|
@@ -249,8 +265,15 @@ class MeshExecutor:
|
|
|
249
265
|
def _to_bgp_global(self, global_options: GlobalOptionsDTO) -> GlobalOptions:
|
|
250
266
|
return to_bgp_global_options(global_options)
|
|
251
267
|
|
|
252
|
-
def _apply_interface_changes(
|
|
253
|
-
|
|
268
|
+
def _apply_interface_changes(
|
|
269
|
+
self, device: Device, neighbor: Device, ports: list[str], changes: InterfaceChanges,
|
|
270
|
+
) -> str:
|
|
271
|
+
# filter ports according to processed in pair
|
|
272
|
+
port_pairs = [
|
|
273
|
+
p
|
|
274
|
+
for p in self._storage.search_connections(device, neighbor)
|
|
275
|
+
if p[0].name in ports
|
|
276
|
+
]
|
|
254
277
|
if len(port_pairs) > 1:
|
|
255
278
|
if changes.lag is changes.svi is None:
|
|
256
279
|
raise ValueError(
|
|
@@ -279,7 +302,7 @@ class MeshExecutor:
|
|
|
279
302
|
def _apply_virtual_interface_changes(self, device: Device, local: VirtualLocalDTO) -> str:
|
|
280
303
|
return device.add_svi(local.svi).name # we check if SVI configured in execute method
|
|
281
304
|
|
|
282
|
-
def execute_for(self, device: Device) ->
|
|
305
|
+
def execute_for(self, device: Device) -> BgpConfig:
|
|
283
306
|
all_fqdns = self._storage.resolve_all_fdnds()
|
|
284
307
|
|
|
285
308
|
global_options = self._to_bgp_global(self._execute_globals(device))
|
|
@@ -289,6 +312,7 @@ class MeshExecutor:
|
|
|
289
312
|
target_interface = self._apply_interface_changes(
|
|
290
313
|
device,
|
|
291
314
|
direct_pair.device,
|
|
315
|
+
direct_pair.ports,
|
|
292
316
|
to_interface_changes(direct_pair.local),
|
|
293
317
|
)
|
|
294
318
|
peers.append(self._to_bgp_peer(direct_pair, target_interface))
|
|
@@ -303,7 +327,7 @@ class MeshExecutor:
|
|
|
303
327
|
for connected_pair in self._execute_indirect(device, all_fqdns):
|
|
304
328
|
peers.append(self._to_bgp_peer(connected_pair, None))
|
|
305
329
|
|
|
306
|
-
return
|
|
330
|
+
return BgpConfig(
|
|
307
331
|
global_options=global_options,
|
|
308
332
|
peers=peers,
|
|
309
333
|
)
|
annet/mesh/peer_models.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Literal, Annotated, Union, Optional
|
|
2
2
|
|
|
3
|
-
from .basemodel import BaseMeshModel, Concat
|
|
3
|
+
from .basemodel import BaseMeshModel, Concat, Unite
|
|
4
4
|
from ..bgp_models import BFDTimers
|
|
5
5
|
|
|
6
6
|
FamilyName = Literal["ipv4_unicast", "ipv6_unicast", "ipv4_labeled_unicast", "ipv6_labeled_unicast"]
|
|
@@ -25,7 +25,7 @@ class MeshSession(_SharedOptionsDTO):
|
|
|
25
25
|
"""
|
|
26
26
|
asnum: Union[int, str]
|
|
27
27
|
vrf: str
|
|
28
|
-
families: Annotated[set[FamilyName],
|
|
28
|
+
families: Annotated[set[FamilyName], Unite()]
|
|
29
29
|
group_name: str
|
|
30
30
|
|
|
31
31
|
bmp_monitor: bool
|
|
@@ -116,7 +116,7 @@ class VirtualPeerDTO(MeshSession):
|
|
|
116
116
|
|
|
117
117
|
class MeshPeerGroup(_OptionsDTO):
|
|
118
118
|
name: str
|
|
119
|
-
families: Annotated[set[FamilyName],
|
|
119
|
+
families: Annotated[set[FamilyName], Unite()]
|
|
120
120
|
local_as: Union[int, str]
|
|
121
121
|
remote_as: Union[int, str]
|
|
122
122
|
internal_name: str
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import Protocol, Sequence
|
|
3
|
+
|
|
4
|
+
PortPair = tuple[str, str]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PortProcessor(Protocol):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def __call__(self, pairs: Sequence[PortPair]) -> Sequence[list[PortPair]]:
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def united_ports(pairs: Sequence[PortPair]) -> Sequence[list[PortPair]]:
|
|
14
|
+
return [list(pairs)]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def separate_ports(pairs: Sequence[PortPair]) -> Sequence[list[PortPair]]:
|
|
18
|
+
return [[pair] for pair in pairs]
|
annet/mesh/registry.py
CHANGED
|
@@ -5,18 +5,21 @@ from .match_args import MatchExpr, PairMatcher, SingleMatcher
|
|
|
5
5
|
from .match_args import MatchedArgs
|
|
6
6
|
from .device_models import GlobalOptionsDTO
|
|
7
7
|
from .peer_models import MeshSession, IndirectPeerDTO, VirtualLocalDTO, VirtualPeerDTO, DirectPeerDTO
|
|
8
|
+
from .port_processor import PortProcessor, united_ports
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class DirectPeer(DirectPeerDTO):
|
|
11
12
|
match: MatchedArgs
|
|
12
13
|
device: Any
|
|
13
14
|
ports: list[str]
|
|
15
|
+
all_connected_ports: list[str]
|
|
14
16
|
|
|
15
|
-
def __init__(self, match: MatchedArgs, device: Any, ports: list[str]) -> None:
|
|
17
|
+
def __init__(self, match: MatchedArgs, device: Any, ports: list[str], all_connected_ports: list[str]) -> None:
|
|
16
18
|
super().__init__()
|
|
17
19
|
self.match = match
|
|
18
20
|
self.device = device
|
|
19
21
|
self.ports = ports
|
|
22
|
+
self.all_connected_ports = all_connected_ports
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
class IndirectPeer(IndirectPeerDTO):
|
|
@@ -69,9 +72,10 @@ VirtualHandler = Callable[[VirtualLocal, VirtualPeer, MeshSession], None]
|
|
|
69
72
|
|
|
70
73
|
@dataclass
|
|
71
74
|
class DirectRule:
|
|
72
|
-
__slots__ = ("matcher", "handler")
|
|
75
|
+
__slots__ = ("matcher", "handler", "port_processor")
|
|
73
76
|
matcher: PairMatcher
|
|
74
77
|
handler: DirectHandler
|
|
78
|
+
port_processor: PortProcessor
|
|
75
79
|
|
|
76
80
|
|
|
77
81
|
@dataclass
|
|
@@ -98,8 +102,9 @@ class MatchedGlobal:
|
|
|
98
102
|
|
|
99
103
|
@dataclass
|
|
100
104
|
class MatchedDirectPair:
|
|
101
|
-
__slots__ = ("handler", "direct_order", "name_left", "name_right", "match_left", "match_right")
|
|
105
|
+
__slots__ = ("handler", "port_processor", "direct_order", "name_left", "name_right", "match_left", "match_right")
|
|
102
106
|
handler: DirectHandler
|
|
107
|
+
port_processor: PortProcessor
|
|
103
108
|
direct_order: bool
|
|
104
109
|
name_left: str
|
|
105
110
|
name_right: str
|
|
@@ -154,11 +159,12 @@ class MeshRulesRegistry:
|
|
|
154
159
|
|
|
155
160
|
def direct(
|
|
156
161
|
self, left_mask: str, right_mask: str, *match: MatchExpr,
|
|
162
|
+
port_processor: PortProcessor = united_ports,
|
|
157
163
|
) -> Callable[[DirectHandler], DirectHandler]:
|
|
158
164
|
matcher = PairMatcher(left_mask, right_mask, match)
|
|
159
165
|
|
|
160
166
|
def register(handler: DirectHandler) -> DirectHandler:
|
|
161
|
-
self.direct_rules.append(DirectRule(matcher, handler))
|
|
167
|
+
self.direct_rules.append(DirectRule(matcher, handler, port_processor))
|
|
162
168
|
return handler
|
|
163
169
|
|
|
164
170
|
return register
|
|
@@ -194,6 +200,7 @@ class MeshRulesRegistry:
|
|
|
194
200
|
if args := rule.matcher.match_pair(device, neighbor):
|
|
195
201
|
found.append(MatchedDirectPair(
|
|
196
202
|
handler=rule.handler,
|
|
203
|
+
port_processor=rule.port_processor,
|
|
197
204
|
direct_order=True,
|
|
198
205
|
name_left=device,
|
|
199
206
|
name_right=neighbor,
|
|
@@ -203,6 +210,7 @@ class MeshRulesRegistry:
|
|
|
203
210
|
if args := rule.matcher.match_pair(neighbor, device):
|
|
204
211
|
found.append(MatchedDirectPair(
|
|
205
212
|
handler=rule.handler,
|
|
213
|
+
port_processor=rule.port_processor,
|
|
206
214
|
direct_order=False,
|
|
207
215
|
name_left=neighbor,
|
|
208
216
|
name_right=device,
|
annet/rpl/match_builder.py
CHANGED
|
@@ -7,7 +7,9 @@ from .condition import SingleCondition, ConditionOperator, AndCondition
|
|
|
7
7
|
|
|
8
8
|
class MatchField(str, Enum):
|
|
9
9
|
community = "community"
|
|
10
|
-
|
|
10
|
+
large_community = "large_community"
|
|
11
|
+
extcommunity_rt = "extcommunity_rt"
|
|
12
|
+
extcommunity_soo = "extcommunity_soo"
|
|
11
13
|
rd = "rd"
|
|
12
14
|
interface = "interface"
|
|
13
15
|
protocol = "protocol"
|
|
@@ -63,14 +65,17 @@ class SetConditionFactory(Generic[ValueT]):
|
|
|
63
65
|
|
|
64
66
|
@dataclass(frozen=True)
|
|
65
67
|
class PrefixMatchValue:
|
|
66
|
-
names:
|
|
67
|
-
|
|
68
|
+
names: tuple[str, ...]
|
|
69
|
+
greater_equal: Optional[int]
|
|
70
|
+
less_equal: Optional[int]
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
class Checkable:
|
|
71
74
|
def __init__(self):
|
|
72
75
|
self.community = SetConditionFactory[str](MatchField.community)
|
|
73
|
-
self.
|
|
76
|
+
self.large_community = SetConditionFactory[str](MatchField.large_community)
|
|
77
|
+
self.extcommunity_rt = SetConditionFactory[str](MatchField.extcommunity_rt)
|
|
78
|
+
self.extcommunity_soo = SetConditionFactory[str](MatchField.extcommunity_soo)
|
|
74
79
|
self.rd = SetConditionFactory[str](MatchField.rd)
|
|
75
80
|
self.interface = ConditionFactory[str](MatchField.interface, ["=="])
|
|
76
81
|
self.protocol = ConditionFactory[str](MatchField.protocol, ["=="])
|
|
@@ -83,11 +88,27 @@ class Checkable:
|
|
|
83
88
|
def as_path_filter(self, name: str) -> SingleCondition[str]:
|
|
84
89
|
return SingleCondition(MatchField.as_path_filter, ConditionOperator.EQ, name)
|
|
85
90
|
|
|
86
|
-
def match_v6(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
def match_v6(
|
|
92
|
+
self,
|
|
93
|
+
*names: str,
|
|
94
|
+
or_longer: tuple[Optional[int], Optional[int]] = (None, None),
|
|
95
|
+
) -> SingleCondition[PrefixMatchValue]:
|
|
96
|
+
return SingleCondition(
|
|
97
|
+
MatchField.ipv6_prefix,
|
|
98
|
+
ConditionOperator.CUSTOM,
|
|
99
|
+
PrefixMatchValue(names, greater_equal=or_longer[0], less_equal=or_longer[1]),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def match_v4(
|
|
103
|
+
self,
|
|
104
|
+
*names: str,
|
|
105
|
+
or_longer: tuple[Optional[int], Optional[int]] = (None, None),
|
|
106
|
+
) -> SingleCondition[PrefixMatchValue]:
|
|
107
|
+
return SingleCondition(
|
|
108
|
+
MatchField.ip_prefix,
|
|
109
|
+
ConditionOperator.CUSTOM,
|
|
110
|
+
PrefixMatchValue(names, greater_equal=or_longer[0], less_equal=or_longer[1]),
|
|
111
|
+
)
|
|
91
112
|
|
|
92
113
|
|
|
93
114
|
def merge_conditions(and_condition: AndCondition) -> AndCondition:
|