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.

Files changed (137) hide show
  1. annet/__init__.py +61 -0
  2. annet/adapters/__init__.py +0 -0
  3. annet/adapters/netbox/__init__.py +0 -0
  4. annet/adapters/netbox/common/__init__.py +0 -0
  5. annet/adapters/netbox/common/client.py +87 -0
  6. annet/adapters/netbox/common/manufacturer.py +62 -0
  7. annet/adapters/netbox/common/models.py +105 -0
  8. annet/adapters/netbox/common/query.py +23 -0
  9. annet/adapters/netbox/common/status_client.py +25 -0
  10. annet/adapters/netbox/common/storage_opts.py +14 -0
  11. annet/adapters/netbox/provider.py +34 -0
  12. annet/adapters/netbox/v24/__init__.py +0 -0
  13. annet/adapters/netbox/v24/api_models.py +73 -0
  14. annet/adapters/netbox/v24/client.py +59 -0
  15. annet/adapters/netbox/v24/storage.py +196 -0
  16. annet/adapters/netbox/v37/__init__.py +0 -0
  17. annet/adapters/netbox/v37/api_models.py +38 -0
  18. annet/adapters/netbox/v37/client.py +62 -0
  19. annet/adapters/netbox/v37/storage.py +149 -0
  20. annet/annet.py +25 -0
  21. annet/annlib/__init__.py +7 -0
  22. annet/annlib/command.py +49 -0
  23. annet/annlib/diff.py +158 -0
  24. annet/annlib/errors.py +8 -0
  25. annet/annlib/filter_acl.py +196 -0
  26. annet/annlib/jsontools.py +116 -0
  27. annet/annlib/lib.py +495 -0
  28. annet/annlib/netdev/__init__.py +0 -0
  29. annet/annlib/netdev/db.py +62 -0
  30. annet/annlib/netdev/devdb/__init__.py +28 -0
  31. annet/annlib/netdev/devdb/data/devdb.json +137 -0
  32. annet/annlib/netdev/views/__init__.py +0 -0
  33. annet/annlib/netdev/views/dump.py +121 -0
  34. annet/annlib/netdev/views/hardware.py +112 -0
  35. annet/annlib/output.py +246 -0
  36. annet/annlib/patching.py +533 -0
  37. annet/annlib/rbparser/__init__.py +0 -0
  38. annet/annlib/rbparser/acl.py +120 -0
  39. annet/annlib/rbparser/deploying.py +55 -0
  40. annet/annlib/rbparser/ordering.py +52 -0
  41. annet/annlib/rbparser/platform.py +51 -0
  42. annet/annlib/rbparser/syntax.py +115 -0
  43. annet/annlib/rulebook/__init__.py +0 -0
  44. annet/annlib/rulebook/common.py +350 -0
  45. annet/annlib/tabparser.py +648 -0
  46. annet/annlib/types.py +35 -0
  47. annet/api/__init__.py +826 -0
  48. annet/argparse.py +415 -0
  49. annet/cli.py +237 -0
  50. annet/cli_args.py +503 -0
  51. annet/configs/context.yml +18 -0
  52. annet/configs/logging.yaml +39 -0
  53. annet/connectors.py +77 -0
  54. annet/deploy.py +536 -0
  55. annet/diff.py +84 -0
  56. annet/executor.py +551 -0
  57. annet/filtering.py +40 -0
  58. annet/gen.py +865 -0
  59. annet/generators/__init__.py +435 -0
  60. annet/generators/base.py +136 -0
  61. annet/generators/common/__init__.py +0 -0
  62. annet/generators/common/initial.py +33 -0
  63. annet/generators/entire.py +97 -0
  64. annet/generators/exceptions.py +10 -0
  65. annet/generators/jsonfragment.py +125 -0
  66. annet/generators/partial.py +119 -0
  67. annet/generators/perf.py +79 -0
  68. annet/generators/ref.py +15 -0
  69. annet/generators/result.py +127 -0
  70. annet/hardware.py +45 -0
  71. annet/implicit.py +139 -0
  72. annet/lib.py +128 -0
  73. annet/output.py +167 -0
  74. annet/parallel.py +448 -0
  75. annet/patching.py +25 -0
  76. annet/reference.py +148 -0
  77. annet/rulebook/__init__.py +114 -0
  78. annet/rulebook/arista/__init__.py +0 -0
  79. annet/rulebook/arista/iface.py +16 -0
  80. annet/rulebook/aruba/__init__.py +16 -0
  81. annet/rulebook/aruba/ap_env.py +146 -0
  82. annet/rulebook/aruba/misc.py +8 -0
  83. annet/rulebook/cisco/__init__.py +0 -0
  84. annet/rulebook/cisco/iface.py +68 -0
  85. annet/rulebook/cisco/misc.py +57 -0
  86. annet/rulebook/cisco/vlandb.py +90 -0
  87. annet/rulebook/common.py +19 -0
  88. annet/rulebook/deploying.py +87 -0
  89. annet/rulebook/huawei/__init__.py +0 -0
  90. annet/rulebook/huawei/aaa.py +75 -0
  91. annet/rulebook/huawei/bgp.py +97 -0
  92. annet/rulebook/huawei/iface.py +33 -0
  93. annet/rulebook/huawei/misc.py +337 -0
  94. annet/rulebook/huawei/vlandb.py +115 -0
  95. annet/rulebook/juniper/__init__.py +107 -0
  96. annet/rulebook/nexus/__init__.py +0 -0
  97. annet/rulebook/nexus/iface.py +92 -0
  98. annet/rulebook/patching.py +143 -0
  99. annet/rulebook/ribbon/__init__.py +12 -0
  100. annet/rulebook/texts/arista.deploy +20 -0
  101. annet/rulebook/texts/arista.order +125 -0
  102. annet/rulebook/texts/arista.rul +59 -0
  103. annet/rulebook/texts/aruba.deploy +20 -0
  104. annet/rulebook/texts/aruba.order +83 -0
  105. annet/rulebook/texts/aruba.rul +87 -0
  106. annet/rulebook/texts/cisco.deploy +27 -0
  107. annet/rulebook/texts/cisco.order +82 -0
  108. annet/rulebook/texts/cisco.rul +105 -0
  109. annet/rulebook/texts/huawei.deploy +188 -0
  110. annet/rulebook/texts/huawei.order +388 -0
  111. annet/rulebook/texts/huawei.rul +471 -0
  112. annet/rulebook/texts/juniper.rul +120 -0
  113. annet/rulebook/texts/nexus.deploy +24 -0
  114. annet/rulebook/texts/nexus.order +85 -0
  115. annet/rulebook/texts/nexus.rul +83 -0
  116. annet/rulebook/texts/nokia.rul +31 -0
  117. annet/rulebook/texts/pc.order +5 -0
  118. annet/rulebook/texts/pc.rul +9 -0
  119. annet/rulebook/texts/ribbon.deploy +22 -0
  120. annet/rulebook/texts/ribbon.rul +77 -0
  121. annet/rulebook/texts/routeros.order +38 -0
  122. annet/rulebook/texts/routeros.rul +45 -0
  123. annet/storage.py +125 -0
  124. annet/tabparser.py +36 -0
  125. annet/text_term_format.py +95 -0
  126. annet/tracing.py +170 -0
  127. annet/types.py +227 -0
  128. annet-0.0.dist-info/AUTHORS +21 -0
  129. annet-0.0.dist-info/LICENSE +21 -0
  130. annet-0.0.dist-info/METADATA +26 -0
  131. annet-0.0.dist-info/RECORD +137 -0
  132. annet-0.0.dist-info/WHEEL +5 -0
  133. annet-0.0.dist-info/entry_points.txt +5 -0
  134. annet-0.0.dist-info/top_level.txt +2 -0
  135. annet_generators/__init__.py +0 -0
  136. annet_generators/example/__init__.py +12 -0
  137. 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