annet 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.

Files changed (113) hide show
  1. annet/__init__.py +61 -0
  2. annet/annet.py +25 -0
  3. annet/annlib/__init__.py +7 -0
  4. annet/annlib/command.py +49 -0
  5. annet/annlib/diff.py +158 -0
  6. annet/annlib/errors.py +8 -0
  7. annet/annlib/filter_acl.py +196 -0
  8. annet/annlib/jsontools.py +89 -0
  9. annet/annlib/lib.py +495 -0
  10. annet/annlib/netdev/__init__.py +0 -0
  11. annet/annlib/netdev/db.py +62 -0
  12. annet/annlib/netdev/devdb/__init__.py +28 -0
  13. annet/annlib/netdev/devdb/data/devdb.json +137 -0
  14. annet/annlib/netdev/views/__init__.py +0 -0
  15. annet/annlib/netdev/views/dump.py +121 -0
  16. annet/annlib/netdev/views/hardware.py +112 -0
  17. annet/annlib/output.py +246 -0
  18. annet/annlib/patching.py +533 -0
  19. annet/annlib/rbparser/__init__.py +0 -0
  20. annet/annlib/rbparser/acl.py +120 -0
  21. annet/annlib/rbparser/deploying.py +55 -0
  22. annet/annlib/rbparser/ordering.py +52 -0
  23. annet/annlib/rbparser/platform.py +51 -0
  24. annet/annlib/rbparser/syntax.py +115 -0
  25. annet/annlib/rulebook/__init__.py +0 -0
  26. annet/annlib/rulebook/common.py +350 -0
  27. annet/annlib/tabparser.py +648 -0
  28. annet/annlib/types.py +35 -0
  29. annet/api/__init__.py +807 -0
  30. annet/argparse.py +415 -0
  31. annet/cli.py +192 -0
  32. annet/cli_args.py +493 -0
  33. annet/configs/context.yml +18 -0
  34. annet/configs/logging.yaml +39 -0
  35. annet/connectors.py +64 -0
  36. annet/deploy.py +441 -0
  37. annet/diff.py +85 -0
  38. annet/executor.py +551 -0
  39. annet/filtering.py +40 -0
  40. annet/gen.py +828 -0
  41. annet/generators/__init__.py +987 -0
  42. annet/generators/common/__init__.py +0 -0
  43. annet/generators/common/initial.py +33 -0
  44. annet/hardware.py +45 -0
  45. annet/implicit.py +139 -0
  46. annet/lib.py +128 -0
  47. annet/output.py +170 -0
  48. annet/parallel.py +448 -0
  49. annet/patching.py +25 -0
  50. annet/reference.py +148 -0
  51. annet/rulebook/__init__.py +114 -0
  52. annet/rulebook/arista/__init__.py +0 -0
  53. annet/rulebook/arista/iface.py +16 -0
  54. annet/rulebook/aruba/__init__.py +16 -0
  55. annet/rulebook/aruba/ap_env.py +146 -0
  56. annet/rulebook/aruba/misc.py +8 -0
  57. annet/rulebook/cisco/__init__.py +0 -0
  58. annet/rulebook/cisco/iface.py +68 -0
  59. annet/rulebook/cisco/misc.py +57 -0
  60. annet/rulebook/cisco/vlandb.py +90 -0
  61. annet/rulebook/common.py +19 -0
  62. annet/rulebook/deploying.py +87 -0
  63. annet/rulebook/huawei/__init__.py +0 -0
  64. annet/rulebook/huawei/aaa.py +75 -0
  65. annet/rulebook/huawei/bgp.py +97 -0
  66. annet/rulebook/huawei/iface.py +33 -0
  67. annet/rulebook/huawei/misc.py +337 -0
  68. annet/rulebook/huawei/vlandb.py +115 -0
  69. annet/rulebook/juniper/__init__.py +107 -0
  70. annet/rulebook/nexus/__init__.py +0 -0
  71. annet/rulebook/nexus/iface.py +92 -0
  72. annet/rulebook/patching.py +143 -0
  73. annet/rulebook/ribbon/__init__.py +12 -0
  74. annet/rulebook/texts/arista.deploy +20 -0
  75. annet/rulebook/texts/arista.order +125 -0
  76. annet/rulebook/texts/arista.rul +59 -0
  77. annet/rulebook/texts/aruba.deploy +20 -0
  78. annet/rulebook/texts/aruba.order +83 -0
  79. annet/rulebook/texts/aruba.rul +87 -0
  80. annet/rulebook/texts/cisco.deploy +27 -0
  81. annet/rulebook/texts/cisco.order +82 -0
  82. annet/rulebook/texts/cisco.rul +105 -0
  83. annet/rulebook/texts/huawei.deploy +188 -0
  84. annet/rulebook/texts/huawei.order +388 -0
  85. annet/rulebook/texts/huawei.rul +471 -0
  86. annet/rulebook/texts/juniper.rul +120 -0
  87. annet/rulebook/texts/nexus.deploy +24 -0
  88. annet/rulebook/texts/nexus.order +85 -0
  89. annet/rulebook/texts/nexus.rul +83 -0
  90. annet/rulebook/texts/nokia.rul +31 -0
  91. annet/rulebook/texts/pc.order +5 -0
  92. annet/rulebook/texts/pc.rul +9 -0
  93. annet/rulebook/texts/ribbon.deploy +22 -0
  94. annet/rulebook/texts/ribbon.rul +77 -0
  95. annet/rulebook/texts/routeros.order +38 -0
  96. annet/rulebook/texts/routeros.rul +45 -0
  97. annet/storage.py +121 -0
  98. annet/tabparser.py +36 -0
  99. annet/text_term_format.py +95 -0
  100. annet/tracing.py +170 -0
  101. annet/types.py +223 -0
  102. annet-0.1.dist-info/AUTHORS +21 -0
  103. annet-0.1.dist-info/LICENSE +21 -0
  104. annet-0.1.dist-info/METADATA +24 -0
  105. annet-0.1.dist-info/RECORD +113 -0
  106. annet-0.1.dist-info/WHEEL +5 -0
  107. annet-0.1.dist-info/entry_points.txt +6 -0
  108. annet-0.1.dist-info/top_level.txt +3 -0
  109. annet_generators/__init__.py +0 -0
  110. annet_generators/example/__init__.py +12 -0
  111. annet_generators/example/lldp.py +52 -0
  112. annet_nbexport/__init__.py +220 -0
  113. annet_nbexport/main.py +46 -0
annet/__init__.py ADDED
@@ -0,0 +1,61 @@
1
+ import logging
2
+ import logging.config
3
+ import os
4
+ import pkgutil
5
+ import sys
6
+ from argparse import SUPPRESS, Namespace
7
+
8
+ import colorama
9
+ import yaml
10
+ from annet.annlib.errors import ( # pylint: disable=wrong-import-position
11
+ DeployCancelled,
12
+ ExecError,
13
+ )
14
+ from contextlog import patch_logging, patch_threading
15
+ from valkit.python import valid_logging_level
16
+
17
+ import annet.argparse
18
+
19
+
20
+ __all__ = ("DeployCancelled", "ExecError")
21
+
22
+ DEBUG2_LEVELV_NUM = 9
23
+
24
+
25
+ def fill_base_args(parser: annet.argparse.ArgParser, pkg_name: str, logging_config: str):
26
+ parser.add_argument("--log-level", default="WARN", type=valid_logging_level,
27
+ help="Уровень детализации логов (DEBUG, DEBUG2 (with comocutor debug), INFO, WARN, CRITICAL)")
28
+ parser.add_argument("--pkg_name", default=pkg_name, help=SUPPRESS)
29
+ parser.add_argument("--logging_config", default=logging_config, help=SUPPRESS)
30
+
31
+
32
+ def init_logging(options: Namespace):
33
+ patch_logging()
34
+ patch_threading()
35
+ logging.captureWarnings(True)
36
+ logging_config = yaml.safe_load(pkgutil.get_data(options.pkg_name, options.logging_config))
37
+ if options.log_level is not None:
38
+ logging_config.setdefault("root", {})
39
+ logging_config["root"]["level"] = options.log_level
40
+ logging.addLevelName(DEBUG2_LEVELV_NUM, "DEBUG2")
41
+ logging.config.dictConfig(logging_config)
42
+
43
+
44
+ def init(options: Namespace):
45
+ init_logging(options)
46
+
47
+ # Отключить colorama.init, если стоит env-переменная. Нужно в тестах
48
+ if os.environ.get("ANN_FORCE_COLOR", None) not in [None, "", "0", "no"]:
49
+ colorama.init = lambda *_, **__: None
50
+ colorama.init()
51
+
52
+ # Workaround for Python 3.8.0: https://bugs.python.org/issue38529
53
+ import asyncio.streams
54
+ if hasattr(asyncio.streams.StreamReaderProtocol, "_on_reader_gc"):
55
+ asyncio.streams.StreamReaderProtocol._on_reader_gc = lambda *args, **kwargs: None # pylint: disable=protected-access
56
+
57
+
58
+ def assert_python_version():
59
+ if sys.version_info < (3, 8, 0):
60
+ sys.stderr.write("Error: you need python 3.8.0 or higher\n")
61
+ sys.exit(1)
annet/annet.py ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+
4
+ import annet
5
+ from annet import argparse, cli, generators, hardware, lib, rulebook
6
+
7
+
8
+ # =====
9
+ @lib.catch_ctrl_c
10
+ def main():
11
+ annet.assert_python_version()
12
+ parser = argparse.ArgParser()
13
+ cli.fill_base_args(parser, annet.__name__, "configs/logging.yaml")
14
+ rulebook.rulebook_provider_connector.set(rulebook.DefaultRulebookProvider)
15
+ hardware.hardware_connector.set(hardware.AnnetHardwareProvider)
16
+
17
+ parser.add_commands(parser.find_subcommands(cli.list_subcommands()))
18
+ try:
19
+ return parser.dispatch(pre_call=annet.init, add_help_command=True)
20
+ except (generators.GeneratorError, annet.ExecError):
21
+ return 1
22
+
23
+
24
+ if __name__ == "__main__":
25
+ sys.exit(main())
@@ -0,0 +1,7 @@
1
+ import os
2
+
3
+ import colorama
4
+
5
+ # отключить colorama.init, если стоит env-переменная. Нужно в тестах
6
+ if os.environ.get("ANN_FORCE_COLOR", None) not in [None, "", "0", "no"]:
7
+ colorama.init = lambda *_, **__: None
@@ -0,0 +1,49 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from typing import List, Optional
4
+
5
+
6
+ FIRST_EXCEPTION = 1
7
+ ALL_COMPLETED = 2
8
+
9
+
10
+ @dataclass
11
+ class Question:
12
+ question: str # frame it using / if it is a regular expression
13
+ answer: str
14
+ is_regexp: Optional[bool] = False
15
+
16
+
17
+ @dataclass
18
+ class Command:
19
+ cmd: str
20
+ questions: Optional[List[Question]] = None
21
+ exc_handler: Optional[List[Question]] = None
22
+ timeout: Optional[int] = None
23
+ suppress_nonzero: bool = False
24
+ suppress_eof: bool = False
25
+
26
+ def __str__(self) -> str:
27
+ return self.cmd
28
+
29
+
30
+ @dataclass
31
+ class CommandList:
32
+ cmss: List[Command] = field(default_factory=list)
33
+
34
+ def __post_init__(self):
35
+ if not self.cmss:
36
+ self.cmss = []
37
+
38
+ def __iter__(self):
39
+ return iter(self.cmss)
40
+
41
+ def __len__(self) -> int:
42
+ return len(self.cmss)
43
+
44
+ def add_cmd(self, cmd: Command) -> None:
45
+ assert isinstance(cmd, Command)
46
+ self.cmss.append(cmd)
47
+
48
+ def as_list(self) -> List[Command]: # TODO: delete
49
+ return self.cmss
annet/annlib/diff.py ADDED
@@ -0,0 +1,158 @@
1
+ import functools
2
+ import ipaddress
3
+ from typing import Dict, Generator
4
+
5
+ import colorama
6
+
7
+ from .types import Diff, Op
8
+
9
+ # NOCDEV-1720
10
+
11
+
12
+ diff_ops = {
13
+ "+": Op.ADDED,
14
+ "-": Op.REMOVED,
15
+ " ": Op.AFFECTED,
16
+ ">": Op.MOVED,
17
+ }
18
+
19
+ ops_sign = {
20
+ v: k
21
+ for k, v in diff_ops.items()
22
+ }
23
+
24
+ ops_order = {
25
+ Op.AFFECTED: 0,
26
+ Op.MOVED: 1,
27
+ Op.REMOVED: 2,
28
+ Op.ADDED: 3,
29
+ }
30
+
31
+ ops_color = {
32
+ Op.REMOVED: colorama.Fore.RED,
33
+ Op.ADDED: colorama.Fore.GREEN,
34
+ Op.AFFECTED: colorama.Fore.CYAN,
35
+ Op.MOVED: colorama.Fore.YELLOW,
36
+ }
37
+
38
+
39
+ def is_int(ts):
40
+ try:
41
+ int(ts)
42
+ return True
43
+ except ValueError:
44
+ return False
45
+
46
+
47
+ def is_ip(ts):
48
+ try:
49
+ ipaddress.ip_interface(ts)
50
+ return True
51
+ except ValueError:
52
+ return False
53
+
54
+
55
+ def diff_cmp(diff_l, diff_r):
56
+ """
57
+ Сборник костылей для сравнения двух строк диффа
58
+ """
59
+ (op_l, line_l, _, _) = diff_l
60
+ (op_r, line_r, _, _) = diff_r
61
+
62
+ cmp_line = (line_l > line_r) - (line_l < line_r)
63
+ cmp_op = ops_order[op_l] - ops_order[op_r]
64
+ if cmp_line == 0:
65
+ # При равенстве строк порядок определяется операцией
66
+ return cmp_op
67
+
68
+ if cmp_op == 0:
69
+ # Если для строк операции одинаковы, то считаем их равными
70
+ # По идее TimSort стабилен и порядок просто не должен измениться
71
+ return 0
72
+
73
+ # Для частично совпадающих строк
74
+ lws = line_l.split(" ")
75
+ rws = line_r.split(" ")
76
+ res = 0
77
+ for i, lw in enumerate(lws):
78
+ if len(rws) > i:
79
+ rw = rws[i]
80
+ if is_int(lw) and is_int(rw):
81
+ # В одинаковом положении в строках есть инты, сортируем по ним
82
+ res = int(lw) - int(rw)
83
+ if res == 0:
84
+ # При равных интах - сортируем по операции
85
+ res = cmp_op
86
+ elif is_ip(lw) and is_ip(rw):
87
+ # Аналогично интам обрабатываем ip-адреса
88
+ ip_l = ipaddress.ip_interface(lw)
89
+ ip_r = ipaddress.ip_interface(rw)
90
+ try:
91
+ res = (ip_l > ip_r) - (ip_l < ip_r)
92
+ except TypeError:
93
+ res = 1
94
+ if ip_l.version == 4:
95
+ res = -1
96
+ if res == 0:
97
+ res = cmp_op
98
+ elif i > 0:
99
+ if lw == rw:
100
+ res = cmp_op
101
+ else:
102
+ continue
103
+ break
104
+ if res != 0:
105
+ return res
106
+ return cmp_line
107
+
108
+
109
+ def resort_diff(diff: Diff) -> Diff:
110
+ res = []
111
+ df = sorted(diff, key=functools.cmp_to_key(diff_cmp))
112
+ for line in df:
113
+ ln = line
114
+ if len(line[2]) > 0:
115
+ ln = (line[0], line[1], resort_diff(line[2]), line[3])
116
+ res.append(ln)
117
+ return res
118
+
119
+
120
+ def colorize_line_with_color(line: str, color: int, no_color: bool):
121
+ stripped = line.rstrip("\n")
122
+ add_newlines = len(line) - len(stripped)
123
+ line = stripped
124
+
125
+ if not no_color:
126
+ line = "%s%s%s%s" % (colorama.Style.BRIGHT, color, line, colorama.Style.RESET_ALL)
127
+
128
+ line += "\n" * add_newlines
129
+ return line
130
+
131
+
132
+ def colorize_line(line, no_color=False):
133
+ op = diff_ops[line[0]]
134
+ color = ops_color[op]
135
+ return colorize_line_with_color(line, color, no_color)
136
+
137
+
138
+ def gen_pre_as_diff(
139
+ pre: Dict,
140
+ show_rules: bool,
141
+ indent: str,
142
+ no_color: bool,
143
+ _level: int = 0
144
+ ) -> Generator[str, None, None]:
145
+ ops = [(order, op) for op, order in ops_order.items()]
146
+ ops.sort()
147
+ for (raw_rule, content) in pre.items():
148
+ items = content["items"].items()
149
+ for (_, diff) in items: # pylint: disable=redefined-outer-name
150
+ if show_rules and not raw_rule == "__MULTILINE_BODY__":
151
+ line = "# %s%s\n" % (indent * _level, raw_rule)
152
+ yield colorize_line_with_color(line, colorama.Fore.BLACK, no_color)
153
+ for (op, rows) in [(op, diff[op]) for (_, op) in ops]:
154
+ for item in rows:
155
+ line = "%s%s %s\n" % (ops_sign[op], indent * _level, item["row"])
156
+ yield colorize_line_with_color(line, ops_color[op], no_color)
157
+ if len(item["children"]) != 0:
158
+ yield from gen_pre_as_diff(item["children"], show_rules, indent, no_color, _level + 1)
annet/annlib/errors.py ADDED
@@ -0,0 +1,8 @@
1
+ class ExecError(Exception):
2
+ """Обработчик этого exception должен залоггировать ошибку и выйти с exit_code 1"""
3
+ pass
4
+
5
+
6
+ class DeployCancelled(Exception):
7
+ """Деплой на устройство был отменен"""
8
+ pass
@@ -0,0 +1,196 @@
1
+ import collections
2
+ import re
3
+ import typing
4
+
5
+ from . import patching, tabparser
6
+ from .diff import diff_ops, ops_sign
7
+ from .rbparser import acl
8
+
9
+ UnifiedInputConfig = str # Конфиг классических сетевых устройств
10
+ FileInputConfig = typing.Dict[str, typing.Any] # Конфиг вайтбоксов и серверов
11
+ InputConfig = typing.Union[UnifiedInputConfig, FileInputConfig]
12
+
13
+ Acl = typing.Dict[str, typing.Any]
14
+
15
+ UnifiedConfigTree = typing.OrderedDict[str, typing.Any]
16
+ FileConfigTree = typing.Dict[str, typing.Any]
17
+ ConfigTree = typing.Union[UnifiedConfigTree, FileConfigTree]
18
+
19
+ DiffTree = typing.OrderedDict[str, typing.Any]
20
+ UnifiedDiff = typing.List[typing.Tuple[str, str, typing.List, typing.Optional[int]]]
21
+ FileConfigDiff = typing.Dict[str, typing.Any]
22
+ Diff = typing.Union[UnifiedDiff, FileConfigDiff]
23
+
24
+
25
+ def make_acl(text: str, vendor: str) -> Acl:
26
+ return acl.compile_acl_text(text, vendor)
27
+
28
+
29
+ def filter_config(acl: Acl, fmtr: tabparser.CommonFormatter, input_config: InputConfig) -> InputConfig:
30
+ if isinstance(input_config, str):
31
+ config: ConfigTree = tabparser.parse_to_tree(input_config, fmtr.split)
32
+ config = patching.apply_acl(config, acl, fatal_acl=False)
33
+ config = fmtr.join(config)
34
+ else:
35
+ config = typing.cast(input_config, FileConfigTree)
36
+ config = apply_acl_fileconfig(input_config, acl)
37
+ return config
38
+
39
+
40
+ def filter_diff(acl: Acl, fmtr: tabparser.CommonFormatter, input_config: InputConfig) -> InputConfig:
41
+ if isinstance(input_config, str):
42
+ input_config = shift_op(input_config)
43
+ diff_tee: DiffTree = tabparser.parse_to_tree(input_config, fmtr.split)
44
+ diff: Diff = tree_to_diff(diff_tee)
45
+ diff = patching.apply_acl_diff(diff, acl)
46
+ config = fmtr.join(diff_to_tree(diff))
47
+ config = unshift_op(config)
48
+ config = config.rstrip()
49
+ else:
50
+ config = typing.cast(input_config, FileConfigTree)
51
+ config = apply_acl_fileconfig(input_config, acl)
52
+ return config
53
+
54
+
55
+ def filter_patch(acl: Acl, fmtr: tabparser.CommonFormatter, text: str) -> str:
56
+ return filter_config(acl, fmtr, text)
57
+
58
+
59
+ # NOCDEV-6378 на патч для Juniper/Nokia нельзя просто так наложить filter_acl
60
+ def filter_patch_jun_nokia(diff_filtered: InputConfig, fmtr: tabparser.CommonFormatter, text: str) -> str:
61
+ """
62
+ Накладываем ACL на патчи для Juniper/Nokia
63
+
64
+ Поскольку в патче уже потерена иерархия команд - они развернуты в строки типа
65
+ Нужна дополнительная информация о изначальном конфиге, которую можно подсмотреть в дифе
66
+ set interface et-0/0/0 unit ....
67
+ delete interface et-0/0/0 unit ....
68
+ /configure port 1/1/c17/1 ...
69
+ /configure delete port 1/1/c18/1 ...
70
+ """
71
+ diff_tree_stripped: DiffTree = tabparser.parse_to_tree(strip_op(diff_filtered), fmtr.split)
72
+ _tree_expand_lists_nokia_jun(diff_tree_stripped)
73
+ patch_lines_passed = []
74
+ for patch_line in text.split("\n"):
75
+ patch_parts = [x for x in patch_line.split(" ") if x]
76
+ diff_current = diff_tree_stripped
77
+ # strip set|delete|/configure
78
+ while patch_parts and patch_parts[0] in {"/configure", "set", "delete"}:
79
+ patch_parts = patch_parts[1:]
80
+ while patch_parts and diff_current:
81
+ for i in range(len(patch_parts), -1, -1):
82
+ key = " ".join(patch_parts[:i])
83
+ # consume parts and go down in diff hierarchy
84
+ if key in diff_current:
85
+ patch_parts = patch_parts[i:]
86
+ diff_current = diff_current[key]
87
+ break
88
+ # no progress has been made
89
+ else:
90
+ break
91
+ if not patch_parts:
92
+ patch_lines_passed.append(patch_line)
93
+ return "\n".join(patch_lines_passed)
94
+
95
+
96
+ def apply_acl_fileconfig(config, rules):
97
+ passed = {}
98
+ for (filename, filecontent) in config.items():
99
+ (match, _) = patching.match_row_to_acl(filename, rules)
100
+ if match:
101
+ if not (match["is_reverse"] and match["attrs"]["cant_delete"]):
102
+ passed[filename] = filecontent
103
+ return passed
104
+
105
+
106
+ def get_op(line: str) -> typing.Tuple[str, str, str]:
107
+ op = " "
108
+ indent = ""
109
+ opidx = -1
110
+ rowstart = 0
111
+ for rowstart in range(len(line)):
112
+ if line[rowstart] not in diff_ops:
113
+ break
114
+ for opidx in range(rowstart):
115
+ if line[opidx] != " ":
116
+ break
117
+ if opidx >= 0:
118
+ op = line[opidx]
119
+ indent = line[:opidx] + line[opidx+1:rowstart]
120
+ if op != " ":
121
+ indent = indent + " "
122
+ return op, indent, line[rowstart:]
123
+
124
+
125
+ def shift_op(text: str) -> str:
126
+ ret = ""
127
+ for line in text.split("\n"):
128
+ op, indent, line = get_op(line)
129
+ ret += indent + op + line + "\n"
130
+ return ret
131
+
132
+
133
+ def unshift_op(text: str) -> str:
134
+ ret = ""
135
+ for line in text.split("\n"):
136
+ op, indent, line = get_op(line)
137
+ ret += op + indent + line + "\n"
138
+ return ret
139
+
140
+
141
+ def strip_op(text: str) -> str:
142
+ ret: str = ""
143
+ for line in text.split("\n"):
144
+ op, indent, line = get_op(line)
145
+ if op != " ":
146
+ indent = indent[1:]
147
+ ret += indent + line + "\n"
148
+ return ret
149
+
150
+
151
+ def tree_to_diff(diff_tree: ConfigTree) -> Diff:
152
+ ret = []
153
+ for row, v in diff_tree.items():
154
+ op, _, row = get_op(row)
155
+ diff_op = diff_ops[op]
156
+ children = []
157
+ d_match = None
158
+ if isinstance(v, dict):
159
+ children = tree_to_diff(v)
160
+ ret.append((diff_op, row, children, d_match))
161
+ return ret
162
+
163
+
164
+ def diff_to_tree(diff: Diff) -> ConfigTree:
165
+ ret = collections.OrderedDict()
166
+ for diff_op, row, children, _ in diff:
167
+ row = ops_sign[diff_op] + row
168
+ ret[row] = diff_to_tree(children)
169
+ return ret
170
+
171
+
172
+ def _tree_expand_lists_nokia_jun(diff_tree: DiffTree):
173
+ """
174
+ Раскрываем списки Nokia/Juniper в отдельные элементы
175
+ {command: {"[a, b, c]": {}}} -> {command a: {}, command b: {}, command c: {}}
176
+
177
+ В неупорядоченном множестве префиксов также стираем ';' на конце - их не бывает в патче
178
+ {prefix-list: {"2a02::/64;": {}, "2a03::/64;": {}}} -> {prefix-list: {"2a02::/64": {}, "2a03::/64": {}}}
179
+ """
180
+ process: typing.List[DiffTree] = [diff_tree]
181
+ list_regexp = re.compile(r"^(.*)\s+\[(.+)\]$")
182
+ while process:
183
+ tree, process = process[0], process[1:]
184
+ for cmd in list(tree.keys()):
185
+ children = tree[cmd]
186
+ matches = list_regexp.search(cmd)
187
+ normalized_cmd = cmd.rstrip(";")
188
+ if matches:
189
+ for c in matches.group(2).split(" "):
190
+ if c.strip():
191
+ tree[" ".join([matches.group(1), c])] = children
192
+ if normalized_cmd != cmd:
193
+ del tree[cmd]
194
+ tree[normalized_cmd] = children
195
+ if isinstance(children, dict):
196
+ process.append(children)
@@ -0,0 +1,89 @@
1
+ """Support JSON patch (RFC 6902) and JSON Pointer (RFC 6901)."""
2
+
3
+ import copy
4
+ import json
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import jsonpatch
8
+ import jsonpointer
9
+
10
+
11
+ def format_json(data: Any, stable: bool = False) -> str:
12
+ """Serialize to json."""
13
+ return json.dumps(data, indent=4, ensure_ascii=False, sort_keys=not stable) + "\n"
14
+
15
+
16
+ def apply_json_fragment(
17
+ old: Dict[str, Any],
18
+ new_fragment: Dict[str, Any],
19
+ acl: List[str],
20
+ ) -> Dict[str, Any]:
21
+ """
22
+ Replace parts of the old document with 'new_fragment' using ACL restrictions.
23
+ """
24
+ full_new_config = copy.deepcopy(old)
25
+ for acl_item in acl:
26
+ pointer = jsonpointer.JsonPointer(acl_item)
27
+
28
+ try:
29
+ new_value = pointer.get(new_fragment)
30
+ except jsonpointer.JsonPointerException:
31
+ # no value found in new_fragment by the pointer, skip the ACL item
32
+ continue
33
+
34
+ _ensure_pointer_exists(full_new_config, pointer)
35
+ pointer.set(full_new_config, new_value)
36
+
37
+ return full_new_config
38
+
39
+
40
+ def _ensure_pointer_exists(doc: Dict[str, Any], pointer: jsonpointer.JsonPointer) -> None:
41
+ """
42
+ Ensure that document has all pointer parts (if possible).
43
+
44
+ This is workaround for errors of type:
45
+
46
+ ```
47
+ jsonpointer.JsonPointerException: member 'MY_PART' not found in {}
48
+ ```
49
+
50
+ See for details: https://github.com/stefankoegl/python-json-pointer/issues/41
51
+ """
52
+ parts_except_the_last = pointer.get_parts()[:-1]
53
+ doc_pointer: Dict[str, Any] = doc
54
+ for part in parts_except_the_last:
55
+ if part not in doc_pointer:
56
+ # create an empty object by the pointer part
57
+ doc_pointer[part] = {}
58
+
59
+ if isinstance(doc_pointer, dict):
60
+ # follow the pointer to delve deeper
61
+ doc_pointer = doc_pointer[part]
62
+ else:
63
+ # not a dict - cannot delve deeper
64
+ break
65
+
66
+
67
+ def make_patch(old: Dict[str, Any], new: Dict[str, Any]) -> List[Dict[str, Any]]:
68
+ """Generate a JSON patch by comparing the old document with the new one."""
69
+ return jsonpatch.make_patch(old, new).patch
70
+
71
+
72
+ def apply_patch(content: Optional[bytes], patch_bytes: bytes) -> bytes:
73
+ """
74
+ Apply JSON patch to file contents.
75
+
76
+ If content is None it is considered that the file does not exist.
77
+ """
78
+ old_doc: Any
79
+ if content is not None:
80
+ old_doc = json.loads(content)
81
+ else:
82
+ old_doc = None
83
+
84
+ patch_data = json.loads(patch_bytes)
85
+ patch = jsonpatch.JsonPatch(patch_data)
86
+ new_doc = patch.apply(old_doc)
87
+
88
+ new_contents = format_json(new_doc, stable=True).encode()
89
+ return new_contents