annet 3.12.1__py3-none-any.whl → 3.14.0__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/models.py +2 -0
- annet/adapters/netbox/provider.py +2 -1
- annet/annlib/netdev/devdb/data/devdb.json +2 -2
- annet/bgp_models.py +12 -3
- annet/mesh/device_models.py +2 -0
- annet/rpl/match_builder.py +10 -2
- annet/rpl_generators/aspath.py +22 -0
- annet/rpl_generators/community.py +73 -1
- annet/rpl_generators/entities.py +50 -3
- annet/rpl_generators/policy.py +367 -2
- annet/rpl_generators/prefix_lists.py +67 -1
- annet/rpl_generators/rd.py +15 -0
- annet/rulebook/texts/huawei.rul +5 -0
- annet/rulebook/texts/juniper.rul +6 -2
- annet/vendors/tabparser.py +13 -31
- {annet-3.12.1.dist-info → annet-3.14.0.dist-info}/METADATA +1 -1
- {annet-3.12.1.dist-info → annet-3.14.0.dist-info}/RECORD +22 -22
- {annet-3.12.1.dist-info → annet-3.14.0.dist-info}/WHEEL +0 -0
- {annet-3.12.1.dist-info → annet-3.14.0.dist-info}/entry_points.txt +0 -0
- {annet-3.12.1.dist-info → annet-3.14.0.dist-info}/licenses/AUTHORS +0 -0
- {annet-3.12.1.dist-info → annet-3.14.0.dist-info}/licenses/LICENSE +0 -0
- {annet-3.12.1.dist-info → annet-3.14.0.dist-info}/top_level.txt +0 -0
|
@@ -189,6 +189,8 @@ class Interface(Entity, Generic[_IpAddressT, _FHRPGroupAssignmentT]):
|
|
|
189
189
|
lag_min_links: int | None = None
|
|
190
190
|
speed: int | None = None
|
|
191
191
|
|
|
192
|
+
custom_fields: Dict[str, Any] = field(default_factory=dict)
|
|
193
|
+
|
|
192
194
|
ip_addresses: List[_IpAddressT] = field(default_factory=list)
|
|
193
195
|
count_ipaddresses: int = 0
|
|
194
196
|
fhrp_groups: List[_FHRPGroupAssignmentT] = field(default_factory=list)
|
|
@@ -45,12 +45,13 @@ def storage_factory(opts: NetboxStorageOpts) -> Storage:
|
|
|
45
45
|
|
|
46
46
|
class NetboxProvider(StorageProvider, AdapterWithName, AdapterWithConfig):
|
|
47
47
|
def __init__(self, url: Optional[str] = None, token: Optional[str] = None, insecure: bool = False,
|
|
48
|
-
exact_host_filter: bool = False, threads: int = 1):
|
|
48
|
+
exact_host_filter: bool = False, threads: int = 1, all_hosts_filter: dict[str, list[str]] | None = None):
|
|
49
49
|
self.url = url
|
|
50
50
|
self.token = token
|
|
51
51
|
self.insecure = insecure
|
|
52
52
|
self.exact_host_filter = exact_host_filter
|
|
53
53
|
self.threads = threads
|
|
54
|
+
self.all_hosts_filter = all_hosts_filter
|
|
54
55
|
|
|
55
56
|
@classmethod
|
|
56
57
|
def with_config(cls, **kwargs: Dict[str, Any]) -> T:
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"PC.Whitebox.Ufispace.S.S9300.S9301_32DB": " S9301-32DB",
|
|
145
145
|
"PC.Whitebox.Ufispace.S.S9300.S9321_64EO": " S9321-64EO",
|
|
146
146
|
"PC.Nebius": "^Nebius",
|
|
147
|
-
"PC.Nebius.
|
|
147
|
+
"PC.Nebius.Octoport": " NB-D-M-CSM-R",
|
|
148
148
|
"PC.Avocent": " [Aa]vocent",
|
|
149
149
|
"PC.Avocent.ACS8000": " (ACS|acs)8000",
|
|
150
150
|
|
|
@@ -189,4 +189,4 @@
|
|
|
189
189
|
"B4com.CS4132U": "^[Bb]4com B4T-CS4132U.*",
|
|
190
190
|
"B4com.CS4148Q": "^[Bb]4com B4T-CS4148Q.*",
|
|
191
191
|
"B4com.CS4164U": "^[Bb]4com B4T-CS4164U.*"
|
|
192
|
-
}
|
|
192
|
+
}
|
annet/bgp_models.py
CHANGED
|
@@ -255,6 +255,8 @@ class FamilyOptions:
|
|
|
255
255
|
rib_group: bool = False
|
|
256
256
|
loops: int = 0
|
|
257
257
|
advertise_bgp_static: bool = False
|
|
258
|
+
import_policy: Optional[str] = None
|
|
259
|
+
export_policy: Optional[str] = None
|
|
258
260
|
|
|
259
261
|
|
|
260
262
|
@dataclass(frozen=True)
|
|
@@ -387,7 +389,7 @@ def _used_policies(peer: Union[Peer, PeerGroup, VrfOptions]) -> Iterable[str]:
|
|
|
387
389
|
yield peer.export_policy
|
|
388
390
|
|
|
389
391
|
|
|
390
|
-
def
|
|
392
|
+
def _used_families_policies(opts: Union[GlobalOptions, VrfOptions]) -> Iterable[str]:
|
|
391
393
|
for family_opts in (
|
|
392
394
|
opts.ipv4_unicast,
|
|
393
395
|
opts.ipv6_unicast,
|
|
@@ -400,6 +402,13 @@ def _used_redistribute_policies(opts: Union[GlobalOptions, VrfOptions]) -> Itera
|
|
|
400
402
|
yield red.policy
|
|
401
403
|
if family_opts.aggregate and family_opts.aggregate.policy:
|
|
402
404
|
yield family_opts.aggregate.policy
|
|
405
|
+
for aggregate in family_opts.aggregates:
|
|
406
|
+
if aggregate.policy:
|
|
407
|
+
yield aggregate.policy
|
|
408
|
+
if family_opts.export_policy:
|
|
409
|
+
yield family_opts.export_policy
|
|
410
|
+
if family_opts.import_policy:
|
|
411
|
+
yield family_opts.import_policy
|
|
403
412
|
|
|
404
413
|
|
|
405
414
|
def extract_policies(config: BgpConfig) -> Sequence[str]:
|
|
@@ -407,11 +416,11 @@ def extract_policies(config: BgpConfig) -> Sequence[str]:
|
|
|
407
416
|
for vrf in config.global_options.vrf.values():
|
|
408
417
|
for group in vrf.groups:
|
|
409
418
|
result.extend(_used_policies(group))
|
|
410
|
-
result.extend(
|
|
419
|
+
result.extend(_used_families_policies(vrf))
|
|
411
420
|
result.extend(_used_policies(vrf))
|
|
412
421
|
for group in config.global_options.groups:
|
|
413
422
|
result.extend(_used_policies(group))
|
|
414
423
|
for peer in config.peers:
|
|
415
424
|
result.extend(_used_policies(peer))
|
|
416
|
-
result.extend(
|
|
425
|
+
result.extend(_used_families_policies(config.global_options))
|
|
417
426
|
return result
|
annet/mesh/device_models.py
CHANGED
annet/rpl/match_builder.py
CHANGED
|
@@ -95,8 +95,12 @@ class Checkable:
|
|
|
95
95
|
def match_v6(
|
|
96
96
|
self,
|
|
97
97
|
*names: str,
|
|
98
|
-
or_longer: OrLonger = (None, None),
|
|
98
|
+
or_longer: bool | OrLonger = (None, None),
|
|
99
99
|
) -> SingleCondition[PrefixMatchValue]:
|
|
100
|
+
if or_longer is True:
|
|
101
|
+
or_longer = (None, 128)
|
|
102
|
+
if or_longer is False:
|
|
103
|
+
or_longer = (None, None)
|
|
100
104
|
return SingleCondition(
|
|
101
105
|
MatchField.ipv6_prefix,
|
|
102
106
|
ConditionOperator.CUSTOM,
|
|
@@ -106,8 +110,12 @@ class Checkable:
|
|
|
106
110
|
def match_v4(
|
|
107
111
|
self,
|
|
108
112
|
*names: str,
|
|
109
|
-
or_longer: OrLonger = (None, None),
|
|
113
|
+
or_longer: bool | OrLonger = (None, None),
|
|
110
114
|
) -> SingleCondition[PrefixMatchValue]:
|
|
115
|
+
if or_longer is True:
|
|
116
|
+
or_longer = (None, 32)
|
|
117
|
+
if or_longer is False:
|
|
118
|
+
or_longer = (None, None)
|
|
111
119
|
return SingleCondition(
|
|
112
120
|
MatchField.ip_prefix,
|
|
113
121
|
ConditionOperator.CUSTOM,
|
annet/rpl_generators/aspath.py
CHANGED
|
@@ -71,3 +71,25 @@ class AsPathFilterGenerator(PartialGenerator, ABC):
|
|
|
71
71
|
else:
|
|
72
72
|
comma = ""
|
|
73
73
|
yield "ios-regex", f"'{filter_item}'{comma}"
|
|
74
|
+
|
|
75
|
+
def acl_juniper(self, _):
|
|
76
|
+
return r"""
|
|
77
|
+
policy-options %cant_delete
|
|
78
|
+
as-path ~
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def _juniper_as_path(self, name: str, as_path_member: str):
|
|
82
|
+
if not as_path_member.isnumeric():
|
|
83
|
+
as_path_member = f'"{as_path_member}"'
|
|
84
|
+
yield "as-path", name, as_path_member
|
|
85
|
+
|
|
86
|
+
def run_juniper(self, device: Any):
|
|
87
|
+
for as_path_filter in self.get_used_as_path_filters(device):
|
|
88
|
+
# TODO could be implemented via as-path-groups
|
|
89
|
+
# But we need to provide as_path_filters to policy generator
|
|
90
|
+
# To select between regular as-path and as-path-groups
|
|
91
|
+
if len(as_path_filter.filters) > 1:
|
|
92
|
+
raise NotImplementedError(f"Multiple elements in as_path_filter {as_path_filter.name} is not supported for Juniper")
|
|
93
|
+
|
|
94
|
+
with self.block("policy-options"):
|
|
95
|
+
yield from self._juniper_as_path(as_path_filter.name, as_path_filter.filters[0])
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from collections
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from collections.abc import Sequence, Collection, Iterator, Mapping
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
5
6
|
from annet.generators import PartialGenerator
|
|
@@ -12,6 +13,7 @@ from .entities import (
|
|
|
12
13
|
def get_used_community_lists(
|
|
13
14
|
communities: Collection[CommunityList], policies: Collection[RoutingPolicy],
|
|
14
15
|
) -> list[CommunityList]:
|
|
16
|
+
assert_unique_names(communities)
|
|
15
17
|
communities_dict = {c.name: c for c in communities}
|
|
16
18
|
used_communities: set[str] = set()
|
|
17
19
|
for policy in policies:
|
|
@@ -44,6 +46,7 @@ def get_used_united_community_lists(
|
|
|
44
46
|
"""
|
|
45
47
|
Return communities united into groups according to HAS_ANY policy
|
|
46
48
|
"""
|
|
49
|
+
assert_unique_names(communities)
|
|
47
50
|
communities_dict = {c.name: c for c in communities}
|
|
48
51
|
used_communities: dict[str, list[CommunityList]] = {}
|
|
49
52
|
for policy in policies:
|
|
@@ -93,6 +96,17 @@ def get_used_united_community_lists(
|
|
|
93
96
|
]
|
|
94
97
|
|
|
95
98
|
|
|
99
|
+
def assert_unique_names(communities: Collection[CommunityList]) -> None:
|
|
100
|
+
duplicated: list[str] = []
|
|
101
|
+
seen_names: set[str] = set()
|
|
102
|
+
for c in communities:
|
|
103
|
+
if c.name in seen_names:
|
|
104
|
+
duplicated.append(c.name)
|
|
105
|
+
seen_names.add(c.name)
|
|
106
|
+
if duplicated:
|
|
107
|
+
raise NotImplementedError(f"Non-unique community-list names are not supported: {duplicated}")
|
|
108
|
+
|
|
109
|
+
|
|
96
110
|
class CommunityListGenerator(PartialGenerator, ABC):
|
|
97
111
|
TAGS = ["policy", "rpl", "routing"]
|
|
98
112
|
|
|
@@ -274,3 +288,61 @@ class CommunityListGenerator(PartialGenerator, ABC):
|
|
|
274
288
|
def run_iosxr(self, device):
|
|
275
289
|
for community_list in self.get_used_community_lists(device):
|
|
276
290
|
yield from self._iosxr_community_list(community_list)
|
|
291
|
+
|
|
292
|
+
def acl_juniper(self, _) -> str:
|
|
293
|
+
return r"""
|
|
294
|
+
policy-options %cant_delete
|
|
295
|
+
community ~
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def _juniper_community_list(self, name: str, community_lists: list[CommunityList]) -> Iterator[Sequence[str]]:
|
|
299
|
+
members: list[str] = []
|
|
300
|
+
logic: set[CommunityLogic] = set()
|
|
301
|
+
for community_list in community_lists:
|
|
302
|
+
prefix: str
|
|
303
|
+
if community_list.type is CommunityType.BASIC:
|
|
304
|
+
prefix = ""
|
|
305
|
+
elif community_list.type is CommunityType.RT:
|
|
306
|
+
prefix = "target:"
|
|
307
|
+
elif community_list.type is CommunityType.SOO:
|
|
308
|
+
prefix = "origin:"
|
|
309
|
+
elif community_list.type is CommunityType.LARGE:
|
|
310
|
+
prefix = "large:"
|
|
311
|
+
else:
|
|
312
|
+
raise NotImplementedError(f"CommunityList {name}: type {community_list.type} not implemented for Juniper")
|
|
313
|
+
|
|
314
|
+
logic.add(community_list.logic)
|
|
315
|
+
for community in community_list.members:
|
|
316
|
+
members.append(prefix + community)
|
|
317
|
+
|
|
318
|
+
if len(members) > 1 and logic != {CommunityLogic.AND}:
|
|
319
|
+
raise NotImplementedError(f"CommunityList {name}: only AND logic between members is implemeted for Juniper")
|
|
320
|
+
|
|
321
|
+
definition = ["community", name, "members"]
|
|
322
|
+
with self.block("policy-options"):
|
|
323
|
+
if len(members) == 1:
|
|
324
|
+
yield *definition, *members
|
|
325
|
+
if len(members) > 1:
|
|
326
|
+
yield *definition, "[", *members, "]"
|
|
327
|
+
|
|
328
|
+
def run_juniper(self, device):
|
|
329
|
+
# Juniper allows different community types
|
|
330
|
+
# so we write generator in a generic way to reflect that.
|
|
331
|
+
#
|
|
332
|
+
# But get_used_community_lists DOES NOT allow multiple names
|
|
333
|
+
# This is in part because juniper does not have a type-aware match
|
|
334
|
+
# It would mean that there is no way to describe a following config via rpl.py:
|
|
335
|
+
#
|
|
336
|
+
# CommunityList("COMM_LIST", ["65000:4000"], CommunityType.BASIC),
|
|
337
|
+
# CommunityList("COMM_LIST", ["65000:4000"], CommunityType.RT),
|
|
338
|
+
#
|
|
339
|
+
# # match only route-target but not basic one
|
|
340
|
+
# with route(R.extcommunity_rt.has("COMM_LIST")) as rule:
|
|
341
|
+
# ...
|
|
342
|
+
used = self.get_used_community_lists(device)
|
|
343
|
+
by_name: Mapping[str, list[CommunityList]] = defaultdict(list)
|
|
344
|
+
for community_list in used:
|
|
345
|
+
by_name[community_list.name].append(community_list)
|
|
346
|
+
|
|
347
|
+
for name, community_lists in by_name.items():
|
|
348
|
+
yield from self._juniper_community_list(name, community_lists)
|
annet/rpl_generators/entities.py
CHANGED
|
@@ -3,7 +3,7 @@ from collections import defaultdict
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from enum import Enum
|
|
6
|
-
from typing import Optional, List
|
|
6
|
+
from typing import Optional, List, Literal
|
|
7
7
|
|
|
8
8
|
from annet.rpl import RoutingPolicy, PrefixMatchValue, OrLonger
|
|
9
9
|
|
|
@@ -103,12 +103,12 @@ class PrefixListNameGenerator:
|
|
|
103
103
|
self._prefix_lists = {x.name: x for x in prefix_lists}
|
|
104
104
|
self._policies = {x.name: x for x in policies} # this is here for a later use ~azryve@
|
|
105
105
|
|
|
106
|
-
def get_prefix(self, name: str, match: PrefixMatchValue) -> IpPrefixList:
|
|
106
|
+
def get_prefix(self, name: str, match: PrefixMatchValue | None) -> IpPrefixList:
|
|
107
107
|
orig_prefix = self._prefix_lists[name]
|
|
108
108
|
override_name: Optional[str] = None
|
|
109
109
|
override_orlonger: Optional[OrLonger] = None
|
|
110
110
|
|
|
111
|
-
if
|
|
111
|
+
if match and match.or_longer != (None, None):
|
|
112
112
|
ge, le = match.or_longer
|
|
113
113
|
ge_str = "unset" if ge is None else str(ge)
|
|
114
114
|
le_str = "unset" if le is None else str(le)
|
|
@@ -135,3 +135,50 @@ def group_community_members(
|
|
|
135
135
|
community = all_communities[community_name]
|
|
136
136
|
members[community.type].extend(community.members)
|
|
137
137
|
return members
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class JuniperPrefixListNameGenerator(PrefixListNameGenerator):
|
|
141
|
+
def get_prefix(self, name: str, match: PrefixMatchValue | None) -> IpPrefixList:
|
|
142
|
+
plist = super().get_prefix(name, match)
|
|
143
|
+
flavour = self.get_plist_flavour(plist)
|
|
144
|
+
# keep the orginal name for an orlonger match
|
|
145
|
+
if flavour != "custom":
|
|
146
|
+
plist.name = name
|
|
147
|
+
return plist
|
|
148
|
+
|
|
149
|
+
def get_type(self, name: str, match: PrefixMatchValue | None) -> Literal["prefix-list", "route-filter"]:
|
|
150
|
+
orig_plist = self.get_prefix(name, None)
|
|
151
|
+
plist = self.get_prefix(name, match)
|
|
152
|
+
orig_flavour = self.get_plist_flavour(orig_plist)
|
|
153
|
+
flavour = self.get_plist_flavour(plist)
|
|
154
|
+
if orig_flavour == "custom" or flavour == "custom":
|
|
155
|
+
return "route-filter"
|
|
156
|
+
else:
|
|
157
|
+
return "prefix-list"
|
|
158
|
+
|
|
159
|
+
def get_plist_flavour(self, plist: IpPrefixList) -> Literal["simple", "orlonger", "custom"]:
|
|
160
|
+
is_orlonger: bool = False
|
|
161
|
+
is_custom: bool = False
|
|
162
|
+
for m in plist.members:
|
|
163
|
+
ge, le = m.or_longer
|
|
164
|
+
|
|
165
|
+
# or_longer != (None, None)
|
|
166
|
+
if ge is not None or le is not None:
|
|
167
|
+
is_orlonger = True
|
|
168
|
+
|
|
169
|
+
# orlonger != (n, None), where n is .../n
|
|
170
|
+
if ge is not None and ge != m.prefix.prefixlen:
|
|
171
|
+
is_custom = True
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
# orlonger != (None, n), where n is 32 or 128 (ipv4/ipv6)
|
|
175
|
+
if le is not None and le != m.prefix.max_prefixlen:
|
|
176
|
+
is_custom = True
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
if is_custom:
|
|
180
|
+
return "custom"
|
|
181
|
+
elif is_orlonger:
|
|
182
|
+
return "orlonger"
|
|
183
|
+
else:
|
|
184
|
+
return "simple"
|
annet/rpl_generators/policy.py
CHANGED
|
@@ -6,13 +6,13 @@ from annet.generators import PartialGenerator
|
|
|
6
6
|
from annet.rpl import (
|
|
7
7
|
CommunityActionValue,
|
|
8
8
|
ResultType, RoutingPolicyStatement, RoutingPolicy, ConditionOperator, SingleCondition, SingleAction, ActionType,
|
|
9
|
-
MatchField, PrefixMatchValue,
|
|
9
|
+
MatchField, PrefixMatchValue, AndCondition, Action,
|
|
10
10
|
)
|
|
11
11
|
from annet.rpl.statement_builder import AsPathActionValue, NextHopActionValue, ThenField
|
|
12
12
|
from annet.rpl_generators.entities import (
|
|
13
13
|
arista_well_known_community,
|
|
14
14
|
CommunityList, RDFilter, PrefixListNameGenerator, CommunityLogic, mangle_united_community_list_name,
|
|
15
|
-
IpPrefixList, group_community_members, CommunityType,
|
|
15
|
+
IpPrefixList, group_community_members, CommunityType, JuniperPrefixListNameGenerator
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
HUAWEI_MATCH_COMMAND_MAP: dict[str, str] = {
|
|
@@ -79,6 +79,31 @@ IOSXR_RESULT_MAP = {
|
|
|
79
79
|
ResultType.DENY: "drop",
|
|
80
80
|
ResultType.NEXT: "pass"
|
|
81
81
|
}
|
|
82
|
+
JUNIPER_MATCH_COMMAND_MAP: dict[str, str] = {
|
|
83
|
+
MatchField.protocol: "protocol {option_value}",
|
|
84
|
+
MatchField.metric: "metric {option_value}",
|
|
85
|
+
MatchField.as_path_filter: "as-path {option_value}",
|
|
86
|
+
MatchField.local_pref: "local-preference {option_value}",
|
|
87
|
+
# unsupported: rd
|
|
88
|
+
# unsupported: interface
|
|
89
|
+
# unsupported: net_len
|
|
90
|
+
# unsupported: family
|
|
91
|
+
}
|
|
92
|
+
JUNIPER_THEN_COMMAND_MAP: dict[str, str] = {
|
|
93
|
+
ThenField.local_pref: "local-preference {option_value}",
|
|
94
|
+
ThenField.origin: "origin {option_value}",
|
|
95
|
+
ThenField.tag: "tag {option_value}",
|
|
96
|
+
ThenField.metric: "metric {option_value}",
|
|
97
|
+
# unsupported: rpki_valid_state
|
|
98
|
+
# unsupported: resolution
|
|
99
|
+
# unsupported: mpls_label
|
|
100
|
+
# unsupported: metric_type
|
|
101
|
+
}
|
|
102
|
+
JUNIPER_RESULT_MAP = {
|
|
103
|
+
ResultType.ALLOW: "accept",
|
|
104
|
+
ResultType.DENY: "reject",
|
|
105
|
+
ResultType.NEXT: "next term"
|
|
106
|
+
}
|
|
82
107
|
|
|
83
108
|
|
|
84
109
|
class RoutingPolicyGenerator(PartialGenerator, ABC):
|
|
@@ -1119,3 +1144,343 @@ class RoutingPolicyGenerator(PartialGenerator, ABC):
|
|
|
1119
1144
|
yield from self._iosxr_statement(
|
|
1120
1145
|
communities, rd_filters, device, policy, statement, prefix_name_generator,
|
|
1121
1146
|
)
|
|
1147
|
+
|
|
1148
|
+
# Juniper
|
|
1149
|
+
def acl_juniper(self, device):
|
|
1150
|
+
return r"""
|
|
1151
|
+
policy-options %cant_delete
|
|
1152
|
+
policy-statement
|
|
1153
|
+
~ %global
|
|
1154
|
+
"""
|
|
1155
|
+
|
|
1156
|
+
def _juniper_match_communities(
|
|
1157
|
+
self,
|
|
1158
|
+
section: Literal["", "from"],
|
|
1159
|
+
conditions: list[SingleCondition],
|
|
1160
|
+
) -> Iterator[Sequence[str]]:
|
|
1161
|
+
names: list[str] = [name for cond in conditions for name in cond.value]
|
|
1162
|
+
operators = {x.operator for x in conditions}
|
|
1163
|
+
if len(names) > 1 and operators != {ConditionOperator.HAS_ANY}:
|
|
1164
|
+
raise NotImplementedError(
|
|
1165
|
+
f"Multiple community match [{' '.join(names)}] without has_any is not supported for Juniper",
|
|
1166
|
+
)
|
|
1167
|
+
yield section, "community", self._juniper_list_bracket(names)
|
|
1168
|
+
|
|
1169
|
+
def _juniper_match_prefix_lists(
|
|
1170
|
+
self,
|
|
1171
|
+
section: Literal["", "from"],
|
|
1172
|
+
conditions: list[SingleCondition[PrefixMatchValue]],
|
|
1173
|
+
name_generator: JuniperPrefixListNameGenerator,
|
|
1174
|
+
) -> Iterator[Sequence[str]]:
|
|
1175
|
+
operators = {x.operator for x in conditions}
|
|
1176
|
+
supported = {ConditionOperator.HAS_ANY}
|
|
1177
|
+
not_supported = operators - supported
|
|
1178
|
+
if len(conditions) > 1 and not_supported:
|
|
1179
|
+
raise NotImplementedError(
|
|
1180
|
+
f"Multiple prefix match with ops {not_supported} is not supported for Juniper",
|
|
1181
|
+
)
|
|
1182
|
+
for cond in conditions:
|
|
1183
|
+
for name in cond.value.names:
|
|
1184
|
+
prefix_list = name_generator.get_prefix(name, cond.value)
|
|
1185
|
+
plist_type = name_generator.get_type(name, cond.value)
|
|
1186
|
+
flavour = name_generator.get_plist_flavour(prefix_list)
|
|
1187
|
+
if plist_type == "prefix-list" and flavour == "simple":
|
|
1188
|
+
yield section, "prefix-list", prefix_list.name
|
|
1189
|
+
elif plist_type == "prefix-list" and flavour == "orlonger":
|
|
1190
|
+
yield section, "prefix-list-filter", prefix_list.name, "orlonger"
|
|
1191
|
+
elif plist_type == "route-filter":
|
|
1192
|
+
yield section, "route-filter-list", prefix_list.name
|
|
1193
|
+
else:
|
|
1194
|
+
raise NotImplementedError(
|
|
1195
|
+
f"Prefix list {prefix_list.name} type {plist_type} flavour {flavour} is not supported for Juniper",
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
def _juniper_match_as_path_length(
|
|
1199
|
+
self,
|
|
1200
|
+
section: Literal["", "from"],
|
|
1201
|
+
conditions: list[SingleCondition],
|
|
1202
|
+
) -> Iterator[Sequence[str]]:
|
|
1203
|
+
for condition in conditions:
|
|
1204
|
+
if condition.operator is ConditionOperator.EQ:
|
|
1205
|
+
yield section, "as-path-calc-length", str(condition.value), "equal"
|
|
1206
|
+
elif condition.operator is ConditionOperator.LE:
|
|
1207
|
+
yield section, "as-path-calc-length", str(condition.value), "orlower"
|
|
1208
|
+
elif condition.operator is ConditionOperator.GE:
|
|
1209
|
+
yield section, "as-path-calc-length", str(condition.value), "orhigher"
|
|
1210
|
+
elif condition.operator is ConditionOperator.BETWEEN_INCLUDED:
|
|
1211
|
+
yield section, "as-path-calc-length", str(condition.value[0]), "orhigher"
|
|
1212
|
+
yield section, "as-path-calc-length", str(condition.value[1]), "orlower"
|
|
1213
|
+
else:
|
|
1214
|
+
raise NotImplementedError(
|
|
1215
|
+
f"Operator {condition.operator} is not supported for {condition.field} on Juniper",
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
def _juniper_match_rd_filter(
|
|
1219
|
+
self,
|
|
1220
|
+
section: Literal["", "from"],
|
|
1221
|
+
conditions: list[SingleCondition[Sequence[str]]],
|
|
1222
|
+
) -> Iterator[Sequence[str]]:
|
|
1223
|
+
names = [x for c in conditions for x in c.value]
|
|
1224
|
+
operators = {x.operator for x in conditions}
|
|
1225
|
+
supported = {ConditionOperator.HAS_ANY}
|
|
1226
|
+
not_supported = operators - supported
|
|
1227
|
+
if len(names) > 1 and not_supported:
|
|
1228
|
+
raise NotImplementedError(
|
|
1229
|
+
f"Multiple rd_filter matches with ops {not_supported} is not supported for Juniper",
|
|
1230
|
+
)
|
|
1231
|
+
yield section, "route-distinguisher", self._juniper_list_bracket(names)
|
|
1232
|
+
|
|
1233
|
+
def _juniper_match_community_fields(self) -> set[MatchField]:
|
|
1234
|
+
return {
|
|
1235
|
+
MatchField.community,
|
|
1236
|
+
MatchField.extcommunity_rt,
|
|
1237
|
+
MatchField.extcommunity_soo,
|
|
1238
|
+
MatchField.large_community,
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
def _juniper_match_prefix_fields(self) -> set[MatchField]:
|
|
1242
|
+
return {
|
|
1243
|
+
MatchField.ip_prefix,
|
|
1244
|
+
MatchField.ipv6_prefix,
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
def _juniper_is_match_inlined(self, conditions: AndCondition) -> bool:
|
|
1248
|
+
used_fields = {x.field for x in conditions}
|
|
1249
|
+
used_prefix_fields = used_fields & self._juniper_match_prefix_fields()
|
|
1250
|
+
used_community_fields = used_fields & self._juniper_match_community_fields()
|
|
1251
|
+
|
|
1252
|
+
# prefix-list match is never inlined
|
|
1253
|
+
if used_prefix_fields:
|
|
1254
|
+
return False
|
|
1255
|
+
|
|
1256
|
+
# as-path-calc-length is never inlined
|
|
1257
|
+
if MatchField.as_path_length in used_fields:
|
|
1258
|
+
return False
|
|
1259
|
+
|
|
1260
|
+
# only community matches and nothing more
|
|
1261
|
+
if used_community_fields and used_fields == used_community_fields:
|
|
1262
|
+
return True
|
|
1263
|
+
|
|
1264
|
+
# inline when empty or just one match
|
|
1265
|
+
if len(used_fields) <= 1:
|
|
1266
|
+
return True
|
|
1267
|
+
return False
|
|
1268
|
+
|
|
1269
|
+
def _juniper_match(
|
|
1270
|
+
self,
|
|
1271
|
+
policy: RoutingPolicy,
|
|
1272
|
+
section: Literal["", "from"],
|
|
1273
|
+
conditions: AndCondition,
|
|
1274
|
+
prefix_name_generator: JuniperPrefixListNameGenerator,
|
|
1275
|
+
) -> Iterator[Sequence[str]]:
|
|
1276
|
+
community_fields = self._juniper_match_community_fields()
|
|
1277
|
+
prefix_fields = self._juniper_match_prefix_fields()
|
|
1278
|
+
community_conditions: list[SingleCondition] = []
|
|
1279
|
+
prefix_conditions: list[SingleCondition] = []
|
|
1280
|
+
simple_conditions: list[SingleCondition] = []
|
|
1281
|
+
as_path_length_conditions: list[SingleCondition] = []
|
|
1282
|
+
rd_filter_conditions: list[SingleCondition] = []
|
|
1283
|
+
for condition in conditions:
|
|
1284
|
+
if condition.field in community_fields:
|
|
1285
|
+
community_conditions.append(condition)
|
|
1286
|
+
elif condition.field in prefix_fields:
|
|
1287
|
+
prefix_conditions.append(condition)
|
|
1288
|
+
elif condition.field == MatchField.as_path_length:
|
|
1289
|
+
as_path_length_conditions.append(condition)
|
|
1290
|
+
elif condition.field == MatchField.rd:
|
|
1291
|
+
rd_filter_conditions.append(condition)
|
|
1292
|
+
else:
|
|
1293
|
+
simple_conditions.append(condition)
|
|
1294
|
+
|
|
1295
|
+
if community_conditions:
|
|
1296
|
+
yield from self._juniper_match_communities(section, community_conditions)
|
|
1297
|
+
if prefix_conditions:
|
|
1298
|
+
yield from self._juniper_match_prefix_lists(section, prefix_conditions, prefix_name_generator)
|
|
1299
|
+
if as_path_length_conditions:
|
|
1300
|
+
yield from self._juniper_match_as_path_length(section, as_path_length_conditions)
|
|
1301
|
+
if rd_filter_conditions:
|
|
1302
|
+
yield from self._juniper_match_rd_filter(section, rd_filter_conditions)
|
|
1303
|
+
|
|
1304
|
+
for condition in simple_conditions:
|
|
1305
|
+
if condition.operator is not ConditionOperator.EQ:
|
|
1306
|
+
raise NotImplementedError(
|
|
1307
|
+
f"`{condition.field}` with operator {condition.operator} in {policy.name} is not supported for Juniper",
|
|
1308
|
+
)
|
|
1309
|
+
if condition.field not in JUNIPER_MATCH_COMMAND_MAP:
|
|
1310
|
+
raise NotImplementedError(f"Match using `{condition.field}` in {policy.name} is not supported for Juniper")
|
|
1311
|
+
yield section, JUNIPER_MATCH_COMMAND_MAP[condition.field].format(option_value=condition.value)
|
|
1312
|
+
|
|
1313
|
+
def _juniper_then_community(
|
|
1314
|
+
self,
|
|
1315
|
+
section: Literal["", "then"],
|
|
1316
|
+
actions: list[SingleAction[CommunityActionValue]]
|
|
1317
|
+
):
|
|
1318
|
+
# juniper community ops are ORDERED
|
|
1319
|
+
# since data model does not support it
|
|
1320
|
+
# we use the order that makes sense: delete, set, add
|
|
1321
|
+
for single_action in actions:
|
|
1322
|
+
action = single_action.value
|
|
1323
|
+
for name in action.removed:
|
|
1324
|
+
yield section, "community", "delete", name
|
|
1325
|
+
|
|
1326
|
+
if action.replaced is not None:
|
|
1327
|
+
if not action.replaced:
|
|
1328
|
+
raise NotImplementedError("Empty community.set() is not supported for Juniper")
|
|
1329
|
+
for name in action.replaced:
|
|
1330
|
+
yield section, "community", "set", name
|
|
1331
|
+
|
|
1332
|
+
for name in action.added:
|
|
1333
|
+
yield section, "community", "add", name
|
|
1334
|
+
|
|
1335
|
+
def _juniper_then_next_hop(
|
|
1336
|
+
self,
|
|
1337
|
+
section: Literal["", "then"],
|
|
1338
|
+
actions: list[SingleAction[NextHopActionValue]],
|
|
1339
|
+
):
|
|
1340
|
+
if len(actions) > 1:
|
|
1341
|
+
raise NotImplementedError("Only single next-hop action is supported for Juniper")
|
|
1342
|
+
|
|
1343
|
+
action = actions[0]
|
|
1344
|
+
if action.value.target == "self":
|
|
1345
|
+
yield section, "next-hop", "self"
|
|
1346
|
+
elif action.value.target == "discard":
|
|
1347
|
+
yield section, "next-hop", "discard"
|
|
1348
|
+
elif action.value.target == "peer":
|
|
1349
|
+
yield section, "next-hop", "peer-address"
|
|
1350
|
+
elif action.value.target == "ipv4_addr":
|
|
1351
|
+
yield section, "next-hop", action.value.addr
|
|
1352
|
+
elif action.value.target == "ipv6_addr":
|
|
1353
|
+
yield section, "next-hop", action.value.addr.lower()
|
|
1354
|
+
elif action.value.target == "mapped_ipv4":
|
|
1355
|
+
yield section, "next-hop", f"::ffff:{action.value.addr}"
|
|
1356
|
+
else:
|
|
1357
|
+
raise NotImplementedError(f"Next_hop target {action.value.target} is not supported for Juniper")
|
|
1358
|
+
|
|
1359
|
+
def _juniper_list_quote(self, items: list[str]) -> str:
|
|
1360
|
+
joined = " ".join(items)
|
|
1361
|
+
if len(items) > 1:
|
|
1362
|
+
joined = f'"{joined}"'
|
|
1363
|
+
return joined
|
|
1364
|
+
|
|
1365
|
+
def _juniper_list_bracket(self, items: list[str]) -> str:
|
|
1366
|
+
joined = " ".join(items)
|
|
1367
|
+
if len(items) > 1:
|
|
1368
|
+
joined = f"[ {joined} ]"
|
|
1369
|
+
return joined
|
|
1370
|
+
|
|
1371
|
+
def _juniper_then_as_path(
|
|
1372
|
+
self,
|
|
1373
|
+
section: Literal["", "then"],
|
|
1374
|
+
actions: list[SingleAction[AsPathActionValue]],
|
|
1375
|
+
):
|
|
1376
|
+
if len(actions) > 1:
|
|
1377
|
+
raise NotImplementedError("Only single next-hop action is supported for Juniper")
|
|
1378
|
+
|
|
1379
|
+
action = actions[0]
|
|
1380
|
+
if action.value.expand and action.value.expand_last_as:
|
|
1381
|
+
raise NotImplementedError("Setting both `as_path.expand` and `as_path.expand_last_as` is not supported for Juniper")
|
|
1382
|
+
|
|
1383
|
+
if action.value.prepend:
|
|
1384
|
+
yield section, "as-path-prepend", self._juniper_list_quote(action.value.prepend)
|
|
1385
|
+
if action.value.expand:
|
|
1386
|
+
yield section, "as-path-expand", self._juniper_list_quote(action.value.expand)
|
|
1387
|
+
if action.value.expand_last_as:
|
|
1388
|
+
yield section, "as-path-expand last-as count", action.value.expand_last_as
|
|
1389
|
+
if action.value.set is not None:
|
|
1390
|
+
raise RuntimeError("as_path.set is not supported for Juniper")
|
|
1391
|
+
if action.value.delete:
|
|
1392
|
+
raise RuntimeError("as_path.delete is not supported for Juniper")
|
|
1393
|
+
|
|
1394
|
+
def _juniper_is_then_inlined(self, action: Action) -> bool:
|
|
1395
|
+
used_fields = {x.field for x in action}
|
|
1396
|
+
# inline when no actions permormed
|
|
1397
|
+
if not used_fields:
|
|
1398
|
+
return True
|
|
1399
|
+
return False
|
|
1400
|
+
|
|
1401
|
+
def _juniper_then(
|
|
1402
|
+
self,
|
|
1403
|
+
policy: RoutingPolicy,
|
|
1404
|
+
section: Literal["", "then"],
|
|
1405
|
+
actions: Action,
|
|
1406
|
+
) -> Iterator[Sequence[str]]:
|
|
1407
|
+
community_actions: list[SingleAction] = []
|
|
1408
|
+
next_hop_actions: list[SingleAction] = []
|
|
1409
|
+
as_path_actions: list[SingleAction] = []
|
|
1410
|
+
simple_actions: list[SingleAction] = []
|
|
1411
|
+
for action in actions:
|
|
1412
|
+
if action.field == ThenField.community:
|
|
1413
|
+
community_actions.append(action)
|
|
1414
|
+
elif action.field == ThenField.extcommunity:
|
|
1415
|
+
community_actions.append(action)
|
|
1416
|
+
elif action.field == ThenField.extcommunity_rt:
|
|
1417
|
+
community_actions.append(action)
|
|
1418
|
+
elif action.field == ThenField.extcommunity_soo:
|
|
1419
|
+
community_actions.append(action)
|
|
1420
|
+
elif action.field == ThenField.large_community:
|
|
1421
|
+
community_actions.append(action)
|
|
1422
|
+
elif action.field == ThenField.next_hop:
|
|
1423
|
+
next_hop_actions.append(action)
|
|
1424
|
+
elif action.field == ThenField.as_path:
|
|
1425
|
+
as_path_actions.append(action)
|
|
1426
|
+
else:
|
|
1427
|
+
simple_actions.append(action)
|
|
1428
|
+
|
|
1429
|
+
if community_actions:
|
|
1430
|
+
yield from self._juniper_then_community(section, community_actions)
|
|
1431
|
+
if next_hop_actions:
|
|
1432
|
+
yield from self._juniper_then_next_hop(section, next_hop_actions)
|
|
1433
|
+
if as_path_actions:
|
|
1434
|
+
yield from self._juniper_then_as_path(section, as_path_actions)
|
|
1435
|
+
|
|
1436
|
+
for action in simple_actions:
|
|
1437
|
+
if action.type not in {ActionType.SET}:
|
|
1438
|
+
raise NotImplementedError(f"Action type {action.type} for `{action.field}` in {policy.name} is not supported for Juniper")
|
|
1439
|
+
if action.field not in JUNIPER_THEN_COMMAND_MAP:
|
|
1440
|
+
raise NotImplementedError(f"Then action using `{action.field}` in {policy.name} is not supported for Juniper")
|
|
1441
|
+
yield section, JUNIPER_THEN_COMMAND_MAP[action.field].format(option_value=action.value)
|
|
1442
|
+
|
|
1443
|
+
def _juniper_statements(
|
|
1444
|
+
self,
|
|
1445
|
+
device: Any,
|
|
1446
|
+
policy: RoutingPolicy,
|
|
1447
|
+
prefix_name_generator: JuniperPrefixListNameGenerator,
|
|
1448
|
+
) -> Iterator[Sequence[str]]:
|
|
1449
|
+
term_number = 0
|
|
1450
|
+
for statement in policy.statements:
|
|
1451
|
+
if statement.number is not None:
|
|
1452
|
+
term_number = statement.number
|
|
1453
|
+
term_name = statement.name
|
|
1454
|
+
if not term_name:
|
|
1455
|
+
term_name = f"{policy.name}_{term_number}"
|
|
1456
|
+
term_number += 1
|
|
1457
|
+
|
|
1458
|
+
with self.block("term", term_name):
|
|
1459
|
+
# see test_juniper_inline
|
|
1460
|
+
match_inlined = self._juniper_is_match_inlined(statement.match)
|
|
1461
|
+
then_inlined = self._juniper_is_then_inlined(statement.then)
|
|
1462
|
+
match_section: Literal["", "from"] = "from" if match_inlined else ""
|
|
1463
|
+
then_section: Literal["", "then"] = "then" if then_inlined else ""
|
|
1464
|
+
|
|
1465
|
+
if statement.match:
|
|
1466
|
+
with self.block_if("from", condition=not match_inlined):
|
|
1467
|
+
yield from self._juniper_match(policy, match_section, statement.match, prefix_name_generator)
|
|
1468
|
+
|
|
1469
|
+
if statement.then:
|
|
1470
|
+
with self.block_if("then", condition=not then_inlined):
|
|
1471
|
+
yield from self._juniper_then(policy, then_section, statement.then)
|
|
1472
|
+
|
|
1473
|
+
with self.block_if("then", condition=not then_inlined):
|
|
1474
|
+
yield then_section, JUNIPER_RESULT_MAP[statement.result]
|
|
1475
|
+
|
|
1476
|
+
def run_juniper(self, device):
|
|
1477
|
+
prefix_lists = self.get_prefix_lists(device)
|
|
1478
|
+
policies = self.get_policies(device)
|
|
1479
|
+
prefix_name_generator = JuniperPrefixListNameGenerator(prefix_lists, policies)
|
|
1480
|
+
|
|
1481
|
+
for policy in policies:
|
|
1482
|
+
with self.block("policy-options"):
|
|
1483
|
+
with self.block("policy-statement", policy.name):
|
|
1484
|
+
yield from self._juniper_statements(
|
|
1485
|
+
device, policy, prefix_name_generator,
|
|
1486
|
+
)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from collections.abc import Sequence, Iterable
|
|
3
3
|
from typing import Any, Literal
|
|
4
|
+
from itertools import chain
|
|
4
5
|
|
|
5
6
|
from annet.generators import PartialGenerator
|
|
6
7
|
from annet.rpl import PrefixMatchValue, MatchField, SingleCondition, RoutingPolicy
|
|
7
|
-
from .entities import IpPrefixList, PrefixListNameGenerator
|
|
8
|
+
from .entities import IpPrefixList, PrefixListNameGenerator, JuniperPrefixListNameGenerator
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class PrefixListFilterGenerator(PartialGenerator, ABC):
|
|
@@ -170,3 +171,68 @@ class PrefixListFilterGenerator(PartialGenerator, ABC):
|
|
|
170
171
|
continue
|
|
171
172
|
yield from self._iosxr_prefixlist(plist)
|
|
172
173
|
processed_names.add(plist.name)
|
|
174
|
+
|
|
175
|
+
def acl_juniper(self, _):
|
|
176
|
+
return r"""
|
|
177
|
+
policy-options %cant_delete
|
|
178
|
+
prefix-list *
|
|
179
|
+
~
|
|
180
|
+
route-filter-list *
|
|
181
|
+
~
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def _juniper_prefixlist(self, name: str, prefixlist: IpPrefixList):
|
|
185
|
+
with self.block("policy-options"):
|
|
186
|
+
with self.block("prefix-list", name):
|
|
187
|
+
for member in prefixlist.members:
|
|
188
|
+
yield f"{member.prefix}"
|
|
189
|
+
|
|
190
|
+
def _juniper_router_filter_list(self, name: str, prefixlist: IpPrefixList):
|
|
191
|
+
with self.block("policy-options"):
|
|
192
|
+
with self.block("route-filter-list", name):
|
|
193
|
+
for member in prefixlist.members:
|
|
194
|
+
ge, le = member.or_longer
|
|
195
|
+
if ge is None and le is None:
|
|
196
|
+
yield f"{member.prefix} exact"
|
|
197
|
+
continue
|
|
198
|
+
if ge is None:
|
|
199
|
+
ge = member.prefix.prefixlen
|
|
200
|
+
if le is None:
|
|
201
|
+
le = member.prefix.max_prefixlen
|
|
202
|
+
# may produce config that is not accepted by commit
|
|
203
|
+
# since juniper enforces that n <= ge <= le
|
|
204
|
+
# where n is prefix len: .../n
|
|
205
|
+
|
|
206
|
+
# this is done specifically to match other generators behaviour
|
|
207
|
+
# can be revised in two ways: exeption or enforce via max/min
|
|
208
|
+
# but need to be consistent across vendors so will leave it for now
|
|
209
|
+
yield f"{member.prefix}", "prefix-length-range", f"/{ge}-/{le}"
|
|
210
|
+
|
|
211
|
+
def run_juniper(self, device: Any):
|
|
212
|
+
prefix_lists = self.get_prefix_lists(device)
|
|
213
|
+
policies = self.get_policies(device)
|
|
214
|
+
|
|
215
|
+
name_generator = JuniperPrefixListNameGenerator(prefix_lists, policies)
|
|
216
|
+
processed_names: set[str] = set()
|
|
217
|
+
for policy in policies:
|
|
218
|
+
for statement in policy.statements:
|
|
219
|
+
conds = chain(
|
|
220
|
+
statement.match.find_all(MatchField.ip_prefix),
|
|
221
|
+
statement.match.find_all(MatchField.ipv6_prefix),
|
|
222
|
+
)
|
|
223
|
+
cond: SingleCondition[PrefixMatchValue]
|
|
224
|
+
for cond in conds:
|
|
225
|
+
for cond_name in cond.value.names:
|
|
226
|
+
plist = name_generator.get_prefix(cond_name, cond.value)
|
|
227
|
+
plist_type = name_generator.get_type(cond_name, cond.value)
|
|
228
|
+
|
|
229
|
+
if plist.name not in processed_names:
|
|
230
|
+
processed_names.add(plist.name)
|
|
231
|
+
if plist_type == "prefix-list":
|
|
232
|
+
yield from self._juniper_prefixlist(plist.name, plist)
|
|
233
|
+
elif plist_type == "route-filter":
|
|
234
|
+
yield from self._juniper_router_filter_list(plist.name, plist)
|
|
235
|
+
else:
|
|
236
|
+
raise NotImplementedError(
|
|
237
|
+
f"Prefix list {cond_name} type {plist_type} is not supported for Juniper",
|
|
238
|
+
)
|
annet/rpl_generators/rd.py
CHANGED
|
@@ -54,3 +54,18 @@ class RDFilterFilterGenerator(PartialGenerator, ABC):
|
|
|
54
54
|
else:
|
|
55
55
|
comma = ""
|
|
56
56
|
yield f"{route_distinguisher}{comma}",
|
|
57
|
+
|
|
58
|
+
def acl_juniper(self, _):
|
|
59
|
+
return r"""
|
|
60
|
+
policy-options %cant_delete
|
|
61
|
+
route-distinguisher
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def run_juniper(self, device: Any):
|
|
65
|
+
for rd_filter in self.get_used_rd_filters(device):
|
|
66
|
+
with self.block("policy-options"):
|
|
67
|
+
if len(rd_filter.members) == 1:
|
|
68
|
+
yield "route-distinguisher", rd_filter.name, "members", rd_filter.members[0]
|
|
69
|
+
elif len(rd_filter.members) > 1:
|
|
70
|
+
joined = " ".join(rd_filter.members)
|
|
71
|
+
yield "route-distinguisher", rd_filter.name, "members", f"[ {joined} ]"
|
annet/rulebook/texts/huawei.rul
CHANGED
annet/rulebook/texts/juniper.rul
CHANGED
annet/vendors/tabparser.py
CHANGED
|
@@ -487,7 +487,7 @@ class JuniperFormatter(CommonFormatter):
|
|
|
487
487
|
|
|
488
488
|
self._sub_regexs = (
|
|
489
489
|
(re.compile(self._block_begin + r"\s*" + self._block_end + r"$"), ""), # collapse empty blocks
|
|
490
|
-
(re.compile(self._block_begin + "(\t# .+)?$"), ""),
|
|
490
|
+
(re.compile(self._block_begin + r"\s*" + r"(\t# .+)?$"), ""),
|
|
491
491
|
(re.compile(self._statement_end + r"$"), ""),
|
|
492
492
|
(re.compile(r"\s*" + self._block_end + "(\t# .+)?$"), ""),
|
|
493
493
|
(re.compile(self._endofline_comment + r".*$"), ""),
|
|
@@ -823,41 +823,23 @@ class RosFormatter(CommonFormatter):
|
|
|
823
823
|
(re.compile(r"^add "), ""),
|
|
824
824
|
(re.compile(r"^print file="), "name="),
|
|
825
825
|
)
|
|
826
|
-
|
|
826
|
+
|
|
827
|
+
commands = odict()
|
|
827
828
|
for item in patch.itms:
|
|
828
829
|
key, childs, context = item.row, item.child, item.context
|
|
829
830
|
if childs:
|
|
830
|
-
|
|
831
|
+
childs_prev = f"{_prev} {key.strip()}".lstrip()
|
|
832
|
+
commands.update(self.cmd_paths(childs, _prev=childs_prev))
|
|
831
833
|
else:
|
|
832
|
-
|
|
834
|
+
if key.startswith("remove"):
|
|
835
|
+
find_cmd = key.removeprefix("remove").strip()
|
|
836
|
+
for regex, repl_line in rm_regexs:
|
|
837
|
+
find_cmd = regex.sub(repl_line, find_cmd)
|
|
838
|
+
cmd = f"/{_prev} remove [ find {find_cmd} ]"
|
|
839
|
+
else:
|
|
840
|
+
cmd = f"/{_prev} {key}"
|
|
841
|
+
commands[(cmd,)] = context
|
|
833
842
|
|
|
834
|
-
commands = odict()
|
|
835
|
-
prev_cmd = None
|
|
836
|
-
prev_context = None
|
|
837
|
-
for childs, items in itertools.groupby(patch_items, lambda x: x[1]):
|
|
838
|
-
if childs is None:
|
|
839
|
-
if prev_cmd:
|
|
840
|
-
commands[(prev_cmd,)] = prev_context
|
|
841
|
-
for key, _, context in items:
|
|
842
|
-
if key.startswith("remove"):
|
|
843
|
-
find_cmd = key.replace("remove", "", 1).strip()
|
|
844
|
-
for (regex, repl_line) in rm_regexs:
|
|
845
|
-
find_cmd = regex.sub(repl_line, find_cmd)
|
|
846
|
-
cmd = "remove [ find " + find_cmd + " ]"
|
|
847
|
-
else:
|
|
848
|
-
cmd = key
|
|
849
|
-
commands[(cmd,)] = context
|
|
850
|
-
else:
|
|
851
|
-
for key, _, context in items:
|
|
852
|
-
if _prev:
|
|
853
|
-
prev_cmd = _prev
|
|
854
|
-
prev_context = context
|
|
855
|
-
block_cmd = f"{_prev} {key}"
|
|
856
|
-
else:
|
|
857
|
-
block_cmd = f"/{key}"
|
|
858
|
-
commands[(block_cmd,)] = context
|
|
859
|
-
for k, v in self.cmd_paths(childs, block_cmd).items():
|
|
860
|
-
commands[k] = v
|
|
861
843
|
return commands
|
|
862
844
|
|
|
863
845
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
annet/__init__.py,sha256=eDQub3o0HdHcTWU4ZJpySrJuffGkbWts5CHXZYwVV9A,1915
|
|
2
2
|
annet/annet.py,sha256=vyQ__n5hkjub3aWO8tksHPoUSbTeK97MyMcR_U8_BSU,1016
|
|
3
3
|
annet/argparse.py,sha256=v1MfhjR0B8qahza0WinmXClpR8UiDFhmwDDWtNroJPA,12855
|
|
4
|
-
annet/bgp_models.py,sha256=
|
|
4
|
+
annet/bgp_models.py,sha256=QTzA6wu2_XWIr-Omq303LOfaNo3z_pHbc5g_zm0vz7g,13062
|
|
5
5
|
annet/cli.py,sha256=shq3hHzrTxFL3x1_zTOR43QHo0JYs8QSwyOvGtL86Co,12733
|
|
6
6
|
annet/cli_args.py,sha256=d0WuGiZfe42-nMvurX1bOb3iUMg0TSK-hxk8J1THxSY,13728
|
|
7
7
|
annet/connectors.py,sha256=aoiDVLPizx8CW2p8SAwGCzyO_WW8H9xc2aujbGC4bDg,4882
|
|
@@ -28,12 +28,12 @@ annet/adapters/fetchers/stub/fetcher.py,sha256=FzpkIzPJBQVuNNSdXKoGzkALmIGMgo2gm
|
|
|
28
28
|
annet/adapters/file/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
29
|
annet/adapters/file/provider.py,sha256=3hNt0QQg46SVymLQ4Bh9G4VNYyhnB7gV5yu5OiIJpZE,7307
|
|
30
30
|
annet/adapters/netbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
-
annet/adapters/netbox/provider.py,sha256=
|
|
31
|
+
annet/adapters/netbox/provider.py,sha256=iUkG0Dc0wBeck1dQMysej-olFLugKW6r-GJVutjMUYs,2375
|
|
32
32
|
annet/adapters/netbox/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
33
|
annet/adapters/netbox/common/adapter.py,sha256=1rR9PZAU72hIreozWFVb9CKpFVjfUH_i9kQEbaNiEUI,2639
|
|
34
34
|
annet/adapters/netbox/common/client.py,sha256=PaxHG4W9H8_uunIwMBNYkLq4eQJYoO6p6gY-ciQs7Nc,2563
|
|
35
35
|
annet/adapters/netbox/common/manufacturer.py,sha256=9jTfzwx5XmETrjSbIJu_FhNaByaUbGQE787c5rBor-4,1137
|
|
36
|
-
annet/adapters/netbox/common/models.py,sha256=
|
|
36
|
+
annet/adapters/netbox/common/models.py,sha256=nMruBugAKNY8D63rqUnPYyMYGMIU-haxX0vnfxLGNWc,8990
|
|
37
37
|
annet/adapters/netbox/common/query.py,sha256=0GeIiW_ttnESaNKHIziF61P-3DX0XD592UHQGn_1m60,1911
|
|
38
38
|
annet/adapters/netbox/common/status_client.py,sha256=POaqiQJ0jPcqUQH-X_fWHVnKB7TBYveNriaT0eNTlfI,769
|
|
39
39
|
annet/adapters/netbox/common/storage_base.py,sha256=lMRiDkzQylpA5Fxa4B6CUOWzq2d1cpjeSDbFGp8jH7o,9787
|
|
@@ -63,7 +63,7 @@ annet/annlib/types.py,sha256=VHU0CBADfYmO0xzB_c5f-mcuU3dUumuJczQnqGlib9M,852
|
|
|
63
63
|
annet/annlib/netdev/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
64
|
annet/annlib/netdev/db.py,sha256=fI_u5aya4l61mbYSjj4JwlVfi3s7obt2jqERSuXGRUI,1634
|
|
65
65
|
annet/annlib/netdev/devdb/__init__.py,sha256=I-NKzenyjsmUKpmIerQOfZExnnnDpPdNZLdRanyu-Nk,1020
|
|
66
|
-
annet/annlib/netdev/devdb/data/devdb.json,sha256=
|
|
66
|
+
annet/annlib/netdev/devdb/data/devdb.json,sha256=ptegQaH_HxmAEePlMx52GaiobM2xp1RXEJcvR7IAHGY,7174
|
|
67
67
|
annet/annlib/netdev/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
68
|
annet/annlib/netdev/views/dump.py,sha256=rIlyvnA3uM8bB_7oq1nS2KDxTp6dQh2hz-FbNhYIpOU,4630
|
|
69
69
|
annet/annlib/netdev/views/hardware.py,sha256=XvHtRn29SROKtxqMyr86oc1ItUKy36N98ppfuJQL8PY,3235
|
|
@@ -91,7 +91,7 @@ annet/generators/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
|
|
|
91
91
|
annet/generators/common/initial.py,sha256=qYBxXFhyOPy34cxc6hsIXseod-lYCmmbuNHpM0uteY0,1244
|
|
92
92
|
annet/mesh/__init__.py,sha256=lcgdnBIxc2MAN7Er1bcErEKPqrjWO4uIp_1FldMXTYg,557
|
|
93
93
|
annet/mesh/basemodel.py,sha256=E6NTOneiMDwB1NCpjDRECoaeQ0f3n_fmTLnKTrSHTU4,4917
|
|
94
|
-
annet/mesh/device_models.py,sha256=
|
|
94
|
+
annet/mesh/device_models.py,sha256=K_JqI03GyikROM6MmfV03jjA1Raxr8btuAoANOY0aEo,4895
|
|
95
95
|
annet/mesh/executor.py,sha256=gfRQXSTivL0DJrhnGH-4kmCTiIxDS_vReQyle3bF2FI,17970
|
|
96
96
|
annet/mesh/match_args.py,sha256=CR3kdIV9NGtyk9E2JbcOQ3TRuYEryTWP3m2yCo2VCWg,5751
|
|
97
97
|
annet/mesh/models_converter.py,sha256=nCtzFR3s65MVkCE_xJm1Ci5c2hUwsr6ajfBiSmy3r_8,3565
|
|
@@ -101,20 +101,20 @@ annet/mesh/registry.py,sha256=xmWF7yxWXmwqX2_jyMAKrbGd2G9sjb4rYDx4Xk61QKc,9607
|
|
|
101
101
|
annet/rpl/__init__.py,sha256=8nSiFpXH4OhzRGKr-013nHwwKk5Y50uh2gL7d_IoV8U,757
|
|
102
102
|
annet/rpl/action.py,sha256=PY6W66j908RuqQ1_ioxayqVN-70rxDk5Z59EGHtxI98,1246
|
|
103
103
|
annet/rpl/condition.py,sha256=MJri4MbWtPkLHIsLMAtsIEF7e8IAS9dIImjmJs5vS5U,3418
|
|
104
|
-
annet/rpl/match_builder.py,sha256=
|
|
104
|
+
annet/rpl/match_builder.py,sha256=Y-FOEDV7n36o-Ul3f6bVSyJ1_020YIIoQiX9lgJLAzI,5068
|
|
105
105
|
annet/rpl/policy.py,sha256=P1Kt-8fHFxEczeP-RwkK_wrGN0p7IR-hOApEd2vC55E,448
|
|
106
106
|
annet/rpl/result.py,sha256=PHFn1zhDeqLBn07nkYw5vsoXew4nTwkklOwqvFWzBLg,141
|
|
107
107
|
annet/rpl/routemap.py,sha256=SIyk73OzPp2oH_XwrDv2xczuY2Zt1VsJmB0TT5r7F5g,2593
|
|
108
108
|
annet/rpl/statement_builder.py,sha256=qKhLS34lygbhtbEIY-6jzs0Aisc6qmNc_iyk9iKPyHE,10076
|
|
109
109
|
annet/rpl_generators/__init__.py,sha256=V4rAZlBaOUSjeQ5eCNmWeD7BSJLIwy0lKU_01grebpc,832
|
|
110
|
-
annet/rpl_generators/aspath.py,sha256=
|
|
111
|
-
annet/rpl_generators/community.py,sha256=
|
|
110
|
+
annet/rpl_generators/aspath.py,sha256=UXB2iMUgJZdHbcoAwPKycSZ34Zscb7OzOxkS85Nc8W0,3575
|
|
111
|
+
annet/rpl_generators/community.py,sha256=LwkDW-nskDhJMx_CssvQoNyJnYikK9pj6mrxTpRPGUQ,15891
|
|
112
112
|
annet/rpl_generators/cumulus_frr.py,sha256=eABVCpn4ru_BFQJDcPZZEi1EL1cBwfNhtC1mDmC6BwA,21548
|
|
113
|
-
annet/rpl_generators/entities.py,sha256=
|
|
113
|
+
annet/rpl_generators/entities.py,sha256=Eia8zXnHKd4NbvLU-fNjoIxwxCWttzuPiWN9Nr8MQtE,5484
|
|
114
114
|
annet/rpl_generators/execute.py,sha256=wS6e6fwcPWywsHB0gBMqZ17eF0s4YOBgDgwPB_cr5Rw,431
|
|
115
|
-
annet/rpl_generators/policy.py,sha256=
|
|
116
|
-
annet/rpl_generators/prefix_lists.py,sha256=
|
|
117
|
-
annet/rpl_generators/rd.py,sha256
|
|
115
|
+
annet/rpl_generators/policy.py,sha256=IGKOD2wpBOqo-NW3aOXDogLIPUMtve54IbWbqfVlwgc,68429
|
|
116
|
+
annet/rpl_generators/prefix_lists.py,sha256=lyx6m0QOTgaIFWKvXjlmU1rV9gsjpxmTQ-vPlZ_XPFw,10054
|
|
117
|
+
annet/rpl_generators/rd.py,sha256=mw5-ictMdCQbkFcMi4R8iKqwmOp52Y9HnQuEitT6yrc,2632
|
|
118
118
|
annet/rulebook/__init__.py,sha256=AmcqrLYaoU1-sO2vmtjWZbzsZ44_w7nXncoEVa_hpyk,3997
|
|
119
119
|
annet/rulebook/common.py,sha256=zK1s2c5lc5HQbIlMUQ4HARQudXSgOYiZ_Sxc2I_tHqg,721
|
|
120
120
|
annet/rulebook/deploying.py,sha256=9CMeOUw5L1OWdrccSRfpJyH9H_jH7xWNU1JldviBNrk,3015
|
|
@@ -160,12 +160,12 @@ annet/rulebook/texts/cisco.order,sha256=DarNICBBAEXR8VOFV8SNnuQmwGAG-TGwXlMWpFMT
|
|
|
160
160
|
annet/rulebook/texts/cisco.rul,sha256=cX9CyqKpgpr8B1Qe3aEe66wZpfaGg7fmsIsmgW6Yip4,3619
|
|
161
161
|
annet/rulebook/texts/huawei.deploy,sha256=5vrvhai2BjaJ793A3EHHCthic3dnGKp2_rAgViVIouI,10951
|
|
162
162
|
annet/rulebook/texts/huawei.order,sha256=4qXuoqfJ8Hck11LACKwaWP2ARHwPrtLfCut1BDRAg7E,11244
|
|
163
|
-
annet/rulebook/texts/huawei.rul,sha256=
|
|
163
|
+
annet/rulebook/texts/huawei.rul,sha256=asuQeZgADkOYsNMP5t2tIRbJnlTYgicxSV6S94ozYXw,13816
|
|
164
164
|
annet/rulebook/texts/iosxr.deploy,sha256=Hu0NkcGv3f1CWUrnbzI3eQOPXJxtH4NNOPRV68IrW4U,1226
|
|
165
165
|
annet/rulebook/texts/iosxr.order,sha256=gUp6XHwzqkDsArCUAwtx3rR1qlGfYsHy2vP9oZN2oDk,1922
|
|
166
166
|
annet/rulebook/texts/iosxr.rul,sha256=JMFJ94ORNeDQo_X73iPS6pFUmXYTBuL5pkUypgHcOig,2966
|
|
167
167
|
annet/rulebook/texts/juniper.order,sha256=PpxmcCgeaeP3TyYe3BWvtb24MKYV_BujjCH3HD4lsc8,256
|
|
168
|
-
annet/rulebook/texts/juniper.rul,sha256=
|
|
168
|
+
annet/rulebook/texts/juniper.rul,sha256=f6kzTXpek3vYd3-vEdn0TjGjbmkvwHpWwkKIPqI4A_0,2941
|
|
169
169
|
annet/rulebook/texts/nexus.deploy,sha256=9YNAQEw7aQxtYZJbE-dMD6qJrTzs_G92Ifrx3Ft4Wn4,1120
|
|
170
170
|
annet/rulebook/texts/nexus.order,sha256=AZMKCD5Zf_mBOlE36aMDvO4w5rdbepTz1Dsyv7xP9Qs,1834
|
|
171
171
|
annet/rulebook/texts/nexus.rul,sha256=Yo87rreD__P_2uKfZ7aPQuq2gHIi2C1l5zpk2x3PI1M,2622
|
|
@@ -183,7 +183,7 @@ annet/rulebook/texts/routeros.rul,sha256=ipfxjj0mjFef6IsUlupqx4BY_Je_OUb8u_U1019
|
|
|
183
183
|
annet/vendors/__init__.py,sha256=gQcDFlKeWDZB6vxJ_MdPWEoE-C5dg-YgXvgGkuV9YLw,569
|
|
184
184
|
annet/vendors/base.py,sha256=AmM3--gqC-Rpw5Xu_-hqthWZ9EoZRL8x6eOHwadZGbo,1145
|
|
185
185
|
annet/vendors/registry.py,sha256=LgPg4oxWrgxsfpLpJ6OWEGFmUzlVlHzziSXsZTi82uc,2540
|
|
186
|
-
annet/vendors/tabparser.py,sha256=
|
|
186
|
+
annet/vendors/tabparser.py,sha256=yRe4EdwP_-ptynmzB7qhqFlFAnGNxGjhTM9EW-WwxPA,32177
|
|
187
187
|
annet/vendors/library/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
188
188
|
annet/vendors/library/arista.py,sha256=J4ltZ7sS_TgIECg2U7fizvxwfS4-s35Of0tNDNWWYbE,1302
|
|
189
189
|
annet/vendors/library/aruba.py,sha256=tvrSFSA43n0uelCv-NLQnqxO01d0y2mrfhncpOX7zoQ,1257
|
|
@@ -199,8 +199,8 @@ annet/vendors/library/optixtrans.py,sha256=VdME69Ca4HAEgoaKN21fZxnmmsqqaxOe_HZja
|
|
|
199
199
|
annet/vendors/library/pc.py,sha256=vfv31_NPi7M-4AUDL89UcpawK2E6xvCpELA209cd1ho,1086
|
|
200
200
|
annet/vendors/library/ribbon.py,sha256=DDOBq-_-FL9dCxqXs2inEWZ-pvw-dJ-A-prA7cKMhec,1216
|
|
201
201
|
annet/vendors/library/routeros.py,sha256=iQa7m_4wjuvcgBOI9gyZwlw1BvzJfOkvUbyoEk-NI9I,1254
|
|
202
|
-
annet-3.
|
|
203
|
-
annet-3.
|
|
202
|
+
annet-3.14.0.dist-info/licenses/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
|
|
203
|
+
annet-3.14.0.dist-info/licenses/LICENSE,sha256=yPxl7dno02Pw7gAcFPIFONzx_gapwDoPXsIsh6Y7lC0,1079
|
|
204
204
|
annet_generators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
205
205
|
annet_generators/example/__init__.py,sha256=OJ77uj8axc-FIyIu_Xdcnzmde3oQW5mk5qbODkhuVc8,355
|
|
206
206
|
annet_generators/example/hostname.py,sha256=RloLzNVetEoWPLITzfJ13Nk3CC0yi-cZB1RTd6dnuhI,2541
|
|
@@ -213,8 +213,8 @@ annet_generators/rpl_example/generator.py,sha256=EWah19gOH8G-QyNyWqxCqdRi0BK7GbM
|
|
|
213
213
|
annet_generators/rpl_example/items.py,sha256=HPgxScDvSqJPdz0c2SppDrH82DZYC4zUaniQwcWmh4A,1176
|
|
214
214
|
annet_generators/rpl_example/mesh.py,sha256=z_WgfDZZ4xnyh3cSf75igyH09hGvtexEVwy1gCD_DzA,288
|
|
215
215
|
annet_generators/rpl_example/route_policy.py,sha256=z6nPb0VDeQtKD1NIg9sFvmUxBD5tVs2frfNIuKdM-5c,2318
|
|
216
|
-
annet-3.
|
|
217
|
-
annet-3.
|
|
218
|
-
annet-3.
|
|
219
|
-
annet-3.
|
|
220
|
-
annet-3.
|
|
216
|
+
annet-3.14.0.dist-info/METADATA,sha256=WWGistGQb0jfdrMuperiDpOYUkyo2lgO2oouTHFoPA8,816
|
|
217
|
+
annet-3.14.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
218
|
+
annet-3.14.0.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
|
|
219
|
+
annet-3.14.0.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
|
|
220
|
+
annet-3.14.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|