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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gnetcli_adapter
3
- Version: 2.0.4
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>=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
@@ -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: Dict[str, Any]) -> Fetcher:
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="pc",
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: Dict[str, Any]) -> DeployDriver:
376
- return GnetcliDeployer(**kwargs)
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
- async def deploy(self, device: Device, cmds: CommandList, args: DeployOptions, progress_bar: ProgressBar | None = None) -> tuple[list[Exception], list[pb.CMDResult]]:
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
- do_reload: bool = args.entire_reload.value == "yes"
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
- for file, cmd in cmds["cmds"].items():
402
- if isinstance(cmd, bytes):
403
- cmd = cmd.decode()
404
- reload_cmds[file] = CommandList([Command(cmd, suppress_nonzero=True) for cmd in cmd.splitlines()])
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
- total_cmds = len(cmds)
411
- run_cmds = cmds
412
-
413
- done_cmds = 0
414
- async with self.api.cmd_session(hostname=device.fqdn) as sess:
415
- result: List[pb.CMDResult] = []
416
- for cmd in run_cmds:
417
- if progress_bar:
418
- progress_bar.set_progress(device.fqdn, done_cmds, total_cmds, suffix=cmd.cmd)
419
- try:
420
- res = await sess.cmd(
421
- cmd=cmd.cmd,
422
- cmd_timeout=cmd.timeout,
423
- host_params=host_params,
424
- qa=parse_annet_qa(cmd.questions or []),
425
- trace=True,
426
- )
427
- except EOFError as e:
428
- if cmd.suppress_eof:
429
- if progress_bar:
430
- progress_bar.set_progress(device.fqdn, total_cmds, total_cmds, suffix=f"suppressed EOF: {cmd.cmd}")
431
- break # we can't exec subsequent cmds
432
- raise e
433
- if progress_bar:
434
- tr = format_trace(res.trace)
435
- progress_bar.set_content(device.fqdn, f"cmd={cmd.cmd} out={res.out_str} status={res.status}\n{tr}")
436
- done_cmds += 1
437
- if res.status != 0:
438
- if cmd.suppress_nonzero:
439
- continue
440
- if progress_bar:
441
- progress_bar.set_exception(device.fqdn, cmd.cmd, str(res.error), total_cmds)
442
- raise Exception("cmd %s error %s status %s", cmd, res.error, res.status)
443
- result.append(res)
444
- if progress_bar:
445
- progress_bar.set_progress(device.fqdn, done_cmds, total_cmds)
446
- if do_reload:
447
- for file, cmds in reload_cmds.items():
448
- _logger.debug("reload %s %s", file, cmds)
449
- for cmd in cmds:
450
- if progress_bar:
451
- progress_bar.set_progress(device.fqdn, done_cmds, total_cmds, suffix=f"{file}:{cmd.cmd}")
452
- res = await sess.cmd(
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
- done_cmds += 1
459
- if progress_bar:
460
- progress_bar.set_content(device.fqdn, f"cmd={cmd.cmd} out={res.out_str} status={res.status}")
461
- if res.status != 0 and cmd.suppress_nonzero:
462
- if progress_bar:
463
- progress_bar.set_exception(device.fqdn, cmd.cmd, str(res.error), total_cmds)
464
- if cmd.suppress_nonzero:
465
- done_cmds += 1
466
- seen_exc.append(Exception("cmd %s error %s status %s", cmd, res.error, res.status))
467
- break # break on command for current file
468
- raise Exception("cmd %s error %s status %s", cmd, res.error, res.status)
469
- result.append(res)
470
- if reload_cmds and progress_bar:
471
- progress_bar.set_progress(device.fqdn, total_cmds, total_cmds)
472
- if seen_exc and progress_bar:
473
- progress_bar.set_exception(device.fqdn, "seen exception", str(seen_exc), total_cmds)
474
- return seen_exc, result
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