annet 1.1.2__py3-none-any.whl → 2.0.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.

@@ -28,11 +28,12 @@ def storage_factory(opts: NetboxStorageOpts) -> Storage:
28
28
 
29
29
  class NetboxProvider(StorageProvider, AdapterWithName, AdapterWithConfig):
30
30
  def __init__(self, url: Optional[str] = None, token: Optional[str] = None, insecure: bool = False,
31
- exact_host_filter: bool = False):
31
+ exact_host_filter: bool = False, threads: int = 1):
32
32
  self.url = url
33
33
  self.token = token
34
34
  self.insecure = insecure
35
35
  self.exact_host_filter = exact_host_filter
36
+ self.threads = threads
36
37
 
37
38
  @classmethod
38
39
  def with_config(cls, **kwargs: Dict[str, Any]) -> T:
annet/annet.py CHANGED
@@ -2,7 +2,7 @@
2
2
  import sys
3
3
 
4
4
  import annet
5
- from annet import argparse, cli, generators, hardware, lib, rulebook
5
+ from annet import argparse, cli, generators, hardware, lib, rulebook, diff
6
6
 
7
7
 
8
8
  # =====
@@ -13,6 +13,7 @@ def main():
13
13
  cli.fill_base_args(parser, annet.__name__, "configs/logging.yaml")
14
14
  rulebook.rulebook_provider_connector.set(rulebook.DefaultRulebookProvider)
15
15
  hardware.hardware_connector.set(hardware.AnnetHardwareProvider)
16
+ diff.file_differ_connector.set(diff.UnifiedFileDiffer)
16
17
 
17
18
  parser.add_commands(parser.find_subcommands(cli.list_subcommands()))
18
19
  try:
@@ -32,7 +32,6 @@ def filter_config(acl: Acl, fmtr: tabparser.CommonFormatter, input_config: Input
32
32
  config = patching.apply_acl(config, acl, fatal_acl=False)
33
33
  config = fmtr.join(config)
34
34
  else:
35
- config = typing.cast(input_config, FileConfigTree)
36
35
  config = apply_acl_fileconfig(input_config, acl)
37
36
  return config
38
37
 
@@ -47,7 +46,6 @@ def filter_diff(acl: Acl, fmtr: tabparser.CommonFormatter, input_config: InputCo
47
46
  config = unshift_op(config)
48
47
  config = config.rstrip()
49
48
  else:
50
- config = typing.cast(input_config, FileConfigTree)
51
49
  config = apply_acl_fileconfig(input_config, acl)
52
50
  return config
53
51
 
@@ -108,17 +106,23 @@ def get_op(line: str) -> typing.Tuple[str, str, str]:
108
106
  indent = ""
109
107
  opidx = -1
110
108
  rowstart = 0
109
+
111
110
  for rowstart in range(len(line)):
111
+ if line[rowstart] != " " and line[0:rowstart].strip():
112
+ break
112
113
  if line[rowstart] not in diff_ops:
113
114
  break
115
+
114
116
  for opidx in range(rowstart):
115
117
  if line[opidx] != " ":
116
118
  break
119
+
117
120
  if opidx >= 0:
118
121
  op = line[opidx]
119
122
  indent = line[:opidx] + line[opidx + 1:rowstart]
120
123
  if op != " ":
121
124
  indent = indent + " "
125
+
122
126
  return op, indent, line[rowstart:]
123
127
 
124
128
 
annet/api/__init__.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import abc
2
- import difflib
3
2
  import os
4
3
  import re
5
4
  import sys
@@ -37,6 +36,7 @@ from annet import diff as ann_diff
37
36
  from annet import filtering
38
37
  from annet import gen as ann_gen
39
38
  from annet import patching, rulebook, tabparser, tracing
39
+ from annet.diff import file_differ_connector
40
40
  from annet.rulebook import deploying
41
41
  from annet.filtering import Filterer
42
42
  from annet.hardware import hardware_connector
@@ -52,8 +52,6 @@ from annet.storage import Device, get_storage
52
52
  from annet.types import Diff, ExitCode, OldNewResult, Op, PCDiff, PCDiffFile
53
53
 
54
54
 
55
- live_configs = ann_gen.live_configs
56
-
57
55
  DEFAULT_INDENT = " "
58
56
 
59
57
 
@@ -242,29 +240,22 @@ def gen(args: cli_args.ShowGenOptions, loader: ann_gen.Loader):
242
240
 
243
241
 
244
242
  # =====
245
- def _diff_file(old_text: Optional[str], new_text: Optional[str], context=3):
246
- old_lines = old_text.splitlines() if old_text else []
247
- new_lines = new_text.splitlines() if new_text else []
248
- context = max(len(old_lines), len(new_lines)) if context is None else context
249
- return list(difflib.unified_diff(old_lines, new_lines, n=context, lineterm=""))
250
-
251
-
252
- def _diff_files(old_files, new_files, context=3):
243
+ def _diff_files(hw, old_files, new_files):
253
244
  ret = {}
245
+ differ = file_differ_connector.get()
254
246
  for (path, (new_text, reload_data)) in new_files.items():
255
247
  old_text = old_files.get(path)
256
248
  is_new = old_text is None
257
- diff_lines = _diff_file(old_text, new_text, context=context)
249
+ diff_lines = differ.diff_file(hw, path, old_text, new_text)
258
250
  ret[path] = (diff_lines, reload_data, is_new)
259
251
  return ret
260
252
 
261
253
 
262
254
  def patch(args: cli_args.ShowPatchOptions, loader: ann_gen.Loader):
263
255
  """ Сгенерировать патч для устройств """
264
- global live_configs # pylint: disable=global-statement
265
256
  if args.config == "running":
266
257
  fetcher = annet.deploy.get_fetcher()
267
- live_configs = annet.lib.do_async(fetcher.fetch(loader.devices, processes=args.parallel))
258
+ ann_gen.live_configs = annet.lib.do_async(fetcher.fetch(loader.devices, processes=args.parallel))
268
259
  stdin = args.stdin(filter_acl=args.filter_acl, config=args.config)
269
260
 
270
261
  filterer = filtering.filterer_connector.get()
@@ -355,9 +346,9 @@ def diff(
355
346
 
356
347
  pc_diff_files = []
357
348
  if res.old_files or new_files:
358
- pc_diff_files.extend(_pc_diff(device.hostname, res.old_files, new_files))
349
+ pc_diff_files.extend(_pc_diff(res.device.hw, device.hostname, res.old_files, new_files))
359
350
  if res.old_json_fragment_files or new_json_fragment_files:
360
- pc_diff_files.extend(_json_fragment_diff(device.hostname, res.old_json_fragment_files, new_json_fragment_files))
351
+ pc_diff_files.extend(_json_fragment_diff(res.device.hw, device.hostname, res.old_json_fragment_files, new_json_fragment_files))
361
352
 
362
353
  if pc_diff_files:
363
354
  pc_diff_files.sort(key=lambda f: f.label)
@@ -478,18 +469,19 @@ class PCDeployerJob(DeployerJob):
478
469
  upload_files: Dict[str, bytes] = {}
479
470
  reload_cmds: Dict[str, bytes] = {}
480
471
  generator_types: Dict[str, GeneratorType] = {}
472
+ differ = file_differ_connector.get()
481
473
  for generator_type, pc_files in [(GeneratorType.ENTIRE, new_files), (GeneratorType.JSON_FRAGMENT, new_json_fragment_files)]:
482
474
  for file, (file_content_or_json_cfg, cmds) in pc_files.items():
483
475
  if generator_type == GeneratorType.ENTIRE:
484
476
  file_content: str = file_content_or_json_cfg
485
- diff_content = "\n".join(_diff_file(old_files.get(file), file_content))
477
+ diff_content = "\n".join(differ.diff_file(res.device.hw, file, old_files.get(file), file_content))
486
478
  else: # generator_type == GeneratorType.JSON_FRAGMENT
487
479
  old_json_cfg = old_json_fragment_files[file]
488
480
  json_patch = jsontools.make_patch(old_json_cfg, file_content_or_json_cfg)
489
481
  file_content = jsontools.format_json(json_patch)
490
482
  old_text = jsontools.format_json(old_json_cfg)
491
483
  new_text = jsontools.format_json(file_content_or_json_cfg)
492
- diff_content = "\n".join(_diff_file(old_text, new_text))
484
+ diff_content = "\n".join(differ.diff_file(res.device.hw, file, old_text, new_text))
493
485
 
494
486
  if diff_content or force_reload:
495
487
  self._has_diff |= True
@@ -624,7 +616,6 @@ class Deployer:
624
616
  return ans
625
617
 
626
618
  def check_diff(self, result: annet.deploy.DeployResult, loader: ann_gen.Loader):
627
- global live_configs # pylint: disable=global-statement
628
619
  success_device_ids = []
629
620
  for host, hres in result.results.items():
630
621
  device = self.fqdn_to_device[host]
@@ -639,7 +630,7 @@ class Deployer:
639
630
  config="running",
640
631
  )
641
632
  if diff_args.query:
642
- live_configs = None
633
+ ann_gen.live_configs = None
643
634
  diffs = diff(diff_args, loader, success_device_ids, self._filterer)
644
635
  non_pc_diffs = {dev: diff for dev, diff in diffs.items() if not isinstance(diff, PCDiff)}
645
636
  devices_to_diff = ann_diff.collapse_diffs(non_pc_diffs)
@@ -682,13 +673,23 @@ async def adeploy(
682
673
  ) -> ExitCode:
683
674
  """ Сгенерировать конфиг для устройств и задеплоить его """
684
675
  ret: ExitCode = 0
685
- global live_configs # pylint: disable=global-statement
686
- live_configs = await fetcher.fetch(devices=loader.devices, processes=args.parallel)
687
- pool = ann_gen.OldNewParallel(args, loader, filterer)
676
+ ann_gen.live_configs = await fetcher.fetch(devices=loader.devices, processes=args.parallel)
688
677
 
689
- for res in pool.generated_configs(loader.devices):
678
+ device_ids = [d.id for d in loader.devices]
679
+ for res in ann_gen.old_new(
680
+ args,
681
+ config=args.config,
682
+ loader=loader,
683
+ no_new=args.clear,
684
+ stdin=args.stdin(filter_acl=args.filter_acl, config=args.config),
685
+ do_files_download=True,
686
+ device_ids=device_ids,
687
+ filterer=filterer,
688
+ ):
690
689
  # Меняем exit code если хоть один device ловил exception
691
690
  if res.err is not None:
691
+ if not args.tolerate_fails:
692
+ raise res.err
692
693
  get_logger(res.device.hostname).error("error generating configs", exc_info=res.err)
693
694
  ret |= 2 ** 3
694
695
  job = DeployerJob.from_device(res.device, args)
@@ -746,12 +747,16 @@ def file_diff(args: cli_args.FileDiffOptions):
746
747
  def file_diff_worker(old_new: Tuple[str, str], args: cli_args.FileDiffOptions) -> Generator[
747
748
  Tuple[str, str, bool], None, None]:
748
749
  old_path, new_path = old_new
750
+ hw = args.hw
751
+ if isinstance(args.hw, str):
752
+ hw = HardwareView(args.hw, "")
753
+
749
754
  if os.path.isdir(old_path) and os.path.isdir(new_path):
750
755
  hostname = os.path.basename(new_path)
751
756
  new_files = {relative_cfg_path: (cfg_text, "") for relative_cfg_path, cfg_text in
752
757
  ann_gen.load_pc_config(new_path).items()}
753
758
  old_files = ann_gen.load_pc_config(old_path)
754
- for diff_file in _pc_diff(hostname, old_files, new_files):
759
+ for diff_file in _pc_diff(hw, hostname, old_files, new_files):
755
760
  diff_text = (
756
761
  "\n".join(diff_file.diff_lines)
757
762
  if args.no_color
@@ -791,8 +796,8 @@ def file_patch_worker(old_new: Tuple[str, str], args: cli_args.FileDiffOptions)
791
796
  yield dest_name, patch_text, False
792
797
 
793
798
 
794
- def _pc_diff(hostname: str, old_files: Dict[str, str], new_files: Dict[str, str]) -> Generator[PCDiffFile, None, None]:
795
- sorted_lines = sorted(_diff_files(old_files, new_files).items())
799
+ def _pc_diff(hw, hostname: str, old_files: Dict[str, str], new_files: Dict[str, str]) -> Generator[PCDiffFile, None, None]:
800
+ sorted_lines = sorted(_diff_files(hw, old_files, new_files).items())
796
801
  for (path, (diff_lines, _reload_data, is_new)) in sorted_lines:
797
802
  if not diff_lines:
798
803
  continue
@@ -803,6 +808,7 @@ def _pc_diff(hostname: str, old_files: Dict[str, str], new_files: Dict[str, str]
803
808
 
804
809
 
805
810
  def _json_fragment_diff(
811
+ hw,
806
812
  hostname: str,
807
813
  old_files: Dict[str, Any],
808
814
  new_files: Dict[str, Tuple[Any, Optional[str]]],
@@ -820,7 +826,7 @@ def _json_fragment_diff(
820
826
  ret[path] = (jsontools.format_json(cfg), reload_cmd)
821
827
  return ret
822
828
  jold, jnew = jsonify_multi(old_files), jsonify_multi_with_cmd(new_files)
823
- return _pc_diff(hostname, jold, jnew)
829
+ return _pc_diff(hw, hostname, jold, jnew)
824
830
 
825
831
 
826
832
  def guess_hw(config_text: str):
annet/diff.py CHANGED
@@ -1,6 +1,9 @@
1
+ import abc
2
+ import difflib
1
3
  import re
2
4
  from itertools import groupby
3
- from typing import Generator, List, Mapping, Tuple, Union
5
+ from pathlib import Path
6
+ from typing import Generator, List, Mapping, Tuple, Union, Protocol
4
7
 
5
8
  from annet.annlib.diff import ( # pylint: disable=unused-import
6
9
  colorize_line,
@@ -9,10 +12,12 @@ from annet.annlib.diff import ( # pylint: disable=unused-import
9
12
  gen_pre_as_diff,
10
13
  resort_diff,
11
14
  )
15
+ from annet.annlib.netdev.views.hardware import HardwareView
12
16
  from annet.annlib.output import format_file_diff
13
17
 
14
- from annet import patching
18
+ from annet import patching, rulebook, tabparser, hardware
15
19
  from annet.cli_args import ShowDiffOptions
20
+ from annet.connectors import CachedConnector
16
21
  from annet.output import output_driver_connector
17
22
  from annet.storage import Device
18
23
  from annet.tabparser import make_formatter
@@ -82,3 +87,67 @@ def collapse_diffs(diffs: Mapping[Device, Diff]) -> Mapping[Tuple[Device, ...],
82
87
  res[tuple(x[0] for x in collapsed_diff)] = collapsed_diff[0][1][0]
83
88
 
84
89
  return res
90
+
91
+
92
+ class FileDiffer(Protocol):
93
+ @abc.abstractmethod
94
+ def diff_file(self, hw: HardwareView, path: str | Path, old: str, new: str) -> list[str]:
95
+ raise NotImplementedError
96
+
97
+
98
+ class UnifiedFileDiffer(FileDiffer):
99
+ def __init__(self):
100
+ self.context: int = 3
101
+
102
+ def diff_file(self, hw: HardwareView, path: str | Path, old: str, new: str) -> list[str]:
103
+ """Calculate the differences for config files.
104
+
105
+ Args:
106
+ hw: device hardware info
107
+ path: path to file on a device
108
+ old (Optional[str]): The old file content.
109
+ new (Optional[str]): The new file content.
110
+
111
+ Returns:
112
+ List[str]: List of difference lines.
113
+ """
114
+ return self._diff_text_file(old, new)
115
+
116
+ def _diff_text_file(self, old, new):
117
+ """Calculate the differences for plaintext files."""
118
+ context = self.context
119
+ old_lines = old.splitlines() if old else []
120
+ new_lines = new.splitlines() if new else []
121
+ context = max(len(old_lines), len(new_lines)) if context is None else context
122
+ return list(difflib.unified_diff(old_lines, new_lines, n=context, lineterm=""))
123
+
124
+
125
+ class FrrFileDiffer(UnifiedFileDiffer):
126
+ def diff_file(self, hw: HardwareView, path: str | Path, old: str, new: str) -> list[str]:
127
+ if (hw.PC.Mellanox or hw.PC.NVIDIA) and (path == "/etc/frr/frr.conf"):
128
+ return self._diff_frr_conf(hw, old, new)
129
+ return super().diff_file(hw, path, old, new)
130
+
131
+ def _diff_frr_conf(self, hw: HardwareView, old_text: str | None, new_text: str | None) -> list[str]:
132
+ """Calculate the differences for frr.conf files."""
133
+ indent = " "
134
+ rb = rulebook.rulebook_provider_connector.get()
135
+ rulebook_data = rb.get_rulebook(hw)
136
+ formatter = tabparser.make_formatter(hw, indent=indent)
137
+
138
+ old_tree = tabparser.parse_to_tree(old_text or "", splitter=formatter.split)
139
+ new_tree = tabparser.parse_to_tree(new_text or "", splitter=formatter.split)
140
+
141
+ diff_tree = patching.make_diff(old_tree, new_tree, rulebook_data, [])
142
+ pre_diff = patching.make_pre(diff_tree)
143
+ diff_iterator = gen_pre_as_diff(pre_diff, show_rules=False, indent=indent, no_color=True)
144
+
145
+ return [line.rstrip() for line in diff_iterator if "frr version" not in line]
146
+
147
+
148
+ class _FileDifferConnector(CachedConnector[FileDiffer]):
149
+ name = "Device file diff processor"
150
+ ep_name = "file_differ"
151
+
152
+
153
+ file_differ_connector = _FileDifferConnector()
annet/gen.py CHANGED
@@ -526,52 +526,6 @@ def worker(device_id, args: ShowGenOptions, stdin, loader: "Loader", filterer: F
526
526
  False)
527
527
 
528
528
 
529
- def old_new_worker(device_id, args: DeployOptions, config, stdin, loader: "Loader", filterer: Filterer):
530
- for res in old_new(
531
- args,
532
- config=config,
533
- loader=loader,
534
- filterer=filterer,
535
- stdin=stdin,
536
- device_ids=[device_id],
537
- no_new=args.clear,
538
- do_files_download=True,
539
- ):
540
- if res.err is not None and not args.tolerate_fails:
541
- raise res.err
542
- yield res
543
-
544
-
545
- class OldNewParallel(Parallel):
546
- def __init__(self, args: DeployOptions, loader: "Loader", filterer: Filterer):
547
- stdin = args.stdin(filter_acl=args.filter_acl, config=args.config)
548
- super().__init__(
549
- old_new_worker,
550
- args,
551
- config=args.config,
552
- stdin=stdin,
553
- loader=loader,
554
- filterer=filterer,
555
- )
556
- self.tune_args(args)
557
- self.tolerate_fails = args.tolerate_fails
558
-
559
- def generated_configs(self, devices: List[Device]) -> Generator[OldNewResult, None, None]:
560
- devices_by_id = {device.id: device for device in devices}
561
- device_ids = list(devices_by_id)
562
-
563
- for task_result in self.irun(device_ids, self.tolerate_fails):
564
- if task_result.exc is not None:
565
- device = devices_by_id.pop(task_result.device_id)
566
- yield OldNewResult(device=device, err=task_result.exc)
567
- elif task_result.result is not None:
568
- yield from task_result.result
569
- devices_by_id.pop(task_result.device_id)
570
-
571
- for device in devices_by_id.values():
572
- yield OldNewResult(device=device, err=Exception(f"No config returned for {device.hostname}"))
573
-
574
-
575
529
  @dataclasses.dataclass
576
530
  class DeviceFilesToDownload:
577
531
  entire: List[str] = dataclasses.field(default_factory=list)
annet/output.py CHANGED
@@ -155,8 +155,15 @@ class OutputDriverBasic(OutputDriver):
155
155
  ret.append((label, getattr(exc, "formatted_output", f"{repr(exc)} (formatted_output is absent)"), True))
156
156
  return ret
157
157
 
158
- def cfg_file_names(self, device: Device) -> List[str]:
159
- return [f"{device.hostname}.cfg"]
158
+ def cfg_file_names(self, device: Device) -> list[str]:
159
+ res = []
160
+ if device.hostname:
161
+ res.append(f"{device.hostname}.cfg")
162
+ if device.id is not None and device.id != "":
163
+ res.append(f"_id_{device.id}.cfg")
164
+ if not res:
165
+ raise RuntimeError("Neither hostname nor id is known for device")
166
+ return res
160
167
 
161
168
  def entire_config_dest_path(self, device, config_path: str) -> str:
162
169
  """Формирует путь к конфигу в директории destname.
annet/rpl/__init__.py CHANGED
@@ -16,11 +16,12 @@ __all__ = [
16
16
  "RoutingPolicy",
17
17
  "CommunityActionValue",
18
18
  "PrefixMatchValue",
19
+ "OrLonger",
19
20
  ]
20
21
 
21
22
  from .action import Action, ActionType, SingleAction
22
23
  from .condition import AndCondition, Condition, ConditionOperator, SingleCondition
23
- from .match_builder import R, MatchField, PrefixMatchValue
24
+ from .match_builder import R, MatchField, PrefixMatchValue, OrLonger
24
25
  from .policy import RoutingPolicyStatement, RoutingPolicy
25
26
  from .result import ResultType
26
27
  from .routemap import RouteMap, Route
@@ -63,11 +63,15 @@ class SetConditionFactory(Generic[ValueT]):
63
63
  return SingleCondition(self.field, ConditionOperator.HAS_ANY, values)
64
64
 
65
65
 
66
+ # OrLonger represents a pair of (le, ge)
67
+ # for prefix mask length match in prefix-lists
68
+ OrLonger = tuple[Optional[int], Optional[int]]
69
+
70
+
66
71
  @dataclass(frozen=True)
67
72
  class PrefixMatchValue:
68
73
  names: tuple[str, ...]
69
- greater_equal: Optional[int]
70
- less_equal: Optional[int]
74
+ or_longer: OrLonger = (None, None)
71
75
 
72
76
 
73
77
  class Checkable:
@@ -91,23 +95,23 @@ class Checkable:
91
95
  def match_v6(
92
96
  self,
93
97
  *names: str,
94
- or_longer: tuple[Optional[int], Optional[int]] = (None, None),
98
+ or_longer: OrLonger = (None, None),
95
99
  ) -> SingleCondition[PrefixMatchValue]:
96
100
  return SingleCondition(
97
101
  MatchField.ipv6_prefix,
98
102
  ConditionOperator.CUSTOM,
99
- PrefixMatchValue(names, greater_equal=or_longer[0], less_equal=or_longer[1]),
103
+ PrefixMatchValue(names, or_longer),
100
104
  )
101
105
 
102
106
  def match_v4(
103
107
  self,
104
108
  *names: str,
105
- or_longer: tuple[Optional[int], Optional[int]] = (None, None),
109
+ or_longer: OrLonger = (None, None),
106
110
  ) -> SingleCondition[PrefixMatchValue]:
107
111
  return SingleCondition(
108
112
  MatchField.ip_prefix,
109
113
  ConditionOperator.CUSTOM,
110
- PrefixMatchValue(names, greater_equal=or_longer[0], less_equal=or_longer[1]),
114
+ PrefixMatchValue(names, or_longer),
111
115
  )
112
116
 
113
117
 
@@ -1,3 +1,4 @@
1
+ import warnings
1
2
  from collections.abc import Callable
2
3
  from dataclasses import dataclass
3
4
  from dataclasses import field
@@ -14,6 +15,7 @@ class ThenField(str, Enum):
14
15
  large_community = "large_community"
15
16
  extcommunity_rt = "extcommunity_rt"
16
17
  extcommunity_soo = "extcommunity_soo"
18
+ extcommunity = "extcommunity"
17
19
  as_path = "as_path"
18
20
  local_pref = "local_pref"
19
21
  metric = "metric"
@@ -144,6 +146,7 @@ class StatementBuilder:
144
146
  self._added_as_path: list[int] = []
145
147
  self._community = CommunityActionValue()
146
148
  self._large_community = CommunityActionValue()
149
+ self._extcommunity = CommunityActionValue()
147
150
  self._extcommunity_rt = CommunityActionValue()
148
151
  self._extcommunity_soo = CommunityActionValue()
149
152
  self._as_path = AsPathActionValue()
@@ -165,12 +168,18 @@ class StatementBuilder:
165
168
  def large_community(self) -> CommunityActionBuilder:
166
169
  return CommunityActionBuilder(self._large_community)
167
170
 
171
+ @property
172
+ def extcommunity(self) -> CommunityActionBuilder:
173
+ return CommunityActionBuilder(self._extcommunity)
174
+
168
175
  @property
169
176
  def extcommunity_rt(self) -> CommunityActionBuilder:
177
+ warnings.warn("extcommunity_rt is deprecated, use extcommunity", DeprecationWarning, stacklevel=2)
170
178
  return CommunityActionBuilder(self._extcommunity_rt)
171
179
 
172
180
  @property
173
181
  def extcommunity_soo(self) -> CommunityActionBuilder:
182
+ warnings.warn("extcommunity_soo is deprecated, use extcommunity", DeprecationWarning, stacklevel=2)
174
183
  return CommunityActionBuilder(self._extcommunity_soo)
175
184
 
176
185
  def _set(self, field: str, value: ValueT) -> None:
@@ -244,7 +253,13 @@ class StatementBuilder:
244
253
  self._statement.then.append(SingleAction(
245
254
  field=ThenField.large_community,
246
255
  type=ActionType.CUSTOM,
247
- value=self._extcommunity_rt,
256
+ value=self._large_community,
257
+ ))
258
+ if self._extcommunity:
259
+ self._statement.then.append(SingleAction(
260
+ field=ThenField.extcommunity,
261
+ type=ActionType.CUSTOM,
262
+ value=self._extcommunity,
248
263
  ))
249
264
  if self._extcommunity_rt:
250
265
  self._statement.then.append(SingleAction(
@@ -256,7 +271,7 @@ class StatementBuilder:
256
271
  self._statement.then.append(SingleAction(
257
272
  field=ThenField.extcommunity_soo,
258
273
  type=ActionType.CUSTOM,
259
- value=self._extcommunity_rt,
274
+ value=self._extcommunity_soo,
260
275
  ))
261
276
  if self._as_path:
262
277
  self._statement.then.append(SingleAction(
@@ -10,14 +10,16 @@ __all__ = [
10
10
  "RDFilterFilterGenerator",
11
11
  "RDFilter",
12
12
  "IpPrefixList",
13
+ "IpPrefixListMember",
13
14
  "PrefixListFilterGenerator",
14
15
  "get_policies",
16
+ "ip_prefix_list",
15
17
  ]
16
18
 
17
19
  from .aspath import AsPathFilterGenerator
18
20
  from .community import CommunityListGenerator
19
21
  from .cumulus_frr import CumulusPolicyGenerator
20
- from .entities import CommunityList, AsPathFilter, CommunityType, CommunityLogic, RDFilter, IpPrefixList
22
+ from .entities import CommunityList, AsPathFilter, CommunityType, CommunityLogic, RDFilter, IpPrefixList, IpPrefixListMember, ip_prefix_list
21
23
  from .execute import get_policies
22
24
  from .policy import RoutingPolicyGenerator
23
25
  from .prefix_lists import PrefixListFilterGenerator