annet 0.16.16__py3-none-any.whl → 0.16.18__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/configs/context.yml CHANGED
@@ -2,6 +2,7 @@ generators:
2
2
  default:
3
3
  - annet_generators.example
4
4
  - annet_generators.mesh_example
5
+ - annet_generators.rpl_example
5
6
 
6
7
  storage:
7
8
  default:
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
@@ -0,0 +1,8 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ResultType(str, Enum):
5
+ ALLOW = "allow"
6
+ DENY = "deny"
7
+ NEXT = "next"
8
+ NEXT_POLICY = "next_policy"
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
@@ -0,0 +1,267 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass
3
+ from dataclasses import field
4
+ from enum import Enum
5
+ from typing import Optional, Literal, TypeVar, Union, Any
6
+
7
+ from .action import SingleAction, ActionType
8
+ from .policy import RoutingPolicyStatement
9
+ from .result import ResultType
10
+
11
+
12
+ class ThenField(str, Enum):
13
+ community = "community"
14
+ extcommunity = "extcommunity"
15
+ as_path = "as_path"
16
+ local_pref = "local_pref"
17
+ metric = "metric"
18
+ rpki_valid_state = "rpki_valid_state"
19
+ resolution = "resolution"
20
+ mpls_label = "mpls_label"
21
+ metric_type = "metric_type"
22
+ origin = "origin"
23
+ tag = "tag"
24
+
25
+ next_hop = "next_hop"
26
+
27
+
28
+ ValueT = TypeVar("ValueT")
29
+ _Setter = Callable[[ValueT], SingleAction[ValueT]]
30
+
31
+
32
+ @dataclass
33
+ class CommunityActionValue:
34
+ replaced: Optional[list[str]] = None # None means no replacement is done
35
+ added: list[str] = field(default_factory=list)
36
+ removed: list[str] = field(default_factory=list)
37
+
38
+ def __bool__(self) -> bool: # check if any action required
39
+ return bool(self.replaced is not None or self.added or self.removed)
40
+
41
+
42
+ class CommunityActionBuilder:
43
+ def __init__(self, community: CommunityActionValue):
44
+ self._community = community
45
+
46
+ def add(self, *community: str) -> None:
47
+ for c in community:
48
+ self._community.added.append(c)
49
+
50
+ def remove(self, *community: str) -> None:
51
+ for c in community:
52
+ self._community.removed.append(c)
53
+
54
+ def set(self, *community: str) -> None:
55
+ self._community.added.clear()
56
+ self._community.removed.clear()
57
+ self._community.replaced = list(community)
58
+
59
+
60
+ @dataclass
61
+ class AsPathActionValue:
62
+ set: Optional[list[str]] = None # None means no replacement is done
63
+ prepend: list[str] = field(default_factory=list)
64
+ expand: list[str] = field(default_factory=list)
65
+ expand_last_as: str = ""
66
+ delete: list[str] = field(default_factory=list)
67
+
68
+ def __bool__(self) -> bool: # check if any action required
69
+ return bool(
70
+ self.set is not None or self.prepend or self.expand or self.expand_last_as or self.delete
71
+ )
72
+
73
+
74
+ RawAsNum = Union[str, int]
75
+
76
+
77
+ class AsPathActionBuilder:
78
+ def __init__(self, as_path_value: AsPathActionValue):
79
+ self._as_path_value = as_path_value
80
+
81
+ def prepend(self, *values: RawAsNum) -> None:
82
+ self._as_path_value.prepend = list(map(str, values))
83
+
84
+ def delete(self, *values: RawAsNum) -> None:
85
+ self._as_path_value.delete = list(map(str, values))
86
+
87
+ def expand(self, *values: RawAsNum) -> None:
88
+ self._as_path_value.expand = list(map(str, values))
89
+
90
+ def expand_last_as(self, value: RawAsNum) -> None:
91
+ self._as_path_value.expand_last_as = str(value)
92
+
93
+ def set(self, *values: RawAsNum) -> None:
94
+ self._as_path_value.expand.clear()
95
+ self._as_path_value.expand_last_as = ""
96
+ self._as_path_value.delete.clear()
97
+ self._as_path_value.prepend.clear()
98
+ self._as_path_value.set = list(map(str, values))
99
+
100
+
101
+ @dataclass
102
+ class NextHopActionValue:
103
+ target: Optional[Literal["self", "discard", "peer", "ipv4_addr", "ipv6_addr", "mapped_ipv4"]] = None
104
+ addr: str = ""
105
+
106
+ def __bool__(self) -> bool:
107
+ return bool(self.target)
108
+
109
+
110
+ class NextHopActionBuilder:
111
+ def __init__(self, next_hop_value: NextHopActionValue):
112
+ self._next_hop_value = next_hop_value
113
+
114
+ def ipv4_addr(self, value: str) -> None:
115
+ self._next_hop_value.target = "ipv4_addr"
116
+ self._next_hop_value.addr = value
117
+
118
+ def ipv6_addr(self, value: str) -> None:
119
+ self._next_hop_value.target = "ipv6_addr"
120
+ self._next_hop_value.addr = value
121
+
122
+ def mapped_ipv4(self, value: str) -> None:
123
+ self._next_hop_value.target = "mapped_ipv4"
124
+ self._next_hop_value.addr = value
125
+
126
+ def self(self) -> None:
127
+ self._next_hop_value.target = "self"
128
+ self._next_hop_value.addr = ""
129
+
130
+ def peer(self) -> None:
131
+ self._next_hop_value.target = "peer"
132
+ self._next_hop_value.addr = ""
133
+
134
+ def discard(self) -> None:
135
+ self._next_hop_value.target = "discard"
136
+ self._next_hop_value.addr = ""
137
+
138
+
139
+ class StatementBuilder:
140
+ def __init__(self, statement: RoutingPolicyStatement) -> None:
141
+ self._statement = statement
142
+ self._added_as_path: list[int] = []
143
+ self._community = CommunityActionValue()
144
+ self._extcommunity = CommunityActionValue()
145
+ self._as_path = AsPathActionValue()
146
+ self._next_hop = NextHopActionValue()
147
+
148
+ @property
149
+ def next_hop(self) -> NextHopActionBuilder:
150
+ return NextHopActionBuilder(self._next_hop)
151
+
152
+ @property
153
+ def as_path(self) -> AsPathActionBuilder:
154
+ return AsPathActionBuilder(self._as_path)
155
+
156
+ @property
157
+ def community(self) -> CommunityActionBuilder:
158
+ return CommunityActionBuilder(self._community)
159
+
160
+ @property
161
+ def extcommunity(self) -> CommunityActionBuilder:
162
+ return CommunityActionBuilder(self._extcommunity)
163
+
164
+ def _set(self, field: str, value: ValueT) -> None:
165
+ action = self._statement.then
166
+ if field in action:
167
+ action[field].type = ActionType.SET
168
+ action[field].value = value
169
+ else:
170
+ action.append(SingleAction(
171
+ field=field,
172
+ type=ActionType.SET,
173
+ value=value,
174
+ ))
175
+
176
+ def set_local_pref(self, value: int) -> None:
177
+ self._set(ThenField.local_pref, value)
178
+
179
+ def set_metric_type(self, value: str) -> None:
180
+ self._set(ThenField.metric_type, value)
181
+
182
+ def set_metric(self, value: int) -> None:
183
+ self._set(ThenField.metric, value)
184
+
185
+ def add_metric(self, value: int) -> None:
186
+ action = self._statement.then
187
+ field = ThenField.metric
188
+ if field in action:
189
+ old_action = action[field]
190
+ if old_action.type == ActionType.SET:
191
+ action[field].value += value
192
+ elif old_action.type == ActionType.ADD:
193
+ action[field].value = value
194
+ else:
195
+ raise RuntimeError(f"Unknown action type {old_action.type} for metric")
196
+ else:
197
+ action.append(SingleAction(
198
+ field=field,
199
+ type=ActionType.ADD,
200
+ value=value,
201
+ ))
202
+
203
+ def set_rpki_valid_state(self, value: str) -> None:
204
+ self._set(ThenField.rpki_valid_state, value)
205
+
206
+ def set_resolution(self, value: str) -> None:
207
+ self._set(ThenField.resolution, value)
208
+
209
+ def set_mpls_label(self) -> None:
210
+ self._set(ThenField.mpls_label, True)
211
+
212
+ def set_origin(self, value: str) -> None:
213
+ self._set(ThenField.origin, value)
214
+
215
+ def set_tag(self, value: int) -> None:
216
+ self._set(ThenField.tag, value)
217
+
218
+ def set_next_hop(self, value: Literal["self", "peer"]) -> None: # ???
219
+ self._set(ThenField.next_hop, value)
220
+
221
+ def __enter__(self) -> "StatementBuilder":
222
+ return self
223
+
224
+ def __exit__(self, exc_type, exc_val, exc_tb):
225
+ if self._community:
226
+ self._statement.then.append(SingleAction(
227
+ field=ThenField.community,
228
+ type=ActionType.CUSTOM,
229
+ value=self._community,
230
+ ))
231
+ if self._extcommunity:
232
+ self._statement.then.append(SingleAction(
233
+ field=ThenField.extcommunity,
234
+ type=ActionType.CUSTOM,
235
+ value=self._extcommunity,
236
+ ))
237
+ if self._as_path:
238
+ self._statement.then.append(SingleAction(
239
+ field=ThenField.as_path,
240
+ type=ActionType.CUSTOM,
241
+ value=self._as_path,
242
+ ))
243
+ if self._next_hop:
244
+ self._statement.then.append(SingleAction(
245
+ field=ThenField.next_hop,
246
+ type=ActionType.CUSTOM,
247
+ value=self._next_hop,
248
+ ))
249
+ return None
250
+
251
+ def allow(self) -> None:
252
+ self._statement.result = ResultType.ALLOW
253
+
254
+ def deny(self) -> None:
255
+ self._statement.result = ResultType.DENY
256
+
257
+ def next(self) -> None:
258
+ self._statement.result = ResultType.NEXT
259
+
260
+ def next_policy(self) -> None:
261
+ self._statement.result = ResultType.NEXT_POLICY
262
+
263
+ def add_as_path(self, *as_path: int) -> None:
264
+ self._added_as_path.extend(as_path)
265
+
266
+ def custom_action(self, action: SingleAction[Any]) -> None:
267
+ self._statement.then.append(action)
@@ -1,4 +1,6 @@
1
1
  import re
2
+ from collections import OrderedDict
3
+ from typing import Any
2
4
 
3
5
  from annet.annlib.types import Op
4
6
 
@@ -55,3 +57,93 @@ def banner_login(rule, key, diff, **_):
55
57
  yield (False, f"banner login {key}", None)
56
58
  else:
57
59
  yield from common.default(rule, key, diff)
60
+
61
+
62
+ def bgp_diff(old, new, diff_pre, _pops=(Op.AFFECTED,)):
63
+ """
64
+ Some oder versions of Cisco IOS doesn't create subsection for address family block.
65
+
66
+ it looks like:
67
+
68
+ router bgp 65111
69
+ bgp router-id 1.1.1.1
70
+ bgp log-neighbor-changes
71
+ neighbor SPINE peer-group
72
+ !
73
+ address-family ipv4
74
+ neighbor SPINE send-community both
75
+ neighbor SPINE soft-reconfiguration inbound
76
+ neighbor SPINE route-map TOR_IMPORT_SPINE in
77
+ neighbor SPINE route-map TOR_EXPORT_SPINE out
78
+ exit-address-family
79
+
80
+ but should be
81
+
82
+ router bgp 65111
83
+ bgp router-id 1.1.1.1
84
+ bgp log-neighbor-changes
85
+ neighbor SPINE peer-group
86
+ !
87
+ address-family ipv4
88
+ neighbor SPINE send-community both
89
+ neighbor SPINE soft-reconfiguration inbound
90
+ neighbor SPINE route-map TOR_IMPORT_SPINE in
91
+ neighbor SPINE route-map TOR_EXPORT_SPINE out
92
+ exit-address-family
93
+
94
+ The diff_logic func do it before make diff.
95
+ """
96
+ corrected_old = _create_subsections(old, "address-family")
97
+
98
+ yield from common.default_diff(corrected_old, new, diff_pre, _pops)
99
+
100
+
101
+ def _create_subsections(data: OrderedDict[str, Any], sub_section_prefix: str) -> OrderedDict[str, Any]:
102
+ """
103
+ Reorganizes the given OrderedDict to nest commands under their respective
104
+ sub_section_prefix keys.
105
+
106
+ This function traverses the entries in the provided OrderedDict and groups
107
+ together all entries that are between keys with sub_section_prefix under those
108
+ keys as nested OrderedDicts. The reorganization keeps the order of entries
109
+ stable, only adding nesting where appropriate.
110
+
111
+ Args:
112
+ data (OrderedDict): The original configuration to be transformed.
113
+ sub_section_prefix (str): Prefix of subsection key
114
+
115
+ Returns:
116
+ OrderedDict: A new OrderedDict with nested 'address-family' sections.
117
+ """
118
+
119
+ result = OrderedDict()
120
+ sub_section = None
121
+ temp: OrderedDict = OrderedDict()
122
+
123
+ for key, value in data.items():
124
+ # make nested loop if found nested values
125
+ if value:
126
+ fixed_value: OrderedDict[str, Any] = _create_subsections(value, sub_section_prefix)
127
+ else:
128
+ fixed_value = value
129
+ if key.startswith(sub_section_prefix):
130
+ # in case of data has already had subsections
131
+ if value:
132
+ result[key] = fixed_value
133
+ continue
134
+ # if previous subsection present save collected data from temporary dict
135
+ if sub_section:
136
+ result[sub_section] = temp
137
+ # find a new subsection and initialize new dict
138
+ sub_section = key
139
+ temp = OrderedDict()
140
+ # put found data to temporary dict
141
+ elif sub_section:
142
+ temp[key] = fixed_value
143
+ else:
144
+ result[key] = fixed_value
145
+ # if data is finished save collected data from temporary dict
146
+ if sub_section:
147
+ result[sub_section] = temp
148
+
149
+ return result
@@ -47,6 +47,7 @@ router bfd
47
47
  router bgp *
48
48
  no bgp default *
49
49
  neighbor * maximum-routes
50
+ ! no neighbor * shutdown
50
51
  ~ %global
51
52
 
52
53
  ip load-sharing trident fields * %logic=common.default_instead_undo
@@ -73,7 +73,7 @@ interface */\w*Ethernet[0-9\/]+$/ %logic=common.permanent %diff_logic=cisco.
73
73
  storm-control * level
74
74
  spanning-tree portfast
75
75
 
76
- router bgp *
76
+ router bgp * %diff_logic=cisco.misc.bgp_diff
77
77
  router-id
78
78
  vrf *
79
79
  router-id
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.1
2
+ Name: annet
3
+ Version: 0.16.18
4
+ Summary: annet
5
+ Home-page: https://github.com/annetutil/annet
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ License-File: LICENSE
9
+ License-File: AUTHORS
10
+ Requires-Dist: colorama>=0.4.6
11
+ Requires-Dist: tabulate>=0.9.0
12
+ Requires-Dist: jsonpatch>=1.33
13
+ Requires-Dist: jsonpointer>=2.4
14
+ Requires-Dist: PyYAML>=6.0.1
15
+ Requires-Dist: Pygments>=2.14.0
16
+ Requires-Dist: Mako>=1.2.4
17
+ Requires-Dist: Jinja2>=3.1.2
18
+ Requires-Dist: psutil>=5.8.0
19
+ Requires-Dist: packaging>=23.2
20
+ Requires-Dist: contextlog>=1.1
21
+ Requires-Dist: valkit>=0.1.4
22
+ Requires-Dist: yarl>=1.8.2
23
+ Requires-Dist: adaptix==3.0.0b7
24
+ Requires-Dist: dataclass-rest==0.4
25
+ Provides-Extra: netbox
26
+ Requires-Dist: annetbox[sync]>=0.1.8; extra == "netbox"
@@ -68,7 +68,7 @@ annet/annlib/rbparser/syntax.py,sha256=iZ7Y-4QQBw4L3UtjEh54qisiRDhobl7HZxFNdP8mi
68
68
  annet/annlib/rulebook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
69
  annet/annlib/rulebook/common.py,sha256=Kd9Xout0xC6ZZDnyaORx0W-1kSM-gTgjQbp1iIXWxic,16489
70
70
  annet/api/__init__.py,sha256=7EB3d89kGXKf4Huw7cqyOULTGC9ydL4sHiVw8K0Aeqg,33722
71
- annet/configs/context.yml,sha256=mzJF3K9lM-Fvj9oFOZrDEjQHWJ4W5t96ewMeeWbKDAY,224
71
+ annet/configs/context.yml,sha256=RVLrKLIHpCty7AGwOnmqf7Uu0iZQCn-AjYhophDJer8,259
72
72
  annet/configs/logging.yaml,sha256=EUagfir99QqA73Scc3k7sfQccbU3E1SvEQdyhLFtCl4,997
73
73
  annet/generators/__init__.py,sha256=rVHHDTPKHPZsml1eNEAj3o-8RweFTN8J7LX3tKMXdIY,16402
74
74
  annet/generators/base.py,sha256=rgQLcQBPZm4ecbKmRhVOpBR-GFJAiVfdb_y5f2-LUR8,3670
@@ -89,6 +89,14 @@ annet/mesh/match_args.py,sha256=CR3kdIV9NGtyk9E2JbcOQ3TRuYEryTWP3m2yCo2VCWg,5751
89
89
  annet/mesh/models_converter.py,sha256=3q2zs7K8S3pfYSUKKRdtl5CJGbeg4TtYxofAVs_MBsk,3085
90
90
  annet/mesh/peer_models.py,sha256=xyLbWtwJ5Y4OLf-OVZeHwRn2qyWhipOvhD4pDHTbvTE,2665
91
91
  annet/mesh/registry.py,sha256=G-FszWc_VKeN3eVpb-MRGbAGzbcSuEVAzbDC2k747XA,8611
92
+ annet/rpl/__init__.py,sha256=0kcIktE3AmS0rlm9xzVDf53xk08OeZXgD-6ZLCt_KCs,731
93
+ annet/rpl/action.py,sha256=PY6W66j908RuqQ1_ioxayqVN-70rxDk5Z59EGHtxI98,1246
94
+ annet/rpl/condition.py,sha256=MJri4MbWtPkLHIsLMAtsIEF7e8IAS9dIImjmJs5vS5U,3418
95
+ annet/rpl/match_builder.py,sha256=6uutlI9TU75ClpcKZx0t8pqYxr20vr0h9itBrtrm_tQ,4194
96
+ annet/rpl/policy.py,sha256=P1Kt-8fHFxEczeP-RwkK_wrGN0p7IR-hOApEd2vC55E,448
97
+ annet/rpl/result.py,sha256=PHFn1zhDeqLBn07nkYw5vsoXew4nTwkklOwqvFWzBLg,141
98
+ annet/rpl/routemap.py,sha256=ZxYSRXWtk3CG5OQuVVVYePpPP9Zbwk-ajWvQ0wqWP_8,2422
99
+ annet/rpl/statement_builder.py,sha256=dSYrLosInUb4ewHzX4PeEzbRlIaz6Df4N1EytIJGccQ,8442
92
100
  annet/rulebook/__init__.py,sha256=14IpOfTbeJtre7JKrfXVYiH0qAXsUSOL7AatUFmSQs0,3847
93
101
  annet/rulebook/common.py,sha256=zK1s2c5lc5HQbIlMUQ4HARQudXSgOYiZ_Sxc2I_tHqg,721
94
102
  annet/rulebook/deploying.py,sha256=XV0XQvc3YvwM8SOgOQlc-fCW4bnjQg_1CTZkTwMp14A,2972
@@ -102,7 +110,7 @@ annet/rulebook/b4com/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
102
110
  annet/rulebook/b4com/file.py,sha256=zK7RwBk1YaVoDSFSg1u7Pt8u0Fk3nhhu27aJRngemwc,137
103
111
  annet/rulebook/cisco/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
112
  annet/rulebook/cisco/iface.py,sha256=WISkzjp_G7WKPpg098FCIm4b7ipOxtRLOQbu-7gMUL0,1792
105
- annet/rulebook/cisco/misc.py,sha256=l9NTR6cAsrfFyeC_yoOeQ0fOFoPiJfFwnRva5W9vyL0,2259
113
+ annet/rulebook/cisco/misc.py,sha256=KBnxBw5vs-GSoldMBzEB8ygyHEntiydn1vchZDBA3No,5286
106
114
  annet/rulebook/cisco/vlandb.py,sha256=pdQ0Ca976_5_cNBbTI6ADN1SP8aAngVBs1AZWltmpsw,3319
107
115
  annet/rulebook/huawei/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
108
116
  annet/rulebook/huawei/aaa.py,sha256=Xi8nWyBFaUz4SgoN1NQeOcXzBGCfINQDNiC-crq08uA,3445
@@ -118,7 +126,7 @@ annet/rulebook/routeros/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
118
126
  annet/rulebook/routeros/file.py,sha256=zK7RwBk1YaVoDSFSg1u7Pt8u0Fk3nhhu27aJRngemwc,137
119
127
  annet/rulebook/texts/arista.deploy,sha256=OS9eFyJpEPztcOHkBwajw_RTJfTT7ivaMHfx4_HXaUg,792
120
128
  annet/rulebook/texts/arista.order,sha256=TKy3S56tIypwfVaw0akK1sXGatCpLVwpB4Jvq-dIu90,1457
121
- annet/rulebook/texts/arista.rul,sha256=HUYiN55s8Y6Wrd5q1Awe7-o5BYBh7gIxthUC5NvrfJI,786
129
+ annet/rulebook/texts/arista.rul,sha256=QQbro8eFlc7DCHk0-CTHX_rnj5rqumRzXlY60ga72jo,815
122
130
  annet/rulebook/texts/aruba.deploy,sha256=hI432Bq-of_LMXuUflCu7eNSEFpx6qmj0KItEw6sgHI,841
123
131
  annet/rulebook/texts/aruba.order,sha256=ZMakkn0EJ9zomgY6VssoptJImrHrUmYnCqivzLBFTRo,1158
124
132
  annet/rulebook/texts/aruba.rul,sha256=zvGVpoYyJvMoL0fb1NQ8we_GCLZXno8nwWpZIOScLQQ,2584
@@ -127,7 +135,7 @@ annet/rulebook/texts/b4com.order,sha256=G3aToAIHHzKzDCM3q7_lyr9wJvuVOXVbVvF3wm5P
127
135
  annet/rulebook/texts/b4com.rul,sha256=5mqyUg_oLRSny2iH6QdhfDWVu6kzgDURtlSATD7DFno,1056
128
136
  annet/rulebook/texts/cisco.deploy,sha256=XvXWeOMahE8Uc9RF0xkJj8jGknD4vit8H_f24ubPX7w,1226
129
137
  annet/rulebook/texts/cisco.order,sha256=OvNHMNqkCc-DN2dEjLCTKv_7ZhiaHt4q2X4Y4Z8dvR4,1901
130
- annet/rulebook/texts/cisco.rul,sha256=FvMxKy4FOcFvFYdmcye5RZkWkU8mxIwmEedN83fM82I,3019
138
+ annet/rulebook/texts/cisco.rul,sha256=xMJJgTGmg5wZXTIp5SUdLJIgwka7AIv9Uj6-DWBV3UQ,3055
131
139
  annet/rulebook/texts/huawei.deploy,sha256=azEC6_jQRzwnTSrNgag0hHh6L7hezS_eMk6ZDZfWyXI,10444
132
140
  annet/rulebook/texts/huawei.order,sha256=ENllPX4kO6xNw2mUQcx11yhxo3tKStZ5mUyc0C6s3d0,10657
133
141
  annet/rulebook/texts/huawei.rul,sha256=02Fi1RG4YYea2clHCluBuJDKNbT0hS9jtsk6_h6GK8k,12958
@@ -152,10 +160,14 @@ annet_generators/example/lldp.py,sha256=24bGvShxbio-JxUdaehyPRu31LhH9YwSwFDrWVRn
152
160
  annet_generators/mesh_example/__init__.py,sha256=NfNWgXn1TNiWI6A5tmU6Y-4QV2i33td0Qs3re0MNNMo,218
153
161
  annet_generators/mesh_example/bgp.py,sha256=jzyDndSSGYyYBquDnLlR-7P5lzmUKcSyYCml3VsoMC0,1385
154
162
  annet_generators/mesh_example/mesh_logic.py,sha256=DJS5JMCTs0rs0LN__0LulNgo2ekUcWiOMe02BlOeFas,1454
155
- annet-0.16.16.dist-info/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
156
- annet-0.16.16.dist-info/LICENSE,sha256=yPxl7dno02Pw7gAcFPIFONzx_gapwDoPXsIsh6Y7lC0,1079
157
- annet-0.16.16.dist-info/METADATA,sha256=HWT2ARwNTk_WK4FcwEtWOa2FrkPAYRZ5BUwr3--hrNA,746
158
- annet-0.16.16.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
159
- annet-0.16.16.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
160
- annet-0.16.16.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
161
- annet-0.16.16.dist-info/RECORD,,
163
+ annet_generators/rpl_example/__init__.py,sha256=z4-gsDv06BBpgTwRohc50VBQYFD26QVu8Zr8Ngi_iqY,244
164
+ annet_generators/rpl_example/items.py,sha256=6x7b0wZ7Vjn6yCaJ-aGbpTHm7fyqO77b-LRqzzhEbh4,615
165
+ annet_generators/rpl_example/policy_generator.py,sha256=KFCqn347CIPcnllOHfINYeKgNSr6Wl-bdM5Xj_YKhYM,11183
166
+ annet_generators/rpl_example/route_policy.py,sha256=QjxFjkePHfTo2CpMeRVaDqZXNXLM-gGlE8EocHuOR4Y,1189
167
+ annet-0.16.18.dist-info/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
168
+ annet-0.16.18.dist-info/LICENSE,sha256=yPxl7dno02Pw7gAcFPIFONzx_gapwDoPXsIsh6Y7lC0,1079
169
+ annet-0.16.18.dist-info/METADATA,sha256=pB-87I1B1EvDeahyJFJD6j2DxvPEBYfWT44e8D0a-8U,728
170
+ annet-0.16.18.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
171
+ annet-0.16.18.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
172
+ annet-0.16.18.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
173
+ annet-0.16.18.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.5.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,9 @@
1
+ from typing import List
2
+
3
+ from annet.generators import BaseGenerator
4
+ from annet.storage import Storage
5
+ from . import policy_generator
6
+
7
+
8
+ def get_generators(store: Storage) -> List[BaseGenerator]:
9
+ return policy_generator.get_generators(store)
@@ -0,0 +1,31 @@
1
+ from collections.abc import Sequence
2
+ from dataclasses import dataclass
3
+
4
+ AS_PATH_FILTERS = {
5
+ "ASP_EXAMPLE": [".*123456.*"],
6
+ }
7
+
8
+ IPV6_PREFIX_LISTS = {
9
+ "IPV6_LIST_EXAMPLE": ["2a13:5941::/32"],
10
+ }
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Community:
15
+ values: Sequence[str]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ExtCommunity:
20
+ values: Sequence[str]
21
+
22
+
23
+ COMMUNITIES = {
24
+ "COMMUNITY_EXAMPLE_ADD": Community(["1234:1000"]),
25
+ "COMMUNITY_EXAMPLE_REMOVE": Community(["12345:999"]),
26
+ }
27
+
28
+ EXT_COMMUNITIES = {
29
+ "COMMUNITY_EXAMPLE_ADD": ExtCommunity(["1234:1000"]),
30
+ "COMMUNITY_EXAMPLE_REMOVE": ExtCommunity(["12345:999"]),
31
+ }
@@ -0,0 +1,233 @@
1
+ from collections.abc import Iterator, Sequence
2
+ from typing import Any, cast
3
+
4
+ from annet.generators import PartialGenerator, BaseGenerator
5
+ from annet.rpl import (
6
+ CommunityActionValue,
7
+ ResultType, RoutingPolicyStatement, RoutingPolicy, ConditionOperator, SingleCondition, SingleAction, ActionType,
8
+ )
9
+ from annet.rpl.statement_builder import AsPathActionValue, NextHopActionValue
10
+ from annet.storage import Storage
11
+ from .items import AS_PATH_FILTERS, COMMUNITIES, EXT_COMMUNITIES
12
+ from .route_policy import routemap
13
+
14
+ HUAWEI_MATCH_COMMAND_MAP = {
15
+ "as_path_filter": "as-path-filter {option_value}",
16
+ "metric": "cost {option_value}",
17
+ "protocol": "protocol {option_value}",
18
+ "interface": "interface {option_value}",
19
+ }
20
+
21
+ HUAWEI_THEN_COMMAND_MAP = {
22
+ "metric": "cost {option_value}",
23
+ "local_pref": "local-preference {option_value}",
24
+ "metric_type": "cost-type {option_value}",
25
+ "mpls_label": "mpls-label",
26
+ "origin": "origin {option_value}",
27
+ "tag": "tag {option_value}",
28
+ # unsupported: resolution
29
+ # unsupported: rpki_valid_state
30
+ }
31
+ HUAWEI_RESULT_MAP = {
32
+ ResultType.ALLOW: "permit",
33
+ ResultType.DENY: "deny",
34
+ ResultType.NEXT: ""
35
+ }
36
+
37
+
38
+ class RoutingPolicyGenerator(PartialGenerator):
39
+ TAGS = ["policy", "rpl", "routing"]
40
+
41
+ def acl_huawei(self, _):
42
+ return r"""
43
+ ip as-path-filter
44
+ route-policy *
45
+ ~ %global=1
46
+ """
47
+
48
+ def _huawei_match(self, device, condition: SingleCondition[Any]) -> Iterator[Sequence[str]]:
49
+ if condition.field == "community":
50
+ if condition.operator is ConditionOperator.HAS:
51
+ if len(condition.value) > 1:
52
+ raise NotImplementedError("Multiple HAS for communities is not supported for huawei")
53
+ elif condition.operator is not ConditionOperator.HAS_ANY:
54
+ raise NotImplementedError("Community operator %r not supported for huawei" % condition.operator)
55
+ for comm_name in condition.value:
56
+ yield "if-match community-filter", comm_name
57
+ return
58
+ if condition.field == "extcommunity":
59
+ if condition.operator is ConditionOperator.HAS:
60
+ if len(condition.value) > 1:
61
+ raise NotImplementedError("Multiple HAS for extcommunities is not supported for huawei")
62
+ elif condition.operator is not ConditionOperator.HAS_ANY:
63
+ raise NotImplementedError("Extcommunity operator %r not supported for huawei" % condition.operator)
64
+ for comm_name in condition.value:
65
+ yield "if-match extcommunity-filter", comm_name
66
+ return
67
+ if condition.field == "ip_prefix":
68
+ for name in condition.value.names:
69
+ yield "if-match", "ip-prefix-filter", name
70
+ return
71
+ if condition.field == "ipv6_prefix":
72
+ for name in condition.value.names:
73
+ yield "if-match", "ipv6 address prefix-list", name
74
+ return
75
+ if condition.field == "as_path_length":
76
+ if condition.operator is ConditionOperator.EQ:
77
+ yield "if-match", "as-path length", condition.value
78
+ elif condition.operator is ConditionOperator.LE:
79
+ yield "if-match", "as-path length less-equal", condition.value
80
+ elif condition.operator is ConditionOperator.GE:
81
+ yield "if-match", "as-path length greater-equal", condition.value
82
+ elif condition.operator is ConditionOperator.BETWEEN_INCLUDED:
83
+ yield "if-match", "as-path length greater-equal", condition.value[0], "less-equal", condition.value[1]
84
+ else:
85
+ raise NotImplementedError(
86
+ f"as_path_length operator {condition.operator} not supported for huawei",
87
+ )
88
+ return
89
+ if condition.operator is not ConditionOperator.EQ:
90
+ raise NotImplementedError(
91
+ f"`{condition.field}` with operator {condition.operator} is not supported for huawei",
92
+ )
93
+ if condition.field not in HUAWEI_MATCH_COMMAND_MAP:
94
+ raise NotImplementedError(f"Match using `{condition.field}` is not supported for huawei")
95
+ cmd = HUAWEI_MATCH_COMMAND_MAP[condition.field]
96
+ yield "if-match", cmd.format(option_value=condition.value)
97
+
98
+ def _huawei_then_community(self, action: SingleAction[CommunityActionValue]) -> Iterator[Sequence[str]]:
99
+ if action.value.replaced is not None:
100
+ if not action.value.replaced:
101
+ yield "apply", "community", "none"
102
+ first = True
103
+ for community_name in action.value.replaced:
104
+ community = COMMUNITIES[community_name]
105
+ for comm_value in community.values:
106
+ if first:
107
+ yield "apply", "community", comm_value
108
+ first = False
109
+ else:
110
+ yield "apply", "community", comm_value, "additive"
111
+ for community_name in action.value.added:
112
+ community = COMMUNITIES[community_name]
113
+ for comm_value in community.values:
114
+ yield "apply", "community", comm_value, "additive"
115
+ for community_name in action.value.removed:
116
+ yield "apply comm-filter", community_name, "delete"
117
+
118
+ def _huawei_then_extcommunity(self, action: SingleAction[CommunityActionValue]) -> Iterator[Sequence[str]]:
119
+ if action.value.replaced is not None:
120
+ if not action.value.replaced:
121
+ yield "apply", "extcommunity", "none"
122
+ first = True
123
+ for community_name in action.value.replaced:
124
+ community = EXT_COMMUNITIES[community_name]
125
+ for comm_value in community.values:
126
+ if first:
127
+ yield "apply", "extcommunity", comm_value
128
+ first = False
129
+ else:
130
+ yield "apply", "extcommunity", comm_value, "additive"
131
+ for community_name in action.value.added:
132
+ community = EXT_COMMUNITIES[community_name]
133
+ for comm_value in community.values:
134
+ yield "apply", "extcommunity", comm_value, "additive"
135
+ for community_name in action.value.removed:
136
+ yield "apply extcommunity-filter", community_name, "delete"
137
+
138
+ def _huawei_then(self, device, action: SingleAction[Any]) -> Iterator[Sequence[str]]:
139
+ if action.field == "community":
140
+ yield from self._huawei_then_community(cast(SingleAction[CommunityActionValue], action))
141
+ return
142
+ if action.field == "extcommunity":
143
+ yield from self._huawei_then_extcommunity(cast(SingleAction[CommunityActionValue], action))
144
+ return
145
+ if action.field == "metric":
146
+ if action.type is ActionType.ADD:
147
+ yield "apply", f"cost + {action.value}"
148
+ elif action.type is ActionType.SET:
149
+ yield "apply", f"cost {action.value}"
150
+ else:
151
+ raise NotImplementedError(f"Action type {action.type} for metric is not supported for huawei")
152
+ return
153
+ if action.field == "as_path":
154
+ as_path_action_value = cast(AsPathActionValue, action.value)
155
+ if as_path_action_value.set is not None:
156
+ if not as_path_action_value.set:
157
+ yield "apply", "as_path", "none overwrite"
158
+ first = True
159
+ for path_item in as_path_action_value.set:
160
+ if first:
161
+ yield "apply as-path", path_item, "overwrite"
162
+ first = False
163
+ else:
164
+ yield "apply as-path", path_item, "additive"
165
+ if as_path_action_value.prepend:
166
+ for path_item in as_path_action_value.prepend:
167
+ yield "apply as-path", path_item, "additive"
168
+ if as_path_action_value.expand: # same as prepend?
169
+ for path_item in as_path_action_value.expand:
170
+ yield "apply as-path", path_item, "additive"
171
+ if as_path_action_value.delete:
172
+ for path_item in as_path_action_value.delete:
173
+ yield "apply as-path", path_item, "delete"
174
+ if as_path_action_value.expand_last_as:
175
+ raise RuntimeError("asp_path.expand_last_as is not supported for huawei")
176
+ return
177
+ if action.field == "next_hop":
178
+ next_hop_action_value = cast(NextHopActionValue, action.value)
179
+ if next_hop_action_value.target == "self":
180
+ yield "apply", "cost 1"
181
+ elif next_hop_action_value.target == "discard":
182
+ pass
183
+ elif next_hop_action_value.target == "peer":
184
+ pass
185
+ elif next_hop_action_value.target == "ipv4_addr":
186
+ yield "apply", f"ip-address next-hop {next_hop_action_value.addr}"
187
+ elif next_hop_action_value.target == "ipv6_addr":
188
+ yield "apply", f"ipv6 next-hop {next_hop_action_value.addr}"
189
+ elif next_hop_action_value.target == "mapped_ipv4":
190
+ yield "apply", f"ipv6 next-hop ::FFFF:{next_hop_action_value.addr}"
191
+ else:
192
+ raise RuntimeError(f"Next_hop target {next_hop_action_value.target} is not supported for huawei")
193
+
194
+ if action.type is not ActionType.SET:
195
+ raise NotImplementedError(f"Action type {action.type} for `{action.field}` is not supported for huawei")
196
+ if action.field not in HUAWEI_THEN_COMMAND_MAP:
197
+ raise NotImplementedError(f"Then action using `{action.field}` is not supported for huawei")
198
+ cmd = HUAWEI_THEN_COMMAND_MAP[action.field]
199
+ yield "apply", cmd.format(option_value=action.value)
200
+
201
+ def _huawei_statement(
202
+ self, device, policy: RoutingPolicy, statement: RoutingPolicyStatement,
203
+ ) -> Iterator[Sequence[str]]:
204
+ if "as_path_filter" in statement.match:
205
+ as_path_condition = statement.match["as_path_filter"]
206
+ as_filter_value = AS_PATH_FILTERS[as_path_condition.value]
207
+ yield "ip as-path-filter", \
208
+ as_path_condition.value, \
209
+ "index 10 permit", \
210
+ "_{}_".format("_".join(("%s" % x for x in as_filter_value if x != ".*")))
211
+
212
+ with self.block(
213
+ "route-policy", policy.name,
214
+ HUAWEI_RESULT_MAP[statement.result],
215
+ "node", statement.number
216
+ ):
217
+ for condition in statement.match:
218
+ yield from self._huawei_match(device, condition)
219
+ for action in statement.then:
220
+ yield from self._huawei_then(device, action)
221
+ if statement.result is ResultType.NEXT:
222
+ yield "goto next-node"
223
+
224
+ def run_huawei(self, device):
225
+ for policy in routemap.apply(device):
226
+ for statement in policy.statements:
227
+ yield from self._huawei_statement(device, policy, statement)
228
+
229
+
230
+ def get_generators(store: Storage) -> list[BaseGenerator]:
231
+ return [
232
+ RoutingPolicyGenerator(store),
233
+ ]
@@ -0,0 +1,33 @@
1
+ from annet.adapters.netbox.common.models import NetboxDevice
2
+ from annet.rpl import R, RouteMap, Route
3
+
4
+ routemap = RouteMap[NetboxDevice]()
5
+
6
+
7
+ @routemap
8
+ def example1(device: NetboxDevice, route: Route):
9
+ condition = (R.interface == "l0.0") & (R.protocol == "bgp")
10
+ with route(condition, number=1, name="n1") as rule:
11
+ rule.set_local_pref(100)
12
+ rule.set_metric(100)
13
+ rule.add_metric(200)
14
+ rule.community.set("COMMUNITY_EXAMPLE_ADD")
15
+ rule.as_path.set(12345, "123456")
16
+ rule.allow()
17
+ with route(R.protocol == "bgp", R.community.has("comm_name"), number=2, name="n2") as rule:
18
+ rule.set_local_pref(100)
19
+ rule.add_metric(200)
20
+ rule.community.add("COMMUNITY_EXAMPLE_ADD")
21
+ rule.community.remove("COMMUNITY_EXAMPLE_REMOVE")
22
+ rule.allow()
23
+
24
+
25
+ @routemap
26
+ def example2(device: NetboxDevice, route: Route):
27
+ with route(R.as_path_filter("ASP_EXAMPLE"), number=3, name="n3") as rule:
28
+ rule.deny()
29
+ with route(R.match_v6("IPV6_LIST_EXAMPLE"), number=4, name="n4") as rule:
30
+ rule.allow()
31
+
32
+ with route(R.as_path_length >= 1, R.as_path_length <= 20, number=4, name="n4") as rule:
33
+ rule.allow()
@@ -1,27 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: annet
3
- Version: 0.16.16
4
- Summary: annet
5
- Home-page: https://github.com/annetutil/annet
6
- License: MIT
7
- Requires-Python: >=3.10
8
- License-File: LICENSE
9
- License-File: AUTHORS
10
- Requires-Dist: colorama >=0.4.6
11
- Requires-Dist: tabulate >=0.9.0
12
- Requires-Dist: jsonpatch >=1.33
13
- Requires-Dist: jsonpointer >=2.4
14
- Requires-Dist: PyYAML >=6.0.1
15
- Requires-Dist: Pygments >=2.14.0
16
- Requires-Dist: Mako >=1.2.4
17
- Requires-Dist: Jinja2 >=3.1.2
18
- Requires-Dist: psutil >=5.8.0
19
- Requires-Dist: packaging >=23.2
20
- Requires-Dist: contextlog >=1.1
21
- Requires-Dist: valkit >=0.1.4
22
- Requires-Dist: yarl >=1.8.2
23
- Requires-Dist: adaptix ==3.0.0b7
24
- Requires-Dist: dataclass-rest ==0.4
25
- Provides-Extra: netbox
26
- Requires-Dist: annetbox[sync] >=0.1.8 ; extra == 'netbox'
27
-