annet 0.16.26__py3-none-any.whl → 0.16.27__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/file/provider.py +28 -10
- annet/adapters/netbox/v37/storage.py +1 -1
- annet/annlib/netdev/devdb/data/devdb.json +3 -2
- annet/bgp_models.py +28 -0
- annet/mesh/__init__.py +4 -0
- annet/mesh/basemodel.py +5 -0
- annet/mesh/device_models.py +2 -0
- annet/mesh/executor.py +90 -66
- annet/mesh/peer_models.py +3 -3
- annet/mesh/port_processor.py +18 -0
- annet/mesh/registry.py +12 -4
- annet/rpl/match_builder.py +30 -9
- annet/rpl/routemap.py +5 -3
- annet/rpl/statement_builder.py +31 -7
- annet/rpl_generators/__init__.py +24 -0
- annet/rpl_generators/aspath.py +57 -0
- annet/rpl_generators/community.py +242 -0
- annet/rpl_generators/cumulus_frr.py +458 -0
- annet/rpl_generators/entities.py +70 -0
- annet/rpl_generators/execute.py +12 -0
- annet/rpl_generators/policy.py +676 -0
- annet/rpl_generators/prefix_lists.py +158 -0
- annet/rpl_generators/rd.py +40 -0
- {annet-0.16.26.dist-info → annet-0.16.27.dist-info}/METADATA +2 -2
- {annet-0.16.26.dist-info → annet-0.16.27.dist-info}/RECORD +35 -24
- annet_generators/rpl_example/__init__.py +3 -5
- annet_generators/rpl_example/generator.py +127 -0
- annet_generators/rpl_example/items.py +21 -31
- annet_generators/rpl_example/mesh.py +9 -0
- annet_generators/rpl_example/route_policy.py +43 -9
- annet_generators/rpl_example/policy_generator.py +0 -233
- {annet-0.16.26.dist-info → annet-0.16.27.dist-info}/AUTHORS +0 -0
- {annet-0.16.26.dist-info → annet-0.16.27.dist-info}/LICENSE +0 -0
- {annet-0.16.26.dist-info → annet-0.16.27.dist-info}/WHEEL +0 -0
- {annet-0.16.26.dist-info → annet-0.16.27.dist-info}/entry_points.txt +0 -0
- {annet-0.16.26.dist-info → annet-0.16.27.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import Iterator, Sequence
|
|
3
|
+
from typing import Any, cast, Literal
|
|
4
|
+
|
|
5
|
+
from annet.generators import PartialGenerator
|
|
6
|
+
from annet.rpl import (
|
|
7
|
+
CommunityActionValue,
|
|
8
|
+
ResultType, RoutingPolicyStatement, RoutingPolicy, ConditionOperator, SingleCondition, SingleAction, ActionType,
|
|
9
|
+
RouteMap, MatchField,
|
|
10
|
+
)
|
|
11
|
+
from annet.rpl.statement_builder import AsPathActionValue, NextHopActionValue, ThenField
|
|
12
|
+
from annet.rpl_generators.entities import (
|
|
13
|
+
arista_well_known_community,
|
|
14
|
+
CommunityList, RDFilter, mangle_ranged_prefix_list_name, CommunityLogic, mangle_united_community_list_name,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
HUAWEI_MATCH_COMMAND_MAP: dict[str, str] = {
|
|
18
|
+
MatchField.as_path_filter: "as-path-filter {option_value}",
|
|
19
|
+
MatchField.metric: "cost {option_value}",
|
|
20
|
+
MatchField.protocol: "protocol {option_value}",
|
|
21
|
+
MatchField.interface: "interface {option_value}",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
HUAWEI_THEN_COMMAND_MAP: dict[str, str] = {
|
|
25
|
+
ThenField.local_pref: "local-preference {option_value}",
|
|
26
|
+
ThenField.metric_type: "cost-type {option_value}",
|
|
27
|
+
ThenField.mpls_label: "mpls-label",
|
|
28
|
+
ThenField.origin: "origin {option_value}",
|
|
29
|
+
ThenField.tag: "tag {option_value}",
|
|
30
|
+
# unsupported: resolution
|
|
31
|
+
# unsupported: rpki_valid_state
|
|
32
|
+
}
|
|
33
|
+
HUAWEI_RESULT_MAP = {
|
|
34
|
+
ResultType.ALLOW: "permit",
|
|
35
|
+
ResultType.DENY: "deny",
|
|
36
|
+
ResultType.NEXT: "permit"
|
|
37
|
+
}
|
|
38
|
+
ARISTA_RESULT_MAP = {
|
|
39
|
+
ResultType.ALLOW: "permit",
|
|
40
|
+
ResultType.DENY: "deny",
|
|
41
|
+
ResultType.NEXT: "permit"
|
|
42
|
+
}
|
|
43
|
+
ARISTA_MATCH_COMMAND_MAP: dict[str, str] = {
|
|
44
|
+
MatchField.interface: "interface {option_value}",
|
|
45
|
+
MatchField.metric: "metric {option_value}",
|
|
46
|
+
MatchField.as_path_filter: "as-path {option_value}",
|
|
47
|
+
MatchField.protocol: "source-protocol {option_value}",
|
|
48
|
+
# unsupported: rd
|
|
49
|
+
}
|
|
50
|
+
ARISTA_THEN_COMMAND_MAP: dict[str, str] = {
|
|
51
|
+
ThenField.local_pref: "local-preference {option_value}",
|
|
52
|
+
ThenField.origin: "origin {option_value}",
|
|
53
|
+
ThenField.tag: "tag {option_value}",
|
|
54
|
+
ThenField.metric_type: "metric-type {option_value}",
|
|
55
|
+
# unsupported: mpls_label
|
|
56
|
+
# unsupported: resolution
|
|
57
|
+
# unsupported: rpki_valid_state
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RoutingPolicyGenerator(PartialGenerator, ABC):
|
|
62
|
+
TAGS = ["policy", "rpl", "routing"]
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def get_policies(self, device: Any) -> list[RoutingPolicy]:
|
|
66
|
+
raise NotImplementedError()
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def get_community_lists(self, device: Any) -> list[CommunityList]:
|
|
70
|
+
raise NotImplementedError()
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def get_rd_filters(self, device: Any) -> list[RDFilter]:
|
|
74
|
+
raise NotImplementedError()
|
|
75
|
+
|
|
76
|
+
# huawei
|
|
77
|
+
def acl_huawei(self, _):
|
|
78
|
+
return r"""
|
|
79
|
+
route-policy *
|
|
80
|
+
~ %global=1
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def _huawei_match(
|
|
84
|
+
self,
|
|
85
|
+
device: Any,
|
|
86
|
+
condition: SingleCondition[Any],
|
|
87
|
+
communities: dict[str, CommunityList],
|
|
88
|
+
rd_filters: dict[str, RDFilter],
|
|
89
|
+
) -> Iterator[Sequence[str]]:
|
|
90
|
+
if condition.field == MatchField.community:
|
|
91
|
+
if condition.operator is ConditionOperator.HAS:
|
|
92
|
+
if len(condition.value) > 1:
|
|
93
|
+
raise NotImplementedError("Multiple HAS for communities is not supported for huawei")
|
|
94
|
+
elif condition.operator is not ConditionOperator.HAS_ANY:
|
|
95
|
+
raise NotImplementedError("Community operator %r not supported for huawei" % condition.operator)
|
|
96
|
+
for comm_name in condition.value:
|
|
97
|
+
yield "if-match community-filter", comm_name
|
|
98
|
+
return
|
|
99
|
+
if condition.field == MatchField.large_community:
|
|
100
|
+
if condition.operator is ConditionOperator.HAS_ANY:
|
|
101
|
+
if len(condition.value) > 1:
|
|
102
|
+
raise NotImplementedError("Multiple HAS_ANY values for large_community is not supported for huawei")
|
|
103
|
+
elif condition.operator is not ConditionOperator.HAS:
|
|
104
|
+
raise NotImplementedError("large_community operator %r not supported for huawei" % condition.operator)
|
|
105
|
+
for comm_name in condition.value:
|
|
106
|
+
yield "if-match large-community-filter", comm_name
|
|
107
|
+
return
|
|
108
|
+
if condition.field == MatchField.extcommunity_rt:
|
|
109
|
+
if condition.operator is ConditionOperator.HAS:
|
|
110
|
+
if len(condition.value) > 1:
|
|
111
|
+
raise NotImplementedError("Multiple HAS values for extcommunity_rt is not supported for huawei")
|
|
112
|
+
elif condition.operator is not ConditionOperator.HAS_ANY:
|
|
113
|
+
raise NotImplementedError("Extcommunity_rt operator %r not supported for huawei" % condition.operator)
|
|
114
|
+
for comm_name in condition.value:
|
|
115
|
+
if communities[comm_name].logic is CommunityLogic.AND:
|
|
116
|
+
yield "if-match extcommunity-filter", comm_name, "matches-all"
|
|
117
|
+
else:
|
|
118
|
+
yield "if-match extcommunity-filter", comm_name
|
|
119
|
+
return
|
|
120
|
+
if condition.field == MatchField.extcommunity_soo:
|
|
121
|
+
if condition.operator is ConditionOperator.HAS_ANY:
|
|
122
|
+
if len(condition.value) > 1:
|
|
123
|
+
raise NotImplementedError("Multiple HAS_ANY for extcommunities_soo is not supported for huawei")
|
|
124
|
+
elif condition.operator is not ConditionOperator.HAS:
|
|
125
|
+
raise NotImplementedError("Extcommunity_soo operator %r not supported for huawei" % condition.operator)
|
|
126
|
+
for comm_name in condition.value:
|
|
127
|
+
yield "if-match extcommunity-list soo", comm_name
|
|
128
|
+
return
|
|
129
|
+
if condition.field == MatchField.rd:
|
|
130
|
+
if len(condition.value) > 1:
|
|
131
|
+
raise NotImplementedError("Multiple RD filters is not supported for huawei")
|
|
132
|
+
rd_filter = rd_filters[condition.value[0]]
|
|
133
|
+
yield "if-match rd-filter", str(rd_filter.number)
|
|
134
|
+
return
|
|
135
|
+
if condition.field == MatchField.ip_prefix:
|
|
136
|
+
for name in condition.value.names:
|
|
137
|
+
mangled_name = mangle_ranged_prefix_list_name(
|
|
138
|
+
name=name,
|
|
139
|
+
greater_equal=condition.value.greater_equal,
|
|
140
|
+
less_equal=condition.value.less_equal,
|
|
141
|
+
)
|
|
142
|
+
yield "if-match", "ip-prefix", mangled_name
|
|
143
|
+
return
|
|
144
|
+
if condition.field == MatchField.ipv6_prefix:
|
|
145
|
+
for name in condition.value.names:
|
|
146
|
+
mangled_name = mangle_ranged_prefix_list_name(
|
|
147
|
+
name=name,
|
|
148
|
+
greater_equal=condition.value.greater_equal,
|
|
149
|
+
less_equal=condition.value.less_equal,
|
|
150
|
+
)
|
|
151
|
+
yield "if-match", "ipv6 address prefix-list", mangled_name
|
|
152
|
+
return
|
|
153
|
+
if condition.field == MatchField.as_path_length:
|
|
154
|
+
if condition.operator is ConditionOperator.EQ:
|
|
155
|
+
yield "if-match", "as-path length", condition.value
|
|
156
|
+
elif condition.operator is ConditionOperator.LE:
|
|
157
|
+
yield "if-match", "as-path length less-equal", condition.value
|
|
158
|
+
elif condition.operator is ConditionOperator.GE:
|
|
159
|
+
yield "if-match", "as-path length greater-equal", condition.value
|
|
160
|
+
elif condition.operator is ConditionOperator.BETWEEN_INCLUDED:
|
|
161
|
+
yield "if-match", "as-path length greater-equal", condition.value[0], "less-equal", condition.value[1]
|
|
162
|
+
else:
|
|
163
|
+
raise NotImplementedError(
|
|
164
|
+
f"as_path_length operator {condition.operator} not supported for huawei",
|
|
165
|
+
)
|
|
166
|
+
return
|
|
167
|
+
if condition.operator is not ConditionOperator.EQ:
|
|
168
|
+
raise NotImplementedError(
|
|
169
|
+
f"`{condition.field}` with operator {condition.operator} is not supported for huawei",
|
|
170
|
+
)
|
|
171
|
+
if condition.field not in HUAWEI_MATCH_COMMAND_MAP:
|
|
172
|
+
raise NotImplementedError(f"Match using `{condition.field}` is not supported for huawei")
|
|
173
|
+
cmd = HUAWEI_MATCH_COMMAND_MAP[condition.field]
|
|
174
|
+
yield "if-match", cmd.format(option_value=condition.value)
|
|
175
|
+
|
|
176
|
+
def _huawei_then_community(
|
|
177
|
+
self,
|
|
178
|
+
communities: dict[str, CommunityList],
|
|
179
|
+
device: Any,
|
|
180
|
+
action: SingleAction[CommunityActionValue],
|
|
181
|
+
) -> Iterator[Sequence[str]]:
|
|
182
|
+
if action.value.replaced is not None:
|
|
183
|
+
if action.value.added or action.value.replaced:
|
|
184
|
+
raise NotImplementedError(
|
|
185
|
+
"Cannot set community together with add/remove on huawei",
|
|
186
|
+
)
|
|
187
|
+
members = [m for name in action.value.replaced for m in communities[name].members]
|
|
188
|
+
if members:
|
|
189
|
+
yield "apply", "community", *members
|
|
190
|
+
else:
|
|
191
|
+
yield "apply", "community", "none"
|
|
192
|
+
if action.value.added:
|
|
193
|
+
members = [m for name in action.value.added for m in communities[name].members]
|
|
194
|
+
yield "apply", "community", *members, "additive"
|
|
195
|
+
for community_name in action.value.removed:
|
|
196
|
+
yield "apply comm-filter", community_name, "delete"
|
|
197
|
+
|
|
198
|
+
def _huawei_then_large_community(
|
|
199
|
+
self,
|
|
200
|
+
communities: dict[str, CommunityList],
|
|
201
|
+
device: Any,
|
|
202
|
+
action: SingleAction[CommunityActionValue],
|
|
203
|
+
) -> Iterator[Sequence[str]]:
|
|
204
|
+
if action.value.replaced is not None:
|
|
205
|
+
if action.value.added or action.value.replaced:
|
|
206
|
+
raise NotImplementedError(
|
|
207
|
+
"Cannot set large-community together with add/remove on huawei",
|
|
208
|
+
)
|
|
209
|
+
members = [m for name in action.value.replaced for m in communities[name].members]
|
|
210
|
+
if members:
|
|
211
|
+
yield "apply", "large-community", *members, "overwrite"
|
|
212
|
+
else:
|
|
213
|
+
yield "apply", "large-community", "none"
|
|
214
|
+
if action.value.added:
|
|
215
|
+
members = [m for name in action.value.added for m in communities[name].members]
|
|
216
|
+
yield "apply", "large-community", *members, "additive"
|
|
217
|
+
if action.value.removed:
|
|
218
|
+
members = [m for name in action.value.removed for m in communities[name].members]
|
|
219
|
+
yield "apply large-community", *members, "delete"
|
|
220
|
+
|
|
221
|
+
def _huawei_then_extcommunity_rt(
|
|
222
|
+
self,
|
|
223
|
+
communities: dict[str, CommunityList],
|
|
224
|
+
device: Any,
|
|
225
|
+
action: SingleAction[CommunityActionValue],
|
|
226
|
+
) -> Iterator[Sequence[str]]:
|
|
227
|
+
if action.value.replaced is not None:
|
|
228
|
+
raise NotImplementedError("Extcommunity_rt replace is not supported for huawei")
|
|
229
|
+
if action.value.added:
|
|
230
|
+
members = [f"rt {m}" for name in action.value.added for m in communities[name].members]
|
|
231
|
+
yield "apply", "extcommunity", *members, "additive"
|
|
232
|
+
for community_name in action.value.removed:
|
|
233
|
+
yield "apply extcommunity-filter rt", community_name, "delete"
|
|
234
|
+
|
|
235
|
+
def _huawei_then_extcommunity_soo(
|
|
236
|
+
self,
|
|
237
|
+
communities: dict[str, CommunityList],
|
|
238
|
+
device: Any,
|
|
239
|
+
action: SingleAction[CommunityActionValue],
|
|
240
|
+
) -> Iterator[Sequence[str]]:
|
|
241
|
+
if action.value.replaced is not None:
|
|
242
|
+
raise NotImplementedError("Extcommunity_soo replace is not supported for huawei")
|
|
243
|
+
if action.value.added:
|
|
244
|
+
members = [f"rt {m}" for name in action.value.added for m in communities[name].members]
|
|
245
|
+
yield "apply", "extcommunity", *members, "additive"
|
|
246
|
+
if action.value.removed:
|
|
247
|
+
raise NotImplementedError("Extcommunity_soo remove is not supported for huawei")
|
|
248
|
+
|
|
249
|
+
def _huawei_then_as_path(
|
|
250
|
+
self,
|
|
251
|
+
device: Any,
|
|
252
|
+
action: SingleAction[AsPathActionValue],
|
|
253
|
+
) -> Iterator[Sequence[str]]:
|
|
254
|
+
if action.value.set is not None:
|
|
255
|
+
if action.value.prepend:
|
|
256
|
+
raise NotImplementedError(
|
|
257
|
+
"Cannot set as_path together with prepend on huawei",
|
|
258
|
+
)
|
|
259
|
+
if action.value.set:
|
|
260
|
+
yield "apply", "as-path", *action.value.set, "overwrite"
|
|
261
|
+
else:
|
|
262
|
+
yield "apply", "as-path", "none overwrite"
|
|
263
|
+
if action.value.prepend:
|
|
264
|
+
yield "apply as-path", *action.value.prepend, "additive"
|
|
265
|
+
if action.value.expand:
|
|
266
|
+
raise RuntimeError("as_path.expand is not supported for huawei")
|
|
267
|
+
if action.value.delete:
|
|
268
|
+
for path_item in action.value.delete:
|
|
269
|
+
yield "apply as-path", path_item, "delete"
|
|
270
|
+
if action.value.expand_last_as:
|
|
271
|
+
raise RuntimeError("as_path.expand_last_as is not supported for huawei")
|
|
272
|
+
|
|
273
|
+
def _huawei_then(
|
|
274
|
+
self,
|
|
275
|
+
communities: dict[str, CommunityList],
|
|
276
|
+
device: Any,
|
|
277
|
+
action: SingleAction[Any],
|
|
278
|
+
) -> Iterator[Sequence[str]]:
|
|
279
|
+
if action.field == ThenField.community:
|
|
280
|
+
yield from self._huawei_then_community(communities, device,
|
|
281
|
+
cast(SingleAction[CommunityActionValue], action))
|
|
282
|
+
return
|
|
283
|
+
if action.field == ThenField.large_community:
|
|
284
|
+
yield from self._huawei_then_large_community(communities, device,
|
|
285
|
+
cast(SingleAction[CommunityActionValue], action))
|
|
286
|
+
return
|
|
287
|
+
if action.field == ThenField.extcommunity_rt:
|
|
288
|
+
yield from self._huawei_then_extcommunity_rt(communities, device,
|
|
289
|
+
cast(SingleAction[CommunityActionValue], action))
|
|
290
|
+
return
|
|
291
|
+
if action.field == ThenField.extcommunity_soo:
|
|
292
|
+
yield from self._huawei_then_extcommunity_soo(communities, device,
|
|
293
|
+
cast(SingleAction[CommunityActionValue], action))
|
|
294
|
+
return
|
|
295
|
+
if action.field == ThenField.metric:
|
|
296
|
+
if action.type is ActionType.ADD:
|
|
297
|
+
yield "apply", f"cost + {action.value}"
|
|
298
|
+
elif action.type is ActionType.SET:
|
|
299
|
+
yield "apply", f"cost {action.value}"
|
|
300
|
+
else:
|
|
301
|
+
raise NotImplementedError(f"Action type {action.type} for metric is not supported for huawei")
|
|
302
|
+
return
|
|
303
|
+
if action.field == ThenField.as_path:
|
|
304
|
+
yield from self._huawei_then_as_path(device, cast(SingleAction[AsPathActionValue], action))
|
|
305
|
+
return
|
|
306
|
+
if action.field == ThenField.next_hop:
|
|
307
|
+
next_hop_action_value = cast(NextHopActionValue, action.value)
|
|
308
|
+
if next_hop_action_value.target == "self":
|
|
309
|
+
yield "apply", "cost 1"
|
|
310
|
+
elif next_hop_action_value.target == "discard":
|
|
311
|
+
pass
|
|
312
|
+
elif next_hop_action_value.target == "peer":
|
|
313
|
+
pass
|
|
314
|
+
elif next_hop_action_value.target == "ipv4_addr":
|
|
315
|
+
yield "apply", f"ip-address next-hop {next_hop_action_value.addr}"
|
|
316
|
+
elif next_hop_action_value.target == "ipv6_addr":
|
|
317
|
+
yield "apply", f"ipv6 next-hop {next_hop_action_value.addr}"
|
|
318
|
+
elif next_hop_action_value.target == "mapped_ipv4":
|
|
319
|
+
yield "apply", f"ipv6 next-hop ::FFFF:{next_hop_action_value.addr}"
|
|
320
|
+
else:
|
|
321
|
+
raise RuntimeError(f"Next_hop target {next_hop_action_value.target} is not supported for huawei")
|
|
322
|
+
|
|
323
|
+
if action.type is not ActionType.SET:
|
|
324
|
+
raise NotImplementedError(f"Action type {action.type} for `{action.field}` is not supported for huawei")
|
|
325
|
+
if action.field not in HUAWEI_THEN_COMMAND_MAP:
|
|
326
|
+
raise NotImplementedError(f"Then action using `{action.field}` is not supported for huawei")
|
|
327
|
+
cmd = HUAWEI_THEN_COMMAND_MAP[action.field]
|
|
328
|
+
yield "apply", cmd.format(option_value=action.value)
|
|
329
|
+
|
|
330
|
+
def _huawei_statement(
|
|
331
|
+
self,
|
|
332
|
+
communities: dict[str, CommunityList],
|
|
333
|
+
rd_filters: dict[str, RDFilter],
|
|
334
|
+
device: Any,
|
|
335
|
+
policy: RoutingPolicy,
|
|
336
|
+
statement: RoutingPolicyStatement,
|
|
337
|
+
) -> Iterator[Sequence[str]]:
|
|
338
|
+
if statement.number is None:
|
|
339
|
+
raise RuntimeError(f"Statement number should not be empty on Huawei (found for policy: {policy.name})")
|
|
340
|
+
with self.block(
|
|
341
|
+
"route-policy", policy.name,
|
|
342
|
+
HUAWEI_RESULT_MAP[statement.result],
|
|
343
|
+
"node", statement.number
|
|
344
|
+
):
|
|
345
|
+
for condition in statement.match:
|
|
346
|
+
yield from self._huawei_match(device, condition, communities, rd_filters)
|
|
347
|
+
for action in statement.then:
|
|
348
|
+
yield from self._huawei_then(communities, device, action)
|
|
349
|
+
if statement.result is ResultType.NEXT:
|
|
350
|
+
yield "goto next-node"
|
|
351
|
+
|
|
352
|
+
def run_huawei(self, device):
|
|
353
|
+
communities = {c.name: c for c in self.get_community_lists(device)}
|
|
354
|
+
rd_filters = {f.name: f for f in self.get_rd_filters(device)}
|
|
355
|
+
|
|
356
|
+
for policy in self.get_policies(device):
|
|
357
|
+
for statement in policy.statements:
|
|
358
|
+
yield from self._huawei_statement(communities, rd_filters, device, policy, statement)
|
|
359
|
+
|
|
360
|
+
# arista
|
|
361
|
+
def acl_arista(self, device):
|
|
362
|
+
return r"""
|
|
363
|
+
route-map
|
|
364
|
+
~ %global=1
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
def _arista_match_community(
|
|
368
|
+
self,
|
|
369
|
+
device: Any,
|
|
370
|
+
community_type: Literal["community", "extcommunity", "large-community"],
|
|
371
|
+
community_names: Sequence[str],
|
|
372
|
+
) -> Iterator[Sequence[str]]:
|
|
373
|
+
yield "match", community_type, *community_names
|
|
374
|
+
|
|
375
|
+
def _arista_match(
|
|
376
|
+
self,
|
|
377
|
+
device: Any,
|
|
378
|
+
condition: SingleCondition[Any],
|
|
379
|
+
communities: dict[str, CommunityList],
|
|
380
|
+
rd_filters: dict[str, RDFilter],
|
|
381
|
+
) -> Iterator[Sequence[str]]:
|
|
382
|
+
if condition.field == MatchField.community:
|
|
383
|
+
if condition.operator is ConditionOperator.HAS_ANY:
|
|
384
|
+
yield from self._arista_match_community(
|
|
385
|
+
device, "community", [mangle_united_community_list_name(condition.value)],
|
|
386
|
+
)
|
|
387
|
+
elif condition.operator is ConditionOperator.HAS:
|
|
388
|
+
yield from self._arista_match_community(
|
|
389
|
+
device, "community", condition.value,
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
raise NotImplementedError(f"Community match operator {condition.field} is not supported on arista")
|
|
393
|
+
return
|
|
394
|
+
if condition.field == MatchField.large_community:
|
|
395
|
+
if condition.operator is ConditionOperator.HAS_ANY:
|
|
396
|
+
yield from self._arista_match_community(
|
|
397
|
+
device, "large-community", [mangle_united_community_list_name(condition.value)],
|
|
398
|
+
)
|
|
399
|
+
elif condition.operator is ConditionOperator.HAS:
|
|
400
|
+
yield from self._arista_match_community(
|
|
401
|
+
device, "large-community", condition.value,
|
|
402
|
+
)
|
|
403
|
+
else:
|
|
404
|
+
raise NotImplementedError(
|
|
405
|
+
f"Large-community match operator {condition.field} is not supported on arista")
|
|
406
|
+
return
|
|
407
|
+
if condition.field == MatchField.extcommunity_rt:
|
|
408
|
+
if condition.operator is ConditionOperator.HAS_ANY:
|
|
409
|
+
yield from self._arista_match_community(
|
|
410
|
+
device, "extcommunity", [mangle_united_community_list_name(condition.value)],
|
|
411
|
+
)
|
|
412
|
+
elif condition.operator is ConditionOperator.HAS:
|
|
413
|
+
yield from self._arista_match_community(
|
|
414
|
+
device, "extcommunity", condition.value,
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
raise NotImplementedError(f"Community match operator {condition.field} is not supported on arista")
|
|
418
|
+
return
|
|
419
|
+
if condition.field == MatchField.extcommunity_soo:
|
|
420
|
+
if condition.operator is ConditionOperator.HAS_ANY:
|
|
421
|
+
yield from self._arista_match_community(
|
|
422
|
+
device, "extcommunity", [mangle_united_community_list_name(condition.value)],
|
|
423
|
+
)
|
|
424
|
+
elif condition.operator is ConditionOperator.HAS:
|
|
425
|
+
yield from self._arista_match_community(
|
|
426
|
+
device, "extcommunity", condition.value,
|
|
427
|
+
)
|
|
428
|
+
else:
|
|
429
|
+
raise NotImplementedError(f"Extcommunity match operator {condition.field} is not supported on arista")
|
|
430
|
+
return
|
|
431
|
+
if condition.field == MatchField.ip_prefix:
|
|
432
|
+
for name in condition.value.names:
|
|
433
|
+
mangled_name = mangle_ranged_prefix_list_name(
|
|
434
|
+
name=name,
|
|
435
|
+
greater_equal=condition.value.greater_equal,
|
|
436
|
+
less_equal=condition.value.less_equal,
|
|
437
|
+
)
|
|
438
|
+
yield "match", "ip address prefix-list", mangled_name
|
|
439
|
+
return
|
|
440
|
+
if condition.field == MatchField.ipv6_prefix:
|
|
441
|
+
for name in condition.value.names:
|
|
442
|
+
mangled_name = mangle_ranged_prefix_list_name(
|
|
443
|
+
name=name,
|
|
444
|
+
greater_equal=condition.value.greater_equal,
|
|
445
|
+
less_equal=condition.value.less_equal,
|
|
446
|
+
)
|
|
447
|
+
yield "match", "ipv6 address prefix-list", mangled_name
|
|
448
|
+
return
|
|
449
|
+
if condition.field == MatchField.as_path_length:
|
|
450
|
+
if condition.operator is ConditionOperator.EQ:
|
|
451
|
+
yield "match", "as-path length =", condition.value
|
|
452
|
+
elif condition.operator is ConditionOperator.LE:
|
|
453
|
+
yield "match", "as-path length <=", condition.value
|
|
454
|
+
elif condition.operator is ConditionOperator.GE:
|
|
455
|
+
yield "match", "as-path length >=", condition.value
|
|
456
|
+
elif condition.operator is ConditionOperator.BETWEEN_INCLUDED:
|
|
457
|
+
yield "match", "as-path length >=", condition.value[0]
|
|
458
|
+
yield "match", "as-path length <=", condition.value[1]
|
|
459
|
+
else:
|
|
460
|
+
raise NotImplementedError(
|
|
461
|
+
f"as_path_length operator {condition.operator} not supported for arista",
|
|
462
|
+
)
|
|
463
|
+
return
|
|
464
|
+
if condition.operator is not ConditionOperator.EQ:
|
|
465
|
+
raise NotImplementedError(
|
|
466
|
+
f"`{condition.field}` with operator {condition.operator} is not supported for arista",
|
|
467
|
+
)
|
|
468
|
+
if condition.field not in ARISTA_MATCH_COMMAND_MAP:
|
|
469
|
+
raise NotImplementedError(f"Match using `{condition.field}` is not supported for arista")
|
|
470
|
+
cmd = ARISTA_MATCH_COMMAND_MAP[condition.field]
|
|
471
|
+
yield "match", cmd.format(option_value=condition.value)
|
|
472
|
+
|
|
473
|
+
def _arista_then_community(
|
|
474
|
+
self,
|
|
475
|
+
communities: dict[str, CommunityList],
|
|
476
|
+
device: Any,
|
|
477
|
+
action: SingleAction[CommunityActionValue],
|
|
478
|
+
) -> Iterator[Sequence[str]]:
|
|
479
|
+
if action.value.replaced is not None:
|
|
480
|
+
if action.value.added or action.value.replaced:
|
|
481
|
+
raise NotImplementedError(
|
|
482
|
+
"Cannot set community together with add/remove on arista",
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if action.value.replaced:
|
|
486
|
+
yield "set", "community community-list", *action.value.replaced
|
|
487
|
+
else:
|
|
488
|
+
yield "set", "community", "none"
|
|
489
|
+
for community_name in action.value.added:
|
|
490
|
+
yield "set", "community community-list", community_name, "additive"
|
|
491
|
+
for community_name in action.value.removed:
|
|
492
|
+
community = communities[community_name]
|
|
493
|
+
for comm_value in community.members:
|
|
494
|
+
yield "set community", arista_well_known_community(comm_value), "delete"
|
|
495
|
+
|
|
496
|
+
def _arista_then_large_community(
|
|
497
|
+
self,
|
|
498
|
+
communities: dict[str, CommunityList],
|
|
499
|
+
device: Any,
|
|
500
|
+
action: SingleAction[CommunityActionValue],
|
|
501
|
+
) -> Iterator[Sequence[str]]:
|
|
502
|
+
if action.value.replaced is not None:
|
|
503
|
+
if action.value.added or action.value.replaced:
|
|
504
|
+
raise NotImplementedError(
|
|
505
|
+
"Cannot set large-community together with add/remove on arista",
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if not action.value.replaced:
|
|
509
|
+
yield "set", "large-community", "none"
|
|
510
|
+
first = True
|
|
511
|
+
for community_name in action.value.replaced:
|
|
512
|
+
if first:
|
|
513
|
+
yield "set", "large-community large-community-list", community_name
|
|
514
|
+
first = False
|
|
515
|
+
else:
|
|
516
|
+
yield "set", "large-community large-community-list", community_name, "additive"
|
|
517
|
+
for community_name in action.value.added:
|
|
518
|
+
yield "set", "large-community large-community-list", community_name, "additive"
|
|
519
|
+
for community_name in action.value.removed:
|
|
520
|
+
yield "set large-community large-community-list", community_name, "delete"
|
|
521
|
+
|
|
522
|
+
def _arista_then_extcommunity_rt(
|
|
523
|
+
self,
|
|
524
|
+
communities: dict[str, CommunityList],
|
|
525
|
+
device: Any,
|
|
526
|
+
action: SingleAction[CommunityActionValue],
|
|
527
|
+
) -> Iterator[Sequence[str]]:
|
|
528
|
+
if action.value.replaced is not None:
|
|
529
|
+
raise NotImplementedError("Extcommunity_rt replace is not supported for arista")
|
|
530
|
+
for community_name in action.value.added:
|
|
531
|
+
community = communities[community_name]
|
|
532
|
+
for comm_value in community.members:
|
|
533
|
+
yield "set", "extcommunity rt", comm_value, "additive"
|
|
534
|
+
for community_name in action.value.removed:
|
|
535
|
+
community = communities[community_name]
|
|
536
|
+
for comm_value in community.members:
|
|
537
|
+
yield "set extcommunity rt", comm_value, "delete"
|
|
538
|
+
|
|
539
|
+
def _arista_then_extcommunity_soo(
|
|
540
|
+
self,
|
|
541
|
+
communities: dict[str, CommunityList],
|
|
542
|
+
device: Any,
|
|
543
|
+
action: SingleAction[CommunityActionValue],
|
|
544
|
+
) -> Iterator[Sequence[str]]:
|
|
545
|
+
if action.value.replaced is not None:
|
|
546
|
+
raise NotImplementedError("Extcommunity_soo replace is not supported for arista")
|
|
547
|
+
for community_name in action.value.added:
|
|
548
|
+
community = communities[community_name]
|
|
549
|
+
for comm_value in community.members:
|
|
550
|
+
yield "set", "extcommunity soo", comm_value, "additive"
|
|
551
|
+
for community_name in action.value.removed:
|
|
552
|
+
community = communities[community_name]
|
|
553
|
+
for comm_value in community.members:
|
|
554
|
+
yield "set", "extcommunity soo", comm_value, "delete"
|
|
555
|
+
|
|
556
|
+
def _arista_then_as_path(
|
|
557
|
+
self,
|
|
558
|
+
device: Any,
|
|
559
|
+
action: SingleAction[AsPathActionValue],
|
|
560
|
+
) -> Iterator[Sequence[str]]:
|
|
561
|
+
if action.value.set is not None:
|
|
562
|
+
if action.value.prepend:
|
|
563
|
+
raise NotImplementedError(
|
|
564
|
+
"Cannot set as_path together with prepend on arista",
|
|
565
|
+
)
|
|
566
|
+
if not action.value.set:
|
|
567
|
+
yield "set", "as-path match all replacement", "none"
|
|
568
|
+
else:
|
|
569
|
+
yield "set", "as-path match all replacement", *action.value.set
|
|
570
|
+
|
|
571
|
+
if action.value.expand_last_as:
|
|
572
|
+
last_as_suffix: Sequence[str] = "last-as", action.value.expand_last_as
|
|
573
|
+
else:
|
|
574
|
+
last_as_suffix = ()
|
|
575
|
+
|
|
576
|
+
if action.value.prepend:
|
|
577
|
+
for path_item in action.value.prepend:
|
|
578
|
+
yield "set", "as-path prepend", path_item, *last_as_suffix
|
|
579
|
+
else:
|
|
580
|
+
yield "set", "as-path prepend", *last_as_suffix
|
|
581
|
+
if action.value.expand:
|
|
582
|
+
raise RuntimeError("as_path.expand is not supported for arista")
|
|
583
|
+
if action.value.delete:
|
|
584
|
+
raise RuntimeError("as_path.delete is not supported for arista")
|
|
585
|
+
|
|
586
|
+
def _arista_then(
|
|
587
|
+
self,
|
|
588
|
+
communities: dict[str, CommunityList],
|
|
589
|
+
device: Any,
|
|
590
|
+
action: SingleAction[Any],
|
|
591
|
+
) -> Iterator[Sequence[str]]:
|
|
592
|
+
if action.field == ThenField.community:
|
|
593
|
+
yield from self._arista_then_community(
|
|
594
|
+
communities, device, cast(SingleAction[CommunityActionValue], action),
|
|
595
|
+
)
|
|
596
|
+
return
|
|
597
|
+
if action.field == ThenField.large_community:
|
|
598
|
+
yield from self._arista_then_large_community(
|
|
599
|
+
communities, device, cast(SingleAction[CommunityActionValue], action),
|
|
600
|
+
)
|
|
601
|
+
return
|
|
602
|
+
if action.field == ThenField.extcommunity_rt:
|
|
603
|
+
yield from self._arista_then_extcommunity_rt(
|
|
604
|
+
communities, device, cast(SingleAction[CommunityActionValue], action),
|
|
605
|
+
)
|
|
606
|
+
return
|
|
607
|
+
if action.field == ThenField.extcommunity_soo:
|
|
608
|
+
yield from self._arista_then_extcommunity_soo(
|
|
609
|
+
communities, device, cast(SingleAction[CommunityActionValue], action),
|
|
610
|
+
)
|
|
611
|
+
return
|
|
612
|
+
if action.field == ThenField.metric:
|
|
613
|
+
if action.type is ActionType.ADD:
|
|
614
|
+
yield "set", f"metric + {action.value}"
|
|
615
|
+
elif action.type is ActionType.REMOVE:
|
|
616
|
+
yield "set", f"metric - {action.value}"
|
|
617
|
+
elif action.type is ActionType.SET:
|
|
618
|
+
yield "set", f"metric {action.value}"
|
|
619
|
+
else:
|
|
620
|
+
raise NotImplementedError(f"Action type {action.type} for metric is not supported for arista")
|
|
621
|
+
return
|
|
622
|
+
if action.field == ThenField.as_path:
|
|
623
|
+
yield from self._arista_then_as_path(device, cast(SingleAction[AsPathActionValue], action))
|
|
624
|
+
return
|
|
625
|
+
if action.field == ThenField.next_hop:
|
|
626
|
+
next_hop_action_value = cast(NextHopActionValue, action.value)
|
|
627
|
+
if next_hop_action_value.target == "self":
|
|
628
|
+
yield "set", "cost 1" # TODO?
|
|
629
|
+
elif next_hop_action_value.target == "discard":
|
|
630
|
+
pass
|
|
631
|
+
elif next_hop_action_value.target == "peer":
|
|
632
|
+
pass
|
|
633
|
+
elif next_hop_action_value.target == "ipv4_addr":
|
|
634
|
+
yield "set", f"ip next-hop {next_hop_action_value.addr}"
|
|
635
|
+
elif next_hop_action_value.target == "ipv6_addr":
|
|
636
|
+
yield "set", f"ipv6 next-hop {next_hop_action_value.addr}"
|
|
637
|
+
elif next_hop_action_value.target == "mapped_ipv4":
|
|
638
|
+
yield "set", f"ipv6 next-hop ::FFFF:{next_hop_action_value.addr}"
|
|
639
|
+
else:
|
|
640
|
+
raise RuntimeError(f"Next_hop target {next_hop_action_value.target} is not supported for arista")
|
|
641
|
+
return
|
|
642
|
+
if action.type is not ActionType.SET:
|
|
643
|
+
raise NotImplementedError(f"Action type {action.type} for `{action.field}` is not supported for arista")
|
|
644
|
+
if action.field not in ARISTA_THEN_COMMAND_MAP:
|
|
645
|
+
raise NotImplementedError(f"Then action using `{action.field}` is not supported for arista")
|
|
646
|
+
cmd = ARISTA_THEN_COMMAND_MAP[action.field]
|
|
647
|
+
yield "set", cmd.format(option_value=action.value)
|
|
648
|
+
|
|
649
|
+
def _arista_statement(
|
|
650
|
+
self,
|
|
651
|
+
communities: dict[str, CommunityList],
|
|
652
|
+
rd_filters: dict[str, RDFilter],
|
|
653
|
+
device: Any,
|
|
654
|
+
policy: RoutingPolicy,
|
|
655
|
+
statement: RoutingPolicyStatement,
|
|
656
|
+
) -> Iterator[Sequence[str]]:
|
|
657
|
+
with self.block(
|
|
658
|
+
"route-map",
|
|
659
|
+
policy.name,
|
|
660
|
+
ARISTA_RESULT_MAP[statement.result],
|
|
661
|
+
statement.number,
|
|
662
|
+
):
|
|
663
|
+
for condition in statement.match:
|
|
664
|
+
yield from self._arista_match(device, condition, communities, rd_filters)
|
|
665
|
+
for action in statement.then:
|
|
666
|
+
yield from self._arista_then(communities, device, action)
|
|
667
|
+
if statement.result is ResultType.NEXT:
|
|
668
|
+
yield "continue"
|
|
669
|
+
|
|
670
|
+
def run_arista(self, device):
|
|
671
|
+
communities = {c.name: c for c in self.get_community_lists(device)}
|
|
672
|
+
rd_filters = {f.name: f for f in self.get_rd_filters(device)}
|
|
673
|
+
|
|
674
|
+
for policy in self.get_policies(device):
|
|
675
|
+
for statement in policy.statements:
|
|
676
|
+
yield from self._arista_statement(communities, rd_filters, device, policy, statement)
|