annet 0.16.8__py3-none-any.whl → 0.16.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of annet might be problematic. Click here for more details.
- annet/adapters/fetchers/__init__.py +0 -0
- annet/adapters/fetchers/stub/__init__.py +0 -0
- annet/adapters/fetchers/stub/fetcher.py +19 -0
- annet/adapters/file/__init__.py +0 -0
- annet/adapters/file/provider.py +226 -0
- annet/adapters/netbox/common/manufacturer.py +2 -0
- annet/adapters/netbox/common/models.py +109 -4
- annet/adapters/netbox/v37/storage.py +31 -3
- annet/annlib/netdev/views/hardware.py +31 -0
- annet/bgp_models.py +266 -0
- annet/configs/context.yml +2 -12
- annet/configs/logging.yaml +1 -0
- annet/generators/__init__.py +18 -4
- annet/generators/jsonfragment.py +13 -12
- annet/mesh/__init__.py +16 -0
- annet/mesh/basemodel.py +180 -0
- annet/mesh/device_models.py +62 -0
- annet/mesh/executor.py +248 -0
- annet/mesh/match_args.py +165 -0
- annet/mesh/models_converter.py +84 -0
- annet/mesh/peer_models.py +98 -0
- annet/mesh/registry.py +212 -0
- annet/rulebook/routeros/__init__.py +0 -0
- annet/rulebook/routeros/file.py +5 -0
- annet/storage.py +41 -2
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/METADATA +3 -1
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/RECORD +36 -17
- annet_generators/example/__init__.py +1 -3
- annet_generators/mesh_example/__init__.py +9 -0
- annet_generators/mesh_example/bgp.py +43 -0
- annet_generators/mesh_example/mesh_logic.py +28 -0
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/AUTHORS +0 -0
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/LICENSE +0 -0
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/WHEEL +0 -0
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/entry_points.txt +0 -0
- {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/top_level.txt +0 -0
annet/bgp_models.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Literal, Union, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ASN(int):
|
|
6
|
+
"""
|
|
7
|
+
Stores ASN number and formats it as в AS1.AS2
|
|
8
|
+
None is treated like 0. Supports integer operations
|
|
9
|
+
Supported formats: https://tools.ietf.org/html/rfc5396#section-1
|
|
10
|
+
"""
|
|
11
|
+
PLAIN_MAX = 0x10000
|
|
12
|
+
|
|
13
|
+
def __new__(cls, asn: Union[int, str, None, "ASN"]):
|
|
14
|
+
if isinstance(asn, ASN):
|
|
15
|
+
return asn
|
|
16
|
+
elif asn is None:
|
|
17
|
+
asn = 0
|
|
18
|
+
elif not isinstance(asn, int):
|
|
19
|
+
if isinstance(asn, str) and "." in asn:
|
|
20
|
+
high, low = [int(token) for token in asn.split(".")]
|
|
21
|
+
if not (0 <= high < ASN.PLAIN_MAX and 0 <= low < ASN.PLAIN_MAX):
|
|
22
|
+
raise ValueError("Invalid ASN asn %r" % asn)
|
|
23
|
+
asn = (high << 16) + low
|
|
24
|
+
asn = int(asn)
|
|
25
|
+
if not 0 <= asn <= 0xffffffff:
|
|
26
|
+
raise ValueError("Invalid ASN asn %r" % asn)
|
|
27
|
+
return int.__new__(cls, asn)
|
|
28
|
+
|
|
29
|
+
def __add__(self, other: int):
|
|
30
|
+
return ASN(super().__add__(other))
|
|
31
|
+
|
|
32
|
+
def __sub__(self, other: int):
|
|
33
|
+
return ASN(super().__sub__(other))
|
|
34
|
+
|
|
35
|
+
def __mul__(self, other: int):
|
|
36
|
+
return ASN(super().__mul__(other))
|
|
37
|
+
|
|
38
|
+
def is_plain(self) -> bool:
|
|
39
|
+
return self < ASN.PLAIN_MAX
|
|
40
|
+
|
|
41
|
+
def asdot(self) -> str:
|
|
42
|
+
if not self.is_plain():
|
|
43
|
+
return "%d.%d" % (self // ASN.PLAIN_MAX, self % ASN.PLAIN_MAX)
|
|
44
|
+
return "%d" % self
|
|
45
|
+
|
|
46
|
+
def asplain(self) -> str:
|
|
47
|
+
return "%d" % self
|
|
48
|
+
|
|
49
|
+
def asdot_high(self) -> int:
|
|
50
|
+
return self // ASN.PLAIN_MAX
|
|
51
|
+
|
|
52
|
+
def asdot_low(self) -> int:
|
|
53
|
+
return self % ASN.PLAIN_MAX
|
|
54
|
+
|
|
55
|
+
__str__ = asdot
|
|
56
|
+
|
|
57
|
+
def __repr__(self) -> str:
|
|
58
|
+
srepr = str(self)
|
|
59
|
+
if "." in srepr:
|
|
60
|
+
srepr = repr(srepr)
|
|
61
|
+
return f"{self.__class__.__name__}({srepr})"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class BFDTimers:
|
|
66
|
+
minimum_interval: int = 500
|
|
67
|
+
multiplier: int = 4
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
Family = Literal["ipv4_unicast", "ipv6_unicast", "ipv4_labeled", "ipv6_labeled"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class PeerOptions:
|
|
75
|
+
"""The same options as for group but any field is optional"""
|
|
76
|
+
local_as: Optional[ASN] = None
|
|
77
|
+
unnumbered: Optional[bool] = None
|
|
78
|
+
rr_client: Optional[bool] = None
|
|
79
|
+
next_hop_self: Optional[bool] = None
|
|
80
|
+
extended_next_hop: Optional[bool] = None
|
|
81
|
+
send_community: Optional[bool] = None
|
|
82
|
+
send_lcommunity: Optional[bool] = None
|
|
83
|
+
send_extcommunity: Optional[bool] = None
|
|
84
|
+
send_labeled: Optional[bool] = None
|
|
85
|
+
import_limit: Optional[bool] = None
|
|
86
|
+
teardown_timeout: Optional[bool] = None
|
|
87
|
+
redistribute: Optional[bool] = None
|
|
88
|
+
passive: Optional[bool] = None
|
|
89
|
+
mtu_discovery: Optional[bool] = None
|
|
90
|
+
advertise_inactive: Optional[bool] = None
|
|
91
|
+
advertise_bgp_static: Optional[bool] = None
|
|
92
|
+
allowas_in: Optional[bool] = None
|
|
93
|
+
auth_key: Optional[bool] = None
|
|
94
|
+
add_path: Optional[bool] = None
|
|
95
|
+
multipath: Optional[bool] = None
|
|
96
|
+
multihop: Optional[bool] = None
|
|
97
|
+
multihop_no_nexthop_change: Optional[bool] = None
|
|
98
|
+
af_no_install: Optional[bool] = None
|
|
99
|
+
bfd: Optional[bool] = None
|
|
100
|
+
rib: Optional[bool] = None
|
|
101
|
+
bfd_timers: Optional[BFDTimers] = None
|
|
102
|
+
resolve_vpn: Optional[bool] = None
|
|
103
|
+
af_rib_group: Optional[str] = None
|
|
104
|
+
af_loops: Optional[int] = None
|
|
105
|
+
hold_time: Optional[int] = None
|
|
106
|
+
listen_network: Optional[bool] = None
|
|
107
|
+
remove_private: Optional[bool] = None
|
|
108
|
+
as_override: Optional[bool] = None
|
|
109
|
+
aigp: Optional[bool] = None
|
|
110
|
+
bmp_monitor: Optional[bool] = None
|
|
111
|
+
no_prepend: Optional[bool] = None
|
|
112
|
+
no_explicit_null: Optional[bool] = None
|
|
113
|
+
uniq_iface: Optional[bool] = None
|
|
114
|
+
advertise_peer_as: Optional[bool] = None
|
|
115
|
+
connect_retry: Optional[bool] = None
|
|
116
|
+
advertise_external: Optional[bool] = None
|
|
117
|
+
advertise_irb: Optional[bool] = None
|
|
118
|
+
listen_only: Optional[bool] = None
|
|
119
|
+
soft_reconfiguration_inbound: Optional[bool] = None
|
|
120
|
+
not_active: Optional[bool] = None
|
|
121
|
+
mtu: Optional[int] = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class Peer:
|
|
126
|
+
addr: str
|
|
127
|
+
interface: Optional[str]
|
|
128
|
+
remote_as: ASN
|
|
129
|
+
families: set[Family] = field(default_factory=set)
|
|
130
|
+
description: str = ""
|
|
131
|
+
vrf_name: str = ""
|
|
132
|
+
group_name: str = ""
|
|
133
|
+
import_policy: str = ""
|
|
134
|
+
export_policy: str = ""
|
|
135
|
+
update_source: Optional[str] = None
|
|
136
|
+
options: Optional[PeerOptions] = None
|
|
137
|
+
hostname: str = ""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class Aggregate:
|
|
142
|
+
policy: str = ""
|
|
143
|
+
routes: tuple[str, ...] = () # "182.168.1.0/24",
|
|
144
|
+
export_policy: str = ""
|
|
145
|
+
as_path: str = ""
|
|
146
|
+
reference: str = ""
|
|
147
|
+
suppress: bool = False
|
|
148
|
+
discard: bool = True
|
|
149
|
+
as_set: bool = False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class Redistribute:
|
|
154
|
+
protocol: str
|
|
155
|
+
policy: str = ""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class FamilyOptions:
|
|
160
|
+
family: Family
|
|
161
|
+
vrf_name: str
|
|
162
|
+
multipath: int = 0
|
|
163
|
+
global_multipath: int = 0
|
|
164
|
+
aggregate: Aggregate = field(default_factory=Aggregate)
|
|
165
|
+
redistributes: tuple[Redistribute, ...] = ()
|
|
166
|
+
allow_default: bool = False
|
|
167
|
+
aspath_relax: bool = False
|
|
168
|
+
igp_ignore: bool = False
|
|
169
|
+
next_hop_policy: bool = False
|
|
170
|
+
rib_import_policy: bool = False
|
|
171
|
+
advertise_l2vpn_evpn: bool = False
|
|
172
|
+
rib_group: bool = False
|
|
173
|
+
loops: int = 0
|
|
174
|
+
advertise_bgp_static: bool = False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class PeerGroup:
|
|
179
|
+
name: str
|
|
180
|
+
remote_as: ASN = ASN(None)
|
|
181
|
+
internal_name: str = ""
|
|
182
|
+
description: str = ""
|
|
183
|
+
update_source: str = ""
|
|
184
|
+
|
|
185
|
+
# more strict version of PeerOptions
|
|
186
|
+
local_as: ASN = ASN(None)
|
|
187
|
+
unnumbered: bool = False
|
|
188
|
+
rr_client: bool = False
|
|
189
|
+
next_hop_self: bool = False
|
|
190
|
+
extended_next_hop: bool = False
|
|
191
|
+
send_community: bool = False
|
|
192
|
+
send_lcommunity: bool = False
|
|
193
|
+
send_extcommunity: bool = False
|
|
194
|
+
send_labeled: bool = False
|
|
195
|
+
import_limit: bool = False
|
|
196
|
+
teardown_timeout: bool = False
|
|
197
|
+
redistribute: bool = False
|
|
198
|
+
passive: bool = False
|
|
199
|
+
mtu_discovery: bool = False
|
|
200
|
+
advertise_inactive: bool = False
|
|
201
|
+
advertise_bgp_static: bool = False
|
|
202
|
+
allowas_in: bool = False
|
|
203
|
+
auth_key: bool = False
|
|
204
|
+
add_path: bool = False
|
|
205
|
+
multipath: bool = False
|
|
206
|
+
multihop: bool = False
|
|
207
|
+
multihop_no_nexthop_change: bool = False
|
|
208
|
+
af_no_install: bool = False
|
|
209
|
+
bfd: bool = False
|
|
210
|
+
rib: bool = False
|
|
211
|
+
bfd_timers: Optional[BFDTimers] = None
|
|
212
|
+
resolve_vpn: bool = False
|
|
213
|
+
af_rib_group: Optional[str] = None
|
|
214
|
+
af_loops: int = 0
|
|
215
|
+
hold_time: int = 0
|
|
216
|
+
listen_network: bool = False
|
|
217
|
+
remove_private: bool = False
|
|
218
|
+
as_override: bool = False
|
|
219
|
+
aigp: bool = False
|
|
220
|
+
bmp_monitor: bool = False
|
|
221
|
+
no_prepend: bool = False
|
|
222
|
+
no_explicit_null: bool = False
|
|
223
|
+
uniq_iface: bool = False
|
|
224
|
+
advertise_peer_as: bool = False
|
|
225
|
+
connect_retry: bool = False
|
|
226
|
+
advertise_external: bool = False
|
|
227
|
+
advertise_irb: bool = False
|
|
228
|
+
listen_only: bool = False
|
|
229
|
+
soft_reconfiguration_inbound: bool = False
|
|
230
|
+
not_active: bool = False
|
|
231
|
+
mtu: int = 0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class VrfOptions:
|
|
236
|
+
vrf_name: str
|
|
237
|
+
vrf_name_global: Optional[str] = None
|
|
238
|
+
import_policy: Optional[str] = None
|
|
239
|
+
export_policy: Optional[str] = None
|
|
240
|
+
rt_import: list[str] = field(default_factory=list)
|
|
241
|
+
rt_export: list[str] = field(default_factory=list)
|
|
242
|
+
rt_import_v4: list[str] = field(default_factory=list)
|
|
243
|
+
rt_export_v4: list[str] = field(default_factory=list)
|
|
244
|
+
route_distinguisher: Optional[str] = None
|
|
245
|
+
static_label: Optional[int] = None # FIXME: str?
|
|
246
|
+
|
|
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
|
+
groups: list[PeerGroup] = field(default_factory=list)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass
|
|
255
|
+
class GlobalOptions:
|
|
256
|
+
local_as: ASN = ASN(None)
|
|
257
|
+
loops: int = 0
|
|
258
|
+
multipath: int = 0
|
|
259
|
+
router_id: str = ""
|
|
260
|
+
vrf: dict[str, VrfOptions] = field(default_factory=dict)
|
|
261
|
+
|
|
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
|
+
groups: list[PeerGroup] = field(default_factory=list)
|
annet/configs/context.yml
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
|
-
connection:
|
|
2
|
-
default:
|
|
3
|
-
login: ~
|
|
4
|
-
passwords: ~
|
|
5
|
-
enable_ssh_conf: false
|
|
6
|
-
no_nocauth: false
|
|
7
|
-
ssh_forward_agent: ~
|
|
8
|
-
tunnel: ~
|
|
9
1
|
generators:
|
|
10
2
|
default:
|
|
11
3
|
- annet_generators.example
|
|
4
|
+
- annet_generators.mesh_example
|
|
12
5
|
|
|
13
6
|
storage:
|
|
14
7
|
default:
|
|
15
|
-
adapter:
|
|
16
|
-
params:
|
|
17
|
-
path: /path/to/file
|
|
8
|
+
adapter: netbox
|
|
18
9
|
|
|
19
10
|
context:
|
|
20
11
|
default:
|
|
21
|
-
connection: default
|
|
22
12
|
generators: default
|
|
23
13
|
storage: default
|
|
24
14
|
|
annet/configs/logging.yaml
CHANGED
annet/generators/__init__.py
CHANGED
|
@@ -2,7 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
import importlib
|
|
5
|
+
import importlib.machinery
|
|
5
6
|
import os
|
|
7
|
+
import re
|
|
6
8
|
import textwrap
|
|
7
9
|
from collections import OrderedDict as odict
|
|
8
10
|
from typing import (
|
|
@@ -379,7 +381,7 @@ def _run_json_fragment_generator(
|
|
|
379
381
|
def _get_generators(module_paths: Union[List[str], dict], storage, device=None):
|
|
380
382
|
if isinstance(module_paths, dict):
|
|
381
383
|
if device is None:
|
|
382
|
-
module_paths = module_paths.get("default")
|
|
384
|
+
module_paths = module_paths.get("default", [])
|
|
383
385
|
else:
|
|
384
386
|
modules = []
|
|
385
387
|
seen = set()
|
|
@@ -396,22 +398,34 @@ def _get_generators(module_paths: Union[List[str], dict], storage, device=None):
|
|
|
396
398
|
if module not in seen:
|
|
397
399
|
modules.append(module)
|
|
398
400
|
seen.add(module)
|
|
399
|
-
module_paths = modules or module_paths.get("default")
|
|
401
|
+
module_paths = modules or module_paths.get("default", [])
|
|
400
402
|
res_generators = []
|
|
401
403
|
for module_path in module_paths:
|
|
402
|
-
module =
|
|
404
|
+
module = _load_gen_module(module_path)
|
|
403
405
|
if hasattr(module, "get_generators"):
|
|
404
406
|
generators: List[BaseGenerator] = module.get_generators(storage)
|
|
405
407
|
res_generators += generators
|
|
406
408
|
return res_generators
|
|
407
409
|
|
|
408
410
|
|
|
411
|
+
def _load_gen_module(module_path: str):
|
|
412
|
+
try:
|
|
413
|
+
module = importlib.import_module(module_path)
|
|
414
|
+
except ModuleNotFoundError as e:
|
|
415
|
+
try: # maybe it's a path to module
|
|
416
|
+
module_abs_path = os.path.abspath(module_path)
|
|
417
|
+
module = importlib.machinery.SourceFileLoader(re.sub(r"[./]", "_", module_abs_path).strip("_"), module_abs_path).load_module()
|
|
418
|
+
except ModuleNotFoundError:
|
|
419
|
+
raise e
|
|
420
|
+
return module
|
|
421
|
+
|
|
422
|
+
|
|
409
423
|
def _get_ref_generators(module_paths: List[str], storage, device):
|
|
410
424
|
if isinstance(module_paths, dict):
|
|
411
425
|
module_paths = module_paths.get("default")
|
|
412
426
|
res_generators = []
|
|
413
427
|
for module_path in module_paths:
|
|
414
|
-
module =
|
|
428
|
+
module = _load_gen_module(module_path)
|
|
415
429
|
if hasattr(module, "get_ref_generators"):
|
|
416
430
|
res_generators += module.get_ref_generators(storage)
|
|
417
431
|
return res_generators
|
annet/generators/jsonfragment.py
CHANGED
|
@@ -86,9 +86,12 @@ class JSONFragment(TreeGenerator):
|
|
|
86
86
|
self._config_pointer.pop()
|
|
87
87
|
|
|
88
88
|
def __call__(self, device: Device, annotate: bool = False):
|
|
89
|
-
|
|
90
|
-
self.
|
|
91
|
-
|
|
89
|
+
try:
|
|
90
|
+
for cfg_fragment in self.run(device):
|
|
91
|
+
self._set_or_replace_dict(self._config_pointer, cfg_fragment)
|
|
92
|
+
return self._json_config
|
|
93
|
+
finally:
|
|
94
|
+
self._json_config = {}
|
|
92
95
|
|
|
93
96
|
def _set_or_replace_dict(self, pointer, value):
|
|
94
97
|
if not pointer:
|
|
@@ -99,27 +102,25 @@ class JSONFragment(TreeGenerator):
|
|
|
99
102
|
else:
|
|
100
103
|
self._set_dict(self._json_config, pointer, value)
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
def _to_str(cls, value: Any) -> str:
|
|
105
|
+
def process_value(self, value: Any) -> Any:
|
|
104
106
|
if isinstance(value, str):
|
|
105
107
|
return value
|
|
106
108
|
elif isinstance(value, list):
|
|
107
|
-
return [
|
|
109
|
+
return [self.process_value(x) for x in value]
|
|
108
110
|
elif isinstance(value, dict):
|
|
109
111
|
for k, v in value.items():
|
|
110
|
-
value[k] =
|
|
112
|
+
value[k] = self.process_value(v)
|
|
111
113
|
return value
|
|
112
114
|
return str(value)
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
def _set_dict(cls, cfg, pointer, value):
|
|
116
|
+
def _set_dict(self, cfg, pointer, value):
|
|
116
117
|
# pointer has at least one key
|
|
117
118
|
if len(pointer) == 1:
|
|
118
119
|
if pointer[0] in cfg:
|
|
119
|
-
cfg[pointer[0]] = [cfg[pointer[0]],
|
|
120
|
+
cfg[pointer[0]] = [cfg[pointer[0]], self.process_value(value)]
|
|
120
121
|
else:
|
|
121
|
-
cfg[pointer[0]] =
|
|
122
|
+
cfg[pointer[0]] = self.process_value(value)
|
|
122
123
|
else:
|
|
123
124
|
if pointer[0] not in cfg:
|
|
124
125
|
cfg[pointer[0]] = {}
|
|
125
|
-
|
|
126
|
+
self._set_dict(cfg[pointer[0]], pointer[1:], self.process_value(value))
|
annet/mesh/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"GlobalOptions",
|
|
3
|
+
"MeshSession",
|
|
4
|
+
"DirectPeer",
|
|
5
|
+
"IndirectPeer",
|
|
6
|
+
"MeshExecutor",
|
|
7
|
+
"MeshRulesRegistry",
|
|
8
|
+
"Left",
|
|
9
|
+
"Right",
|
|
10
|
+
"Match",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
from .executor import MeshExecutor
|
|
14
|
+
from .match_args import Left, Right, Match
|
|
15
|
+
from .registry import DirectPeer, IndirectPeer, MeshSession, GlobalOptions
|
|
16
|
+
from .registry import MeshRulesRegistry
|
annet/mesh/basemodel.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from copy import copy
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import (
|
|
5
|
+
TypeVar, Any, Annotated, get_origin, get_type_hints, get_args, Callable, Union, ClassVar, overload,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MergeForbiddenError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Special(Enum):
|
|
14
|
+
NOT_SET = "<NOT SET>"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Merger(ABC):
|
|
21
|
+
def __call__(self, name: str, x: Union[T, Special], y: Union[T, Special]) -> Union[T, Special]:
|
|
22
|
+
if x is Special.NOT_SET:
|
|
23
|
+
return y
|
|
24
|
+
if y is Special.NOT_SET:
|
|
25
|
+
return x
|
|
26
|
+
return self._merge(name, x, y)
|
|
27
|
+
|
|
28
|
+
def _merge(self, name: str, x: T, y: T) -> T:
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UseFirst(Merger):
|
|
33
|
+
def _merge(self, name: str, x: T, y: T) -> T:
|
|
34
|
+
return x
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UseLast(Merger):
|
|
38
|
+
def _merge(self, name: str, x: T, y: T) -> T:
|
|
39
|
+
return y
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Forbid(Merger):
|
|
43
|
+
def _merge(self, name: str, x: T, y: T) -> T:
|
|
44
|
+
raise MergeForbiddenError(f"Override is forbidden for field {name}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ForbidChange(Merger):
|
|
48
|
+
def _merge(self, name: str, x: T, y: T) -> T:
|
|
49
|
+
if x == y:
|
|
50
|
+
return x
|
|
51
|
+
raise MergeForbiddenError(
|
|
52
|
+
f"Override with different value is forbidden for field {name}:\n"
|
|
53
|
+
f"Old: {x}\n"
|
|
54
|
+
f"New: {y}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Concat(Merger):
|
|
59
|
+
def _merge(self, name: str, x: T, y: T) -> T:
|
|
60
|
+
return x + y # type: ignore[operator]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Merge(Merger):
|
|
64
|
+
def _merge(self, name: str, x: "ModelT", y: "ModelT") -> "ModelT": # type: ignore[override]
|
|
65
|
+
return merge(x, y)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DictMerge(Merger):
|
|
69
|
+
def __init__(self, value_merger: Merger = Forbid()):
|
|
70
|
+
self.value_merger = value_merger
|
|
71
|
+
|
|
72
|
+
def _merge(self, name: str, x: dict, y: dict) -> dict: # type: ignore[override]
|
|
73
|
+
result = copy(x)
|
|
74
|
+
for key, value in y.items():
|
|
75
|
+
if key in result:
|
|
76
|
+
result[key] = self.value_merger(key, result[key], value)
|
|
77
|
+
else:
|
|
78
|
+
result[key] = value
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ApplyFunc(Merger):
|
|
83
|
+
def __init__(self, func: Callable):
|
|
84
|
+
self.func = func
|
|
85
|
+
|
|
86
|
+
def __call__(self, name: str, x: Union[T, Special], y: Union[T, Special]) -> Union[T, Special]:
|
|
87
|
+
if x is Special.NOT_SET:
|
|
88
|
+
return y
|
|
89
|
+
if y is Special.NOT_SET:
|
|
90
|
+
return x
|
|
91
|
+
return self.func(x, y)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_merger(hint: Any):
|
|
95
|
+
if get_origin(hint) is not Annotated:
|
|
96
|
+
return ForbidChange()
|
|
97
|
+
for arg in get_args(hint):
|
|
98
|
+
if isinstance(arg, Merger):
|
|
99
|
+
return arg
|
|
100
|
+
return ForbidChange()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class BaseMeshModel:
|
|
104
|
+
_field_mergers: ClassVar[dict[str, Merger]]
|
|
105
|
+
|
|
106
|
+
def __init__(self, **kwargs):
|
|
107
|
+
if extra_keys := (kwargs.keys() - self._field_mergers.keys()):
|
|
108
|
+
raise ValueError(f"Extra arguments: {extra_keys}")
|
|
109
|
+
self.__dict__.update(kwargs)
|
|
110
|
+
|
|
111
|
+
def unset_attrs(self):
|
|
112
|
+
return type(self)._field_mergers.keys() - vars(self).keys()
|
|
113
|
+
|
|
114
|
+
def __repr__(self):
|
|
115
|
+
return f"{self.__class__.__name__}(" + ", ".join(
|
|
116
|
+
f"{key}={value}" for key, value in vars(self).items()
|
|
117
|
+
) + ")"
|
|
118
|
+
|
|
119
|
+
def __init_subclass__(cls, **kwargs):
|
|
120
|
+
cls._field_mergers = {
|
|
121
|
+
field: _get_merger(hint)
|
|
122
|
+
for field, hint in get_type_hints(cls, include_extras=True).items()
|
|
123
|
+
if get_origin(hint) is not ClassVar
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def __setattr__(self, key, value):
|
|
127
|
+
if key not in self._field_mergers:
|
|
128
|
+
raise AttributeError(f"{self.__class__.__name__} has no field {key}")
|
|
129
|
+
super().__setattr__(key, value)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
ModelT = TypeVar("ModelT", bound=BaseMeshModel)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _merge(a: ModelT, b: BaseMeshModel) -> ModelT:
|
|
136
|
+
result = copy(a)
|
|
137
|
+
for attr_name, merger in a._field_mergers.items():
|
|
138
|
+
new_value = merger(
|
|
139
|
+
attr_name,
|
|
140
|
+
getattr(a, attr_name, Special.NOT_SET),
|
|
141
|
+
getattr(b, attr_name, Special.NOT_SET),
|
|
142
|
+
)
|
|
143
|
+
if new_value is not Special.NOT_SET:
|
|
144
|
+
setattr(result, attr_name, new_value)
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@overload
|
|
149
|
+
def merge(first: Special, second: ModelT, /, *others: BaseMeshModel) -> ModelT:
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@overload
|
|
154
|
+
def merge(first: ModelT, /, *others: BaseMeshModel) -> ModelT:
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@overload
|
|
159
|
+
def merge(first: Special, /) -> Special:
|
|
160
|
+
...
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def merge(first: Any, /, *others: Any) -> Any:
|
|
164
|
+
if first is Special.NOT_SET:
|
|
165
|
+
if not others:
|
|
166
|
+
return Special.NOT_SET
|
|
167
|
+
return merge(*others)
|
|
168
|
+
for second in others:
|
|
169
|
+
first = _merge(first, second)
|
|
170
|
+
return first
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class KeyDefaultDict(dict):
|
|
174
|
+
def __init__(self, factory: Callable[[str], Any]):
|
|
175
|
+
super().__init__()
|
|
176
|
+
self.factory = factory
|
|
177
|
+
|
|
178
|
+
def __missing__(self, key):
|
|
179
|
+
x = self[key] = self.factory(key)
|
|
180
|
+
return x
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Annotated, Optional, Union
|
|
2
|
+
|
|
3
|
+
from annet.bgp_models import Family, Aggregate, Redistribute
|
|
4
|
+
from .basemodel import BaseMeshModel, Concat, DictMerge, Merge, KeyDefaultDict
|
|
5
|
+
from .peer_models import MeshPeerGroup
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FamilyOptions(BaseMeshModel):
|
|
9
|
+
family: Family
|
|
10
|
+
vrf_name: str
|
|
11
|
+
multipath: int = 0
|
|
12
|
+
global_multipath: int
|
|
13
|
+
aggregate: Aggregate
|
|
14
|
+
redistributes: Annotated[tuple[Redistribute, ...], Concat()]
|
|
15
|
+
allow_default: bool
|
|
16
|
+
aspath_relax: bool
|
|
17
|
+
igp_ignore: bool
|
|
18
|
+
next_hop_policy: bool
|
|
19
|
+
rib_import_policy: bool
|
|
20
|
+
advertise_l2vpn_evpn: bool
|
|
21
|
+
rib_group: bool
|
|
22
|
+
loops: int
|
|
23
|
+
advertise_bgp_static: bool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _FamiliesMixin:
|
|
27
|
+
ipv4_unicast: Optional[FamilyOptions]
|
|
28
|
+
ipv6_unicast: Optional[FamilyOptions]
|
|
29
|
+
ipv4_labeled_unicast: Optional[FamilyOptions]
|
|
30
|
+
ipv6_labeled_unicast: Optional[FamilyOptions]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VrfOptions(BaseMeshModel, _FamiliesMixin):
|
|
34
|
+
def __init__(self, **kwargs):
|
|
35
|
+
kwargs.setdefault("groups", KeyDefaultDict(lambda x: MeshPeerGroup(name=x)))
|
|
36
|
+
super().__init__(**kwargs)
|
|
37
|
+
|
|
38
|
+
vrf_name: str
|
|
39
|
+
vrf_name_global: Optional[str]
|
|
40
|
+
import_policy: Optional[str]
|
|
41
|
+
export_policy: Optional[str]
|
|
42
|
+
rt_import: Annotated[tuple[str, ...], Concat()]
|
|
43
|
+
rt_export: Annotated[tuple[str, ...], Concat()]
|
|
44
|
+
rt_import_v4: Annotated[tuple[str, ...], Concat()]
|
|
45
|
+
rt_export_v4: Annotated[tuple[str, ...], Concat()]
|
|
46
|
+
route_distinguisher: Optional[str]
|
|
47
|
+
static_label: Optional[int] # FIXME: str?
|
|
48
|
+
groups: Annotated[dict[str, MeshPeerGroup], DictMerge(Merge())]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GlobalOptionsDTO(BaseMeshModel, _FamiliesMixin):
|
|
52
|
+
def __init__(self, **kwargs):
|
|
53
|
+
kwargs.setdefault("groups", KeyDefaultDict(lambda x: MeshPeerGroup(name=x)))
|
|
54
|
+
kwargs.setdefault("vrf", KeyDefaultDict(lambda x: VrfOptions(vrf_name=x)))
|
|
55
|
+
super().__init__(**kwargs)
|
|
56
|
+
|
|
57
|
+
local_as: Union[int, str]
|
|
58
|
+
loops: int
|
|
59
|
+
multipath: int
|
|
60
|
+
router_id: str
|
|
61
|
+
vrf: Annotated[dict[str, VrfOptions], DictMerge(Merge())]
|
|
62
|
+
groups: Annotated[dict[str, MeshPeerGroup], DictMerge(Merge())]
|