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.

Files changed (37) hide show
  1. annet/adapters/file/provider.py +28 -10
  2. annet/adapters/netbox/v37/storage.py +1 -1
  3. annet/annlib/netdev/devdb/data/devdb.json +3 -2
  4. annet/bgp_models.py +28 -0
  5. annet/implicit.py +23 -11
  6. annet/mesh/__init__.py +4 -0
  7. annet/mesh/basemodel.py +5 -0
  8. annet/mesh/device_models.py +2 -0
  9. annet/mesh/executor.py +90 -66
  10. annet/mesh/peer_models.py +3 -3
  11. annet/mesh/port_processor.py +18 -0
  12. annet/mesh/registry.py +12 -4
  13. annet/rpl/match_builder.py +30 -9
  14. annet/rpl/routemap.py +5 -3
  15. annet/rpl/statement_builder.py +31 -7
  16. annet/rpl_generators/__init__.py +24 -0
  17. annet/rpl_generators/aspath.py +57 -0
  18. annet/rpl_generators/community.py +242 -0
  19. annet/rpl_generators/cumulus_frr.py +458 -0
  20. annet/rpl_generators/entities.py +70 -0
  21. annet/rpl_generators/execute.py +12 -0
  22. annet/rpl_generators/policy.py +676 -0
  23. annet/rpl_generators/prefix_lists.py +158 -0
  24. annet/rpl_generators/rd.py +40 -0
  25. {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/METADATA +2 -2
  26. {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/RECORD +36 -25
  27. annet_generators/rpl_example/__init__.py +3 -5
  28. annet_generators/rpl_example/generator.py +127 -0
  29. annet_generators/rpl_example/items.py +21 -31
  30. annet_generators/rpl_example/mesh.py +9 -0
  31. annet_generators/rpl_example/route_policy.py +43 -9
  32. annet_generators/rpl_example/policy_generator.py +0 -233
  33. {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/AUTHORS +0 -0
  34. {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/LICENSE +0 -0
  35. {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/WHEEL +0 -0
  36. {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/entry_points.txt +0 -0
  37. {annet-0.16.26.dist-info → annet-0.16.28.dist-info}/top_level.txt +0 -0
@@ -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] = []
@@ -122,7 +122,7 @@ class NetboxStorageV37(Storage):
122
122
  if self._all_fqdns is None:
123
123
  self._all_fqdns = [
124
124
  d.name
125
- for d in self.netbox.dcim_all_devices().results
125
+ for d in self.netbox.dcim_all_devices_brief().results
126
126
  ]
127
127
  return self._all_fqdns
128
128
 
@@ -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
- text += r"""
137
- !interface */\S*Ethernet\S+/
138
- mtu 1500
139
- no shutdown
140
- !interface */Loopback[0-9.]+/
141
- mtu 1500
142
- no shutdown
143
- !interface */port-channel[0-9.]+/
144
- mtu 1500
145
- no shutdown
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)
@@ -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
- peer_neighbor = DirectPeer(rule.match_left, neighbor_device, [])
95
- peer_device = DirectPeer(rule.match_right, device, [])
96
-
97
- interfaces = self._storage.search_connections(device, neighbor_device)
98
- for local_port, remote_port in interfaces:
99
- peer_device.ports.append(local_port.name)
100
- peer_neighbor.ports.append(remote_port.name)
101
-
102
- if rule.direct_order:
103
- rule.handler(peer_device, peer_neighbor, session)
104
- else:
105
- rule.handler(peer_neighbor, peer_device, session)
106
-
107
- try:
108
- neighbor_dto = merge(DirectPeerDTO(), peer_neighbor, session)
109
- except MergeForbiddenError as e:
110
- raise ValueError(
111
- f"Handler `{handler_name}` provided session data conflicting with "
112
- f"peer data for device `{neighbor_device.fqdn}`:\n" + str(e)
113
- ) from e
114
- try:
115
- device_dto = merge(DirectPeerDTO(), peer_device, session)
116
- except MergeForbiddenError as e:
117
- raise ValueError(
118
- f"Handler `{handler_name}` provided session data conflicting with "
119
- f"peer data for device `{device.fqdn}`:\n" + str(e)
120
- ) from e
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(self, device: Device, neighbor: Device, changes: InterfaceChanges) -> str:
253
- port_pairs = self._storage.search_connections(device, neighbor)
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) -> MeshExecutionResult:
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 MeshExecutionResult(
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], Concat()]
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], Concat()]
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,
@@ -7,7 +7,9 @@ from .condition import SingleCondition, ConditionOperator, AndCondition
7
7
 
8
8
  class MatchField(str, Enum):
9
9
  community = "community"
10
- extcommunity = "extcommunity"
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: Sequence[str]
67
- or_longer: Optional[tuple[int, int]] # ????
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.extcommunity = SetConditionFactory[str](MatchField.extcommunity)
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(self, *names: str, or_longer: Optional[tuple[int, int]] = None) -> SingleCondition[PrefixMatchValue]:
87
- return SingleCondition(MatchField.ipv6_prefix, ConditionOperator.CUSTOM, PrefixMatchValue(names, or_longer))
88
-
89
- def match_v4(self, *names: str, or_longer: Optional[tuple[int, int]] = None) -> SingleCondition[PrefixMatchValue]:
90
- return SingleCondition(MatchField.ip_prefix, ConditionOperator.CUSTOM, PrefixMatchValue(names, or_longer))
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: