annet 0.16.17__py3-none-any.whl → 0.16.19__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/netbox/common/manufacturer.py +4 -1
- annet/annlib/netdev/devdb/data/devdb.json +1 -0
- annet/annlib/rbparser/platform.py +6 -0
- annet/annlib/rulebook/common.py +4 -0
- annet/annlib/tabparser.py +57 -2
- annet/configs/context.yml +1 -0
- annet/rpl/__init__.py +27 -0
- annet/rpl/action.py +51 -0
- annet/rpl/condition.py +94 -0
- annet/rpl/match_builder.py +103 -0
- annet/rpl/policy.py +22 -0
- annet/rpl/result.py +8 -0
- annet/rpl/routemap.py +76 -0
- annet/rpl/statement_builder.py +267 -0
- annet/rulebook/__init__.py +3 -2
- annet/rulebook/cisco/misc.py +0 -90
- annet/rulebook/texts/arista.rul +1 -0
- annet/rulebook/texts/cisco.rul +1 -1
- annet/tabparser.py +2 -0
- {annet-0.16.17.dist-info → annet-0.16.19.dist-info}/METADATA +1 -1
- {annet-0.16.17.dist-info → annet-0.16.19.dist-info}/RECORD +30 -18
- annet_generators/rpl_example/__init__.py +9 -0
- annet_generators/rpl_example/items.py +31 -0
- annet_generators/rpl_example/policy_generator.py +233 -0
- annet_generators/rpl_example/route_policy.py +33 -0
- {annet-0.16.17.dist-info → annet-0.16.19.dist-info}/AUTHORS +0 -0
- {annet-0.16.17.dist-info → annet-0.16.19.dist-info}/LICENSE +0 -0
- {annet-0.16.17.dist-info → annet-0.16.19.dist-info}/WHEEL +0 -0
- {annet-0.16.17.dist-info → annet-0.16.19.dist-info}/entry_points.txt +0 -0
- {annet-0.16.17.dist-info → annet-0.16.19.dist-info}/top_level.txt +0 -0
|
@@ -17,7 +17,8 @@ _VENDORS = {
|
|
|
17
17
|
"aruba": "Aruba",
|
|
18
18
|
"routeros": "RouterOS",
|
|
19
19
|
"ribbon": "Ribbon",
|
|
20
|
-
"b4com": "B4com"
|
|
20
|
+
"b4com": "B4com",
|
|
21
|
+
"h3c": "H3C",
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
|
|
@@ -37,6 +38,8 @@ def get_breed(manufacturer: str, model: str):
|
|
|
37
38
|
return "vrp85"
|
|
38
39
|
elif manufacturer == "Huawei":
|
|
39
40
|
return "vrp55"
|
|
41
|
+
elif manufacturer == "H3C":
|
|
42
|
+
return "h3c"
|
|
40
43
|
elif manufacturer in ("Mellanox", "NVIDIA"):
|
|
41
44
|
return "cuml2"
|
|
42
45
|
elif manufacturer == "Juniper":
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"Huawei.CE.CE8800.CE8850": " CE8850",
|
|
54
54
|
"Huawei.CE.CE8800.CE8851": " CE8851",
|
|
55
55
|
"Huawei.CE.CE8800.CE8855": " CE8855",
|
|
56
|
+
"Huawei.CE.CE8800.CE8875": " CE8875",
|
|
56
57
|
"Huawei.CE.CE9800": " CE98\\d\\d",
|
|
57
58
|
"Huawei.CE.CE9800.CE9855": " CE9855",
|
|
58
59
|
"Huawei.CE.CE9800.CE9860": " CE9860",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
VENDOR_REVERSES = {
|
|
2
2
|
"huawei": "undo",
|
|
3
|
+
"h3c": "undo",
|
|
3
4
|
"optixtrans": "undo",
|
|
4
5
|
"cisco": "no",
|
|
5
6
|
"nexus": "no",
|
|
@@ -45,6 +46,7 @@ VENDOR_DIFF_ORDERED = {
|
|
|
45
46
|
|
|
46
47
|
VENDOR_EXIT = {
|
|
47
48
|
"huawei": "quit",
|
|
49
|
+
"h3c": "quit",
|
|
48
50
|
"optixtrans": "quit",
|
|
49
51
|
"cisco": "exit",
|
|
50
52
|
"nexus": "exit",
|
|
@@ -57,3 +59,7 @@ VENDOR_EXIT = {
|
|
|
57
59
|
"ribbon": "exit",
|
|
58
60
|
"b4com": "exit",
|
|
59
61
|
}
|
|
62
|
+
|
|
63
|
+
VENDOR_ALIASES = {
|
|
64
|
+
"h3c": "huawei",
|
|
65
|
+
}
|
annet/annlib/rulebook/common.py
CHANGED
|
@@ -356,6 +356,10 @@ def apply(hw, do_commit, do_finalize, **_):
|
|
|
356
356
|
after.add_cmd(Command("commit"))
|
|
357
357
|
if do_finalize:
|
|
358
358
|
after.add_cmd(Command("write", timeout=40))
|
|
359
|
+
elif hw.H3C:
|
|
360
|
+
before.add_cmd(Command("system-view"))
|
|
361
|
+
if do_finalize:
|
|
362
|
+
after.add_cmd(Command("save force", timeout=20))
|
|
359
363
|
else:
|
|
360
364
|
raise Exception("unknown hw %s" % hw)
|
|
361
365
|
|
annet/annlib/tabparser.py
CHANGED
|
@@ -2,7 +2,7 @@ import dataclasses
|
|
|
2
2
|
import itertools
|
|
3
3
|
import re
|
|
4
4
|
from collections import OrderedDict as odict
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Tuple, Union
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Tuple, Union, List
|
|
6
6
|
|
|
7
7
|
from .types import Op
|
|
8
8
|
|
|
@@ -271,8 +271,63 @@ class CiscoFormatter(BlockExitFormatter):
|
|
|
271
271
|
def __init__(self, indent=" "):
|
|
272
272
|
super().__init__("exit", indent)
|
|
273
273
|
|
|
274
|
+
def _split_indent(
|
|
275
|
+
self, line: str, indent: int, block_exit_strings: List[str]
|
|
276
|
+
) -> Tuple[List[str], int]:
|
|
277
|
+
"""
|
|
278
|
+
The small helper calculates indent shift based on block exit string.
|
|
279
|
+
If configuration line has non-default block exit string it means that
|
|
280
|
+
new subsection is started and indent should be increased.
|
|
281
|
+
If configuration line exists in list of block exit strings,
|
|
282
|
+
it means that subsection is finished and indent should be decreased
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
line: just configuration line
|
|
286
|
+
indent: current indent
|
|
287
|
+
block_exit_strings: list of previously seen block exit strings
|
|
288
|
+
Returns:
|
|
289
|
+
new indent and list of previously seen block exit strings
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
if line.strip() in block_exit_strings:
|
|
293
|
+
indent -= 1
|
|
294
|
+
block_exit_strings.remove(line.strip())
|
|
295
|
+
return block_exit_strings, indent
|
|
296
|
+
|
|
297
|
+
block_exit_wrapped = [
|
|
298
|
+
v for v in self.block_exit(FormatterContext(current=(line.strip(), {})))
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
if not block_exit_wrapped or len(block_exit_wrapped) != 3:
|
|
302
|
+
return block_exit_strings, indent
|
|
303
|
+
if not isinstance(block_exit_wrapped[1], str):
|
|
304
|
+
return block_exit_strings, indent
|
|
305
|
+
if block_exit_wrapped[1] == self._block_exit:
|
|
306
|
+
return block_exit_strings, indent
|
|
307
|
+
|
|
308
|
+
indent += 1
|
|
309
|
+
block_exit_strings.append(block_exit_wrapped[1])
|
|
310
|
+
return block_exit_strings, indent
|
|
311
|
+
|
|
274
312
|
def split(self, text):
|
|
275
|
-
|
|
313
|
+
additional_indent = 0
|
|
314
|
+
block_exit_strings = [self._block_exit]
|
|
315
|
+
tree = self.split_remove_spaces(text)
|
|
316
|
+
for i, item in enumerate(tree):
|
|
317
|
+
block_exit_strings, new_indent = self._split_indent(
|
|
318
|
+
item, additional_indent, block_exit_strings
|
|
319
|
+
)
|
|
320
|
+
tree[i] = f"{' ' * additional_indent}{item}"
|
|
321
|
+
additional_indent = new_indent
|
|
322
|
+
return tree
|
|
323
|
+
|
|
324
|
+
def block_exit(self, context: Optional[FormatterContext]) -> str:
|
|
325
|
+
current = context and context.row or ""
|
|
326
|
+
|
|
327
|
+
if current.startswith(("address-family")):
|
|
328
|
+
yield from block_wrapper("exit-address-family")
|
|
329
|
+
else:
|
|
330
|
+
yield from super().block_exit(context)
|
|
276
331
|
|
|
277
332
|
|
|
278
333
|
class AsrFormatter(BlockExitFormatter):
|
annet/configs/context.yml
CHANGED
annet/rpl/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"MatchField",
|
|
3
|
+
"ThenField",
|
|
4
|
+
"RouteMap",
|
|
5
|
+
"Route",
|
|
6
|
+
"ResultType",
|
|
7
|
+
"ActionType",
|
|
8
|
+
"Action",
|
|
9
|
+
"SingleAction",
|
|
10
|
+
"AndCondition",
|
|
11
|
+
"R",
|
|
12
|
+
"ConditionOperator",
|
|
13
|
+
"Condition",
|
|
14
|
+
"SingleCondition",
|
|
15
|
+
"RoutingPolicyStatement",
|
|
16
|
+
"RoutingPolicy",
|
|
17
|
+
"CommunityActionValue",
|
|
18
|
+
"PrefixMatchValue",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
from .action import Action, ActionType, SingleAction
|
|
22
|
+
from .condition import AndCondition, Condition, ConditionOperator, SingleCondition
|
|
23
|
+
from .match_builder import R, MatchField, PrefixMatchValue
|
|
24
|
+
from .policy import RoutingPolicyStatement, RoutingPolicy
|
|
25
|
+
from .result import ResultType
|
|
26
|
+
from .routemap import RouteMap, Route
|
|
27
|
+
from .statement_builder import ThenField, CommunityActionValue
|
annet/rpl/action.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from collections.abc import Iterator, Iterable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Generic, TypeVar, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ActionType(Enum):
|
|
8
|
+
SET = "set"
|
|
9
|
+
ADD = "add"
|
|
10
|
+
REMOVE = "delete"
|
|
11
|
+
|
|
12
|
+
CUSTOM = "custom"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
ValueT = TypeVar("ValueT")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SingleAction(Generic[ValueT]):
|
|
20
|
+
field: str
|
|
21
|
+
type: ActionType
|
|
22
|
+
value: ValueT
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Action:
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self.actions: list[SingleAction[Any]] = []
|
|
28
|
+
|
|
29
|
+
def append(self, action: SingleAction[Any]) -> None:
|
|
30
|
+
self.actions.append(action)
|
|
31
|
+
|
|
32
|
+
def __repr__(self):
|
|
33
|
+
actions = ", ".join(repr(c) for c in self.actions)
|
|
34
|
+
return f"Action({actions})"
|
|
35
|
+
|
|
36
|
+
def __getitem__(self, item: str) -> SingleAction[Any]:
|
|
37
|
+
return next(c for c in self.actions if c.field == item)
|
|
38
|
+
|
|
39
|
+
def __contains__(self, item: str) -> bool:
|
|
40
|
+
return any(c.field == item for c in self.actions)
|
|
41
|
+
|
|
42
|
+
def __len__(self) -> int:
|
|
43
|
+
return len(self.actions)
|
|
44
|
+
|
|
45
|
+
def __iter__(self) -> Iterator[SingleAction[Any]]:
|
|
46
|
+
return iter(self.actions)
|
|
47
|
+
|
|
48
|
+
def find_all(self, item: str) -> Iterable[SingleAction[Any]]:
|
|
49
|
+
for action in self.actions:
|
|
50
|
+
if action.field == item:
|
|
51
|
+
yield action
|
annet/rpl/condition.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from collections.abc import Iterator, Iterable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Generic, TypeVar, Sequence, Union, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConditionOperator(Enum):
|
|
8
|
+
EQ = "=="
|
|
9
|
+
GE = ">="
|
|
10
|
+
GT = ">"
|
|
11
|
+
LE = "<="
|
|
12
|
+
LT = "<"
|
|
13
|
+
BETWEEN_INCLUDED = "BETWEEN_INCLUDED"
|
|
14
|
+
|
|
15
|
+
HAS = "has"
|
|
16
|
+
HAS_ANY = "has_any"
|
|
17
|
+
|
|
18
|
+
CUSTOM = "custom"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ValueT = TypeVar("ValueT")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class SingleCondition(Generic[ValueT]):
|
|
26
|
+
field: str
|
|
27
|
+
operator: ConditionOperator
|
|
28
|
+
value: ValueT
|
|
29
|
+
|
|
30
|
+
def __and__(self, other: "Condition") -> "Condition":
|
|
31
|
+
return AndCondition(self, other)
|
|
32
|
+
|
|
33
|
+
def merge(self, other: "SingleCondition[Any]") -> "SingleCondition[Any]":
|
|
34
|
+
if other.field != self.field:
|
|
35
|
+
raise ValueError(f"Cannot merge conditions with different fields: {self.field} != {other.field}")
|
|
36
|
+
if self.operator is ConditionOperator.LE:
|
|
37
|
+
if other.operator is ConditionOperator.GE:
|
|
38
|
+
return SingleCondition(self.field, ConditionOperator.BETWEEN_INCLUDED, (other.value, self.value))
|
|
39
|
+
elif other.operator is ConditionOperator.LE:
|
|
40
|
+
return SingleCondition(self.field, ConditionOperator.LE, min(self.value, other.value))
|
|
41
|
+
elif self.operator is ConditionOperator.GE:
|
|
42
|
+
if other.operator is ConditionOperator.LE:
|
|
43
|
+
return other.merge(self)
|
|
44
|
+
elif other.operator is ConditionOperator.GE:
|
|
45
|
+
return SingleCondition(self.field, ConditionOperator.GE, max(self.value, other.value))
|
|
46
|
+
elif self.operator is ConditionOperator.LT:
|
|
47
|
+
if other.operator is ConditionOperator.LT:
|
|
48
|
+
return SingleCondition(self.field, ConditionOperator.LT, min(self.value, other.value))
|
|
49
|
+
elif self.operator is ConditionOperator.GT:
|
|
50
|
+
if other.operator is ConditionOperator.GT:
|
|
51
|
+
return SingleCondition(self.field, ConditionOperator.GT, max(self.value, other.value))
|
|
52
|
+
raise ValueError(f"Cannot merge condition with operator {self.operator} and {other.operator}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
Condition = Union[SingleCondition, "AndCondition"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AndCondition:
|
|
59
|
+
def __init__(self, *conditions: Condition):
|
|
60
|
+
self.conditions: list[SingleCondition[Any]] = []
|
|
61
|
+
for c in conditions:
|
|
62
|
+
self.conditions.extend(self._unpack(c))
|
|
63
|
+
|
|
64
|
+
def _unpack(self, other: Condition) -> Sequence[SingleCondition]:
|
|
65
|
+
if isinstance(other, AndCondition):
|
|
66
|
+
return other.conditions
|
|
67
|
+
return [other]
|
|
68
|
+
|
|
69
|
+
def __and__(self, other: Condition) -> "AndCondition":
|
|
70
|
+
return AndCondition(*self.conditions, other)
|
|
71
|
+
|
|
72
|
+
def __iadd__(self, other):
|
|
73
|
+
self.conditions.extend(self._unpack(other))
|
|
74
|
+
|
|
75
|
+
def __repr__(self):
|
|
76
|
+
conditions = ", ".join(repr(c) for c in self.conditions)
|
|
77
|
+
return f"AndCondition({conditions})"
|
|
78
|
+
|
|
79
|
+
def __getitem__(self, item: str) -> SingleCondition[Any]:
|
|
80
|
+
return next(c for c in self.conditions if c.field == item)
|
|
81
|
+
|
|
82
|
+
def __contains__(self, item: str) -> bool:
|
|
83
|
+
return any(c.field == item for c in self.conditions)
|
|
84
|
+
|
|
85
|
+
def __len__(self) -> int:
|
|
86
|
+
return len(self.conditions)
|
|
87
|
+
|
|
88
|
+
def __iter__(self) -> Iterator[SingleCondition[Any]]:
|
|
89
|
+
return iter(self.conditions)
|
|
90
|
+
|
|
91
|
+
def find_all(self, item: str) -> Iterable[SingleCondition[Any]]:
|
|
92
|
+
for condition in self.conditions:
|
|
93
|
+
if condition.field == item:
|
|
94
|
+
yield condition
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Generic, Sequence, Callable, Optional, TypeVar, Any
|
|
4
|
+
|
|
5
|
+
from .condition import SingleCondition, ConditionOperator, AndCondition
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MatchField(str, Enum):
|
|
9
|
+
community = "community"
|
|
10
|
+
extcommunity = "extcommunity"
|
|
11
|
+
rd = "rd"
|
|
12
|
+
interface = "interface"
|
|
13
|
+
protocol = "protocol"
|
|
14
|
+
net_len = "net_len"
|
|
15
|
+
local_pref = "local_pref"
|
|
16
|
+
metric = "metric"
|
|
17
|
+
family = "family"
|
|
18
|
+
|
|
19
|
+
as_path_length = "as_path_length"
|
|
20
|
+
as_path_filter = "as_path_filter"
|
|
21
|
+
ipv6_prefix = "ipv6_prefix"
|
|
22
|
+
ip_prefix = "ip_prefix"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ValueT = TypeVar("ValueT")
|
|
26
|
+
_ConditionMethod = Callable[["ConditionFactory[ValueT]", ValueT], SingleCondition[ValueT]]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def condition_method(operator: ConditionOperator) -> _ConditionMethod:
|
|
30
|
+
def method(self: "ConditionFactory[ValueT]", other: ValueT) -> SingleCondition[ValueT]:
|
|
31
|
+
if operator.value not in self.supported_ops:
|
|
32
|
+
raise NotImplementedError(f"Operator {operator.value} is not supported for field {self.field}")
|
|
33
|
+
return SingleCondition(self.field, operator, other)
|
|
34
|
+
|
|
35
|
+
method.__name__ = operator.value
|
|
36
|
+
return method
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ConditionFactory(Generic[ValueT]):
|
|
40
|
+
def __init__(self, field: str, supported_ops: list[str]):
|
|
41
|
+
self.field = field
|
|
42
|
+
self.supported_ops = supported_ops
|
|
43
|
+
|
|
44
|
+
# https://github.com/python/typeshed/issues/3685
|
|
45
|
+
eq = __eq__ = condition_method(ConditionOperator.EQ) # type: ignore[assignment]
|
|
46
|
+
gt = __gt__ = condition_method(ConditionOperator.GT)
|
|
47
|
+
ge = __ge__ = condition_method(ConditionOperator.GE)
|
|
48
|
+
lt = __lt__ = condition_method(ConditionOperator.LT)
|
|
49
|
+
le = __le__ = condition_method(ConditionOperator.LE)
|
|
50
|
+
between_included = condition_method(ConditionOperator.BETWEEN_INCLUDED)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SetConditionFactory(Generic[ValueT]):
|
|
54
|
+
def __init__(self, field: str) -> None:
|
|
55
|
+
self.field = field
|
|
56
|
+
|
|
57
|
+
def has(self, *values: ValueT) -> SingleCondition[Sequence[ValueT]]:
|
|
58
|
+
return SingleCondition(self.field, ConditionOperator.HAS, values)
|
|
59
|
+
|
|
60
|
+
def has_any(self, *values: ValueT) -> SingleCondition[Sequence[ValueT]]:
|
|
61
|
+
return SingleCondition(self.field, ConditionOperator.HAS_ANY, values)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class PrefixMatchValue:
|
|
66
|
+
names: Sequence[str]
|
|
67
|
+
or_longer: Optional[tuple[int, int]] # ????
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Checkable:
|
|
71
|
+
def __init__(self):
|
|
72
|
+
self.community = SetConditionFactory[str](MatchField.community)
|
|
73
|
+
self.extcommunity = SetConditionFactory[str](MatchField.extcommunity)
|
|
74
|
+
self.rd = SetConditionFactory[str](MatchField.rd)
|
|
75
|
+
self.interface = ConditionFactory[str](MatchField.interface, ["=="])
|
|
76
|
+
self.protocol = ConditionFactory[str](MatchField.protocol, ["=="])
|
|
77
|
+
self.net_len = ConditionFactory[int](MatchField.net_len, ["==", "!="])
|
|
78
|
+
self.local_pref = ConditionFactory[int](MatchField.local_pref, ["<"])
|
|
79
|
+
self.metric = ConditionFactory[int](MatchField.metric, ["=="])
|
|
80
|
+
self.family = ConditionFactory[int](MatchField.family, ["=="])
|
|
81
|
+
self.as_path_length = ConditionFactory[int](MatchField.as_path_length, ["==", ">=", "<=", "BETWEEN_INCLUDED"])
|
|
82
|
+
|
|
83
|
+
def as_path_filter(self, name: str) -> SingleCondition[str]:
|
|
84
|
+
return SingleCondition(MatchField.as_path_filter, ConditionOperator.EQ, name)
|
|
85
|
+
|
|
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
|
+
|
|
92
|
+
|
|
93
|
+
def merge_conditions(and_condition: AndCondition) -> AndCondition:
|
|
94
|
+
conditions: dict[str, SingleCondition[Any]] = {}
|
|
95
|
+
for condition in and_condition.conditions:
|
|
96
|
+
if condition.field in conditions:
|
|
97
|
+
conditions[condition.field] = conditions[condition.field].merge(condition)
|
|
98
|
+
else:
|
|
99
|
+
conditions[condition.field] = condition
|
|
100
|
+
return AndCondition(*conditions.values())
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
R = Checkable()
|
annet/rpl/policy.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .action import Action
|
|
6
|
+
from .condition import AndCondition
|
|
7
|
+
from .result import ResultType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class RoutingPolicyStatement:
|
|
12
|
+
name: Optional[str]
|
|
13
|
+
number: Optional[int]
|
|
14
|
+
match: AndCondition
|
|
15
|
+
then: Action
|
|
16
|
+
result: ResultType
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RoutingPolicy:
|
|
21
|
+
name: str
|
|
22
|
+
statements: Sequence[RoutingPolicyStatement]
|
annet/rpl/result.py
ADDED
annet/rpl/routemap.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, Callable, Generic, TypeVar, Union
|
|
3
|
+
|
|
4
|
+
from .action import Action
|
|
5
|
+
from .condition import AndCondition, Condition
|
|
6
|
+
from .match_builder import merge_conditions
|
|
7
|
+
from .policy import RoutingPolicy, RoutingPolicyStatement
|
|
8
|
+
from .result import ResultType
|
|
9
|
+
from .statement_builder import StatementBuilder
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Route:
|
|
13
|
+
def __init__(self, name: str):
|
|
14
|
+
self.name = name
|
|
15
|
+
self.statements: list[RoutingPolicyStatement] = []
|
|
16
|
+
|
|
17
|
+
def __call__(
|
|
18
|
+
self,
|
|
19
|
+
*conditions: Condition,
|
|
20
|
+
name: Optional[str] = None,
|
|
21
|
+
number: Optional[int] = None,
|
|
22
|
+
) -> "StatementBuilder":
|
|
23
|
+
statement = RoutingPolicyStatement(
|
|
24
|
+
name=name,
|
|
25
|
+
number=number,
|
|
26
|
+
match=merge_conditions(AndCondition(*conditions)),
|
|
27
|
+
then=Action(),
|
|
28
|
+
result=ResultType.NEXT,
|
|
29
|
+
)
|
|
30
|
+
self.statements.append(statement)
|
|
31
|
+
return StatementBuilder(statement=statement)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DeviceT = TypeVar("DeviceT")
|
|
35
|
+
RouteHandlerFunc = Callable[[DeviceT, Route], None]
|
|
36
|
+
Decorator = Callable[[RouteHandlerFunc[DeviceT]], RouteHandlerFunc[DeviceT]]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Handler(Generic[DeviceT]):
|
|
41
|
+
name: str
|
|
42
|
+
func: RouteHandlerFunc[DeviceT]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RouteMap(Generic[DeviceT]):
|
|
46
|
+
def __init__(self) -> None:
|
|
47
|
+
self.handlers: list[Handler[DeviceT]] = []
|
|
48
|
+
self.submaps: list[RouteMap[DeviceT]] = []
|
|
49
|
+
|
|
50
|
+
def __call__(
|
|
51
|
+
self, func: Optional[RouteHandlerFunc[DeviceT]] = None, *, name: str = "",
|
|
52
|
+
) -> Union[RouteHandlerFunc[DeviceT], Decorator[DeviceT]]:
|
|
53
|
+
def decorator(func: RouteHandlerFunc[DeviceT]) -> RouteHandlerFunc[DeviceT]:
|
|
54
|
+
nonlocal name
|
|
55
|
+
if not name:
|
|
56
|
+
name = func.__name__
|
|
57
|
+
self.handlers.append(Handler(name, func))
|
|
58
|
+
return func
|
|
59
|
+
|
|
60
|
+
if func is None:
|
|
61
|
+
return decorator
|
|
62
|
+
return decorator(func)
|
|
63
|
+
|
|
64
|
+
def include(self, other: "RouteMap[DeviceT]") -> None:
|
|
65
|
+
self.submaps.append(other)
|
|
66
|
+
|
|
67
|
+
def apply(self, device: DeviceT) -> list[RoutingPolicy]:
|
|
68
|
+
result: list[RoutingPolicy] = []
|
|
69
|
+
|
|
70
|
+
for handler in self.handlers:
|
|
71
|
+
route = Route(handler.name)
|
|
72
|
+
handler.func(device, route)
|
|
73
|
+
result.append(RoutingPolicy(route.name, route.statements))
|
|
74
|
+
for submap in self.submaps:
|
|
75
|
+
result.extend(submap.apply(device))
|
|
76
|
+
return result
|