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.

Files changed (36) hide show
  1. annet/adapters/fetchers/__init__.py +0 -0
  2. annet/adapters/fetchers/stub/__init__.py +0 -0
  3. annet/adapters/fetchers/stub/fetcher.py +19 -0
  4. annet/adapters/file/__init__.py +0 -0
  5. annet/adapters/file/provider.py +226 -0
  6. annet/adapters/netbox/common/manufacturer.py +2 -0
  7. annet/adapters/netbox/common/models.py +109 -4
  8. annet/adapters/netbox/v37/storage.py +31 -3
  9. annet/annlib/netdev/views/hardware.py +31 -0
  10. annet/bgp_models.py +266 -0
  11. annet/configs/context.yml +2 -12
  12. annet/configs/logging.yaml +1 -0
  13. annet/generators/__init__.py +18 -4
  14. annet/generators/jsonfragment.py +13 -12
  15. annet/mesh/__init__.py +16 -0
  16. annet/mesh/basemodel.py +180 -0
  17. annet/mesh/device_models.py +62 -0
  18. annet/mesh/executor.py +248 -0
  19. annet/mesh/match_args.py +165 -0
  20. annet/mesh/models_converter.py +84 -0
  21. annet/mesh/peer_models.py +98 -0
  22. annet/mesh/registry.py +212 -0
  23. annet/rulebook/routeros/__init__.py +0 -0
  24. annet/rulebook/routeros/file.py +5 -0
  25. annet/storage.py +41 -2
  26. {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/METADATA +3 -1
  27. {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/RECORD +36 -17
  28. annet_generators/example/__init__.py +1 -3
  29. annet_generators/mesh_example/__init__.py +9 -0
  30. annet_generators/mesh_example/bgp.py +43 -0
  31. annet_generators/mesh_example/mesh_logic.py +28 -0
  32. {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/AUTHORS +0 -0
  33. {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/LICENSE +0 -0
  34. {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/WHEEL +0 -0
  35. {annet-0.16.8.dist-info → annet-0.16.10.dist-info}/entry_points.txt +0 -0
  36. {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: file
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
 
@@ -7,6 +7,7 @@ formatters:
7
7
  style: "{"
8
8
  datefmt: "%H:%M:%S"
9
9
  format: "[{asctime}] {log_color}{levelname:>7}{reset} {processName} - {pathname}:{lineno} - {message} -- {cyan}{_extra}{reset}"
10
+ max_vars_lines: 20
10
11
  host_progress:
11
12
  (): contextlog.SmartFormatter
12
13
  style: "{"
@@ -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 = importlib.import_module(module_path)
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 = importlib.import_module(module_path)
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
@@ -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
- for cfg_fragment in self.run(device):
90
- self._set_or_replace_dict(self._config_pointer, cfg_fragment)
91
- return self._json_config
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
- @classmethod
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 [cls._to_str(x) for x in value]
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] = cls._to_str(v)
112
+ value[k] = self.process_value(v)
111
113
  return value
112
114
  return str(value)
113
115
 
114
- @classmethod
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]], cls._to_str(value)]
120
+ cfg[pointer[0]] = [cfg[pointer[0]], self.process_value(value)]
120
121
  else:
121
- cfg[pointer[0]] = cls._to_str(value)
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
- cls._set_dict(cfg[pointer[0]], pointer[1:], cls._to_str(value))
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
@@ -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())]