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.
- annet/adapters/netbox/provider.py +2 -1
- annet/annet.py +2 -1
- annet/annlib/filter_acl.py +6 -2
- annet/api/__init__.py +34 -28
- annet/diff.py +71 -2
- annet/gen.py +0 -46
- annet/output.py +9 -2
- annet/rpl/__init__.py +2 -1
- annet/rpl/match_builder.py +10 -6
- annet/rpl/statement_builder.py +17 -2
- annet/rpl_generators/__init__.py +3 -1
- annet/rpl_generators/cumulus_frr.py +65 -53
- annet/rpl_generators/entities.py +80 -26
- annet/rpl_generators/policy.py +159 -55
- annet/rpl_generators/prefix_lists.py +36 -81
- {annet-1.1.2.dist-info → annet-2.0.1.dist-info}/METADATA +1 -1
- {annet-1.1.2.dist-info → annet-2.0.1.dist-info}/RECORD +23 -23
- {annet-1.1.2.dist-info → annet-2.0.1.dist-info}/WHEEL +1 -1
- annet_generators/rpl_example/items.py +3 -3
- {annet-1.1.2.dist-info → annet-2.0.1.dist-info}/AUTHORS +0 -0
- {annet-1.1.2.dist-info → annet-2.0.1.dist-info}/LICENSE +0 -0
- {annet-1.1.2.dist-info → annet-2.0.1.dist-info}/entry_points.txt +0 -0
- {annet-1.1.2.dist-info → annet-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -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:
|
annet/annlib/filter_acl.py
CHANGED
|
@@ -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
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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) ->
|
|
159
|
-
|
|
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
|
annet/rpl/match_builder.py
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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,
|
|
103
|
+
PrefixMatchValue(names, or_longer),
|
|
100
104
|
)
|
|
101
105
|
|
|
102
106
|
def match_v4(
|
|
103
107
|
self,
|
|
104
108
|
*names: str,
|
|
105
|
-
or_longer:
|
|
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,
|
|
114
|
+
PrefixMatchValue(names, or_longer),
|
|
111
115
|
)
|
|
112
116
|
|
|
113
117
|
|
annet/rpl/statement_builder.py
CHANGED
|
@@ -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.
|
|
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.
|
|
274
|
+
value=self._extcommunity_soo,
|
|
260
275
|
))
|
|
261
276
|
if self._as_path:
|
|
262
277
|
self._statement.then.append(SingleAction(
|
annet/rpl_generators/__init__.py
CHANGED
|
@@ -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
|