annet 0.16.34__py3-none-any.whl → 1.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/adapters/fetchers/stub/fetcher.py +11 -5
- annet/adapters/netbox/common/query.py +36 -1
- annet/adapters/netbox/v37/storage.py +79 -42
- annet/api/__init__.py +32 -8
- annet/deploy.py +37 -343
- annet/deploy_ui.py +774 -0
- annet/gen.py +5 -5
- annet/lib.py +19 -3
- annet/rulebook/texts/huawei.deploy +1 -1
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/METADATA +1 -1
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/RECORD +16 -15
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/AUTHORS +0 -0
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/LICENSE +0 -0
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/WHEEL +0 -0
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/entry_points.txt +0 -0
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/top_level.txt +0 -0
annet/deploy_ui.py
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
|
|
2
|
+
import re
|
|
3
|
+
import tempfile
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import math
|
|
7
|
+
import asyncio
|
|
8
|
+
import textwrap
|
|
9
|
+
import time
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from typing import Dict, List, Optional, Any
|
|
12
|
+
|
|
13
|
+
from contextlog import get_logger
|
|
14
|
+
|
|
15
|
+
from annet import text_term_format
|
|
16
|
+
from annet.output import TextArgs
|
|
17
|
+
try:
|
|
18
|
+
import curses
|
|
19
|
+
except ImportError:
|
|
20
|
+
curses = None
|
|
21
|
+
|
|
22
|
+
uname = os.uname()[0]
|
|
23
|
+
NCURSES_SIZE_T = 2 ** 15 - 1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AskConfirm:
|
|
27
|
+
CUT_WARN_MSG = "WARNING: the text was cut because of curses limits."
|
|
28
|
+
|
|
29
|
+
def __init__(self, text: str, text_type="diff", alternative_text: str = "",
|
|
30
|
+
alternative_text_type: str = "diff", allow_force_yes: bool = False):
|
|
31
|
+
self.text = [text, text_type]
|
|
32
|
+
self.alternative_text = [alternative_text, alternative_text_type]
|
|
33
|
+
self.color_to_curses: Dict[Optional[str], int] = {}
|
|
34
|
+
self.lines: Dict[int, List[TextArgs]] = {}
|
|
35
|
+
self.rows = 0
|
|
36
|
+
self.cols = None
|
|
37
|
+
self.top = 0
|
|
38
|
+
self.left = 0
|
|
39
|
+
self.pad: Optional["curses.window"] = None
|
|
40
|
+
self.screen = None
|
|
41
|
+
self.found_pos: dict[int, list[TextArgs]] = {}
|
|
42
|
+
self.curses_lines = None
|
|
43
|
+
self.debug_prompt = TextArgs("")
|
|
44
|
+
self.page_position = TextArgs("")
|
|
45
|
+
s_force = "/f" if allow_force_yes else ""
|
|
46
|
+
self.prompt = [
|
|
47
|
+
TextArgs("Execute these commands? [Y%s/q] (/ - search, a - patch/cmds)" % s_force, "blue", offset=0),
|
|
48
|
+
self.page_position,
|
|
49
|
+
self.debug_prompt]
|
|
50
|
+
|
|
51
|
+
def _parse_text(self):
|
|
52
|
+
txt = self.text[0]
|
|
53
|
+
txt_split = txt.splitlines()
|
|
54
|
+
# curses pad, который тут используется, имеет ограничение на количество линий
|
|
55
|
+
if (len(txt_split) + 1) >= NCURSES_SIZE_T: # +1 для того чтобы курсор можно было переместить на пустую строку
|
|
56
|
+
del txt_split[NCURSES_SIZE_T - 3:]
|
|
57
|
+
txt_split.insert(0, self.CUT_WARN_MSG)
|
|
58
|
+
txt_split.append(self.CUT_WARN_MSG)
|
|
59
|
+
txt = "\n".join(txt_split)
|
|
60
|
+
self.rows = len(txt_split)
|
|
61
|
+
self.cols = max(len(line) for line in txt_split)
|
|
62
|
+
res = text_term_format.curses_format(txt, self.text[1])
|
|
63
|
+
self.lines = res
|
|
64
|
+
|
|
65
|
+
def _update_search_pos(self, pattern: str) -> None:
|
|
66
|
+
self.found_pos = {}
|
|
67
|
+
if not pattern:
|
|
68
|
+
return
|
|
69
|
+
try:
|
|
70
|
+
expr = re.compile(pattern)
|
|
71
|
+
except Exception:
|
|
72
|
+
return None
|
|
73
|
+
lines = self.text[0].splitlines()
|
|
74
|
+
for (line_no, line) in enumerate(lines):
|
|
75
|
+
for match in re.finditer(expr, line):
|
|
76
|
+
if line_no not in self.found_pos:
|
|
77
|
+
self.found_pos[line_no] = []
|
|
78
|
+
self.found_pos[line_no].append(TextArgs(match.group(0), "highlight", match.start()))
|
|
79
|
+
|
|
80
|
+
def _init_colors(self):
|
|
81
|
+
self.color_to_curses = init_colors()
|
|
82
|
+
|
|
83
|
+
def _init_pad(self):
|
|
84
|
+
import curses
|
|
85
|
+
|
|
86
|
+
with self._store_xy():
|
|
87
|
+
self.pad = curses.newpad(self.rows + 1, self.cols)
|
|
88
|
+
self.pad.keypad(True) # accept arrow keys
|
|
89
|
+
self._render_to_pad(self.lines)
|
|
90
|
+
|
|
91
|
+
def _render_to_pad(self, lines: dict):
|
|
92
|
+
"""
|
|
93
|
+
Рендерим данный на pad
|
|
94
|
+
:param lines: словарь проиндексированный по номерам линий
|
|
95
|
+
:return:
|
|
96
|
+
"""
|
|
97
|
+
with self._store_xy():
|
|
98
|
+
for line_no, line_data in sorted(lines.items()):
|
|
99
|
+
line_pos_calc = 0
|
|
100
|
+
for line_part in line_data:
|
|
101
|
+
if line_part.offset is not None:
|
|
102
|
+
line_pos = line_part.offset
|
|
103
|
+
else:
|
|
104
|
+
line_pos = line_pos_calc
|
|
105
|
+
if line_part.color:
|
|
106
|
+
self.pad.addstr(line_no, line_pos, line_part.text, self.color_to_curses[line_part.color])
|
|
107
|
+
else:
|
|
108
|
+
self.pad.addstr(line_no, line_pos, line_part.text)
|
|
109
|
+
line_pos_calc += len(line_part.text)
|
|
110
|
+
|
|
111
|
+
def _add_prompt(self):
|
|
112
|
+
for prompt_part in self.prompt:
|
|
113
|
+
if not prompt_part:
|
|
114
|
+
continue
|
|
115
|
+
if prompt_part.offset is None:
|
|
116
|
+
offset = 0
|
|
117
|
+
else:
|
|
118
|
+
offset = prompt_part.offset
|
|
119
|
+
self.screen.addstr(self.curses_lines - 1, offset, prompt_part.text, self.color_to_curses[prompt_part.color])
|
|
120
|
+
|
|
121
|
+
def _clear_prompt(self):
|
|
122
|
+
with self._store_xy():
|
|
123
|
+
self.screen.move(self.curses_lines - 1, 0)
|
|
124
|
+
self.screen.clrtoeol()
|
|
125
|
+
|
|
126
|
+
def show(self):
|
|
127
|
+
self._add_prompt()
|
|
128
|
+
self.screen.refresh()
|
|
129
|
+
size = self.screen.getmaxyx()
|
|
130
|
+
self.pad.refresh(self.top, self.left, 0, 0, size[0] - 2, size[1] - 2)
|
|
131
|
+
|
|
132
|
+
@contextmanager
|
|
133
|
+
def _store_xy(self):
|
|
134
|
+
if self.pad is not None:
|
|
135
|
+
current_y, current_x = self.pad.getyx()
|
|
136
|
+
yield current_y, current_x
|
|
137
|
+
max_y, max_x = self.pad.getmaxyx()
|
|
138
|
+
current_y = min(max_y - 1, current_y)
|
|
139
|
+
current_x = min(max_x - 1, current_x)
|
|
140
|
+
|
|
141
|
+
self.pad.move(current_y, current_x)
|
|
142
|
+
else:
|
|
143
|
+
yield
|
|
144
|
+
|
|
145
|
+
def search_next(self, prev=False):
|
|
146
|
+
to = None
|
|
147
|
+
current_y, current_x = self.pad.getyx()
|
|
148
|
+
if prev:
|
|
149
|
+
for line_index in sorted(self.found_pos, reverse=True):
|
|
150
|
+
for text_args in self.found_pos[line_index]:
|
|
151
|
+
if line_index > current_y:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
if line_index < current_y or line_index == current_y and text_args.offset < current_x:
|
|
155
|
+
to = line_index, text_args.offset
|
|
156
|
+
break
|
|
157
|
+
if to:
|
|
158
|
+
break
|
|
159
|
+
else:
|
|
160
|
+
for line_index in sorted([i for i in self.found_pos if i >= current_y]):
|
|
161
|
+
for text_args in self.found_pos[line_index]:
|
|
162
|
+
if line_index > current_y or line_index == current_y and text_args.offset > current_x:
|
|
163
|
+
to = line_index, text_args.offset
|
|
164
|
+
break
|
|
165
|
+
if to:
|
|
166
|
+
break
|
|
167
|
+
if to:
|
|
168
|
+
return to[0] - current_y, to[1] - current_x
|
|
169
|
+
else:
|
|
170
|
+
return 0, 0
|
|
171
|
+
|
|
172
|
+
def _search_prompt(self) -> tuple[int, int]:
|
|
173
|
+
import curses
|
|
174
|
+
|
|
175
|
+
search_prompt = [TextArgs("Search: ", "green_bold", offset=0)]
|
|
176
|
+
current_prompt = self.prompt
|
|
177
|
+
self.prompt = search_prompt
|
|
178
|
+
with self._store_xy():
|
|
179
|
+
self._clear_prompt()
|
|
180
|
+
self.show()
|
|
181
|
+
curses.echo()
|
|
182
|
+
expr = self.screen.getstr().decode()
|
|
183
|
+
curses.noecho()
|
|
184
|
+
self._update_search_pos(expr)
|
|
185
|
+
self._parse_text()
|
|
186
|
+
self._init_pad()
|
|
187
|
+
# срендерем поверх pad слой с подстветкой
|
|
188
|
+
self._render_to_pad(self.found_pos)
|
|
189
|
+
y_offset, x_offset = self.search_next()
|
|
190
|
+
self.prompt = current_prompt
|
|
191
|
+
return y_offset, x_offset
|
|
192
|
+
|
|
193
|
+
def _do_commands(self) -> str:
|
|
194
|
+
import curses
|
|
195
|
+
|
|
196
|
+
while True:
|
|
197
|
+
self._clear_prompt()
|
|
198
|
+
try:
|
|
199
|
+
ch = self.pad.getch()
|
|
200
|
+
except KeyboardInterrupt:
|
|
201
|
+
return "n"
|
|
202
|
+
max_y, max_x = self.screen.getmaxyx()
|
|
203
|
+
_, pad_max_x = self.pad.getmaxyx()
|
|
204
|
+
max_y -= 2 # prompt
|
|
205
|
+
y_offset = 0
|
|
206
|
+
x_offset = 0
|
|
207
|
+
margin = 0
|
|
208
|
+
y_delta = 0
|
|
209
|
+
x_delta = 0
|
|
210
|
+
|
|
211
|
+
y, x = self.pad.getyx()
|
|
212
|
+
if ch == ord("q"):
|
|
213
|
+
return "exit"
|
|
214
|
+
elif ch in [ord("y"), ord("Y")]:
|
|
215
|
+
return "y"
|
|
216
|
+
elif ch in [ord("f"), ord("F")]:
|
|
217
|
+
return "force-yes"
|
|
218
|
+
elif ch == ord("a"):
|
|
219
|
+
if self.alternative_text:
|
|
220
|
+
self.text, self.alternative_text = self.alternative_text, self.text
|
|
221
|
+
self.screen.clear()
|
|
222
|
+
self._parse_text()
|
|
223
|
+
self._init_pad()
|
|
224
|
+
elif ch == ord("d"):
|
|
225
|
+
if self.debug_prompt.text == "":
|
|
226
|
+
self.debug_prompt.text = "init"
|
|
227
|
+
else:
|
|
228
|
+
self.debug_prompt.text = ""
|
|
229
|
+
elif ch == ord("n"):
|
|
230
|
+
y_offset, x_offset = self.search_next()
|
|
231
|
+
margin = 10
|
|
232
|
+
elif ch == ord("N"):
|
|
233
|
+
y_offset, x_offset = self.search_next(prev=True)
|
|
234
|
+
margin = 10
|
|
235
|
+
elif ch == ord("/"):
|
|
236
|
+
y_offset, x_offset = self._search_prompt()
|
|
237
|
+
margin = 10
|
|
238
|
+
elif ch == curses.KEY_UP:
|
|
239
|
+
y_offset = -1
|
|
240
|
+
elif ch == curses.KEY_PPAGE:
|
|
241
|
+
y_offset = -10
|
|
242
|
+
elif ch == curses.KEY_HOME:
|
|
243
|
+
y_offset = -len(self.lines)
|
|
244
|
+
elif ch == curses.KEY_DOWN:
|
|
245
|
+
y_offset = 1
|
|
246
|
+
elif ch == curses.KEY_NPAGE:
|
|
247
|
+
y_offset = 10
|
|
248
|
+
elif ch == curses.KEY_END:
|
|
249
|
+
y_offset = len(self.lines)
|
|
250
|
+
elif ch == curses.KEY_LEFT:
|
|
251
|
+
x_offset = -1
|
|
252
|
+
elif ch == curses.KEY_RIGHT:
|
|
253
|
+
x_offset = 1
|
|
254
|
+
|
|
255
|
+
if y_offset or x_offset:
|
|
256
|
+
y = max(0, y + y_offset)
|
|
257
|
+
y = min(self.rows, y)
|
|
258
|
+
x = max(0, x + x_offset)
|
|
259
|
+
x = min(self.cols, x)
|
|
260
|
+
|
|
261
|
+
y_delta = y - (self.top + max_y - margin)
|
|
262
|
+
if y_delta > 0:
|
|
263
|
+
self.top += y_delta
|
|
264
|
+
elif (y - margin) < self.top:
|
|
265
|
+
self.top = y
|
|
266
|
+
|
|
267
|
+
self.top = min(self.top, len(self.lines) - max_y)
|
|
268
|
+
|
|
269
|
+
x_delta = x - (self.left + max_x)
|
|
270
|
+
if x_delta > 0:
|
|
271
|
+
self.left += x_delta
|
|
272
|
+
elif x < self.left:
|
|
273
|
+
self.left = x
|
|
274
|
+
|
|
275
|
+
x = min(x, pad_max_x - 1)
|
|
276
|
+
self.pad.move(y, x)
|
|
277
|
+
|
|
278
|
+
if self.debug_prompt.text != "":
|
|
279
|
+
debug_line = "y=%s x=%s, x_delta=%s y_delta=%s top=%s, max_y=%s max_x=%s lines=%s" % \
|
|
280
|
+
(y, x, x_delta, y_delta, self.top, max_y, max_x, len(self.lines))
|
|
281
|
+
self.debug_prompt.text = debug_line
|
|
282
|
+
self.debug_prompt.color = "green_bold"
|
|
283
|
+
self.debug_prompt.offset = max_x - len(debug_line) - 1
|
|
284
|
+
|
|
285
|
+
if self.debug_prompt.text == "":
|
|
286
|
+
self.page_position.color = "highlight"
|
|
287
|
+
self.page_position.text = "line %s/%s" % (y, len(self.lines))
|
|
288
|
+
self.page_position.offset = max_x - len(self.page_position.text) - 1
|
|
289
|
+
|
|
290
|
+
self.show()
|
|
291
|
+
|
|
292
|
+
def loop(self) -> str:
|
|
293
|
+
import curses
|
|
294
|
+
|
|
295
|
+
res = ""
|
|
296
|
+
old_cursor = None
|
|
297
|
+
try:
|
|
298
|
+
self.screen = curses.initscr()
|
|
299
|
+
self.screen.leaveok(True)
|
|
300
|
+
self.curses_lines = curses.LINES # pylint: disable=maybe-no-member
|
|
301
|
+
curses.start_color()
|
|
302
|
+
curses.noecho() # no echo key input
|
|
303
|
+
curses.cbreak() # input with no-enter keyed
|
|
304
|
+
try:
|
|
305
|
+
old_cursor = curses.curs_set(2)
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
self._init_colors()
|
|
309
|
+
self._parse_text()
|
|
310
|
+
self._init_pad()
|
|
311
|
+
self.pad.move(0, 0)
|
|
312
|
+
self.show()
|
|
313
|
+
res = self._do_commands()
|
|
314
|
+
except Exception as err:
|
|
315
|
+
get_logger().exception("%s", err)
|
|
316
|
+
finally:
|
|
317
|
+
if old_cursor is not None:
|
|
318
|
+
curses.curs_set(old_cursor)
|
|
319
|
+
curses.nocbreak()
|
|
320
|
+
curses.echo()
|
|
321
|
+
curses.endwin()
|
|
322
|
+
return res
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def init_colors():
|
|
326
|
+
import curses
|
|
327
|
+
|
|
328
|
+
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
|
329
|
+
curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK)
|
|
330
|
+
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
|
331
|
+
curses.init_pair(4, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
|
|
332
|
+
curses.init_pair(5, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
|
333
|
+
curses.init_pair(6, curses.COLOR_BLUE, curses.COLOR_WHITE)
|
|
334
|
+
curses.init_pair(7, curses.COLOR_RED, curses.COLOR_WHITE)
|
|
335
|
+
curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_WHITE)
|
|
336
|
+
curses.init_pair(9, curses.COLOR_CYAN, curses.COLOR_BLUE)
|
|
337
|
+
return {
|
|
338
|
+
"green": curses.color_pair(1),
|
|
339
|
+
"green_bold": curses.color_pair(1) | curses.A_BOLD,
|
|
340
|
+
"cyan": curses.color_pair(2),
|
|
341
|
+
"red": curses.color_pair(3),
|
|
342
|
+
"magenta": curses.color_pair(4),
|
|
343
|
+
"yellow": curses.color_pair(5),
|
|
344
|
+
"blue": curses.color_pair(6),
|
|
345
|
+
"highlight": curses.color_pair(7),
|
|
346
|
+
None: curses.color_pair(8),
|
|
347
|
+
"cyan_blue": curses.color_pair(9),
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class ProgressBars:
|
|
352
|
+
TAIL_MODE_UNIFORM = 1 # экран разбивается на несколько равнозначных частей и в них есть заголовок и текст
|
|
353
|
+
TAIL_MODE_ONE_CONTENT = 2 # есть только одно окно с контентом, остальные только с заголовками
|
|
354
|
+
TAIL_MODE_NO_CONTENT_ONE_COLUMN = 3
|
|
355
|
+
|
|
356
|
+
def __init__(self, tiles_params: dict[str, dict[Any, Any]]):
|
|
357
|
+
self.tiles_params = tiles_params
|
|
358
|
+
self.mode = self.TAIL_MODE_UNIFORM
|
|
359
|
+
self.screen: "curses.window" = None
|
|
360
|
+
self.tiles: dict[str, dict[str, Any]] = {}
|
|
361
|
+
self.offset = [0, 0]
|
|
362
|
+
self.terminal_refresher_coro = None
|
|
363
|
+
self.color_to_curses: dict[Optional[str], int] = {}
|
|
364
|
+
self.state = "INIT"
|
|
365
|
+
|
|
366
|
+
# context
|
|
367
|
+
self.enter_ok = False
|
|
368
|
+
self._new_stderr = None
|
|
369
|
+
self._saved_stderr_fd = None
|
|
370
|
+
self.progress_length = 10
|
|
371
|
+
|
|
372
|
+
def __enter__(self):
|
|
373
|
+
self.enter_ok = False
|
|
374
|
+
sys.stderr.flush()
|
|
375
|
+
self._new_stderr = tempfile.TemporaryFile()
|
|
376
|
+
self._saved_stderr_fd = os.dup(sys.stderr.fileno())
|
|
377
|
+
os.dup2(self._new_stderr.fileno(), sys.stderr.fileno())
|
|
378
|
+
self.enter_ok = True
|
|
379
|
+
try:
|
|
380
|
+
self.init()
|
|
381
|
+
except Exception:
|
|
382
|
+
self.__exit__(*sys.exc_info())
|
|
383
|
+
raise
|
|
384
|
+
return self
|
|
385
|
+
|
|
386
|
+
def __exit__(self, exc_type, exc_value, trace):
|
|
387
|
+
try:
|
|
388
|
+
self.stop_curses()
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
391
|
+
if self.enter_ok:
|
|
392
|
+
sys.stderr.flush()
|
|
393
|
+
self._new_stderr.seek(0)
|
|
394
|
+
stderr_data = self._new_stderr.read().decode()
|
|
395
|
+
os.dup2(self._saved_stderr_fd, sys.stderr.fileno())
|
|
396
|
+
sys.stderr.write(stderr_data)
|
|
397
|
+
|
|
398
|
+
def evaluate_mode(self, scree_size):
|
|
399
|
+
height = scree_size[0] // len(self.tiles_params)
|
|
400
|
+
if height > 20:
|
|
401
|
+
return self.TAIL_MODE_UNIFORM
|
|
402
|
+
if scree_size[0] - len(self.tiles_params) > 20:
|
|
403
|
+
return self.TAIL_MODE_ONE_CONTENT
|
|
404
|
+
if scree_size[0] // len(self.tiles_params) > 1:
|
|
405
|
+
return self.TAIL_MODE_NO_CONTENT_ONE_COLUMN
|
|
406
|
+
else:
|
|
407
|
+
return self.TAIL_MODE_NO_CONTENT_ONE_COLUMN
|
|
408
|
+
|
|
409
|
+
def make_tiles(self):
|
|
410
|
+
import curses
|
|
411
|
+
|
|
412
|
+
i = 0
|
|
413
|
+
scree_size = self.screen.getmaxyx()
|
|
414
|
+
scree_size = scree_size[0] - 1, scree_size[1]
|
|
415
|
+
scree_offset = self.screen.getbegyx()
|
|
416
|
+
mode = self.evaluate_mode(scree_size)
|
|
417
|
+
tiles_count = len(self.tiles_params)
|
|
418
|
+
max_height = scree_size[0]
|
|
419
|
+
width = scree_size[1]
|
|
420
|
+
begin_y = 0
|
|
421
|
+
begin_x = 0
|
|
422
|
+
tile_no = 0
|
|
423
|
+
status_bar_win = curses.newwin(1, width, scree_size[0], 0)
|
|
424
|
+
self.tiles["status:"] = {"win": status_bar_win, "content": "init", "title": [""], "height": 1,
|
|
425
|
+
"width": width, "need_draw": True}
|
|
426
|
+
max_tile_name_len = max(len(tile_name) for tile_name in self.tiles_params)
|
|
427
|
+
|
|
428
|
+
for tile_name in self.tiles_params:
|
|
429
|
+
win = None
|
|
430
|
+
height = 0
|
|
431
|
+
tile_no += 1
|
|
432
|
+
|
|
433
|
+
if mode == self.TAIL_MODE_UNIFORM:
|
|
434
|
+
height = int(scree_size[0] // len(self.tiles_params)) # TODO:остаток от деления прибавить к последнему
|
|
435
|
+
win = curses.newwin(height, width, begin_y, begin_x)
|
|
436
|
+
elif mode == self.TAIL_MODE_ONE_CONTENT:
|
|
437
|
+
if i == tiles_count - 1:
|
|
438
|
+
height = scree_size[0] - tiles_count + 1
|
|
439
|
+
else:
|
|
440
|
+
height = 1
|
|
441
|
+
win = curses.newwin(height, width, begin_y, begin_x)
|
|
442
|
+
elif mode == self.TAIL_MODE_NO_CONTENT_ONE_COLUMN:
|
|
443
|
+
height = 1
|
|
444
|
+
|
|
445
|
+
if tile_no < max_height:
|
|
446
|
+
begin_y = scree_offset[0] + begin_y
|
|
447
|
+
get_logger().debug("newwin height=%s, width=%s, begin_y=%s, begin_x=%s", height, width, begin_y,
|
|
448
|
+
begin_x)
|
|
449
|
+
win = curses.newwin(height, width, begin_y, begin_x)
|
|
450
|
+
if tile_no == max_height:
|
|
451
|
+
left = len(self.tiles_params) - max_height + 1
|
|
452
|
+
self.tiles["dumb"] = {"win": curses.newwin(height, width, begin_y, begin_x),
|
|
453
|
+
"content": "init",
|
|
454
|
+
"title": ["... and %s more" % left],
|
|
455
|
+
"height": height,
|
|
456
|
+
"width": width,
|
|
457
|
+
"need_draw": True}
|
|
458
|
+
|
|
459
|
+
title = [("{:<%s}" % (max_tile_name_len)).format(tile_name)]
|
|
460
|
+
|
|
461
|
+
self.tiles[tile_name] = {
|
|
462
|
+
"win": win,
|
|
463
|
+
"content": "init",
|
|
464
|
+
"title": title,
|
|
465
|
+
"height": height,
|
|
466
|
+
"width": width,
|
|
467
|
+
"need_draw": True
|
|
468
|
+
}
|
|
469
|
+
i += 1
|
|
470
|
+
begin_y += height
|
|
471
|
+
|
|
472
|
+
def set_status(self):
|
|
473
|
+
total = 0
|
|
474
|
+
iteration = 0
|
|
475
|
+
done_percent = 0
|
|
476
|
+
for tile_name, tile in self.tiles.items():
|
|
477
|
+
if tile_name == "status:":
|
|
478
|
+
continue
|
|
479
|
+
if "total" not in tile:
|
|
480
|
+
continue
|
|
481
|
+
total += tile["total"]
|
|
482
|
+
iteration += tile["iteration"]
|
|
483
|
+
if total > 0 and iteration > 0:
|
|
484
|
+
done_percent = float(iteration) / total * 100
|
|
485
|
+
|
|
486
|
+
if done_percent == 100:
|
|
487
|
+
msg = "All is done. Press q for exit."
|
|
488
|
+
else:
|
|
489
|
+
msg = "Deploying... %3.0f%%" % math.floor(done_percent)
|
|
490
|
+
self.set_title("status:", msg)
|
|
491
|
+
|
|
492
|
+
def start_terminal_refresher(self, max_refresh_rate=200):
|
|
493
|
+
if self.terminal_refresher_coro:
|
|
494
|
+
return
|
|
495
|
+
self.terminal_refresher_coro = asyncio.ensure_future(self._terminal_refresher(max_refresh_rate))
|
|
496
|
+
|
|
497
|
+
def stop_terminal_refresher(self):
|
|
498
|
+
if not self.terminal_refresher_coro:
|
|
499
|
+
return
|
|
500
|
+
self.terminal_refresher_coro.cancel()
|
|
501
|
+
self.terminal_refresher_coro = None
|
|
502
|
+
self.refresh_all()
|
|
503
|
+
|
|
504
|
+
async def _terminal_refresher(self, max_refresh_rate: int):
|
|
505
|
+
sleep = 1 / max_refresh_rate
|
|
506
|
+
try:
|
|
507
|
+
while True:
|
|
508
|
+
await asyncio.sleep(sleep)
|
|
509
|
+
self.refresh_all()
|
|
510
|
+
except asyncio.CancelledError:
|
|
511
|
+
return
|
|
512
|
+
|
|
513
|
+
def init(self):
|
|
514
|
+
import curses
|
|
515
|
+
|
|
516
|
+
self.screen = curses.initscr()
|
|
517
|
+
self.screen.nodelay(True) # getch() will be non-blocking
|
|
518
|
+
curses.noecho()
|
|
519
|
+
curses.cbreak()
|
|
520
|
+
scree_size = self.screen.getmaxyx()
|
|
521
|
+
get_logger().debug("orig scree_size y=%s, x=%s offset=%s %s", scree_size[0], scree_size[1], self.offset[0],
|
|
522
|
+
self.offset[1])
|
|
523
|
+
|
|
524
|
+
if self.offset[0] == 0:
|
|
525
|
+
new_y = 0
|
|
526
|
+
nlines = scree_size[0]
|
|
527
|
+
elif self.offset[0] < 0:
|
|
528
|
+
nlines = abs(self.offset[0])
|
|
529
|
+
new_y = scree_size[0] - nlines
|
|
530
|
+
else:
|
|
531
|
+
nlines = self.offset[0]
|
|
532
|
+
new_y = scree_size[0]
|
|
533
|
+
|
|
534
|
+
if self.offset[1] == 0:
|
|
535
|
+
new_x = 0
|
|
536
|
+
ncols = scree_size[1]
|
|
537
|
+
elif self.offset[1] < 0:
|
|
538
|
+
ncols = abs(self.offset[1])
|
|
539
|
+
new_x = scree_size[1] - ncols
|
|
540
|
+
else:
|
|
541
|
+
ncols = self.offset[1]
|
|
542
|
+
new_x = scree_size[1]
|
|
543
|
+
|
|
544
|
+
get_logger().debug("nlines=%s, ncols=%s, new_y=%s, new_x=%s", nlines, ncols, new_y, new_x)
|
|
545
|
+
|
|
546
|
+
self.screen.mvwin(new_y, new_x)
|
|
547
|
+
|
|
548
|
+
curses.curs_set(0)
|
|
549
|
+
curses.start_color()
|
|
550
|
+
self.color_to_curses = init_colors()
|
|
551
|
+
self.state = "OK"
|
|
552
|
+
self.make_tiles()
|
|
553
|
+
self.progress_length = scree_size[1] // 3
|
|
554
|
+
|
|
555
|
+
def stop_curses(self):
|
|
556
|
+
import curses
|
|
557
|
+
|
|
558
|
+
curses.curs_set(1)
|
|
559
|
+
curses.nocbreak()
|
|
560
|
+
curses.echo()
|
|
561
|
+
curses.endwin()
|
|
562
|
+
self.state = "CLOSED"
|
|
563
|
+
|
|
564
|
+
def draw_content(self, tile_name):
|
|
565
|
+
tile = self.tiles[tile_name]
|
|
566
|
+
win = tile["win"]
|
|
567
|
+
size = win.getmaxyx()
|
|
568
|
+
margin = 1
|
|
569
|
+
if (size[0] - 2 * margin) <= 0:
|
|
570
|
+
return
|
|
571
|
+
res = text_term_format.curses_format(tile["content"], "switch_out")
|
|
572
|
+
draw_lines_in_win(res, win, color_to_curses=self.color_to_curses, margin=margin)
|
|
573
|
+
|
|
574
|
+
def draw_title(self, tile_name):
|
|
575
|
+
tile = self.tiles[tile_name]
|
|
576
|
+
title = tile["title"]
|
|
577
|
+
win = tile["win"]
|
|
578
|
+
if not isinstance(title, (tuple, list)):
|
|
579
|
+
title = [title]
|
|
580
|
+
draw_lines_in_win({0: title}, win, color_to_curses=self.color_to_curses, x_margin=1)
|
|
581
|
+
|
|
582
|
+
def refresh(self, tile_name: str, noutrefresh: bool = False):
|
|
583
|
+
# see noutrefresh in curses doc
|
|
584
|
+
tile = self.tiles[tile_name]
|
|
585
|
+
win = tile["win"]
|
|
586
|
+
if not tile["need_draw"] or win is None:
|
|
587
|
+
return
|
|
588
|
+
win.clear()
|
|
589
|
+
if tile["height"] > 1:
|
|
590
|
+
win.border()
|
|
591
|
+
self.draw_title(tile_name)
|
|
592
|
+
self.draw_content(tile_name)
|
|
593
|
+
if noutrefresh:
|
|
594
|
+
win.noutrefresh()
|
|
595
|
+
else:
|
|
596
|
+
win.refresh()
|
|
597
|
+
tile["need_draw"] = False
|
|
598
|
+
|
|
599
|
+
def refresh_all(self):
|
|
600
|
+
if self.state != "OK":
|
|
601
|
+
return
|
|
602
|
+
self.get_pressed_keys()
|
|
603
|
+
self.screen.refresh()
|
|
604
|
+
self.set_status()
|
|
605
|
+
tile_name = None
|
|
606
|
+
for tile_name in self.tiles:
|
|
607
|
+
self.refresh(tile_name, True)
|
|
608
|
+
if tile_name is not None:
|
|
609
|
+
self.refresh(tile_name, False)
|
|
610
|
+
|
|
611
|
+
def set_title(self, tile_name, title):
|
|
612
|
+
tile = self.tiles[tile_name]
|
|
613
|
+
# в 0 элементе хранится выровненный хостнейм
|
|
614
|
+
title0 = tile["title"][0]
|
|
615
|
+
new_title = (title0, title)
|
|
616
|
+
if new_title == tile["title"]:
|
|
617
|
+
return
|
|
618
|
+
tile["title"] = new_title
|
|
619
|
+
tile["need_draw"] = True
|
|
620
|
+
if not self.terminal_refresher_coro:
|
|
621
|
+
self.refresh(tile_name)
|
|
622
|
+
|
|
623
|
+
def set_content(self, tile_name: str, content: str):
|
|
624
|
+
tile = self.tiles[tile_name]
|
|
625
|
+
if content == tile["content"]:
|
|
626
|
+
return
|
|
627
|
+
tile["need_draw"] = True
|
|
628
|
+
tile["content"] = content
|
|
629
|
+
if not self.terminal_refresher_coro:
|
|
630
|
+
self.refresh(tile_name)
|
|
631
|
+
|
|
632
|
+
def set_progress(self,
|
|
633
|
+
tile_name: str,
|
|
634
|
+
iteration: int,
|
|
635
|
+
total: int,
|
|
636
|
+
prefix="",
|
|
637
|
+
suffix="",
|
|
638
|
+
fill="█",
|
|
639
|
+
error=False,
|
|
640
|
+
):
|
|
641
|
+
"""
|
|
642
|
+
Call in a loop to create terminal progress bar
|
|
643
|
+
@params:
|
|
644
|
+
iteration - Required : current iteration (Int)
|
|
645
|
+
total - Required : total iterations (Int)
|
|
646
|
+
prefix - Optional : prefix string (Str)
|
|
647
|
+
suffix - Optional : suffix string (Str)
|
|
648
|
+
length - Optional : character length of bar (Int)
|
|
649
|
+
fill - Optional : bar fill character (Str)
|
|
650
|
+
"""
|
|
651
|
+
if uname == "FreeBSD":
|
|
652
|
+
fill = "#"
|
|
653
|
+
percent = "{0:.1f}".format(100 * (iteration / float(total)))
|
|
654
|
+
filled_length = int(self.progress_length * iteration // total)
|
|
655
|
+
bar = fill * filled_length + "-" * (self.progress_length - filled_length)
|
|
656
|
+
res = "%s |%s| %s%% %s" % (prefix, bar, percent, suffix)
|
|
657
|
+
tile = self.tiles[tile_name]
|
|
658
|
+
tile["total"] = total
|
|
659
|
+
tile["iteration"] = iteration
|
|
660
|
+
if error:
|
|
661
|
+
res = TextArgs(res, "red")
|
|
662
|
+
else:
|
|
663
|
+
res = TextArgs(res, "cyan")
|
|
664
|
+
self.set_title(tile_name, res)
|
|
665
|
+
|
|
666
|
+
def set_exception(self, fqdn, cmd_exc, last_cmd, progress_max):
|
|
667
|
+
suffix = "cmd error: %s %s" % (str(cmd_exc).strip().replace("\n", "--"), last_cmd)
|
|
668
|
+
self.set_progress(fqdn, progress_max, progress_max, suffix=suffix, error=True)
|
|
669
|
+
|
|
670
|
+
def get_pressed_keys(self):
|
|
671
|
+
ch_list = []
|
|
672
|
+
while True:
|
|
673
|
+
try:
|
|
674
|
+
ch = self.screen.getkey()
|
|
675
|
+
ch_list.append(ch)
|
|
676
|
+
except Exception:
|
|
677
|
+
time.sleep(0.01)
|
|
678
|
+
break
|
|
679
|
+
return ch_list
|
|
680
|
+
|
|
681
|
+
async def wait_for_exit(self):
|
|
682
|
+
while True:
|
|
683
|
+
ch_list = self.get_pressed_keys()
|
|
684
|
+
if ch_list:
|
|
685
|
+
get_logger().debug("read ch %s", ch_list)
|
|
686
|
+
if "q" in ch_list:
|
|
687
|
+
return
|
|
688
|
+
else:
|
|
689
|
+
await asyncio.sleep(0.001)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def draw_lines_in_win(lines, win, color_to_curses: dict[Optional[str], int], margin=0, x_margin=0, y_margin=0):
|
|
693
|
+
max_y, max_x = win.getmaxyx()
|
|
694
|
+
max_y -= 2 * (margin or y_margin)
|
|
695
|
+
max_x -= 2 * (margin or x_margin)
|
|
696
|
+
lines_count = max(lines.keys())
|
|
697
|
+
if lines_count > max_y:
|
|
698
|
+
start_line = lines_count - max_y + 1
|
|
699
|
+
else:
|
|
700
|
+
start_line = 0
|
|
701
|
+
for line_no, line_data in sorted(lines.items())[start_line:]:
|
|
702
|
+
line_no = line_no - start_line
|
|
703
|
+
line_pos_calc = 0
|
|
704
|
+
for line_part in line_data:
|
|
705
|
+
if not isinstance(line_part, TextArgs):
|
|
706
|
+
line_part = TextArgs(line_part)
|
|
707
|
+
if line_part.offset is not None:
|
|
708
|
+
line_pos = line_part.offset
|
|
709
|
+
else:
|
|
710
|
+
line_pos = line_pos_calc
|
|
711
|
+
y = (margin or y_margin) + line_no
|
|
712
|
+
x = (margin or x_margin) + line_pos
|
|
713
|
+
max_line_len = max_x - x + 1
|
|
714
|
+
text = line_part.text[0:max_line_len]
|
|
715
|
+
try:
|
|
716
|
+
if line_part.color:
|
|
717
|
+
win.addstr(y, x, text, color_to_curses[line_part.color])
|
|
718
|
+
else:
|
|
719
|
+
win.addstr(y, x, text)
|
|
720
|
+
except Exception as exp:
|
|
721
|
+
get_logger().error("y=%s, x=%s, text=%s %s", y, x, text, exp)
|
|
722
|
+
line_pos_calc += len(text)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def wrap_to_windows(window, text: str, margin: int = 2) -> List[str]:
|
|
726
|
+
y, x = window.getmaxyx()
|
|
727
|
+
wrapped_text = textwrap.wrap(text, x - margin, max_lines=y, subsequent_indent="+")
|
|
728
|
+
return wrapped_text
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def curses_input(prompt: str, screen, header: Dict[int, str], hide_input=False):
|
|
732
|
+
import curses
|
|
733
|
+
|
|
734
|
+
if not header:
|
|
735
|
+
text = {0: prompt}
|
|
736
|
+
else:
|
|
737
|
+
text = header.copy()
|
|
738
|
+
text[max(header) + 1] = prompt
|
|
739
|
+
lines = [line for text_item in text.values() for line in wrap_to_windows(screen, text_item)]
|
|
740
|
+
text = dict(enumerate(lines))
|
|
741
|
+
maxy, maxx = screen.getmaxyx()
|
|
742
|
+
curses.init_pair(12, curses.COLOR_RED, curses.COLOR_WHITE)
|
|
743
|
+
input_len = 35
|
|
744
|
+
res: list[str] = []
|
|
745
|
+
text_width = max([len(x) for x in text.values()] + [len(prompt) + input_len])
|
|
746
|
+
header_height = max(list(text) + [0]) + 1
|
|
747
|
+
begin_y = int(maxy / 2)
|
|
748
|
+
begin_x = int(maxx / 2 - text_width / 2)
|
|
749
|
+
nlines = header_height + 2
|
|
750
|
+
ncols = text_width + 2
|
|
751
|
+
win = curses.newwin(nlines, ncols, begin_y, begin_x)
|
|
752
|
+
curses.noecho()
|
|
753
|
+
prev_curs = curses.curs_set(1)
|
|
754
|
+
while True:
|
|
755
|
+
win.clear()
|
|
756
|
+
win.refresh()
|
|
757
|
+
win.bkgd(curses.color_pair(12))
|
|
758
|
+
win.box()
|
|
759
|
+
draw_lines_in_win(text, win, margin=1, color_to_curses={})
|
|
760
|
+
if hide_input:
|
|
761
|
+
win.addstr(header_height, len(prompt) + 1, "*" * len(res))
|
|
762
|
+
else:
|
|
763
|
+
win.addstr(header_height, len(prompt) + 1, "".join(res))
|
|
764
|
+
ch = win.getkey()
|
|
765
|
+
if ch == "\x7f":
|
|
766
|
+
if res:
|
|
767
|
+
res.pop(-1)
|
|
768
|
+
elif ch == "\n":
|
|
769
|
+
break
|
|
770
|
+
else:
|
|
771
|
+
res.append(ch)
|
|
772
|
+
win.erase()
|
|
773
|
+
curses.curs_set(prev_curs)
|
|
774
|
+
return "".join(res)
|