annet 0.16.13__py3-none-any.whl → 0.16.15__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.

@@ -139,6 +139,13 @@ class Interface(Entity):
139
139
  family = IpFamily(value=6, label="IPv6")
140
140
  else:
141
141
  family = IpFamily(value=4, label="IPv4")
142
+
143
+ for existing_addr in self.ip_addresses:
144
+ if existing_addr.address == address_mask and (
145
+ (existing_addr.vrf is None and vrf is None) or
146
+ (existing_addr.vrf is not None and existing_addr.vrf.name == vrf)
147
+ ):
148
+ return
142
149
  self.ip_addresses.append(IpAddress(
143
150
  id=0,
144
151
  display=address_mask,
@@ -5,10 +5,11 @@ DEFAULT_URL = "http://localhost"
5
5
 
6
6
 
7
7
  class NetboxStorageOpts:
8
- def __init__(self, url: str, token: str, insecure: bool = False):
8
+ def __init__(self, url: str, token: str, insecure: bool = False, exact_host_filter: bool = False):
9
9
  self.url = url
10
10
  self.token = token
11
11
  self.insecure = insecure
12
+ self.exact_host_filter = exact_host_filter
12
13
 
13
14
  @classmethod
14
15
  def parse_params(cls, conf_params: Optional[dict[str, str]], cli_opts: Any):
@@ -19,4 +20,8 @@ class NetboxStorageOpts:
19
20
  insecure = insecure_env in ("true", "1", "t")
20
21
  else:
21
22
  insecure = bool(conf_params.get("insecure") or False)
22
- return cls(url=url, token=token, insecure=insecure)
23
+ if exact_host_filter_env := os.getenv("NETBOX_EXACT_HOST_FILTER", "").lower():
24
+ exact_host_filter = exact_host_filter_env in ("true", "1", "t")
25
+ else:
26
+ exact_host_filter = bool(conf_params.get("exact_host_filter") or False)
27
+ return cls(url=url, token=token, insecure=insecure, exact_host_filter=exact_host_filter)
@@ -27,10 +27,12 @@ def storage_factory(opts: NetboxStorageOpts) -> Storage:
27
27
 
28
28
 
29
29
  class NetboxProvider(StorageProvider, AdapterWithName, AdapterWithConfig):
30
- def __init__(self, url: Optional[str] = None, token: Optional[str] = None, insecure: bool = False):
30
+ def __init__(self, url: Optional[str] = None, token: Optional[str] = None, insecure: bool = False,
31
+ exact_host_filter: bool = False):
31
32
  self.url = url
32
33
  self.token = token
33
34
  self.insecure = insecure
35
+ self.exact_host_filter = exact_host_filter
34
36
 
35
37
  @classmethod
36
38
  def with_config(cls, **kwargs: Dict[str, Any]) -> T:
@@ -87,16 +87,19 @@ def extend_ip_address(
87
87
 
88
88
  class NetboxStorageV37(Storage):
89
89
  def __init__(self, opts: Optional[NetboxStorageOpts] = None):
90
- ctx: ssl.con | ssl.SSLContext = None
91
- if opts.insecure:
92
- ctx = ssl.create_default_context()
93
- ctx.check_hostname = False
94
- ctx.verify_mode = ssl.CERT_NONE
95
- self.netbox = NetboxV37(
96
- url=opts.url,
97
- token=opts.token,
98
- ssl_context=ctx,
99
- )
90
+ ctx: Optional[ssl.SSLContext] = None
91
+ url = ""
92
+ token = ""
93
+ self.exact_host_filter = False
94
+ if opts:
95
+ if opts.insecure:
96
+ ctx = ssl.create_default_context()
97
+ ctx.check_hostname = False
98
+ ctx.verify_mode = ssl.CERT_NONE
99
+ url = opts.url
100
+ token = opts.token
101
+ self.exact_host_filter = opts.exact_host_filter
102
+ self.netbox = NetboxV37(url=url, token=token, ssl_context=ctx)
100
103
  self._all_fqdns: Optional[list[str]] = None
101
104
 
102
105
  def __enter__(self):
@@ -147,7 +150,7 @@ class NetboxStorageV37(Storage):
147
150
 
148
151
  interfaces = self._load_interfaces(list(device_ids))
149
152
  neighbours = {x.id: x for x in self._load_neighbours(interfaces)}
150
- neighbours_seen = defaultdict(set)
153
+ neighbours_seen: dict[str, set] = defaultdict(set)
151
154
 
152
155
  for interface in interfaces:
153
156
  device_ids[interface.device.id].interfaces.append(interface)
@@ -162,14 +165,18 @@ class NetboxStorageV37(Storage):
162
165
  def _load_devices(self, query: NetboxQuery) -> List[api_models.Device]:
163
166
  if not query.globs:
164
167
  return []
165
- query = _hostname_dot_hack(query)
166
- return [
167
- device
168
- for device in self.netbox.dcim_all_devices(
169
- name__ic=query.globs,
170
- ).results
171
- if _match_query(query, device)
172
- ]
168
+ if self.exact_host_filter:
169
+ devices = self.netbox.dcim_all_devices(name__ie=query.globs).results
170
+ else:
171
+ query = _hostname_dot_hack(query)
172
+ devices = [
173
+ device
174
+ for device in self.netbox.dcim_all_devices(
175
+ name__ic=query.globs,
176
+ ).results
177
+ if _match_query(query, device)
178
+ ]
179
+ return devices
173
180
 
174
181
  def _extend_interfaces(self, interfaces: List[models.Interface]) -> List[models.Interface]:
175
182
  extended_ifaces = {
@@ -54,6 +54,8 @@
54
54
  "Huawei.CE.CE8800.CE8851": " CE8851",
55
55
  "Huawei.CE.CE8800.CE8855": " CE8855",
56
56
  "Huawei.CE.CE9800": " CE98\\d\\d",
57
+ "Huawei.CE.CE9800.CE9855": " CE9855",
58
+ "Huawei.CE.CE9800.CE9860": " CE9860",
57
59
  "Huawei.Quidway": " (LS-)?S",
58
60
  "Huawei.Quidway.S2x": "2\\d{3}",
59
61
  "Huawei.Quidway.S2x.S2300": "23\\d\\d",
annet/bgp_models.py CHANGED
@@ -103,7 +103,7 @@ class PeerOptions:
103
103
  af_rib_group: Optional[str] = None
104
104
  af_loops: Optional[int] = None
105
105
  hold_time: Optional[int] = None
106
- listen_network: Optional[bool] = None
106
+ listen_network: Optional[list[str]] = None
107
107
  remove_private: Optional[bool] = None
108
108
  as_override: Optional[bool] = None
109
109
  aigp: Optional[bool] = None
@@ -158,7 +158,7 @@ class Redistribute:
158
158
  @dataclass
159
159
  class FamilyOptions:
160
160
  family: Family
161
- vrf_name: str
161
+ vrf_name: str = ""
162
162
  multipath: int = 0
163
163
  global_multipath: int = 0
164
164
  aggregate: Aggregate = field(default_factory=Aggregate)
@@ -178,6 +178,7 @@ class FamilyOptions:
178
178
  class PeerGroup:
179
179
  name: str
180
180
  remote_as: ASN = ASN(None)
181
+ families: set[Family] = field(default_factory=set)
181
182
  internal_name: str = ""
182
183
  description: str = ""
183
184
  update_source: str = ""
@@ -213,7 +214,7 @@ class PeerGroup:
213
214
  af_rib_group: Optional[str] = None
214
215
  af_loops: int = 0
215
216
  hold_time: int = 0
216
- listen_network: bool = False
217
+ listen_network: list[str] = field(default_factory=list)
217
218
  remove_private: bool = False
218
219
  as_override: bool = False
219
220
  aigp: bool = False
@@ -234,6 +235,12 @@ class PeerGroup:
234
235
  @dataclass
235
236
  class VrfOptions:
236
237
  vrf_name: str
238
+
239
+ ipv4_unicast: FamilyOptions
240
+ ipv6_unicast: FamilyOptions
241
+ ipv4_labeled_unicast: FamilyOptions
242
+ ipv6_labeled_unicast: FamilyOptions
243
+
237
244
  vrf_name_global: Optional[str] = None
238
245
  import_policy: Optional[str] = None
239
246
  export_policy: Optional[str] = None
@@ -244,23 +251,20 @@ class VrfOptions:
244
251
  route_distinguisher: Optional[str] = None
245
252
  static_label: Optional[int] = None # FIXME: str?
246
253
 
247
- ipv4_unicast: Optional[FamilyOptions] = None
248
- ipv6_unicast: Optional[FamilyOptions] = None
249
- ipv4_labeled_unicast: Optional[FamilyOptions] = None
250
- ipv6_labeled_unicast: Optional[FamilyOptions] = None
251
254
  groups: list[PeerGroup] = field(default_factory=list)
252
255
 
253
256
 
254
257
  @dataclass
255
258
  class GlobalOptions:
259
+ ipv4_unicast: FamilyOptions
260
+ ipv6_unicast: FamilyOptions
261
+ ipv4_labeled_unicast: FamilyOptions
262
+ ipv6_labeled_unicast: FamilyOptions
263
+
256
264
  local_as: ASN = ASN(None)
257
265
  loops: int = 0
258
266
  multipath: int = 0
259
267
  router_id: str = ""
260
268
  vrf: dict[str, VrfOptions] = field(default_factory=dict)
261
269
 
262
- ipv4_unicast: Optional[FamilyOptions] = None
263
- ipv6_unicast: Optional[FamilyOptions] = None
264
- ipv4_labeled_unicast: Optional[FamilyOptions] = None
265
- ipv6_labeled_unicast: Optional[FamilyOptions] = None
266
270
  groups: list[PeerGroup] = field(default_factory=list)
annet/implicit.py CHANGED
@@ -71,14 +71,14 @@ def _implicit_tree(device):
71
71
  netconf
72
72
  """
73
73
  elif device.hw.Arista:
74
- # эта часть конфигурации будет не видна в конфиге, если она включена с таким набором полей:
74
+ # This part of configuration will not be visible in configuration
75
75
  text = r"""
76
76
  ip load-sharing trident fields ipv6 destination-port source-ip ingress-interface destination-ip source-port flow-label
77
77
  ip load-sharing trident fields ip source-ip source-port destination-ip destination-port ingress-interface
78
78
  """
79
79
  elif device.hw.Nexus:
80
80
  text = r"""
81
- # часть конфигурации скрытая если включена
81
+ # This part of configuration will not be visible in configuration if enabled
82
82
  snmp-server enable traps link linkDown
83
83
  snmp-server enable traps link linkUp
84
84
  """
@@ -107,25 +107,24 @@ def _implicit_tree(device):
107
107
  """
108
108
 
109
109
  elif device.hw.Nexus.N3x:
110
- # У нексуса сложные взаимоотношения с
111
- # shutdown командой вездe
112
- # в данный момент поведение проверенно для Cisco Nexus 3132Q 6.0(2)U6(7)
110
+ # Cisco Nexus has some specific related to "shutdown" command
111
+ # Behavior is cheked on Cisco Nexus 3132Q 6.0(2)U6(7)
113
112
  text += r"""
114
113
  # SVI
115
114
  !interface Vlan*
116
115
  !shutdown
117
116
  !interface mgmt[0-9]*
118
117
  no shutdown
119
- # Физические НЕ сплитованные интерфейсы и subif'ы
118
+ # Physical and NOT splitted interfaces and subifs
120
119
  !interface */Ethernet1\/[0-9.]*/
121
120
  no shutdown
122
- # Физические НЕ сплитованные интерфейсы и subif'ы
121
+ # Physical and NOT splitted interfaces and subifs
123
122
  !interface */Ethernet1\/[0-9]+\/[0-9.]+/
124
123
  # только explicit
125
- # Лупбеки
124
+ # Loopbacks
126
125
  !interface */Loopback[0-9.]+/
127
126
  no shutdown
128
- # Агрегаты
127
+ # Port-Channels
129
128
  !interface */port-channel[0-9.]+/
130
129
  no shutdown
131
130
  # BGP
@@ -135,7 +134,14 @@ def _implicit_tree(device):
135
134
  """
136
135
  elif device.hw.Cisco:
137
136
  text += r"""
138
- !interface
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
139
145
  no shutdown
140
146
  """
141
147
  if device.hw.Cisco.Catalyst:
annet/mesh/__init__.py CHANGED
@@ -8,9 +8,12 @@ __all__ = [
8
8
  "Left",
9
9
  "Right",
10
10
  "Match",
11
+ "VirtualLocal",
12
+ "VirtualPeer",
11
13
  ]
12
14
 
13
15
  from .executor import MeshExecutor
14
16
  from .match_args import Left, Right, Match
15
- from .registry import DirectPeer, IndirectPeer, MeshSession, GlobalOptions
16
- from .registry import MeshRulesRegistry
17
+ from .registry import (
18
+ DirectPeer, IndirectPeer, MeshSession, GlobalOptions, MeshRulesRegistry, VirtualLocal, VirtualPeer,
19
+ )
@@ -1,16 +1,30 @@
1
1
  from typing import Annotated, Optional, Union
2
2
 
3
- from annet.bgp_models import Family, Aggregate, Redistribute
3
+ from annet.bgp_models import Family, Redistribute
4
4
  from .basemodel import BaseMeshModel, Concat, DictMerge, Merge, KeyDefaultDict
5
5
  from .peer_models import MeshPeerGroup
6
6
 
7
7
 
8
+ class Aggregate(BaseMeshModel):
9
+ policy: str
10
+ routes: Annotated[tuple[str, ...], Concat()]
11
+ export_policy: str
12
+ as_path: str
13
+ reference: str
14
+ suppress: bool
15
+ discard: bool
16
+ as_set: bool
17
+
18
+
8
19
  class FamilyOptions(BaseMeshModel):
20
+ def __init__(self, **kwargs):
21
+ kwargs.setdefault("aggregate", Aggregate())
22
+ super().__init__(**kwargs)
9
23
  family: Family
10
24
  vrf_name: str
11
25
  multipath: int = 0
12
26
  global_multipath: int
13
- aggregate: Aggregate
27
+ aggregate: Annotated[Aggregate, Merge()]
14
28
  redistributes: Annotated[tuple[Redistribute, ...], Concat()]
15
29
  allow_default: bool
16
30
  aspath_relax: bool
@@ -24,16 +38,26 @@ class FamilyOptions(BaseMeshModel):
24
38
 
25
39
 
26
40
  class _FamiliesMixin:
27
- ipv4_unicast: Optional[FamilyOptions]
28
- ipv6_unicast: Optional[FamilyOptions]
29
- ipv4_labeled_unicast: Optional[FamilyOptions]
30
- ipv6_labeled_unicast: Optional[FamilyOptions]
41
+ def __init__(self, **kwargs):
42
+ kwargs.setdefault("ipv4_unicast", FamilyOptions(family="ipv4_unicast"))
43
+ kwargs.setdefault("ipv6_unicast", FamilyOptions(family="ipv6_unicast"))
44
+ kwargs.setdefault("ipv4_labeled_unicast", FamilyOptions(family="ipv4_labeled"))
45
+ kwargs.setdefault("ipv6_labeled_unicast", FamilyOptions(family="ipv6_labeled"))
46
+ super().__init__(**kwargs)
47
+ ipv4_unicast: Annotated[FamilyOptions, Merge()]
48
+ ipv6_unicast: Annotated[FamilyOptions, Merge()]
49
+ ipv4_labeled_unicast: Annotated[FamilyOptions, Merge()]
50
+ ipv6_labeled_unicast: Annotated[FamilyOptions, Merge()]
31
51
 
32
52
 
33
- class VrfOptions(BaseMeshModel, _FamiliesMixin):
34
- def __init__(self, **kwargs):
53
+ class VrfOptions(_FamiliesMixin, BaseMeshModel):
54
+ def __init__(self, vrf_name: str, **kwargs):
55
+ kwargs.setdefault("ipv4_unicast", FamilyOptions(family="ipv4_unicast", vrf_name=vrf_name))
56
+ kwargs.setdefault("ipv6_unicast", FamilyOptions(family="ipv6_unicast", vrf_name=vrf_name))
57
+ kwargs.setdefault("ipv4_labeled_unicast", FamilyOptions(family="ipv4_labeled", vrf_name=vrf_name))
58
+ kwargs.setdefault("ipv6_labeled_unicast", FamilyOptions(family="ipv6_labeled", vrf_name=vrf_name))
35
59
  kwargs.setdefault("groups", KeyDefaultDict(lambda x: MeshPeerGroup(name=x)))
36
- super().__init__(**kwargs)
60
+ super().__init__(vrf_name=vrf_name, **kwargs)
37
61
 
38
62
  vrf_name: str
39
63
  vrf_name_global: Optional[str]
@@ -48,7 +72,7 @@ class VrfOptions(BaseMeshModel, _FamiliesMixin):
48
72
  groups: Annotated[dict[str, MeshPeerGroup], DictMerge(Merge())]
49
73
 
50
74
 
51
- class GlobalOptionsDTO(BaseMeshModel, _FamiliesMixin):
75
+ class GlobalOptionsDTO(_FamiliesMixin, BaseMeshModel):
52
76
  def __init__(self, **kwargs):
53
77
  kwargs.setdefault("groups", KeyDefaultDict(lambda x: MeshPeerGroup(name=x)))
54
78
  kwargs.setdefault("vrf", KeyDefaultDict(lambda x: VrfOptions(vrf_name=x)))
annet/mesh/executor.py CHANGED
@@ -1,15 +1,22 @@
1
1
  from dataclasses import dataclass
2
2
  from logging import getLogger
3
- from typing import Annotated, Callable, Optional
3
+ from typing import Annotated, Callable, Optional, Union
4
4
 
5
5
  from annet.bgp_models import Peer, GlobalOptions
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
9
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
-
10
+ from .peer_models import DirectPeerDTO, IndirectPeerDTO, VirtualLocalDTO, VirtualPeerDTO
11
+ from .registry import (
12
+ DirectPeer,
13
+ GlobalOptions as MeshGlobalOptions,
14
+ IndirectPeer,
15
+ MeshRulesRegistry,
16
+ MeshSession,
17
+ VirtualLocal,
18
+ VirtualPeer,
19
+ )
13
20
 
14
21
  logger = getLogger(__name__)
15
22
 
@@ -28,11 +35,16 @@ class PeerKey:
28
35
 
29
36
 
30
37
  class Pair(BaseMeshModel):
31
- local: Annotated[PeerDTO, Merge()]
32
- connected: Annotated[PeerDTO, Merge()]
38
+ local: Annotated[Union[DirectPeerDTO, IndirectPeerDTO], Merge()]
39
+ connected: Annotated[Union[DirectPeerDTO, IndirectPeerDTO], Merge()]
33
40
  device: Annotated[Device, UseLast()]
34
41
 
35
42
 
43
+ class VirtualPair(BaseMeshModel):
44
+ local: Annotated[VirtualLocalDTO, Merge()]
45
+ connected: Annotated[VirtualPeerDTO, Merge()]
46
+
47
+
36
48
  class MeshExecutor:
37
49
  def __init__(
38
50
  self,
@@ -93,14 +105,14 @@ class MeshExecutor:
93
105
  rule.handler(peer_neighbor, peer_device, session)
94
106
 
95
107
  try:
96
- neighbor_dto = merge(PeerDTO(), peer_neighbor, session)
108
+ neighbor_dto = merge(DirectPeerDTO(), peer_neighbor, session)
97
109
  except MergeForbiddenError as e:
98
110
  raise ValueError(
99
111
  f"Handler `{handler_name}` provided session data conflicting with "
100
112
  f"peer data for device `{neighbor_device.fqdn}`:\n" + str(e)
101
113
  ) from e
102
114
  try:
103
- device_dto = merge(PeerDTO(), peer_device, session)
115
+ device_dto = merge(DirectPeerDTO(), peer_device, session)
104
116
  except MergeForbiddenError as e:
105
117
  raise ValueError(
106
118
  f"Handler `{handler_name}` provided session data conflicting with "
@@ -132,6 +144,42 @@ class MeshExecutor:
132
144
  neighbor_peers[peer_key] = pair
133
145
  return list(neighbor_peers.values())
134
146
 
147
+ def _execute_virtual(self, device: Device) -> list[VirtualPair]:
148
+ virtual_peers: list[VirtualPair] = []
149
+ for rule in self._registry.lookup_virtual(device.fqdn):
150
+ for order_number in rule.num:
151
+ handler_name = self._handler_name(rule.handler)
152
+ logger.debug("Running direct handler: %s", handler_name)
153
+ session = MeshSession()
154
+ peer_device = VirtualLocal(rule.match, device)
155
+ peer_virtual = VirtualPeer(num=order_number)
156
+
157
+ rule.handler(peer_device, peer_virtual, session)
158
+
159
+ try:
160
+ virtual_dto = merge(VirtualPeerDTO(), peer_virtual, session)
161
+ except MergeForbiddenError as e:
162
+ raise ValueError(
163
+ f"Handler `{handler_name}` provided session data conflicting with "
164
+ f"virtual peer data for device `{device.fqdn}` and num={order_number}:\n" + str(e)
165
+ ) from e
166
+ try:
167
+ device_dto = merge(VirtualLocalDTO(), peer_device, session)
168
+ except MergeForbiddenError as e:
169
+ raise ValueError(
170
+ f"Handler `{handler_name}` provided session data conflicting with "
171
+ f"peer data for device `{device.fqdn}`:\n" + str(e)
172
+ ) from e
173
+
174
+ if not hasattr(device_dto, "svi"):
175
+ raise ValueError(
176
+ f"Handler `{handler_name}` did not provide `svi` number. "
177
+ "Virtual peer must be connected to SVI interface."
178
+ )
179
+ pair = VirtualPair(local=device_dto, connected=virtual_dto)
180
+ virtual_peers.append(pair)
181
+ return virtual_peers
182
+
135
183
  def _execute_indirect(self, device: Device, all_fqdns: list[str]) -> list[Pair]:
136
184
  # we can have multiple rules for the same pair
137
185
  # we merge them according to remote fqdn
@@ -152,14 +200,14 @@ class MeshExecutor:
152
200
  rule.handler(peer_connected, peer_device, session)
153
201
 
154
202
  try:
155
- connected_dto = merge(PeerDTO(), peer_connected, session)
203
+ connected_dto = merge(IndirectPeerDTO(), peer_connected, session)
156
204
  except MergeForbiddenError as e:
157
205
  raise ValueError(
158
206
  f"Handler `{handler_name}` provided session data conflicting with "
159
207
  f"peer data for device `{connected_device.fqdn}`:\n" + str(e)
160
208
  ) from e
161
209
  try:
162
- device_dto = merge(PeerDTO(), peer_device, session)
210
+ device_dto = merge(IndirectPeerDTO(), peer_device, session)
163
211
  except MergeForbiddenError as e:
164
212
  raise ValueError(
165
213
  f"Handler `{handler_name}` provided session data conflicting with "
@@ -193,7 +241,10 @@ class MeshExecutor:
193
241
  return list(connected_peers.values()) # FIXME
194
242
 
195
243
  def _to_bgp_peer(self, pair: Pair, interface: Optional[str]) -> Peer:
196
- return to_bgp_peer(pair.local, pair.connected, pair.device, interface)
244
+ return to_bgp_peer(pair.local, pair.connected, pair.device.hostname, interface)
245
+
246
+ def _virtual_to_bgp_peer(self, pair: VirtualPair, interface: Optional[str]) -> Peer:
247
+ return to_bgp_peer(pair.local, pair.connected, "", interface)
197
248
 
198
249
  def _to_bgp_global(self, global_options: GlobalOptionsDTO) -> GlobalOptions:
199
250
  return to_bgp_global_options(global_options)
@@ -225,6 +276,9 @@ class MeshExecutor:
225
276
  target_interface.add_addr(changes.addr, changes.vrf)
226
277
  return target_interface.name
227
278
 
279
+ def _apply_virtual_interface_changes(self, device: Device, local: VirtualLocalDTO) -> str:
280
+ return device.add_svi(local.svi).name # we check if SVI configured in execute method
281
+
228
282
  def execute_for(self, device: Device) -> MeshExecutionResult:
229
283
  all_fqdns = self._storage.resolve_all_fdnds()
230
284
 
@@ -239,6 +293,13 @@ class MeshExecutor:
239
293
  )
240
294
  peers.append(self._to_bgp_peer(direct_pair, target_interface))
241
295
 
296
+ for virtual_pair in self._execute_virtual(device):
297
+ target_interface = self._apply_virtual_interface_changes(
298
+ device,
299
+ virtual_pair.local,
300
+ )
301
+ peers.append(self._virtual_to_bgp_peer(virtual_pair, target_interface))
302
+
242
303
  for connected_pair in self._execute_indirect(device, all_fqdns):
243
304
  peers.append(self._to_bgp_peer(connected_pair, None))
244
305
 
@@ -1,12 +1,18 @@
1
1
  from dataclasses import dataclass
2
2
  from ipaddress import ip_interface
3
- from typing import Optional
3
+ from typing import Optional, Union
4
4
 
5
- from adaptix import Retort, loader, Chain, name_mapping
5
+ from adaptix import Retort, loader, Chain, name_mapping, as_is_loader
6
6
 
7
- from .peer_models import PeerDTO
8
- from ..bgp_models import GlobalOptions, VrfOptions, FamilyOptions, Peer, PeerGroup, ASN, PeerOptions
9
- from ..storage import Device
7
+ from .peer_models import DirectPeerDTO, IndirectPeerDTO, VirtualPeerDTO, VirtualLocalDTO
8
+ from ..bgp_models import (
9
+ Aggregate, GlobalOptions, VrfOptions, FamilyOptions, Peer, PeerGroup, ASN, PeerOptions,
10
+ Redistribute, BFDTimers,
11
+ )
12
+
13
+
14
+ PeerDTO = Union[DirectPeerDTO, IndirectPeerDTO, VirtualPeerDTO]
15
+ LocalDTO = Union[DirectPeerDTO, IndirectPeerDTO, VirtualLocalDTO]
10
16
 
11
17
 
12
18
  @dataclass
@@ -46,7 +52,10 @@ retort = Retort(
46
52
  loader(GlobalOptions, ObjMapping, Chain.FIRST),
47
53
  loader(VrfOptions, ObjMapping, Chain.FIRST),
48
54
  loader(FamilyOptions, ObjMapping, Chain.FIRST),
55
+ loader(Aggregate, ObjMapping, Chain.FIRST),
49
56
  loader(PeerOptions, ObjMapping, Chain.FIRST),
57
+ as_is_loader(Redistribute),
58
+ as_is_loader(BFDTimers),
50
59
  name_mapping(PeerOptions, map={
51
60
  "local_as": "asnum",
52
61
  }),
@@ -62,14 +71,13 @@ to_bgp_global_options = retort.get_loader(GlobalOptions)
62
71
  to_interface_changes = retort.get_loader(InterfaceChanges)
63
72
 
64
73
 
65
- def to_bgp_peer(local: PeerDTO, connected: PeerDTO, connected_device: Device, interface: Optional[str]) -> Peer:
74
+ def to_bgp_peer(local: LocalDTO, connected: PeerDTO, connected_hostname: str, interface: Optional[str]) -> Peer:
66
75
  options = retort.load(local, PeerOptions)
67
- # TODO validate `lagg_links` before conversion
68
76
  result = Peer(
69
77
  addr=str(ip_interface(connected.addr).ip),
70
78
  interface=interface,
71
79
  remote_as=ASN(connected.asnum),
72
- hostname=connected_device.hostname,
80
+ hostname=connected_hostname,
73
81
  options=options,
74
82
  )
75
83
  # connected
@@ -78,7 +86,7 @@ def to_bgp_peer(local: PeerDTO, connected: PeerDTO, connected_device: Device, in
78
86
  result.description = getattr(connected, "description", result.description)
79
87
  result.families = getattr(connected, "families", result.families)
80
88
  # 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)
89
+ result.import_policy = getattr(local, "import_policy", result.import_policy)
90
+ result.export_policy = getattr(local, "export_policy", result.export_policy)
91
+ result.update_source = getattr(local, "update_source", result.update_source)
84
92
  return result
annet/mesh/peer_models.py CHANGED
@@ -61,7 +61,7 @@ class _OptionsDTO(_SharedOptionsDTO):
61
61
  af_rib_group: Optional[str]
62
62
  af_loops: int
63
63
  hold_time: int
64
- listen_network: bool
64
+ listen_network: list[str]
65
65
  remove_private: bool
66
66
  as_override: bool
67
67
  aigp: bool
@@ -77,21 +77,46 @@ class _OptionsDTO(_SharedOptionsDTO):
77
77
  mtu: int
78
78
 
79
79
 
80
- class PeerDTO(MeshSession, _OptionsDTO):
80
+ class DirectPeerDTO(MeshSession, _OptionsDTO):
81
81
  pod: int
82
82
  addr: str
83
83
  description: str
84
+ update_source: str
84
85
 
85
86
  subif: int
86
87
  lag: Optional[int]
87
88
  lag_links_min: Optional[int]
88
89
  svi: Optional[int]
89
90
 
90
- group_name: str
91
+
92
+ class IndirectPeerDTO(MeshSession, _OptionsDTO):
93
+ pod: int
94
+ addr: str
95
+ description: str
96
+ update_source: str
97
+
98
+
99
+ class VirtualLocalDTO(_OptionsDTO):
100
+ asnum: int
101
+ pod: int
102
+ addr: str
103
+ description: str
104
+
105
+ import_policy: str
106
+ export_policy: str
107
+ update_source: str
108
+
109
+ svi: int
110
+
111
+
112
+ class VirtualPeerDTO(MeshSession):
113
+ addr: str
114
+ description: str
91
115
 
92
116
 
93
117
  class MeshPeerGroup(_OptionsDTO):
94
118
  name: str
119
+ families: Annotated[set[FamilyName], Concat()]
95
120
  remote_as: Union[int, str]
96
121
  internal_name: str
97
122
  update_source: str
annet/mesh/registry.py CHANGED
@@ -1,13 +1,13 @@
1
1
  from dataclasses import dataclass
2
- from typing import Callable, Any
2
+ from typing import Callable, Any, Sequence
3
3
 
4
4
  from .match_args import MatchExpr, PairMatcher, SingleMatcher
5
5
  from .match_args import MatchedArgs
6
6
  from .device_models import GlobalOptionsDTO
7
- from .peer_models import PeerDTO, MeshSession
7
+ from .peer_models import MeshSession, IndirectPeerDTO, VirtualLocalDTO, VirtualPeerDTO, DirectPeerDTO
8
8
 
9
9
 
10
- class DirectPeer(PeerDTO):
10
+ class DirectPeer(DirectPeerDTO):
11
11
  match: MatchedArgs
12
12
  device: Any
13
13
  ports: list[str]
@@ -19,7 +19,7 @@ class DirectPeer(PeerDTO):
19
19
  self.ports = ports
20
20
 
21
21
 
22
- class IndirectPeer(PeerDTO):
22
+ class IndirectPeer(IndirectPeerDTO):
23
23
  match: MatchedArgs
24
24
  device: Any
25
25
 
@@ -29,6 +29,20 @@ class IndirectPeer(PeerDTO):
29
29
  self.device = device
30
30
 
31
31
 
32
+ class VirtualLocal(VirtualLocalDTO):
33
+ match: MatchedArgs
34
+ device: Any
35
+
36
+ def __init__(self, match: MatchedArgs, device: Any) -> None:
37
+ super().__init__()
38
+ self.match = match
39
+ self.device = device
40
+
41
+
42
+ class VirtualPeer(VirtualPeerDTO):
43
+ num: int
44
+
45
+
32
46
  class GlobalOptions(GlobalOptionsDTO):
33
47
  match: MatchedArgs
34
48
  device: Any
@@ -50,6 +64,7 @@ class GlobalRule:
50
64
 
51
65
  DirectHandler = Callable[[DirectPeer, DirectPeer, MeshSession], None]
52
66
  IndirectHandler = Callable[[IndirectPeer, IndirectPeer, MeshSession], None]
67
+ VirtualHandler = Callable[[VirtualLocal, VirtualPeer, MeshSession], None]
53
68
 
54
69
 
55
70
  @dataclass
@@ -66,6 +81,14 @@ class IndirectRule:
66
81
  handler: IndirectHandler
67
82
 
68
83
 
84
+ @dataclass
85
+ class VirtualRule:
86
+ __slots__ = ("matcher", "num", "handler")
87
+ matcher: SingleMatcher
88
+ num: Sequence[int]
89
+ handler: VirtualHandler
90
+
91
+
69
92
  @dataclass
70
93
  class MatchedGlobal:
71
94
  __slots__ = ("match", "handler")
@@ -95,11 +118,20 @@ class MatchedIndirectPair:
95
118
  match_right: MatchedArgs
96
119
 
97
120
 
121
+ @dataclass
122
+ class MatchedVirtualPair:
123
+ __slots__ = ("match", "num", "handler")
124
+ match: MatchedArgs
125
+ num: Sequence[int]
126
+ handler: VirtualHandler
127
+
128
+
98
129
  class MeshRulesRegistry:
99
130
  def __init__(self, match_short_name: bool = False):
100
131
  self.direct_rules: list[DirectRule] = []
101
132
  self.indirect_rules: list[IndirectRule] = []
102
133
  self.global_rules: list[GlobalRule] = []
134
+ self.virtual_rules: list[VirtualRule] = []
103
135
  self.nested: list[MeshRulesRegistry] = []
104
136
  self.match_short_name = match_short_name
105
137
 
@@ -142,6 +174,17 @@ class MeshRulesRegistry:
142
174
 
143
175
  return register
144
176
 
177
+ def virtual(
178
+ self, peer_mask: str, num: Sequence[int], *match: MatchExpr,
179
+ ) -> Callable[[VirtualHandler], VirtualHandler]:
180
+ matcher = SingleMatcher(peer_mask, match)
181
+
182
+ def register(handler: VirtualHandler) -> VirtualHandler:
183
+ self.virtual_rules.append(VirtualRule(matcher, num, handler))
184
+ return handler
185
+
186
+ return register
187
+
145
188
  def lookup_direct(self, device: str, neighbors: list[str]) -> list[MatchedDirectPair]:
146
189
  found = []
147
190
  device = self._normalize_host(device)
@@ -198,6 +241,20 @@ class MeshRulesRegistry:
198
241
  found.extend(registry.lookup_indirect(device, devices))
199
242
  return found
200
243
 
244
+ def lookup_virtual(self, device: str) -> list[MatchedVirtualPair]:
245
+ found = []
246
+ device = self._normalize_host(device)
247
+ for rule in self.virtual_rules:
248
+ if args := rule.matcher.match_one(device):
249
+ found.append(MatchedVirtualPair(
250
+ handler=rule.handler,
251
+ match=args,
252
+ num=rule.num,
253
+ ))
254
+ for registry in self.nested:
255
+ found.extend(registry.lookup_virtual(device))
256
+ return found
257
+
201
258
  def lookup_global(self, device: str) -> list[MatchedGlobal]:
202
259
  found = []
203
260
  device = self._normalize_host(device)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: annet
3
- Version: 0.16.13
3
+ Version: 0.16.15
4
4
  Summary: annet
5
5
  Home-page: https://github.com/annetutil/annet
6
6
  License: MIT
@@ -1,7 +1,7 @@
1
1
  annet/__init__.py,sha256=k2dq8aOYjkjZszKoOlxEmR49uYoKh-8_HC2K8FeqLwA,2139
2
2
  annet/annet.py,sha256=TMdEuM7GJQ4TjRVmuK3bCTZN-21lxjQ9sXqEdILUuBk,725
3
3
  annet/argparse.py,sha256=v1MfhjR0B8qahza0WinmXClpR8UiDFhmwDDWtNroJPA,12855
4
- annet/bgp_models.py,sha256=lV9TW7oPiaiWxD9c5pMIpAnghVIUFK_cTpxNbUUfIaM,8082
4
+ annet/bgp_models.py,sha256=SUBiOFVK4PCzFO7AjcypF3E9SrvbNx8i7hdUYmLmDMo,8041
5
5
  annet/cli.py,sha256=hDpjIr3w47lgQ_CvCQS1SXFDK-SJrf5slbT__5u6GIA,12342
6
6
  annet/cli_args.py,sha256=KQlihxSl-Phhq1-9oJDdNSbIllEX55LlPfH6viEKOuw,13483
7
7
  annet/connectors.py,sha256=-Lghz3PtWCBU8Ohrp0KKQcmm1AUZtN0EnOaZ6IQgCQI,5105
@@ -11,7 +11,7 @@ annet/executor.py,sha256=Jny-hm0otZA1naPpFWR-R16SbaZioSQ8pkx-Yd2PYlM,19004
11
11
  annet/filtering.py,sha256=ZtqxPsKdV9reZoRxtQyBg22BqyMqd-2SotYcxZ-68AQ,903
12
12
  annet/gen.py,sha256=rOAveprvBaEXbLyaQUhQ0QtOvcDfndlIOpchNhu8M1U,33499
13
13
  annet/hardware.py,sha256=_iR28dWiPtt6ZYdk-qg1sxazkSRJE3ukqKB-fFFfQak,1141
14
- annet/implicit.py,sha256=1_FdVCMdIM7O1lYVMY9AuHaBp9E6h6Hc5kuog55bCzI,5573
14
+ annet/implicit.py,sha256=kGKO-mdvPw4HROx4VhnQWxbNlnbG2LuSYEzZbQX2Elo,5518
15
15
  annet/lib.py,sha256=lOdz3UJFYkO0wnuzIRRq3FpULQliqBtfmGmKOGVFO4Q,4238
16
16
  annet/output.py,sha256=FYMcWCc43-b51KsCiKnXPZHawhgWNoVtY9gRqw__Ce0,7473
17
17
  annet/parallel.py,sha256=hLkzEht0KhzmzUWDdO4QFYQHzhxs3wPlTA8DxbB2ziw,17160
@@ -29,18 +29,18 @@ annet/adapters/fetchers/stub/fetcher.py,sha256=bJGNJcvjrxSa4d8gly6NkaRcxoK57zbZh
29
29
  annet/adapters/file/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  annet/adapters/file/provider.py,sha256=Wn3sG2TOI6ZWuRHhPSyg8S0Dnrz5LN3VyuqE6S7r5GM,5930
31
31
  annet/adapters/netbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- annet/adapters/netbox/provider.py,sha256=cEPdHKw4zWA3Wrfmj01spQrkGpwcQdZ0Jjy51m5mvs4,1542
32
+ annet/adapters/netbox/provider.py,sha256=3IrfZ6CfCxf-lTnJlIC2TQ8M_rDxOB_B7HGXZ92vAgA,1643
33
33
  annet/adapters/netbox/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  annet/adapters/netbox/common/client.py,sha256=PaxHG4W9H8_uunIwMBNYkLq4eQJYoO6p6gY-ciQs7Nc,2563
35
35
  annet/adapters/netbox/common/manufacturer.py,sha256=QoTjmuBAg5klOJqeo8HuS2HybIUGsjTkknLkQL_Tiec,1606
36
- annet/adapters/netbox/common/models.py,sha256=SeWQZV-zvaKvK2YT2Bwf-iapkIs5pr7I7rAI7vlFMB8,6886
36
+ annet/adapters/netbox/common/models.py,sha256=JAAtbUB6UOzA3-VF5Fa33XktgKC3C2csareTZnHyqd0,7177
37
37
  annet/adapters/netbox/common/query.py,sha256=ziUFM7cUEbEIf3k1szTll4aO-OCUa-2Ogxbebi7Tegs,646
38
38
  annet/adapters/netbox/common/status_client.py,sha256=W4nTb2yvBlJ2UkWUmUhKQ2PaSQb1shjhHj5ebb4s2s4,591
39
- annet/adapters/netbox/common/storage_opts.py,sha256=0xQeO_OGex73cV9UA5XZn7hLiyhGYwNHYtO5JtMSR4o,815
39
+ annet/adapters/netbox/common/storage_opts.py,sha256=5tt6wxUUJTIzNbOVXMnYBwZedNAIqYlve3YWl6GdbZM,1197
40
40
  annet/adapters/netbox/v24/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  annet/adapters/netbox/v24/storage.py,sha256=THI592VLx3ehixsPZQ1Ko3mYIAZQbnDY-toIziqBbL8,6005
42
42
  annet/adapters/netbox/v37/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- annet/adapters/netbox/v37/storage.py,sha256=nCcQtjrAvi9sW0_XHZDCFnOk1XIbWUO_pRt1m6E0ELk,9957
43
+ annet/adapters/netbox/v37/storage.py,sha256=a_KnjkEQrK6o2pG9-c9xHYGtj05E2F01JZy7_GIUp7g,10330
44
44
  annet/annlib/__init__.py,sha256=fT1l4xV5fqqg8HPw9HqmZVN2qwS8i6X1aIm2zGDjxKY,252
45
45
  annet/annlib/command.py,sha256=uuBddMQphtn8P5MO5kzIa8_QrtMns-k05VeKv1bcAuA,1043
46
46
  annet/annlib/diff.py,sha256=MZ6eQAU3cadQp8KaSE6uAYFtcfMDCIe_eNuVROnYkCk,4496
@@ -55,7 +55,7 @@ annet/annlib/types.py,sha256=VHU0CBADfYmO0xzB_c5f-mcuU3dUumuJczQnqGlib9M,852
55
55
  annet/annlib/netdev/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
56
  annet/annlib/netdev/db.py,sha256=fI_u5aya4l61mbYSjj4JwlVfi3s7obt2jqERSuXGRUI,1634
57
57
  annet/annlib/netdev/devdb/__init__.py,sha256=aKYjjLbJebdKBjnGDpVLQdSqrV2JL24spGm58tmMWVU,892
58
- annet/annlib/netdev/devdb/data/devdb.json,sha256=Qph7WxZBEbgBqbCFeYu6ZM0qkEBn80WmJEjfJkcaZ-0,5940
58
+ annet/annlib/netdev/devdb/data/devdb.json,sha256=_apUe1EsJORxD5CfAvBcpPlnIf_H7I_hjLuVTgS5-H0,6024
59
59
  annet/annlib/netdev/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  annet/annlib/netdev/views/dump.py,sha256=rIlyvnA3uM8bB_7oq1nS2KDxTp6dQh2hz-FbNhYIpOU,4630
61
61
  annet/annlib/netdev/views/hardware.py,sha256=3JCZLH7deIHhCguwPJTUX-WDvWjG_xt6BdSEZSO6zkQ,4226
@@ -81,14 +81,14 @@ annet/generators/ref.py,sha256=QVdeL8po1D0kBsVLOpCjFR81D8yNTk-kaQj5WUM4hng,438
81
81
  annet/generators/result.py,sha256=zMAvGOYQU803bGy6datZduHLgrEqK2Zba_Jcf9Qn9p0,4976
82
82
  annet/generators/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
83
  annet/generators/common/initial.py,sha256=qYBxXFhyOPy34cxc6hsIXseod-lYCmmbuNHpM0uteY0,1244
84
- annet/mesh/__init__.py,sha256=_Ho7ZwPuEZ7wsOu_oG0avvPzQVn8g-A0w2fqZ42Gf30,369
84
+ annet/mesh/__init__.py,sha256=ZVFPnNOBgP42d7dUnCypGh3RKy0Ne_ObWVzROlE1nFA,423
85
85
  annet/mesh/basemodel.py,sha256=bBBx1wu3ftENgf_4PGCV-SkEcxH3tFekvoointT3894,4739
86
- annet/mesh/device_models.py,sha256=m3S2IUW7dH0drAop3imwuTk27X93c8lLC2ot8S6rA5Y,2068
87
- annet/mesh/executor.py,sha256=KoFrDd-6XsN2IO-AnLKYiKYebMmN6KpbUVq8AWXUYVE,10926
86
+ annet/mesh/device_models.py,sha256=Jv6g9blnvTVxHGgHhnGd3b_8C3EHGpV7txyyR0Yt_Rg,3301
87
+ annet/mesh/executor.py,sha256=iDarSYfq9H6MSf-M1HkQOFIaUb8j-nG0sQNbgzq4XIo,13726
88
88
  annet/mesh/match_args.py,sha256=CR3kdIV9NGtyk9E2JbcOQ3TRuYEryTWP3m2yCo2VCWg,5751
89
- annet/mesh/models_converter.py,sha256=EdAQIkE-AfdCqMuzVQEdsG5lQdsaAwmqNqq9b8GRnGQ,2888
90
- annet/mesh/peer_models.py,sha256=sHny5nvLTWY2gqrq-SzZJ-lzEa90E_RCuBoiLb-Rub8,2129
91
- annet/mesh/registry.py,sha256=N2QyfyH0Qt73l16CO8VRKIBKHscBntEfS-j9B4xs-ys,6906
89
+ annet/mesh/models_converter.py,sha256=9K-B-Y5gQktr6ycHiCIM0OHsCM8yPUpO89AceU0Of3A,3168
90
+ annet/mesh/peer_models.py,sha256=T8sgKcg1a0BAkRVS5Kfc65CrlIB2AqQcwrehfEqWhLk,2572
91
+ annet/mesh/registry.py,sha256=G-FszWc_VKeN3eVpb-MRGbAGzbcSuEVAzbDC2k747XA,8611
92
92
  annet/rulebook/__init__.py,sha256=14IpOfTbeJtre7JKrfXVYiH0qAXsUSOL7AatUFmSQs0,3847
93
93
  annet/rulebook/common.py,sha256=zK1s2c5lc5HQbIlMUQ4HARQudXSgOYiZ_Sxc2I_tHqg,721
94
94
  annet/rulebook/deploying.py,sha256=XV0XQvc3YvwM8SOgOQlc-fCW4bnjQg_1CTZkTwMp14A,2972
@@ -150,12 +150,12 @@ annet_generators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
150
150
  annet_generators/example/__init__.py,sha256=1z030I00c9KeqW0ntkT1IRLYKD5LZAHYjiNHrOLen1Q,221
151
151
  annet_generators/example/lldp.py,sha256=24bGvShxbio-JxUdaehyPRu31LhH9YwSwFDrWVRn6yo,2100
152
152
  annet_generators/mesh_example/__init__.py,sha256=NfNWgXn1TNiWI6A5tmU6Y-4QV2i33td0Qs3re0MNNMo,218
153
- annet_generators/mesh_example/bgp.py,sha256=ck--yU_lUdAX9DWaaVQ-C29eQN-EGf1gEpOTBxmMnWA,1317
154
- annet_generators/mesh_example/mesh_logic.py,sha256=5Wt17LPt-EWe126IFAdaQQGWIOR3mxTJjAMUY8K0c0w,971
155
- annet-0.16.13.dist-info/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
156
- annet-0.16.13.dist-info/LICENSE,sha256=yPxl7dno02Pw7gAcFPIFONzx_gapwDoPXsIsh6Y7lC0,1079
157
- annet-0.16.13.dist-info/METADATA,sha256=cl7oaJlf7U2dc3ZZuRFwguqPK0XWbPaoLjdNmP52Mek,745
158
- annet-0.16.13.dist-info/WHEEL,sha256=a7TGlA-5DaHMRrarXjVbQagU3Man_dCnGIWMJr5kRWo,91
159
- annet-0.16.13.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
160
- annet-0.16.13.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
161
- annet-0.16.13.dist-info/RECORD,,
153
+ annet_generators/mesh_example/bgp.py,sha256=jzyDndSSGYyYBquDnLlR-7P5lzmUKcSyYCml3VsoMC0,1385
154
+ annet_generators/mesh_example/mesh_logic.py,sha256=DJS5JMCTs0rs0LN__0LulNgo2ekUcWiOMe02BlOeFas,1454
155
+ annet-0.16.15.dist-info/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
156
+ annet-0.16.15.dist-info/LICENSE,sha256=yPxl7dno02Pw7gAcFPIFONzx_gapwDoPXsIsh6Y7lC0,1079
157
+ annet-0.16.15.dist-info/METADATA,sha256=VI-njmyEaLFgh_aLn0WYEXAxnsims000_VWqTUu5ujc,745
158
+ annet-0.16.15.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
159
+ annet-0.16.15.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
160
+ annet-0.16.15.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
161
+ annet-0.16.15.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.4.0)
2
+ Generator: setuptools (75.5.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -21,6 +21,7 @@ class Bgp(PartialGenerator):
21
21
  res = executor.execute_for(device)
22
22
  yield f"bgp {res.global_options.local_as}"
23
23
  for peer in res.peers:
24
+ yield f" peer {peer.addr} interface {peer.interface}"
24
25
  if peer.group_name:
25
26
  yield f" peer {peer.addr} {peer.group_name}"
26
27
  if peer.remote_as is not None:
@@ -1,4 +1,5 @@
1
- from annet.mesh import Right, MeshRulesRegistry, GlobalOptions, MeshSession, DirectPeer
1
+ from annet.bgp_models import Redistribute, BFDTimers
2
+ from annet.mesh import Right, MeshRulesRegistry, GlobalOptions, MeshSession, DirectPeer, VirtualLocal, VirtualPeer
2
3
 
3
4
  registry = MeshRulesRegistry()
4
5
 
@@ -6,6 +7,9 @@ registry = MeshRulesRegistry()
6
7
  @registry.device("{name:.*}")
7
8
  def device_handler(global_opts: GlobalOptions):
8
9
  global_opts.local_as = 12345
10
+ global_opts.ipv4_unicast.redistributes = (Redistribute(
11
+ protocol="ipv4", policy="sss",
12
+ ),)
9
13
  global_opts.groups["GROP_NAME"].remote_as = 11111
10
14
 
11
15
 
@@ -16,6 +20,15 @@ def direct_handler(device: DirectPeer, neighbor: DirectPeer, session: MeshSessio
16
20
  neighbor.addr = f"192.168.1.{neighbor.match.x}"
17
21
 
18
22
 
23
+ @registry.virtual("{name:.*}", num=[1, 2, 3])
24
+ def virtual_handler(device: VirtualLocal, peer: VirtualPeer, session: MeshSession):
25
+ session.asnum = 12345
26
+ device.svi = 1
27
+ device.addr = "192.168.1.254"
28
+ device.listen_network = ["10.0.0.0/8"]
29
+ peer.addr = f"192.168.127.{peer.num}"
30
+
31
+
19
32
  @registry.direct("{name:.*}", "m9-sgw{x}.{domain:.*}", Right.x.in_([0, 1]))
20
33
  def direct_handler2(device: DirectPeer, neighbor: DirectPeer, session: MeshSession):
21
34
  session.asnum = 12345