gnetcli-adapter 2.0.4__tar.gz → 2.1.0__tar.gz
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.
- {gnetcli_adapter-2.0.4 → gnetcli_adapter-2.1.0}/PKG-INFO +2 -2
- {gnetcli_adapter-2.0.4 → gnetcli_adapter-2.1.0}/pyproject.toml +1 -1
- {gnetcli_adapter-2.0.4 → gnetcli_adapter-2.1.0}/src/gnetcli_adapter/gnetcli_adapter.py +136 -88
- gnetcli_adapter-2.1.0/src/gnetcli_adapter/progress_tracker.py +296 -0
- {gnetcli_adapter-2.0.4 → gnetcli_adapter-2.1.0}/LICENSE +0 -0
- {gnetcli_adapter-2.0.4 → gnetcli_adapter-2.1.0}/README.md +0 -0
- {gnetcli_adapter-2.0.4 → gnetcli_adapter-2.1.0}/src/gnetcli_adapter/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gnetcli_adapter
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Gnetcli-server adapter for Annet
|
|
5
5
|
Author-email: Aleksandr Balezin <gescheit12@gmail.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: annet>=
|
|
15
|
+
Requires-Dist: annet>=2.2.0
|
|
16
16
|
Requires-Dist: gnetclisdk>=1.0.31
|
|
17
17
|
Requires-Dist: pydantic_settings
|
|
18
18
|
Requires-Dist: pydantic
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import json
|
|
3
5
|
import subprocess
|
|
@@ -14,7 +16,11 @@ from annet.rulebook import common
|
|
|
14
16
|
from annet.connectors import AdapterWithConfig, AdapterWithName
|
|
15
17
|
from typing import Dict, List, Any, Optional, Tuple
|
|
16
18
|
from annet.storage import Device
|
|
17
|
-
|
|
19
|
+
|
|
20
|
+
from gnetcli_adapter.progress_tracker import (
|
|
21
|
+
ProgressTracker, ProgressBarTracker, FileProgressTracker, LogProgressTracker, CompositeTracker,
|
|
22
|
+
)
|
|
23
|
+
from gnetclisdk.client import Credentials, Gnetcli, HostParams, QA, File, GnetcliSessionCmd
|
|
18
24
|
from gnetclisdk.exceptions import EOFError
|
|
19
25
|
import gnetclisdk.proto.server_pb2 as pb
|
|
20
26
|
from pydantic import Field, field_validator, FieldValidationInfo
|
|
@@ -33,6 +39,7 @@ breed_to_device = {
|
|
|
33
39
|
"bcom-os": "bcomos",
|
|
34
40
|
"pc": "pc",
|
|
35
41
|
"cuml2": "pc",
|
|
42
|
+
"moxa": "pc",
|
|
36
43
|
"jun10": "juniper",
|
|
37
44
|
"eos4": "arista",
|
|
38
45
|
"h3c": "h3c",
|
|
@@ -218,7 +225,7 @@ class GnetcliFetcher(Fetcher, AdapterWithConfig, AdapterWithName):
|
|
|
218
225
|
return "gnetcli"
|
|
219
226
|
|
|
220
227
|
@classmethod
|
|
221
|
-
def with_config(cls, **kwargs:
|
|
228
|
+
def with_config(cls, **kwargs: Any) -> Fetcher:
|
|
222
229
|
return cls(**kwargs)
|
|
223
230
|
|
|
224
231
|
async def fetch_packages(self, devices: List[Device], processes: int = 1, max_slots: int = 0):
|
|
@@ -283,13 +290,14 @@ class GnetcliFetcher(Fetcher, AdapterWithConfig, AdapterWithName):
|
|
|
283
290
|
return b"\n".join(dev_result).decode()
|
|
284
291
|
|
|
285
292
|
async def adownload_dev(self, device: Device, files: List[str]) -> Dict[str, str]:
|
|
293
|
+
gnetcli_device = breed_to_device.get(device.breed, device.breed)
|
|
286
294
|
ip = get_device_ip(device)
|
|
287
295
|
downloaded = await self.api.download(
|
|
288
296
|
hostname=device.fqdn,
|
|
289
297
|
paths=files,
|
|
290
298
|
host_params=HostParams(
|
|
291
299
|
credentials=self.conf.make_dev_credentials(),
|
|
292
|
-
device=
|
|
300
|
+
device=gnetcli_device,
|
|
293
301
|
ip=ip,
|
|
294
302
|
),
|
|
295
303
|
)
|
|
@@ -331,16 +339,6 @@ def make_api(conf: AppSettings) -> Gnetcli:
|
|
|
331
339
|
)
|
|
332
340
|
return api
|
|
333
341
|
|
|
334
|
-
def format_trace(trace: list[pb.CMDTraceItem]) -> str:
|
|
335
|
-
res:list[str] = []
|
|
336
|
-
for t in trace:
|
|
337
|
-
op = "unknown" # TODO: get from pb
|
|
338
|
-
if t.operation == 2:
|
|
339
|
-
op = "write"
|
|
340
|
-
elif t.operation == 3:
|
|
341
|
-
op = "read"
|
|
342
|
-
res.append(f"{op}={t.data}")
|
|
343
|
-
return "\n".join(res)
|
|
344
342
|
|
|
345
343
|
class GnetcliDeployer(DeployDriver, AdapterWithConfig, AdapterWithName):
|
|
346
344
|
def __init__(
|
|
@@ -353,6 +351,7 @@ class GnetcliDeployer(DeployDriver, AdapterWithConfig, AdapterWithName):
|
|
|
353
351
|
ssh_agent_enabled: bool = True,
|
|
354
352
|
server_path: Optional[str] = None,
|
|
355
353
|
server_conf: Optional[str] = DEFAULT_GNETCLI_SERVER_CONF,
|
|
354
|
+
logs_dir: Optional[str] = None,
|
|
356
355
|
):
|
|
357
356
|
conf_args = {
|
|
358
357
|
"login": login,
|
|
@@ -366,14 +365,15 @@ class GnetcliDeployer(DeployDriver, AdapterWithConfig, AdapterWithName):
|
|
|
366
365
|
}
|
|
367
366
|
self.conf = AppSettings(**{k: v for k,v in conf_args.items() if v is not None})
|
|
368
367
|
self.api = make_api(self.conf)
|
|
368
|
+
self.logs_dir = logs_dir
|
|
369
369
|
|
|
370
370
|
@classmethod
|
|
371
371
|
def name(cls) -> str:
|
|
372
372
|
return "gnetcli"
|
|
373
373
|
|
|
374
374
|
@classmethod
|
|
375
|
-
def with_config(cls, **kwargs:
|
|
376
|
-
return
|
|
375
|
+
def with_config(cls, **kwargs: Any) -> DeployDriver:
|
|
376
|
+
return cls(**kwargs)
|
|
377
377
|
|
|
378
378
|
async def bulk_deploy(self, deploy_cmds: dict[Device, CommandList], args: DeployOptions, progress_bar: ProgressBar | None = None) -> DeployResult:
|
|
379
379
|
if progress_bar:
|
|
@@ -385,7 +385,45 @@ class GnetcliDeployer(DeployDriver, AdapterWithConfig, AdapterWithName):
|
|
|
385
385
|
res.add_results(results={dev.fqdn: dev_res for (dev, _), dev_res in zip(deploy_items, result)})
|
|
386
386
|
return res
|
|
387
387
|
|
|
388
|
-
|
|
388
|
+
def _get_files(self, cmds: dict[str, Any]) -> dict[str, File]:
|
|
389
|
+
return {
|
|
390
|
+
file: File(content=content, status=None)
|
|
391
|
+
for file, content in cmds["files"].items()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
def _get_reload_cmds(self, cmds: dict[str, Any]):
|
|
395
|
+
reload_cmds: dict[str, CommandList] = {}
|
|
396
|
+
for file, cmd in cmds["cmds"].items():
|
|
397
|
+
if isinstance(cmd, bytes):
|
|
398
|
+
cmd = cmd.decode()
|
|
399
|
+
reload_cmds[file] = CommandList([Command(cmd, suppress_nonzero=True) for cmd in cmd.splitlines()])
|
|
400
|
+
return reload_cmds
|
|
401
|
+
|
|
402
|
+
def _get_total(
|
|
403
|
+
self, command_groups: list[tuple[str, CommandList]], files: dict[str, File],
|
|
404
|
+
) -> int:
|
|
405
|
+
run_cmds = 0
|
|
406
|
+
for _, cmds in command_groups:
|
|
407
|
+
run_cmds += len(cmds)
|
|
408
|
+
if files:
|
|
409
|
+
run_cmds += 1
|
|
410
|
+
return run_cmds
|
|
411
|
+
|
|
412
|
+
def _init_progress_tracker(self, device: Device, progress_bar: ProgressBar | None) -> ProgressTracker:
|
|
413
|
+
tracker = CompositeTracker(LogProgressTracker(device))
|
|
414
|
+
if progress_bar:
|
|
415
|
+
tracker.add_tracker(ProgressBarTracker(device, progress_bar))
|
|
416
|
+
if self.logs_dir:
|
|
417
|
+
tracker.add_tracker(FileProgressTracker(device, self.logs_dir))
|
|
418
|
+
return tracker
|
|
419
|
+
|
|
420
|
+
async def deploy(
|
|
421
|
+
self,
|
|
422
|
+
device: Device,
|
|
423
|
+
cmds: CommandList,
|
|
424
|
+
args: DeployOptions,
|
|
425
|
+
progress_bar: ProgressBar | None = None,
|
|
426
|
+
) -> tuple[list[Exception], list[pb.CMDResult]]:
|
|
389
427
|
gnetcli_device = breed_to_device.get(device.breed, device.breed)
|
|
390
428
|
ip = get_device_ip(device)
|
|
391
429
|
host_params = HostParams(
|
|
@@ -393,85 +431,95 @@ class GnetcliDeployer(DeployDriver, AdapterWithConfig, AdapterWithName):
|
|
|
393
431
|
device=gnetcli_device,
|
|
394
432
|
ip=ip,
|
|
395
433
|
)
|
|
396
|
-
|
|
397
|
-
seen_exc: list[Exception] = []
|
|
398
|
-
reload_cmds: dict[str, CommandList] = {}
|
|
399
|
-
total_cmds = 0
|
|
434
|
+
command_groups: list[tuple[str, CommandList]]= []
|
|
400
435
|
if isinstance(cmds, dict): # PC
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
total_cmds += len(reload_cmds[file])
|
|
406
|
-
run_cmds = CommandList()
|
|
407
|
-
files: Dict[str, File] = {file: File(content=content, status=None) for file, content in cmds["files"].items()}
|
|
408
|
-
await self.api.upload(hostname=device.fqdn, files=files, host_params=host_params)
|
|
436
|
+
files = self._get_files(cmds)
|
|
437
|
+
if args.entire_reload.value == "yes":
|
|
438
|
+
for file, cmdlist in self._get_reload_cmds(cmds).items():
|
|
439
|
+
command_groups.append((f"Reload {file}", cmdlist))
|
|
409
440
|
else:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
441
|
+
files = {}
|
|
442
|
+
# treat each command as a group
|
|
443
|
+
for cmd in cmds:
|
|
444
|
+
command_groups.append(("Run command", CommandList([cmd])))
|
|
445
|
+
|
|
446
|
+
with self._init_progress_tracker(device, progress_bar) as tracker:
|
|
447
|
+
tracker.set_total(self._get_total(command_groups, files))
|
|
448
|
+
try:
|
|
449
|
+
seen_exc, results = await self._deploy(
|
|
450
|
+
device=device,
|
|
451
|
+
host_params=host_params,
|
|
452
|
+
command_groups=command_groups,
|
|
453
|
+
tracker=tracker,
|
|
454
|
+
files=files,
|
|
455
|
+
)
|
|
456
|
+
except Exception as e:
|
|
457
|
+
trace = traceback.format_exc()
|
|
458
|
+
tracker.command_done_error(trace)
|
|
459
|
+
seen_exc = [e]
|
|
460
|
+
results = []
|
|
461
|
+
if seen_exc:
|
|
462
|
+
tracker.finish_err(f"Seen {len(seen_exc)} exceptions")
|
|
463
|
+
else:
|
|
464
|
+
tracker.finish_ok("All done")
|
|
465
|
+
return seen_exc, results
|
|
466
|
+
|
|
467
|
+
async def _deploy(
|
|
468
|
+
self,
|
|
469
|
+
device: Device,
|
|
470
|
+
host_params: HostParams,
|
|
471
|
+
command_groups: list[tuple[str, CommandList]],
|
|
472
|
+
files: dict[str, File],
|
|
473
|
+
tracker: ProgressTracker,
|
|
474
|
+
) -> tuple[list[Exception], list[pb.CMDResult]]:
|
|
475
|
+
seen_exc = []
|
|
476
|
+
results = []
|
|
477
|
+
if files:
|
|
478
|
+
tracker.upload_files(list(files))
|
|
479
|
+
await self.api.upload(hostname=device.fqdn, files=files, host_params=host_params)
|
|
480
|
+
tracker.files_uploaded()
|
|
481
|
+
|
|
482
|
+
old_group_name = ""
|
|
483
|
+
async with self.api.cmd_session(hostname=device.fqdn) as session:
|
|
484
|
+
for group_number, (group_name, cmdlist) in enumerate(command_groups):
|
|
485
|
+
if group_name != old_group_name:
|
|
486
|
+
tracker.start_group(group_name)
|
|
487
|
+
old_group_name = group_name
|
|
488
|
+
for cmd_number, cmd in enumerate(cmdlist):
|
|
489
|
+
tracker.run_command(cmd.cmd)
|
|
490
|
+
try:
|
|
491
|
+
res = await session.cmd(
|
|
453
492
|
cmd=cmd.cmd,
|
|
454
493
|
cmd_timeout=cmd.timeout,
|
|
455
494
|
host_params=host_params,
|
|
456
495
|
qa=parse_annet_qa(cmd.questions or []),
|
|
496
|
+
trace=True,
|
|
457
497
|
)
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
498
|
+
except EOFError as e:
|
|
499
|
+
# we can't execute subsequent commands
|
|
500
|
+
if cmd.suppress_eof:
|
|
501
|
+
# some commands left
|
|
502
|
+
if group_number + 1 != len(command_groups) or cmd_number + 1 != len(cmd):
|
|
503
|
+
tracker.command_done_error("EOF detected before all commands executed.")
|
|
504
|
+
seen_exc.append(Exception("EOF detected before all commands executed."))
|
|
505
|
+
tracker.command_done_error("Suppressed EOF")
|
|
506
|
+
return seen_exc, results
|
|
507
|
+
seen_exc.append(e)
|
|
508
|
+
tracker.command_done_error("Unexpected EOFError")
|
|
509
|
+
return seen_exc, results
|
|
510
|
+
if res.status == 0:
|
|
511
|
+
tracker.command_done_ok(res)
|
|
512
|
+
results.append(res)
|
|
513
|
+
else:
|
|
514
|
+
e = Exception("cmd %s error %s status %s", cmd, res.error, res.status)
|
|
515
|
+
seen_exc.append(e)
|
|
516
|
+
if cmd.suppress_nonzero:
|
|
517
|
+
tracker.command_done_ok(res)
|
|
518
|
+
break # go to next command group
|
|
519
|
+
else:
|
|
520
|
+
tracker.command_done_error(res.error.decode())
|
|
521
|
+
return seen_exc, results
|
|
522
|
+
return seen_exc, results
|
|
475
523
|
|
|
476
524
|
def apply_deploy_rulebook(
|
|
477
525
|
self,
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from contextlib import ExitStack
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import TextIO
|
|
8
|
+
|
|
9
|
+
import gnetclisdk.proto.server_pb2 as pb
|
|
10
|
+
from annet.deploy import ProgressBar
|
|
11
|
+
from annet.storage import Device
|
|
12
|
+
|
|
13
|
+
logger = getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProgressTracker:
|
|
17
|
+
def __enter__(self) -> "ProgressTracker":
|
|
18
|
+
return self
|
|
19
|
+
|
|
20
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def start_group(self, group_name: str) -> None:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def set_total(self, total: int) -> None:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def upload_files(self, files: list[str]) -> None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def files_uploaded(self) -> None:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def run_command(self, cmd: str) -> None:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def run_reload_command(self, file: str, cmd: str):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def command_done_ok(self, output: pb.CMDResult) -> None:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def command_done_error(self, error: str) -> None:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def finish_ok(self, notification: str) -> None:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def finish_err(self, notification: str) -> None:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def format_trace(trace: list[pb.CMDTraceItem]) -> str:
|
|
55
|
+
res: list[str] = []
|
|
56
|
+
for t in trace:
|
|
57
|
+
op = "unknown" # TODO: get from pb
|
|
58
|
+
if t.operation == 2:
|
|
59
|
+
op = "write"
|
|
60
|
+
elif t.operation == 3:
|
|
61
|
+
op = "read"
|
|
62
|
+
res.append(f"{op}={t.data}")
|
|
63
|
+
return "\n".join(res)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _render_cmd_res(res: pb.CMDResult) -> str:
|
|
67
|
+
if res.trace:
|
|
68
|
+
trace_str = "\n" + format_trace(res.trace)
|
|
69
|
+
else:
|
|
70
|
+
trace_str = ""
|
|
71
|
+
try:
|
|
72
|
+
error_str = res.error.decode("utf-8")
|
|
73
|
+
except UnicodeDecodeError:
|
|
74
|
+
error_str = str(res.error)
|
|
75
|
+
if error_str:
|
|
76
|
+
error_str = "\n" + error_str
|
|
77
|
+
return f"out: {res.out_str}\nstatus: {res.status}{error_str}{trace_str}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ProgressBarTracker(ProgressTracker):
|
|
81
|
+
def __init__(self, device: Device, progress_bar: ProgressBar) -> None:
|
|
82
|
+
self.progress_bar = progress_bar
|
|
83
|
+
self.tile = device.fqdn
|
|
84
|
+
self.total_steps = 0
|
|
85
|
+
self.done_steps = 0
|
|
86
|
+
self.last_cmd = ""
|
|
87
|
+
|
|
88
|
+
def set_total(self, total: int) -> None:
|
|
89
|
+
self.total_steps = total
|
|
90
|
+
|
|
91
|
+
def start_group(self, group_name: str) -> None:
|
|
92
|
+
self.progress_bar.add_content(self.tile, f"=== {group_name} ===")
|
|
93
|
+
|
|
94
|
+
def upload_files(self, files: list[str]) -> None:
|
|
95
|
+
filelist = "".join(f"- {f}\n" for f in files)
|
|
96
|
+
self.progress_bar.add_content(self.tile, f"=== Uploading files ===\n{filelist}")
|
|
97
|
+
|
|
98
|
+
def files_uploaded(self) -> None:
|
|
99
|
+
self.done_steps += 1
|
|
100
|
+
self.progress_bar.add_content(self.tile, f"=== Files uploaded ===\n")
|
|
101
|
+
self.progress_bar.set_progress(self.tile, self.done_steps, self.total_steps)
|
|
102
|
+
|
|
103
|
+
def run_command(self, cmd: str):
|
|
104
|
+
self.progress_bar.add_content(self.tile, f"cmd: {cmd}")
|
|
105
|
+
self.progress_bar.set_progress(
|
|
106
|
+
self.tile,
|
|
107
|
+
self.done_steps,
|
|
108
|
+
self.total_steps,
|
|
109
|
+
cmd,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def run_reload_command(self, file: str, cmd: str):
|
|
113
|
+
self.last_cmd = cmd
|
|
114
|
+
self.progress_bar.add_content(self.tile, f"{file}: {cmd}")
|
|
115
|
+
self.progress_bar.set_progress(self.tile, self.done_steps, self.total_steps, suffix=cmd)
|
|
116
|
+
|
|
117
|
+
def command_done_ok(self, output: pb.CMDResult) -> None:
|
|
118
|
+
self.done_steps += 1
|
|
119
|
+
output_str = _render_cmd_res(output)
|
|
120
|
+
self.progress_bar.add_content(self.tile, output_str)
|
|
121
|
+
self.progress_bar.set_progress(
|
|
122
|
+
self.tile,
|
|
123
|
+
self.done_steps,
|
|
124
|
+
self.total_steps,
|
|
125
|
+
"Ok: " + self.last_cmd,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def command_done_error(self, error: str) -> None:
|
|
129
|
+
logger.error(error)
|
|
130
|
+
self.done_steps += 1
|
|
131
|
+
self.progress_bar.add_content(self.tile, error)
|
|
132
|
+
self.progress_bar.set_progress(
|
|
133
|
+
self.tile,
|
|
134
|
+
self.done_steps,
|
|
135
|
+
self.total_steps,
|
|
136
|
+
"Error: " + self.last_cmd,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def finish_ok(self, notification: str) -> None:
|
|
140
|
+
self.done_steps = self.total_steps
|
|
141
|
+
self.progress_bar.set_progress(self.tile,self.done_steps, self.total_steps, suffix=notification)
|
|
142
|
+
|
|
143
|
+
def finish_err(self, notification: str) -> None:
|
|
144
|
+
self.done_steps = self.total_steps
|
|
145
|
+
self.progress_bar.set_exception(self.tile, notification, "", self.total_steps)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class FileProgressTracker(ProgressTracker):
|
|
149
|
+
def __init__(self, device: Device, dirname: str):
|
|
150
|
+
self.dirname = dirname
|
|
151
|
+
self.device = device
|
|
152
|
+
self.file: TextIO | None = None
|
|
153
|
+
|
|
154
|
+
def __enter__(self) -> ProgressTracker:
|
|
155
|
+
filepath = self._make_file_path()
|
|
156
|
+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
|
157
|
+
logger.info("Writing deploy logs to file: %s", filepath)
|
|
158
|
+
self.file = open(filepath, "a")
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
def _make_file_path(self) -> str:
|
|
162
|
+
now = datetime.now()
|
|
163
|
+
datedir = f"{now:%Y-%m-%d-%H-%M}"
|
|
164
|
+
return os.path.join(
|
|
165
|
+
self.dirname,
|
|
166
|
+
datedir,
|
|
167
|
+
f"{self.device.fqdn}_{now.timestamp():.0f}",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
171
|
+
self.file.close()
|
|
172
|
+
|
|
173
|
+
def _log(self, msg: str) -> None:
|
|
174
|
+
self.file.write(f"{datetime.now()}: {msg}\n")
|
|
175
|
+
|
|
176
|
+
def start_group(self, group_name: str) -> None:
|
|
177
|
+
self._log(f"=== {group_name} ===")
|
|
178
|
+
|
|
179
|
+
def set_total(self, total: int) -> None:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
def upload_files(self, files: list[str]) -> None:
|
|
183
|
+
filelist = "\n".join(f"- {f}" for f in files)
|
|
184
|
+
self._log(f"=== Uploading files ===\n{filelist}")
|
|
185
|
+
|
|
186
|
+
def files_uploaded(self) -> None:
|
|
187
|
+
self._log(f"=== Files uploaded ===")
|
|
188
|
+
|
|
189
|
+
def run_command(self, cmd: str) -> None:
|
|
190
|
+
self._log(f"cmd: {cmd}")
|
|
191
|
+
|
|
192
|
+
def run_reload_command(self, file: str, cmd: str):
|
|
193
|
+
self._log(f"file: {cmd}")
|
|
194
|
+
|
|
195
|
+
def command_done_ok(self, output: pb.CMDResult) -> None:
|
|
196
|
+
output_str = _render_cmd_res(output)
|
|
197
|
+
self._log(output_str)
|
|
198
|
+
|
|
199
|
+
def command_done_error(self, error: str) -> None:
|
|
200
|
+
self._log(f"Error: {error}")
|
|
201
|
+
|
|
202
|
+
def finish_ok(self, notification: str) -> None:
|
|
203
|
+
self._log("Finished with success: " + notification)
|
|
204
|
+
|
|
205
|
+
def finish_err(self, notification: str) -> None:
|
|
206
|
+
self._log("Finished with failure: " + notification)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class LogProgressTracker(ProgressTracker):
|
|
211
|
+
def __init__(self, device: Device):
|
|
212
|
+
self.fqdn = device.fqdn
|
|
213
|
+
|
|
214
|
+
def __enter__(self) -> ProgressTracker:
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
def start_group(self, group_name: str) -> None:
|
|
218
|
+
logger.info(f"{self.fqdn} - {group_name}")
|
|
219
|
+
|
|
220
|
+
def upload_files(self, files: list[str]) -> None:
|
|
221
|
+
logger.info(f"{self.fqdn} - upload %s files", len(files))
|
|
222
|
+
|
|
223
|
+
def command_done_ok(self, output: pb.CMDResult) -> None:
|
|
224
|
+
if output.error:
|
|
225
|
+
try:
|
|
226
|
+
error = output.error.decode("utf-8")
|
|
227
|
+
except UnicodeDecodeError:
|
|
228
|
+
error = str(output.error)
|
|
229
|
+
logger.warning(f"{self.fqdn} - {error}")
|
|
230
|
+
|
|
231
|
+
def command_done_error(self, error: str) -> None:
|
|
232
|
+
logger.error(f"{self.fqdn} - {error}")
|
|
233
|
+
|
|
234
|
+
def finish_ok(self, notification: str) -> None:
|
|
235
|
+
logger.info(f"{self.fqdn} - finished with success - {notification}")
|
|
236
|
+
|
|
237
|
+
def finish_err(self, notification: str) -> None:
|
|
238
|
+
logger.info(f"{self.fqdn} - finished with failure - {notification}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class CompositeTracker(ProgressTracker):
|
|
243
|
+
def __init__(self, *trackers: ProgressTracker) -> None:
|
|
244
|
+
self.trackers = list(trackers)
|
|
245
|
+
self.stack = ExitStack()
|
|
246
|
+
|
|
247
|
+
def __enter__(self):
|
|
248
|
+
for tracker in self.trackers:
|
|
249
|
+
self.stack.enter_context(tracker)
|
|
250
|
+
return self
|
|
251
|
+
|
|
252
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
253
|
+
self.stack.close()
|
|
254
|
+
|
|
255
|
+
def add_tracker(self, tracker: ProgressTracker) -> None:
|
|
256
|
+
self.trackers.append(tracker)
|
|
257
|
+
|
|
258
|
+
def start_group(self, group_name: str) -> None:
|
|
259
|
+
for tracker in self.trackers:
|
|
260
|
+
tracker.start_group(group_name)
|
|
261
|
+
|
|
262
|
+
def set_total(self, total: int) -> None:
|
|
263
|
+
for tracker in self.trackers:
|
|
264
|
+
tracker.set_total(total)
|
|
265
|
+
|
|
266
|
+
def upload_files(self, files: list[str]) -> None:
|
|
267
|
+
for tracker in self.trackers:
|
|
268
|
+
tracker.upload_files(files)
|
|
269
|
+
|
|
270
|
+
def files_uploaded(self) -> None:
|
|
271
|
+
for tracker in self.trackers:
|
|
272
|
+
tracker.files_uploaded()
|
|
273
|
+
|
|
274
|
+
def run_command(self, cmd: str) -> None:
|
|
275
|
+
for tracker in self.trackers:
|
|
276
|
+
tracker.run_command(cmd)
|
|
277
|
+
|
|
278
|
+
def run_reload_command(self, file: str, cmd: str):
|
|
279
|
+
for tracker in self.trackers:
|
|
280
|
+
tracker.run_reload_command(file, cmd)
|
|
281
|
+
|
|
282
|
+
def command_done_ok(self, output: pb.CMDResult) -> None:
|
|
283
|
+
for tracker in self.trackers:
|
|
284
|
+
tracker.command_done_ok(output)
|
|
285
|
+
|
|
286
|
+
def command_done_error(self, error: str) -> None:
|
|
287
|
+
for tracker in self.trackers:
|
|
288
|
+
tracker.command_done_error(error)
|
|
289
|
+
|
|
290
|
+
def finish_ok(self, notification: str) -> None:
|
|
291
|
+
for tracker in self.trackers:
|
|
292
|
+
tracker.finish_ok(notification)
|
|
293
|
+
|
|
294
|
+
def finish_err(self, notification: str) -> None:
|
|
295
|
+
for tracker in self.trackers:
|
|
296
|
+
tracker.finish_err(notification)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|