gnetcli-adapter 2.0.5__tar.gz → 2.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gnetcli_adapter
3
- Version: 2.0.5
3
+ Version: 2.2.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>=1.0.0
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
@@ -5,7 +5,7 @@ build-backend = "flit_core.buildapi"
5
5
  [project]
6
6
  name = "gnetcli_adapter"
7
7
  dependencies = [
8
- "annet>=1.0.0",
8
+ "annet>=2.2.0",
9
9
  "gnetclisdk>=1.0.31",
10
10
  "pydantic_settings",
11
11
  "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
- from gnetclisdk.client import Credentials, Gnetcli, HostParams, QA, File
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
@@ -156,7 +162,7 @@ def run_gnetcli_server(server_path: str, config: str = DEFAULT_GNETCLI_SERVER_CO
156
162
  try:
157
163
  proc = subprocess.Popen(
158
164
  [gnetcli_abs_path, "--conf-file", "-"],
159
- stdout=subprocess.PIPE,
165
+ stdout=subprocess.DEVNULL, # we do not read stdout
160
166
  stderr=subprocess.PIPE,
161
167
  stdin=subprocess.PIPE,
162
168
  bufsize=1,
@@ -182,6 +188,8 @@ def run_gnetcli_server(server_path: str, config: str = DEFAULT_GNETCLI_SERVER_CO
182
188
  else:
183
189
  if data.get("msg") == "init tcp socket":
184
190
  _local_gnetcli_url = data.get("address")
191
+ if data.get("msg") == "init unix socket":
192
+ _local_gnetcli_url = "unix:" + data.get("path")
185
193
  if data.get("level") == "panic":
186
194
  _logger.error("gnetcli error %s", data)
187
195
  _logger.debug("set gnetcli server exit code %s", proc.returncode)
@@ -219,7 +227,7 @@ class GnetcliFetcher(Fetcher, AdapterWithConfig, AdapterWithName):
219
227
  return "gnetcli"
220
228
 
221
229
  @classmethod
222
- def with_config(cls, **kwargs: Dict[str, Any]) -> Fetcher:
230
+ def with_config(cls, **kwargs: Any) -> Fetcher:
223
231
  return cls(**kwargs)
224
232
 
225
233
  async def fetch_packages(self, devices: List[Device], processes: int = 1, max_slots: int = 0):
@@ -333,16 +341,6 @@ def make_api(conf: AppSettings) -> Gnetcli:
333
341
  )
334
342
  return api
335
343
 
336
- def format_trace(trace: list[pb.CMDTraceItem]) -> str:
337
- res:list[str] = []
338
- for t in trace:
339
- op = "unknown" # TODO: get from pb
340
- if t.operation == 2:
341
- op = "write"
342
- elif t.operation == 3:
343
- op = "read"
344
- res.append(f"{op}={t.data}")
345
- return "\n".join(res)
346
344
 
347
345
  class GnetcliDeployer(DeployDriver, AdapterWithConfig, AdapterWithName):
348
346
  def __init__(
@@ -355,6 +353,7 @@ class GnetcliDeployer(DeployDriver, AdapterWithConfig, AdapterWithName):
355
353
  ssh_agent_enabled: bool = True,
356
354
  server_path: Optional[str] = None,
357
355
  server_conf: Optional[str] = DEFAULT_GNETCLI_SERVER_CONF,
356
+ logs_dir: Optional[str] = None,
358
357
  ):
359
358
  conf_args = {
360
359
  "login": login,
@@ -368,14 +367,15 @@ class GnetcliDeployer(DeployDriver, AdapterWithConfig, AdapterWithName):
368
367
  }
369
368
  self.conf = AppSettings(**{k: v for k,v in conf_args.items() if v is not None})
370
369
  self.api = make_api(self.conf)
370
+ self.logs_dir = logs_dir
371
371
 
372
372
  @classmethod
373
373
  def name(cls) -> str:
374
374
  return "gnetcli"
375
375
 
376
376
  @classmethod
377
- def with_config(cls, **kwargs: Dict[str, Any]) -> DeployDriver:
378
- return GnetcliDeployer(**kwargs)
377
+ def with_config(cls, **kwargs: Any) -> DeployDriver:
378
+ return cls(**kwargs)
379
379
 
380
380
  async def bulk_deploy(self, deploy_cmds: dict[Device, CommandList], args: DeployOptions, progress_bar: ProgressBar | None = None) -> DeployResult:
381
381
  if progress_bar:
@@ -387,7 +387,45 @@ class GnetcliDeployer(DeployDriver, AdapterWithConfig, AdapterWithName):
387
387
  res.add_results(results={dev.fqdn: dev_res for (dev, _), dev_res in zip(deploy_items, result)})
388
388
  return res
389
389
 
390
- async def deploy(self, device: Device, cmds: CommandList, args: DeployOptions, progress_bar: ProgressBar | None = None) -> tuple[list[Exception], list[pb.CMDResult]]:
390
+ def _get_files(self, cmds: dict[str, Any]) -> dict[str, File]:
391
+ return {
392
+ file: File(content=content, status=None)
393
+ for file, content in cmds["files"].items()
394
+ }
395
+
396
+ def _get_reload_cmds(self, cmds: dict[str, Any]):
397
+ reload_cmds: dict[str, CommandList] = {}
398
+ for file, cmd in cmds["cmds"].items():
399
+ if isinstance(cmd, bytes):
400
+ cmd = cmd.decode()
401
+ reload_cmds[file] = CommandList([Command(cmd, suppress_nonzero=True) for cmd in cmd.splitlines()])
402
+ return reload_cmds
403
+
404
+ def _get_total(
405
+ self, command_groups: list[tuple[str, CommandList]], files: dict[str, File],
406
+ ) -> int:
407
+ run_cmds = 0
408
+ for _, cmds in command_groups:
409
+ run_cmds += len(cmds)
410
+ if files:
411
+ run_cmds += 1
412
+ return run_cmds
413
+
414
+ def _init_progress_tracker(self, device: Device, progress_bar: ProgressBar | None) -> ProgressTracker:
415
+ tracker = CompositeTracker(LogProgressTracker(device))
416
+ if progress_bar:
417
+ tracker.add_tracker(ProgressBarTracker(device, progress_bar))
418
+ if self.logs_dir:
419
+ tracker.add_tracker(FileProgressTracker(device, self.logs_dir))
420
+ return tracker
421
+
422
+ async def deploy(
423
+ self,
424
+ device: Device,
425
+ cmds: CommandList,
426
+ args: DeployOptions,
427
+ progress_bar: ProgressBar | None = None,
428
+ ) -> tuple[list[Exception], list[pb.CMDResult]]:
391
429
  gnetcli_device = breed_to_device.get(device.breed, device.breed)
392
430
  ip = get_device_ip(device)
393
431
  host_params = HostParams(
@@ -395,85 +433,95 @@ class GnetcliDeployer(DeployDriver, AdapterWithConfig, AdapterWithName):
395
433
  device=gnetcli_device,
396
434
  ip=ip,
397
435
  )
398
- do_reload: bool = args.entire_reload.value == "yes"
399
- seen_exc: list[Exception] = []
400
- reload_cmds: dict[str, CommandList] = {}
401
- total_cmds = 0
436
+ command_groups: list[tuple[str, CommandList]]= []
402
437
  if isinstance(cmds, dict): # PC
403
- for file, cmd in cmds["cmds"].items():
404
- if isinstance(cmd, bytes):
405
- cmd = cmd.decode()
406
- reload_cmds[file] = CommandList([Command(cmd, suppress_nonzero=True) for cmd in cmd.splitlines()])
407
- total_cmds += len(reload_cmds[file])
408
- run_cmds = CommandList()
409
- files: Dict[str, File] = {file: File(content=content, status=None) for file, content in cmds["files"].items()}
410
- await self.api.upload(hostname=device.fqdn, files=files, host_params=host_params)
438
+ files = self._get_files(cmds)
439
+ if args.entire_reload.value == "yes":
440
+ for file, cmdlist in self._get_reload_cmds(cmds).items():
441
+ command_groups.append((f"Reload {file}", cmdlist))
411
442
  else:
412
- total_cmds = len(cmds)
413
- run_cmds = cmds
414
-
415
- done_cmds = 0
416
- async with self.api.cmd_session(hostname=device.fqdn) as sess:
417
- result: List[pb.CMDResult] = []
418
- for cmd in run_cmds:
419
- if progress_bar:
420
- progress_bar.set_progress(device.fqdn, done_cmds, total_cmds, suffix=cmd.cmd)
421
- try:
422
- res = await sess.cmd(
423
- cmd=cmd.cmd,
424
- cmd_timeout=cmd.timeout,
425
- host_params=host_params,
426
- qa=parse_annet_qa(cmd.questions or []),
427
- trace=True,
428
- )
429
- except EOFError as e:
430
- if cmd.suppress_eof:
431
- if progress_bar:
432
- progress_bar.set_progress(device.fqdn, total_cmds, total_cmds, suffix=f"suppressed EOF: {cmd.cmd}")
433
- break # we can't exec subsequent cmds
434
- raise e
435
- if progress_bar:
436
- tr = format_trace(res.trace)
437
- progress_bar.set_content(device.fqdn, f"cmd={cmd.cmd} out={res.out_str} status={res.status}\n{tr}")
438
- done_cmds += 1
439
- if res.status != 0:
440
- if cmd.suppress_nonzero:
441
- continue
442
- if progress_bar:
443
- progress_bar.set_exception(device.fqdn, cmd.cmd, str(res.error), total_cmds)
444
- raise Exception("cmd %s error %s status %s", cmd, res.error, res.status)
445
- result.append(res)
446
- if progress_bar:
447
- progress_bar.set_progress(device.fqdn, done_cmds, total_cmds)
448
- if do_reload:
449
- for file, cmds in reload_cmds.items():
450
- _logger.debug("reload %s %s", file, cmds)
451
- for cmd in cmds:
452
- if progress_bar:
453
- progress_bar.set_progress(device.fqdn, done_cmds, total_cmds, suffix=f"{file}:{cmd.cmd}")
454
- res = await sess.cmd(
443
+ files = {}
444
+ # treat each command as a group
445
+ for cmd in cmds:
446
+ command_groups.append(("Run command", CommandList([cmd])))
447
+
448
+ with self._init_progress_tracker(device, progress_bar) as tracker:
449
+ tracker.set_total(self._get_total(command_groups, files))
450
+ try:
451
+ seen_exc, results = await self._deploy(
452
+ device=device,
453
+ host_params=host_params,
454
+ command_groups=command_groups,
455
+ tracker=tracker,
456
+ files=files,
457
+ )
458
+ except Exception as e:
459
+ trace = traceback.format_exc()
460
+ tracker.command_done_error(trace)
461
+ seen_exc = [e]
462
+ results = []
463
+ if seen_exc:
464
+ tracker.finish_err(f"Seen {len(seen_exc)} exceptions")
465
+ else:
466
+ tracker.finish_ok("All done")
467
+ return seen_exc, results
468
+
469
+ async def _deploy(
470
+ self,
471
+ device: Device,
472
+ host_params: HostParams,
473
+ command_groups: list[tuple[str, CommandList]],
474
+ files: dict[str, File],
475
+ tracker: ProgressTracker,
476
+ ) -> tuple[list[Exception], list[pb.CMDResult]]:
477
+ seen_exc = []
478
+ results = []
479
+ if files:
480
+ tracker.upload_files(list(files))
481
+ await self.api.upload(hostname=device.fqdn, files=files, host_params=host_params)
482
+ tracker.files_uploaded()
483
+
484
+ old_group_name = ""
485
+ async with self.api.cmd_session(hostname=device.fqdn) as session:
486
+ for group_number, (group_name, cmdlist) in enumerate(command_groups):
487
+ if group_name != old_group_name:
488
+ tracker.start_group(group_name)
489
+ old_group_name = group_name
490
+ for cmd_number, cmd in enumerate(cmdlist):
491
+ tracker.run_command(cmd.cmd)
492
+ try:
493
+ res = await session.cmd(
455
494
  cmd=cmd.cmd,
456
495
  cmd_timeout=cmd.timeout,
457
496
  host_params=host_params,
458
497
  qa=parse_annet_qa(cmd.questions or []),
498
+ trace=True,
459
499
  )
460
- done_cmds += 1
461
- if progress_bar:
462
- progress_bar.set_content(device.fqdn, f"cmd={cmd.cmd} out={res.out_str} status={res.status}")
463
- if res.status != 0 and cmd.suppress_nonzero:
464
- if progress_bar:
465
- progress_bar.set_exception(device.fqdn, cmd.cmd, str(res.error), total_cmds)
466
- if cmd.suppress_nonzero:
467
- done_cmds += 1
468
- seen_exc.append(Exception("cmd %s error %s status %s", cmd, res.error, res.status))
469
- break # break on command for current file
470
- raise Exception("cmd %s error %s status %s", cmd, res.error, res.status)
471
- result.append(res)
472
- if reload_cmds and progress_bar:
473
- progress_bar.set_progress(device.fqdn, total_cmds, total_cmds)
474
- if seen_exc and progress_bar:
475
- progress_bar.set_exception(device.fqdn, "seen exception", str(seen_exc), total_cmds)
476
- return seen_exc, result
500
+ except EOFError as e:
501
+ # we can't execute subsequent commands
502
+ if cmd.suppress_eof:
503
+ # some commands left
504
+ if group_number + 1 != len(command_groups) or cmd_number + 1 != len(cmd):
505
+ tracker.command_done_error("EOF detected before all commands executed.")
506
+ seen_exc.append(Exception("EOF detected before all commands executed."))
507
+ tracker.command_done_error("Suppressed EOF")
508
+ return seen_exc, results
509
+ seen_exc.append(e)
510
+ tracker.command_done_error("Unexpected EOFError")
511
+ return seen_exc, results
512
+ if res.status == 0:
513
+ tracker.command_done_ok(res)
514
+ results.append(res)
515
+ else:
516
+ e = Exception("cmd %s error %s status %s", cmd, res.error, res.status)
517
+ seen_exc.append(e)
518
+ if cmd.suppress_nonzero:
519
+ tracker.command_done_ok(res)
520
+ break # go to next command group
521
+ else:
522
+ tracker.command_done_error(res.error.decode())
523
+ return seen_exc, results
524
+ return seen_exc, results
477
525
 
478
526
  def apply_deploy_rulebook(
479
527
  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