annet 3.13.0__py3-none-any.whl → 3.14.1__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.

@@ -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.NB-E-BR-DCU-AST2600": "^Nebius NB-E-BR-DCU-AST2600",
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
+ }
@@ -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,
@@ -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.abc import Sequence, Collection, Iterator
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)
@@ -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 any(match.or_longer):
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"
@@ -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
+ )
@@ -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} ]"
@@ -109,8 +109,12 @@ policy-options
109
109
  community ~
110
110
  prefix-list ~
111
111
  policy-statement *
112
- ~ %ordered
113
- ~ %global
112
+ term * %ordered
113
+ then
114
+ community ~ %ordered
115
+ ~
116
+ ~ %global
117
+ ~ %global
114
118
 
115
119
 
116
120
  firewall
@@ -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".*$"), ""),
@@ -694,46 +694,35 @@ class RosFormatter(CommonFormatter):
694
694
  is_patch: bool,
695
695
  context: Optional[FormatterContext] = None
696
696
  ):
697
- rows = []
698
-
699
697
  if is_patch:
700
- items = ((item.row, item.child, item.context) for item in tree.itms)
701
- else:
702
- items = ((row, child, {}) for row, child in tree.items())
698
+ raise RuntimeError("Ros not supported blocks in patch")
703
699
 
700
+ rows = []
701
+
702
+ items = ((row, child, {}) for row, child in tree.items())
704
703
  for row, sub_config, row_context in items:
705
- if sub_config or (is_patch and sub_config is not None):
706
- rows.append((row, sub_config, row_context))
707
- else:
708
- rows.append((row, None, row_context))
704
+ rows.append((row, sub_config if sub_config else None, row_context))
709
705
 
710
- prev_prow = None
711
- prev_prow_context = {}
712
706
  for sub_config, row_group in itertools.groupby(rows, lambda x: x[1]):
713
707
  if sub_config is None:
714
- if prev_prow:
715
- yield prev_prow, prev_prow_context
716
- yield BlockBegin, None
708
+ blocks, leaf = [], context
709
+ while leaf:
710
+ if leaf.current:
711
+ blocks.append(leaf.current[0])
712
+ leaf = leaf.parent
713
+
714
+ yield " ".join(reversed(blocks)), None
715
+ yield BlockBegin, None
717
716
  for row, _, row_context in row_group:
718
717
  yield row, row_context
719
- if prev_prow:
720
- yield BlockEnd, None
718
+ yield BlockEnd, None
721
719
  else:
722
720
  for row, _, row_context in row_group:
723
- if context and context.parent and context.parent.row:
724
- prev_prow, prev_prow_context = context.parent.current
725
- prow = f"{context.parent.row} {row}"
726
- else:
727
- prow = row
728
- yield prow, row_context
729
-
730
- yield BlockBegin, None
731
721
  yield from self.blocks_and_context(
732
722
  sub_config,
733
723
  is_patch,
734
- context=FormatterContext(parent=context, current=(prow, row_context))
724
+ context=FormatterContext(parent=context, current=(row, row_context))
735
725
  )
736
- yield BlockEnd, None
737
726
 
738
727
  def _formatted_blocks(self, blocks):
739
728
  line = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: annet
3
- Version: 3.13.0
3
+ Version: 3.14.1
4
4
  Summary: annet
5
5
  Home-page: https://github.com/annetutil/annet
6
6
  License: MIT
@@ -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=HI43va6tlOAFRSRnjXR7Ex32B4ZbRH0yvrjzud2apJM,7198
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
@@ -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=mCxRlUp7qrNJDPi0ESvBzdpMUiOpgIk630m26jIzb0s,4787
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=hIH3GNDos8hVzeyCdsD7dD3Ku-meyevfEBWtyLsiwws,2613
111
- annet/rpl_generators/community.py,sha256=vvjBupVrMn3B2OFDLXxgUq0QQBpp5TWdlJOAYpUxFkc,12818
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=uO78iG2zHAGra5DqzpfnBgoc6slHEc6wDLvlnoySvJc,3750
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=a0xuLJ7duYT0YrXipLe2_KvBRJlUt-npM4PM4O5IFqg,52158
116
- annet/rpl_generators/prefix_lists.py,sha256=sxTPeOJufwogguuhjjMbrGSgW3wm2HNboX9tN3qLitU,6910
117
- annet/rpl_generators/rd.py,sha256=-l0Dy-t1lIFCLLIzRb_okwU1jxr_kNEds92FyoVS7qs,1999
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
@@ -165,7 +165,7 @@ annet/rulebook/texts/iosxr.deploy,sha256=Hu0NkcGv3f1CWUrnbzI3eQOPXJxtH4NNOPRV68I
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=wwYGDycZNvQBWxYmxMcOb9rAskidBL-G4ewXhrhX-cQ,2825
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=cPHCqq9oZ3TBR0GlV4oCaPA_UZuaEJfyfYb-uh3n5Oo,32167
186
+ annet/vendors/tabparser.py,sha256=-wu5ErbTKtAl4SWGhXG0R1EkDbZkJcCC_7_ZkDpBEuk,31701
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.13.0.dist-info/licenses/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
203
- annet-3.13.0.dist-info/licenses/LICENSE,sha256=yPxl7dno02Pw7gAcFPIFONzx_gapwDoPXsIsh6Y7lC0,1079
202
+ annet-3.14.1.dist-info/licenses/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
203
+ annet-3.14.1.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.13.0.dist-info/METADATA,sha256=ZdefKvmfVnjAqz3UKtqNkKtSgGfwzgrNP2J0j5Q-WYY,816
217
- annet-3.13.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
218
- annet-3.13.0.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
219
- annet-3.13.0.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
220
- annet-3.13.0.dist-info/RECORD,,
216
+ annet-3.14.1.dist-info/METADATA,sha256=BKLGnMeB4wRe5P-wSYsu3zPFBxoTIS6_y75rZJ6lz04,816
217
+ annet-3.14.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
218
+ annet-3.14.1.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
219
+ annet-3.14.1.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
220
+ annet-3.14.1.dist-info/RECORD,,
File without changes