annet 0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of annet might be problematic. Click here for more details.
- annet/__init__.py +61 -0
- annet/adapters/__init__.py +0 -0
- annet/adapters/netbox/__init__.py +0 -0
- annet/adapters/netbox/common/__init__.py +0 -0
- annet/adapters/netbox/common/client.py +87 -0
- annet/adapters/netbox/common/manufacturer.py +62 -0
- annet/adapters/netbox/common/models.py +105 -0
- annet/adapters/netbox/common/query.py +23 -0
- annet/adapters/netbox/common/status_client.py +25 -0
- annet/adapters/netbox/common/storage_opts.py +14 -0
- annet/adapters/netbox/provider.py +34 -0
- annet/adapters/netbox/v24/__init__.py +0 -0
- annet/adapters/netbox/v24/api_models.py +73 -0
- annet/adapters/netbox/v24/client.py +59 -0
- annet/adapters/netbox/v24/storage.py +196 -0
- annet/adapters/netbox/v37/__init__.py +0 -0
- annet/adapters/netbox/v37/api_models.py +38 -0
- annet/adapters/netbox/v37/client.py +62 -0
- annet/adapters/netbox/v37/storage.py +149 -0
- annet/annet.py +25 -0
- annet/annlib/__init__.py +7 -0
- annet/annlib/command.py +49 -0
- annet/annlib/diff.py +158 -0
- annet/annlib/errors.py +8 -0
- annet/annlib/filter_acl.py +196 -0
- annet/annlib/jsontools.py +116 -0
- annet/annlib/lib.py +495 -0
- annet/annlib/netdev/__init__.py +0 -0
- annet/annlib/netdev/db.py +62 -0
- annet/annlib/netdev/devdb/__init__.py +28 -0
- annet/annlib/netdev/devdb/data/devdb.json +137 -0
- annet/annlib/netdev/views/__init__.py +0 -0
- annet/annlib/netdev/views/dump.py +121 -0
- annet/annlib/netdev/views/hardware.py +112 -0
- annet/annlib/output.py +246 -0
- annet/annlib/patching.py +533 -0
- annet/annlib/rbparser/__init__.py +0 -0
- annet/annlib/rbparser/acl.py +120 -0
- annet/annlib/rbparser/deploying.py +55 -0
- annet/annlib/rbparser/ordering.py +52 -0
- annet/annlib/rbparser/platform.py +51 -0
- annet/annlib/rbparser/syntax.py +115 -0
- annet/annlib/rulebook/__init__.py +0 -0
- annet/annlib/rulebook/common.py +350 -0
- annet/annlib/tabparser.py +648 -0
- annet/annlib/types.py +35 -0
- annet/api/__init__.py +826 -0
- annet/argparse.py +415 -0
- annet/cli.py +237 -0
- annet/cli_args.py +503 -0
- annet/configs/context.yml +18 -0
- annet/configs/logging.yaml +39 -0
- annet/connectors.py +77 -0
- annet/deploy.py +536 -0
- annet/diff.py +84 -0
- annet/executor.py +551 -0
- annet/filtering.py +40 -0
- annet/gen.py +865 -0
- annet/generators/__init__.py +435 -0
- annet/generators/base.py +136 -0
- annet/generators/common/__init__.py +0 -0
- annet/generators/common/initial.py +33 -0
- annet/generators/entire.py +97 -0
- annet/generators/exceptions.py +10 -0
- annet/generators/jsonfragment.py +125 -0
- annet/generators/partial.py +119 -0
- annet/generators/perf.py +79 -0
- annet/generators/ref.py +15 -0
- annet/generators/result.py +127 -0
- annet/hardware.py +45 -0
- annet/implicit.py +139 -0
- annet/lib.py +128 -0
- annet/output.py +167 -0
- annet/parallel.py +448 -0
- annet/patching.py +25 -0
- annet/reference.py +148 -0
- annet/rulebook/__init__.py +114 -0
- annet/rulebook/arista/__init__.py +0 -0
- annet/rulebook/arista/iface.py +16 -0
- annet/rulebook/aruba/__init__.py +16 -0
- annet/rulebook/aruba/ap_env.py +146 -0
- annet/rulebook/aruba/misc.py +8 -0
- annet/rulebook/cisco/__init__.py +0 -0
- annet/rulebook/cisco/iface.py +68 -0
- annet/rulebook/cisco/misc.py +57 -0
- annet/rulebook/cisco/vlandb.py +90 -0
- annet/rulebook/common.py +19 -0
- annet/rulebook/deploying.py +87 -0
- annet/rulebook/huawei/__init__.py +0 -0
- annet/rulebook/huawei/aaa.py +75 -0
- annet/rulebook/huawei/bgp.py +97 -0
- annet/rulebook/huawei/iface.py +33 -0
- annet/rulebook/huawei/misc.py +337 -0
- annet/rulebook/huawei/vlandb.py +115 -0
- annet/rulebook/juniper/__init__.py +107 -0
- annet/rulebook/nexus/__init__.py +0 -0
- annet/rulebook/nexus/iface.py +92 -0
- annet/rulebook/patching.py +143 -0
- annet/rulebook/ribbon/__init__.py +12 -0
- annet/rulebook/texts/arista.deploy +20 -0
- annet/rulebook/texts/arista.order +125 -0
- annet/rulebook/texts/arista.rul +59 -0
- annet/rulebook/texts/aruba.deploy +20 -0
- annet/rulebook/texts/aruba.order +83 -0
- annet/rulebook/texts/aruba.rul +87 -0
- annet/rulebook/texts/cisco.deploy +27 -0
- annet/rulebook/texts/cisco.order +82 -0
- annet/rulebook/texts/cisco.rul +105 -0
- annet/rulebook/texts/huawei.deploy +188 -0
- annet/rulebook/texts/huawei.order +388 -0
- annet/rulebook/texts/huawei.rul +471 -0
- annet/rulebook/texts/juniper.rul +120 -0
- annet/rulebook/texts/nexus.deploy +24 -0
- annet/rulebook/texts/nexus.order +85 -0
- annet/rulebook/texts/nexus.rul +83 -0
- annet/rulebook/texts/nokia.rul +31 -0
- annet/rulebook/texts/pc.order +5 -0
- annet/rulebook/texts/pc.rul +9 -0
- annet/rulebook/texts/ribbon.deploy +22 -0
- annet/rulebook/texts/ribbon.rul +77 -0
- annet/rulebook/texts/routeros.order +38 -0
- annet/rulebook/texts/routeros.rul +45 -0
- annet/storage.py +125 -0
- annet/tabparser.py +36 -0
- annet/text_term_format.py +95 -0
- annet/tracing.py +170 -0
- annet/types.py +227 -0
- annet-0.0.dist-info/AUTHORS +21 -0
- annet-0.0.dist-info/LICENSE +21 -0
- annet-0.0.dist-info/METADATA +26 -0
- annet-0.0.dist-info/RECORD +137 -0
- annet-0.0.dist-info/WHEEL +5 -0
- annet-0.0.dist-info/entry_points.txt +5 -0
- annet-0.0.dist-info/top_level.txt +2 -0
- annet_generators/__init__.py +0 -0
- annet_generators/example/__init__.py +12 -0
- annet_generators/example/lldp.py +53 -0
annet/deploy.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
# pylint: disable=unused-argument
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import abc
|
|
5
|
+
import itertools
|
|
6
|
+
import re
|
|
7
|
+
from collections import namedtuple
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import Dict, List, Optional, Type, Any, OrderedDict
|
|
10
|
+
|
|
11
|
+
from contextlog import get_logger
|
|
12
|
+
|
|
13
|
+
from annet import text_term_format
|
|
14
|
+
from annet.annlib.command import Command, Question, CommandList
|
|
15
|
+
from annet.annlib.netdev.views.hardware import HardwareView
|
|
16
|
+
from annet.annlib.rbparser.deploying import MakeMessageMatcher, Answer
|
|
17
|
+
from annet.cli_args import DeployOptions
|
|
18
|
+
from annet.connectors import Connector
|
|
19
|
+
from annet.output import TextArgs
|
|
20
|
+
from annet.rulebook import get_rulebook, deploying
|
|
21
|
+
from annet.storage import Device
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
NCURSES_SIZE_T = 2 ** 15 - 1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_DeployResultBase = namedtuple("_DeployResultBase", ("hostnames", "results", "durations", "original_states"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DeployResult(_DeployResultBase): # noqa: E302
|
|
31
|
+
def add_results(self, results: Dict[str, Optional[Exception]]) -> None:
|
|
32
|
+
for hostname, result in results.items():
|
|
33
|
+
self.hostnames.append(hostname)
|
|
34
|
+
self.results[hostname] = result
|
|
35
|
+
self.durations[hostname] = 0.0
|
|
36
|
+
self.original_states[hostname] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _FetcherConnector(Connector["Fetcher"]):
|
|
40
|
+
name = "Fetcher"
|
|
41
|
+
ep_name = "deploy_fetcher"
|
|
42
|
+
|
|
43
|
+
def _get_default(self) -> Type["Fetcher"]:
|
|
44
|
+
return StubFetcher
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _DriverConnector(Connector["DeployDriver"]):
|
|
48
|
+
name = "DeployDriver"
|
|
49
|
+
ep_name = "deploy_driver"
|
|
50
|
+
|
|
51
|
+
def _get_default(self) -> Type["DeployDriver"]:
|
|
52
|
+
return StubDeployDriver
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
fetcher_connector = _FetcherConnector()
|
|
56
|
+
driver_connector = _DriverConnector()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Fetcher(abc.ABC):
|
|
60
|
+
@abc.abstractmethod
|
|
61
|
+
def fetch_packages(self, devices: List[Device],
|
|
62
|
+
processes: int = 1, max_slots: int = 0):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abc.abstractmethod
|
|
66
|
+
def fetch(self, devices: List[Device],
|
|
67
|
+
files_to_download: Dict[str, List[str]] = None,
|
|
68
|
+
processes: int = 1, max_slots: int = 0):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class StubFetcher(Fetcher):
|
|
73
|
+
def fetch_packages(self, devices: List[Device],
|
|
74
|
+
processes: int = 1, max_slots: int = 0):
|
|
75
|
+
raise NotImplementedError()
|
|
76
|
+
|
|
77
|
+
def fetch(self, devices: List[Device],
|
|
78
|
+
files_to_download: Dict[str, List[str]] = None,
|
|
79
|
+
processes: int = 1, max_slots: int = 0):
|
|
80
|
+
raise NotImplementedError()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DeployDriver(abc.ABC):
|
|
84
|
+
@abc.abstractmethod
|
|
85
|
+
async def bulk_deploy(self, deploy_cmds: dict, args: DeployOptions) -> DeployResult:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@abc.abstractmethod
|
|
89
|
+
def apply_deploy_rulebook(self, hw, cmd_paths, do_finalize=True, do_commit=True):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@abc.abstractmethod
|
|
93
|
+
def build_configuration_cmdlist(self, hw, do_finalize=True, do_commit=True):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
@abc.abstractmethod
|
|
97
|
+
def build_exit_cmdlist(self, hw):
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class StubDeployDriver(DeployDriver):
|
|
102
|
+
async def bulk_deploy(self, deploy_cmds: dict, args: DeployOptions) -> DeployResult:
|
|
103
|
+
NotImplementedError()
|
|
104
|
+
|
|
105
|
+
def apply_deploy_rulebook(self, hw, cmd_paths, do_finalize=True, do_commit=True):
|
|
106
|
+
NotImplementedError()
|
|
107
|
+
|
|
108
|
+
def build_configuration_cmdlist(self, hw, do_finalize=True, do_commit=True):
|
|
109
|
+
NotImplementedError()
|
|
110
|
+
|
|
111
|
+
def build_exit_cmdlist(self, hw):
|
|
112
|
+
raise NotImplementedError()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ===
|
|
116
|
+
def scrub_config(text, breed):
|
|
117
|
+
return text
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def show_bulk_report(hostnames, results, durations, log_dir):
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AskConfirm:
|
|
125
|
+
CUT_WARN_MSG = "WARNING: the text was cut because of curses limits."
|
|
126
|
+
|
|
127
|
+
def __init__(self, text: str, text_type="diff", alternative_text: str = "",
|
|
128
|
+
alternative_text_type: str = "diff", allow_force_yes: bool = False):
|
|
129
|
+
self.text = [text, text_type]
|
|
130
|
+
self.alternative_text = [alternative_text, alternative_text_type]
|
|
131
|
+
self.color_to_curses: Dict[Optional[str], int] = {}
|
|
132
|
+
self.lines: Dict[int, List[TextArgs]] = {}
|
|
133
|
+
self.rows = None
|
|
134
|
+
self.cols = None
|
|
135
|
+
self.top = 0
|
|
136
|
+
self.left = 0
|
|
137
|
+
self.pad = None
|
|
138
|
+
self.screen = None
|
|
139
|
+
self.found_pos = {}
|
|
140
|
+
self.curses_lines = None
|
|
141
|
+
self.debug_prompt = TextArgs("")
|
|
142
|
+
self.page_position = TextArgs("")
|
|
143
|
+
s_force = "/f" if allow_force_yes else ""
|
|
144
|
+
self.prompt = [
|
|
145
|
+
TextArgs("Execute these commands? [Y%s/q] (/ - search, a - patch/cmds)" % s_force, "blue", offset=0),
|
|
146
|
+
self.page_position,
|
|
147
|
+
self.debug_prompt]
|
|
148
|
+
|
|
149
|
+
def _parse_text(self):
|
|
150
|
+
txt = self.text[0]
|
|
151
|
+
txt_split = txt.splitlines()
|
|
152
|
+
# curses pad, который тут используется, имеет ограничение на количество линий
|
|
153
|
+
if (len(txt_split) + 1) >= NCURSES_SIZE_T: # +1 для того чтобы курсор можно было переместить на пустую строку
|
|
154
|
+
del txt_split[NCURSES_SIZE_T - 3:]
|
|
155
|
+
txt_split.insert(0, self.CUT_WARN_MSG)
|
|
156
|
+
txt_split.append(self.CUT_WARN_MSG)
|
|
157
|
+
txt = "\n".join(txt_split)
|
|
158
|
+
self.rows = len(txt_split)
|
|
159
|
+
self.cols = max(len(line) for line in txt_split)
|
|
160
|
+
res = text_term_format.curses_format(txt, self.text[1])
|
|
161
|
+
self.lines = res
|
|
162
|
+
|
|
163
|
+
def _update_search_pos(self, expr):
|
|
164
|
+
self.found_pos = {}
|
|
165
|
+
if not expr:
|
|
166
|
+
return
|
|
167
|
+
try:
|
|
168
|
+
expr = re.compile(expr)
|
|
169
|
+
except Exception:
|
|
170
|
+
return None
|
|
171
|
+
lines = self.text[0].splitlines()
|
|
172
|
+
for (line_no, line) in enumerate(lines):
|
|
173
|
+
for match in re.finditer(expr, line):
|
|
174
|
+
if line_no not in self.found_pos:
|
|
175
|
+
self.found_pos[line_no] = []
|
|
176
|
+
self.found_pos[line_no].append(TextArgs(match.group(0), "highlight", match.start()))
|
|
177
|
+
|
|
178
|
+
def _init_colors(self):
|
|
179
|
+
self.color_to_curses = init_colors()
|
|
180
|
+
|
|
181
|
+
def _init_pad(self):
|
|
182
|
+
import curses
|
|
183
|
+
|
|
184
|
+
with self._store_xy():
|
|
185
|
+
self.pad = curses.newpad(self.rows + 1, self.cols)
|
|
186
|
+
self.pad.keypad(True) # accept arrow keys
|
|
187
|
+
self._render_to_pad(self.lines)
|
|
188
|
+
|
|
189
|
+
def _render_to_pad(self, lines: dict):
|
|
190
|
+
"""
|
|
191
|
+
Рендерим данный на pad
|
|
192
|
+
:param lines: словарь проиндексированный по номерам линий
|
|
193
|
+
:return:
|
|
194
|
+
"""
|
|
195
|
+
with self._store_xy():
|
|
196
|
+
for line_no, line_data in sorted(lines.items()):
|
|
197
|
+
line_pos_calc = 0
|
|
198
|
+
for line_part in line_data:
|
|
199
|
+
if line_part.offset is not None:
|
|
200
|
+
line_pos = line_part.offset
|
|
201
|
+
else:
|
|
202
|
+
line_pos = line_pos_calc
|
|
203
|
+
if line_part.color:
|
|
204
|
+
self.pad.addstr(line_no, line_pos, line_part.text, self.color_to_curses[line_part.color])
|
|
205
|
+
else:
|
|
206
|
+
self.pad.addstr(line_no, line_pos, line_part.text)
|
|
207
|
+
line_pos_calc += len(line_part.text)
|
|
208
|
+
|
|
209
|
+
def _add_prompt(self):
|
|
210
|
+
for prompt_part in self.prompt:
|
|
211
|
+
if not prompt_part:
|
|
212
|
+
continue
|
|
213
|
+
if prompt_part.offset is None:
|
|
214
|
+
offset = 0
|
|
215
|
+
else:
|
|
216
|
+
offset = prompt_part.offset
|
|
217
|
+
self.screen.addstr(self.curses_lines - 1, offset, prompt_part.text, self.color_to_curses[prompt_part.color])
|
|
218
|
+
|
|
219
|
+
def _clear_prompt(self):
|
|
220
|
+
with self._store_xy():
|
|
221
|
+
self.screen.move(self.curses_lines - 1, 0)
|
|
222
|
+
self.screen.clrtoeol()
|
|
223
|
+
|
|
224
|
+
def show(self):
|
|
225
|
+
self._add_prompt()
|
|
226
|
+
self.screen.refresh()
|
|
227
|
+
size = self.screen.getmaxyx()
|
|
228
|
+
self.pad.refresh(self.top, self.left, 0, 0, size[0] - 2, size[1] - 2)
|
|
229
|
+
|
|
230
|
+
@contextmanager
|
|
231
|
+
def _store_xy(self):
|
|
232
|
+
if self.pad is not None:
|
|
233
|
+
current_y, current_x = self.pad.getyx()
|
|
234
|
+
yield current_y, current_x
|
|
235
|
+
max_y, max_x = self.pad.getmaxyx()
|
|
236
|
+
current_y = min(max_y - 1, current_y)
|
|
237
|
+
current_x = min(max_x - 1, current_x)
|
|
238
|
+
|
|
239
|
+
self.pad.move(current_y, current_x)
|
|
240
|
+
else:
|
|
241
|
+
yield
|
|
242
|
+
|
|
243
|
+
def search_next(self, prev=False):
|
|
244
|
+
to = None
|
|
245
|
+
current_y, current_x = self.pad.getyx()
|
|
246
|
+
if prev:
|
|
247
|
+
for line_index in sorted(self.found_pos, reverse=True):
|
|
248
|
+
for text_args in self.found_pos[line_index]:
|
|
249
|
+
if line_index > current_y:
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
if line_index < current_y or line_index == current_y and text_args.offset < current_x:
|
|
253
|
+
to = line_index, text_args.offset
|
|
254
|
+
break
|
|
255
|
+
if to:
|
|
256
|
+
break
|
|
257
|
+
else:
|
|
258
|
+
for line_index in sorted([i for i in self.found_pos if i >= current_y]):
|
|
259
|
+
for text_args in self.found_pos[line_index]:
|
|
260
|
+
if line_index > current_y or line_index == current_y and text_args.offset > current_x:
|
|
261
|
+
to = line_index, text_args.offset
|
|
262
|
+
break
|
|
263
|
+
if to:
|
|
264
|
+
break
|
|
265
|
+
if to:
|
|
266
|
+
return to[0] - current_y, to[1] - current_x
|
|
267
|
+
else:
|
|
268
|
+
return 0, 0
|
|
269
|
+
|
|
270
|
+
def _search_prompt(self):
|
|
271
|
+
import curses
|
|
272
|
+
|
|
273
|
+
search_prompt = [TextArgs("Search: ", "green_bold", offset=0)]
|
|
274
|
+
current_prompt = self.prompt
|
|
275
|
+
self.prompt = search_prompt
|
|
276
|
+
with self._store_xy():
|
|
277
|
+
self._clear_prompt()
|
|
278
|
+
self.show()
|
|
279
|
+
curses.echo()
|
|
280
|
+
expr = self.screen.getstr().decode()
|
|
281
|
+
curses.noecho()
|
|
282
|
+
self._update_search_pos(expr)
|
|
283
|
+
self._parse_text()
|
|
284
|
+
self._init_pad()
|
|
285
|
+
# срендерем поверх pad слой с подстветкой
|
|
286
|
+
self._render_to_pad(self.found_pos)
|
|
287
|
+
y_offset, x_offset = self.search_next()
|
|
288
|
+
self.prompt = current_prompt
|
|
289
|
+
return y_offset, x_offset
|
|
290
|
+
|
|
291
|
+
def _do_commands(self):
|
|
292
|
+
import curses
|
|
293
|
+
|
|
294
|
+
while True:
|
|
295
|
+
self._clear_prompt()
|
|
296
|
+
try:
|
|
297
|
+
ch = self.pad.getch()
|
|
298
|
+
except KeyboardInterrupt:
|
|
299
|
+
return "n"
|
|
300
|
+
max_y, max_x = self.screen.getmaxyx()
|
|
301
|
+
_, pad_max_x = self.pad.getmaxyx()
|
|
302
|
+
max_y -= 2 # prompt
|
|
303
|
+
y_offset = 0
|
|
304
|
+
x_offset = 0
|
|
305
|
+
margin = 0
|
|
306
|
+
y_delta = 0
|
|
307
|
+
x_delta = 0
|
|
308
|
+
|
|
309
|
+
y, x = self.pad.getyx()
|
|
310
|
+
if ch == ord("q"):
|
|
311
|
+
return "exit"
|
|
312
|
+
elif ch in [ord("y"), ord("Y")]:
|
|
313
|
+
return "y"
|
|
314
|
+
elif ch in [ord("f"), ord("F")]:
|
|
315
|
+
return "force-yes"
|
|
316
|
+
elif ch == ord("a"):
|
|
317
|
+
if self.alternative_text:
|
|
318
|
+
self.text, self.alternative_text = self.alternative_text, self.text
|
|
319
|
+
self.screen.clear()
|
|
320
|
+
self._parse_text()
|
|
321
|
+
self._init_pad()
|
|
322
|
+
elif ch == ord("d"):
|
|
323
|
+
if self.debug_prompt.text == "":
|
|
324
|
+
self.debug_prompt.text = "init"
|
|
325
|
+
else:
|
|
326
|
+
self.debug_prompt.text = ""
|
|
327
|
+
elif ch == ord("n"):
|
|
328
|
+
y_offset, x_offset = self.search_next()
|
|
329
|
+
margin = 10
|
|
330
|
+
elif ch == ord("N"):
|
|
331
|
+
y_offset, x_offset = self.search_next(prev=True)
|
|
332
|
+
margin = 10
|
|
333
|
+
elif ch == ord("/"):
|
|
334
|
+
y_offset, x_offset = self._search_prompt()
|
|
335
|
+
margin = 10
|
|
336
|
+
elif ch == curses.KEY_UP:
|
|
337
|
+
y_offset = -1
|
|
338
|
+
elif ch == curses.KEY_PPAGE:
|
|
339
|
+
y_offset = -10
|
|
340
|
+
elif ch == curses.KEY_HOME:
|
|
341
|
+
y_offset = -len(self.lines)
|
|
342
|
+
elif ch == curses.KEY_DOWN:
|
|
343
|
+
y_offset = 1
|
|
344
|
+
elif ch == curses.KEY_NPAGE:
|
|
345
|
+
y_offset = 10
|
|
346
|
+
elif ch == curses.KEY_END:
|
|
347
|
+
y_offset = len(self.lines)
|
|
348
|
+
elif ch == curses.KEY_LEFT:
|
|
349
|
+
x_offset = -1
|
|
350
|
+
elif ch == curses.KEY_RIGHT:
|
|
351
|
+
x_offset = 1
|
|
352
|
+
|
|
353
|
+
if y_offset or x_offset:
|
|
354
|
+
y = max(0, y + y_offset)
|
|
355
|
+
y = min(self.rows, y)
|
|
356
|
+
x = max(0, x + x_offset)
|
|
357
|
+
x = min(self.cols, x)
|
|
358
|
+
|
|
359
|
+
y_delta = y - (self.top + max_y - margin)
|
|
360
|
+
if y_delta > 0:
|
|
361
|
+
self.top += y_delta
|
|
362
|
+
elif (y - margin) < self.top:
|
|
363
|
+
self.top = y
|
|
364
|
+
|
|
365
|
+
self.top = min(self.top, len(self.lines) - max_y)
|
|
366
|
+
|
|
367
|
+
x_delta = x - (self.left + max_x)
|
|
368
|
+
if x_delta > 0:
|
|
369
|
+
self.left += x_delta
|
|
370
|
+
elif x < self.left:
|
|
371
|
+
self.left = x
|
|
372
|
+
|
|
373
|
+
x = min(x, pad_max_x - 1)
|
|
374
|
+
self.pad.move(y, x)
|
|
375
|
+
|
|
376
|
+
if self.debug_prompt.text != "":
|
|
377
|
+
debug_line = "y=%s x=%s, x_delta=%s y_delta=%s top=%s, max_y=%s max_x=%s lines=%s" % \
|
|
378
|
+
(y, x, x_delta, y_delta, self.top, max_y, max_x, len(self.lines))
|
|
379
|
+
self.debug_prompt.text = debug_line
|
|
380
|
+
self.debug_prompt.color = "green_bold"
|
|
381
|
+
self.debug_prompt.offset = max_x - len(debug_line) - 1
|
|
382
|
+
|
|
383
|
+
if self.debug_prompt.text == "":
|
|
384
|
+
self.page_position.color = "highlight"
|
|
385
|
+
self.page_position.text = "line %s/%s" % (y, len(self.lines))
|
|
386
|
+
self.page_position.offset = max_x - len(self.page_position.text) - 1
|
|
387
|
+
|
|
388
|
+
self.show()
|
|
389
|
+
|
|
390
|
+
def loop(self):
|
|
391
|
+
import curses
|
|
392
|
+
|
|
393
|
+
res = None
|
|
394
|
+
old_cursor = None
|
|
395
|
+
try:
|
|
396
|
+
self.screen = curses.initscr()
|
|
397
|
+
self.screen.leaveok(True)
|
|
398
|
+
self.curses_lines = curses.LINES # pylint: disable=maybe-no-member
|
|
399
|
+
curses.start_color()
|
|
400
|
+
curses.noecho() # no echo key input
|
|
401
|
+
curses.cbreak() # input with no-enter keyed
|
|
402
|
+
try:
|
|
403
|
+
old_cursor = curses.curs_set(2)
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
406
|
+
self._init_colors()
|
|
407
|
+
self._parse_text()
|
|
408
|
+
self._init_pad()
|
|
409
|
+
self.pad.move(0, 0)
|
|
410
|
+
self.show()
|
|
411
|
+
res = self._do_commands()
|
|
412
|
+
except Exception as err:
|
|
413
|
+
get_logger().exception("%s", err)
|
|
414
|
+
finally:
|
|
415
|
+
if old_cursor is not None:
|
|
416
|
+
curses.curs_set(old_cursor)
|
|
417
|
+
curses.nocbreak()
|
|
418
|
+
curses.echo()
|
|
419
|
+
curses.endwin()
|
|
420
|
+
return res
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def init_colors():
|
|
424
|
+
import curses
|
|
425
|
+
|
|
426
|
+
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
|
427
|
+
curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK)
|
|
428
|
+
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
|
429
|
+
curses.init_pair(4, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
|
|
430
|
+
curses.init_pair(5, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
|
431
|
+
curses.init_pair(6, curses.COLOR_BLUE, curses.COLOR_WHITE)
|
|
432
|
+
curses.init_pair(7, curses.COLOR_RED, curses.COLOR_WHITE)
|
|
433
|
+
curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_WHITE)
|
|
434
|
+
curses.init_pair(9, curses.COLOR_CYAN, curses.COLOR_BLUE)
|
|
435
|
+
return {
|
|
436
|
+
"green": curses.color_pair(1),
|
|
437
|
+
"green_bold": curses.color_pair(1) | curses.A_BOLD,
|
|
438
|
+
"cyan": curses.color_pair(2),
|
|
439
|
+
"red": curses.color_pair(3),
|
|
440
|
+
"magenta": curses.color_pair(4),
|
|
441
|
+
"yellow": curses.color_pair(5),
|
|
442
|
+
"blue": curses.color_pair(6),
|
|
443
|
+
"highlight": curses.color_pair(7),
|
|
444
|
+
None: curses.color_pair(8),
|
|
445
|
+
"cyan_blue": curses.color_pair(9),
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class RulebookQuestionHandler:
|
|
450
|
+
def __init__(self, dialogs):
|
|
451
|
+
self._dialogs = dialogs
|
|
452
|
+
|
|
453
|
+
def __call__(self, dev: Connector, cmd: Command, match_content: bytes):
|
|
454
|
+
content = match_content.strip()
|
|
455
|
+
content = content.decode()
|
|
456
|
+
for matcher, answer in self._dialogs.items():
|
|
457
|
+
if matcher(content):
|
|
458
|
+
return Command(answer.text)
|
|
459
|
+
|
|
460
|
+
get_logger().info("no answer in rulebook. dialogs=%s match_content=%s", self._dialogs, match_content)
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def rb_question_to_question(q: MakeMessageMatcher, a: Answer) -> Question: # TODO: drop MakeMessageMatcher
|
|
465
|
+
if not a.send_nl:
|
|
466
|
+
raise Exception("not supported false send_nl")
|
|
467
|
+
text: str = q._text # pylint: disable=protected-access
|
|
468
|
+
is_regexp = False
|
|
469
|
+
if text.startswith("/") and text.endswith("/"):
|
|
470
|
+
is_regexp = True
|
|
471
|
+
text = text[1:-1]
|
|
472
|
+
res = Question(question=text, answer=a.text, is_regexp=is_regexp)
|
|
473
|
+
return res
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def make_cmd_params(rule: Dict[str, Any]) -> Dict[str, Any]:
|
|
477
|
+
if rule:
|
|
478
|
+
qa_handler = RulebookQuestionHandler(rule["attrs"]["dialogs"])
|
|
479
|
+
qa_list: List[Question] = []
|
|
480
|
+
for matcher, answer in qa_handler._dialogs.items(): # pylint: disable=protected-access
|
|
481
|
+
qa_list.append(rb_question_to_question(matcher, answer))
|
|
482
|
+
return {
|
|
483
|
+
"questions": qa_list,
|
|
484
|
+
"timeout": rule["attrs"]["timeout"],
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
"timeout": 30,
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def make_apply_commands(rule, hw, do_commit, do_finalize):
|
|
492
|
+
apply_logic = rule["attrs"]["apply_logic"]
|
|
493
|
+
before, after = apply_logic(hw, do_commit=do_commit, do_finalize=do_finalize)
|
|
494
|
+
return before, after
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def fill_cmd_params(rules: OrderedDict, cmd: Command):
|
|
498
|
+
rule = deploying.match_deploy_rule(rules, (cmd.cmd,), {})
|
|
499
|
+
if rule:
|
|
500
|
+
cmd_params = make_cmd_params(rule)
|
|
501
|
+
cmd.questions = cmd_params.get("questions", None)
|
|
502
|
+
cmd.timeout = cmd_params["timeout"]
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def apply_deploy_rulebook(hw: HardwareView, cmd_paths, do_finalize=True, do_commit=True):
|
|
506
|
+
rules = get_rulebook(hw)["deploying"]
|
|
507
|
+
cmds_with_apply = []
|
|
508
|
+
for cmd_path, context in cmd_paths.items():
|
|
509
|
+
rule = deploying.match_deploy_rule(rules, cmd_path, context)
|
|
510
|
+
cmd_params = make_cmd_params(rule)
|
|
511
|
+
before, after = make_apply_commands(rule, hw, do_commit, do_finalize)
|
|
512
|
+
|
|
513
|
+
cmd = Command(cmd_path[-1], **cmd_params)
|
|
514
|
+
# XXX более чистый способ передавать-мета инфу о команде
|
|
515
|
+
cmd.level = len(cmd_path) - 1
|
|
516
|
+
cmds_with_apply.append((cmd, before, after))
|
|
517
|
+
|
|
518
|
+
def _key(item):
|
|
519
|
+
_cmd, before, after = item
|
|
520
|
+
return (tuple(cmd.cmd for cmd in before), tuple(cmd.cmd for cmd in after))
|
|
521
|
+
|
|
522
|
+
cmdlist = CommandList()
|
|
523
|
+
for _k, cmd_before_after in itertools.groupby(cmds_with_apply, key=_key):
|
|
524
|
+
cmd_before_after = list(cmd_before_after)
|
|
525
|
+
_, before, after = cmd_before_after[0]
|
|
526
|
+
for c in before:
|
|
527
|
+
c.level = 0
|
|
528
|
+
fill_cmd_params(rules, c)
|
|
529
|
+
cmdlist.add_cmd(c)
|
|
530
|
+
for cmd, _before, _after in cmd_before_after:
|
|
531
|
+
cmdlist.add_cmd(cmd)
|
|
532
|
+
for c in after:
|
|
533
|
+
c.level = 0
|
|
534
|
+
fill_cmd_params(rules, c)
|
|
535
|
+
cmdlist.add_cmd(c)
|
|
536
|
+
return cmdlist
|
annet/diff.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from itertools import groupby
|
|
3
|
+
from typing import Generator, List, Mapping, Tuple, Union
|
|
4
|
+
|
|
5
|
+
from annet.annlib.diff import ( # pylint: disable=unused-import
|
|
6
|
+
colorize_line,
|
|
7
|
+
diff_cmp,
|
|
8
|
+
diff_ops,
|
|
9
|
+
gen_pre_as_diff,
|
|
10
|
+
resort_diff,
|
|
11
|
+
)
|
|
12
|
+
from annet.annlib.output import format_file_diff
|
|
13
|
+
|
|
14
|
+
from annet import patching
|
|
15
|
+
from annet.cli_args import ShowDiffOptions
|
|
16
|
+
from annet.output import output_driver_connector
|
|
17
|
+
from annet.storage import Device
|
|
18
|
+
from annet.tabparser import make_formatter
|
|
19
|
+
from annet.types import Diff, PCDiff
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# NOCDEV-1720
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def gen_sort_diff(
|
|
26
|
+
diffs: Mapping[Device, Union[Diff, PCDiff]], args: ShowDiffOptions
|
|
27
|
+
) -> Generator[Tuple[str, Generator[str, None, None], bool], None, None]:
|
|
28
|
+
"""
|
|
29
|
+
Возвращает осортированный дифф, совместимый с write_output
|
|
30
|
+
:param diffs: Маппинг устройства в дифф
|
|
31
|
+
:param args: Параметры коммандной строки
|
|
32
|
+
"""
|
|
33
|
+
if args.no_collapse:
|
|
34
|
+
devices_to_diff = {(dev,): diff for dev, diff in diffs.items()}
|
|
35
|
+
else:
|
|
36
|
+
non_pc_diffs = {dev: diff for dev, diff in diffs.items() if not isinstance(diff, PCDiff)}
|
|
37
|
+
devices_to_diff = collapse_diffs(non_pc_diffs)
|
|
38
|
+
devices_to_diff.update({(dev,): diff for dev, diff in diffs.items() if isinstance(diff, PCDiff)})
|
|
39
|
+
for devices, diff_obj in devices_to_diff.items():
|
|
40
|
+
if not diff_obj:
|
|
41
|
+
continue
|
|
42
|
+
if isinstance(diff_obj, PCDiff):
|
|
43
|
+
for diff_file in diff_obj.diff_files:
|
|
44
|
+
diff_text = (
|
|
45
|
+
"\n".join(diff_file.diff_lines)
|
|
46
|
+
if args.no_color
|
|
47
|
+
else "\n".join(format_file_diff(diff_file.diff_lines))
|
|
48
|
+
)
|
|
49
|
+
yield diff_file.label, diff_text, False
|
|
50
|
+
else:
|
|
51
|
+
output_driver = output_driver_connector.get()
|
|
52
|
+
dest_name = ", ".join([output_driver.cfg_file_names(dev)[0] for dev in devices])
|
|
53
|
+
pd = patching.make_pre(resort_diff(diff_obj))
|
|
54
|
+
yield dest_name, gen_pre_as_diff(pd, args.show_rules, args.indent, args.no_color), False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _transform_text_diff_for_collapsing(text_diff) -> List[str]:
|
|
58
|
+
for line_no, line in enumerate(text_diff):
|
|
59
|
+
text_diff[line_no] = re.sub(r"(snmp-agent .+) cipher \S+ (.+)", r"\1 cipher ENCRYPTED \2", line)
|
|
60
|
+
return text_diff
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _make_text_diff(device: Device, diff: Diff) -> List[str]:
|
|
64
|
+
formatter = make_formatter(device.hw)
|
|
65
|
+
res = formatter.diff(diff)
|
|
66
|
+
return res
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def collapse_diffs(diffs: Mapping[Device, Diff]) -> Mapping[Tuple[Device, ...], Diff]:
|
|
70
|
+
"""
|
|
71
|
+
Группировка диффов.
|
|
72
|
+
:param diffs:
|
|
73
|
+
:return: дикт аналогичный типу Diff, но с несколькими dev в ключе.
|
|
74
|
+
Нужно учесть что дифы сверяются в отформатированном виде
|
|
75
|
+
"""
|
|
76
|
+
diffs_with_test = {dev: [diff, _transform_text_diff_for_collapsing(_make_text_diff(dev, diff))] for dev, diff in
|
|
77
|
+
diffs.items()}
|
|
78
|
+
res = {}
|
|
79
|
+
for _, collapsed_diff_iter in groupby(sorted(diffs_with_test.items(), key=lambda x: (x[0].hw.vendor, x[1][1])),
|
|
80
|
+
key=lambda x: x[1][1]):
|
|
81
|
+
collapsed_diff = list(collapsed_diff_iter)
|
|
82
|
+
res[tuple(x[0] for x in collapsed_diff)] = collapsed_diff[0][1][0]
|
|
83
|
+
|
|
84
|
+
return res
|