annet 0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of annet might be problematic. Click here for more details.

Files changed (137) hide show
  1. annet/__init__.py +61 -0
  2. annet/adapters/__init__.py +0 -0
  3. annet/adapters/netbox/__init__.py +0 -0
  4. annet/adapters/netbox/common/__init__.py +0 -0
  5. annet/adapters/netbox/common/client.py +87 -0
  6. annet/adapters/netbox/common/manufacturer.py +62 -0
  7. annet/adapters/netbox/common/models.py +105 -0
  8. annet/adapters/netbox/common/query.py +23 -0
  9. annet/adapters/netbox/common/status_client.py +25 -0
  10. annet/adapters/netbox/common/storage_opts.py +14 -0
  11. annet/adapters/netbox/provider.py +34 -0
  12. annet/adapters/netbox/v24/__init__.py +0 -0
  13. annet/adapters/netbox/v24/api_models.py +73 -0
  14. annet/adapters/netbox/v24/client.py +59 -0
  15. annet/adapters/netbox/v24/storage.py +196 -0
  16. annet/adapters/netbox/v37/__init__.py +0 -0
  17. annet/adapters/netbox/v37/api_models.py +38 -0
  18. annet/adapters/netbox/v37/client.py +62 -0
  19. annet/adapters/netbox/v37/storage.py +149 -0
  20. annet/annet.py +25 -0
  21. annet/annlib/__init__.py +7 -0
  22. annet/annlib/command.py +49 -0
  23. annet/annlib/diff.py +158 -0
  24. annet/annlib/errors.py +8 -0
  25. annet/annlib/filter_acl.py +196 -0
  26. annet/annlib/jsontools.py +116 -0
  27. annet/annlib/lib.py +495 -0
  28. annet/annlib/netdev/__init__.py +0 -0
  29. annet/annlib/netdev/db.py +62 -0
  30. annet/annlib/netdev/devdb/__init__.py +28 -0
  31. annet/annlib/netdev/devdb/data/devdb.json +137 -0
  32. annet/annlib/netdev/views/__init__.py +0 -0
  33. annet/annlib/netdev/views/dump.py +121 -0
  34. annet/annlib/netdev/views/hardware.py +112 -0
  35. annet/annlib/output.py +246 -0
  36. annet/annlib/patching.py +533 -0
  37. annet/annlib/rbparser/__init__.py +0 -0
  38. annet/annlib/rbparser/acl.py +120 -0
  39. annet/annlib/rbparser/deploying.py +55 -0
  40. annet/annlib/rbparser/ordering.py +52 -0
  41. annet/annlib/rbparser/platform.py +51 -0
  42. annet/annlib/rbparser/syntax.py +115 -0
  43. annet/annlib/rulebook/__init__.py +0 -0
  44. annet/annlib/rulebook/common.py +350 -0
  45. annet/annlib/tabparser.py +648 -0
  46. annet/annlib/types.py +35 -0
  47. annet/api/__init__.py +826 -0
  48. annet/argparse.py +415 -0
  49. annet/cli.py +237 -0
  50. annet/cli_args.py +503 -0
  51. annet/configs/context.yml +18 -0
  52. annet/configs/logging.yaml +39 -0
  53. annet/connectors.py +77 -0
  54. annet/deploy.py +536 -0
  55. annet/diff.py +84 -0
  56. annet/executor.py +551 -0
  57. annet/filtering.py +40 -0
  58. annet/gen.py +865 -0
  59. annet/generators/__init__.py +435 -0
  60. annet/generators/base.py +136 -0
  61. annet/generators/common/__init__.py +0 -0
  62. annet/generators/common/initial.py +33 -0
  63. annet/generators/entire.py +97 -0
  64. annet/generators/exceptions.py +10 -0
  65. annet/generators/jsonfragment.py +125 -0
  66. annet/generators/partial.py +119 -0
  67. annet/generators/perf.py +79 -0
  68. annet/generators/ref.py +15 -0
  69. annet/generators/result.py +127 -0
  70. annet/hardware.py +45 -0
  71. annet/implicit.py +139 -0
  72. annet/lib.py +128 -0
  73. annet/output.py +167 -0
  74. annet/parallel.py +448 -0
  75. annet/patching.py +25 -0
  76. annet/reference.py +148 -0
  77. annet/rulebook/__init__.py +114 -0
  78. annet/rulebook/arista/__init__.py +0 -0
  79. annet/rulebook/arista/iface.py +16 -0
  80. annet/rulebook/aruba/__init__.py +16 -0
  81. annet/rulebook/aruba/ap_env.py +146 -0
  82. annet/rulebook/aruba/misc.py +8 -0
  83. annet/rulebook/cisco/__init__.py +0 -0
  84. annet/rulebook/cisco/iface.py +68 -0
  85. annet/rulebook/cisco/misc.py +57 -0
  86. annet/rulebook/cisco/vlandb.py +90 -0
  87. annet/rulebook/common.py +19 -0
  88. annet/rulebook/deploying.py +87 -0
  89. annet/rulebook/huawei/__init__.py +0 -0
  90. annet/rulebook/huawei/aaa.py +75 -0
  91. annet/rulebook/huawei/bgp.py +97 -0
  92. annet/rulebook/huawei/iface.py +33 -0
  93. annet/rulebook/huawei/misc.py +337 -0
  94. annet/rulebook/huawei/vlandb.py +115 -0
  95. annet/rulebook/juniper/__init__.py +107 -0
  96. annet/rulebook/nexus/__init__.py +0 -0
  97. annet/rulebook/nexus/iface.py +92 -0
  98. annet/rulebook/patching.py +143 -0
  99. annet/rulebook/ribbon/__init__.py +12 -0
  100. annet/rulebook/texts/arista.deploy +20 -0
  101. annet/rulebook/texts/arista.order +125 -0
  102. annet/rulebook/texts/arista.rul +59 -0
  103. annet/rulebook/texts/aruba.deploy +20 -0
  104. annet/rulebook/texts/aruba.order +83 -0
  105. annet/rulebook/texts/aruba.rul +87 -0
  106. annet/rulebook/texts/cisco.deploy +27 -0
  107. annet/rulebook/texts/cisco.order +82 -0
  108. annet/rulebook/texts/cisco.rul +105 -0
  109. annet/rulebook/texts/huawei.deploy +188 -0
  110. annet/rulebook/texts/huawei.order +388 -0
  111. annet/rulebook/texts/huawei.rul +471 -0
  112. annet/rulebook/texts/juniper.rul +120 -0
  113. annet/rulebook/texts/nexus.deploy +24 -0
  114. annet/rulebook/texts/nexus.order +85 -0
  115. annet/rulebook/texts/nexus.rul +83 -0
  116. annet/rulebook/texts/nokia.rul +31 -0
  117. annet/rulebook/texts/pc.order +5 -0
  118. annet/rulebook/texts/pc.rul +9 -0
  119. annet/rulebook/texts/ribbon.deploy +22 -0
  120. annet/rulebook/texts/ribbon.rul +77 -0
  121. annet/rulebook/texts/routeros.order +38 -0
  122. annet/rulebook/texts/routeros.rul +45 -0
  123. annet/storage.py +125 -0
  124. annet/tabparser.py +36 -0
  125. annet/text_term_format.py +95 -0
  126. annet/tracing.py +170 -0
  127. annet/types.py +227 -0
  128. annet-0.0.dist-info/AUTHORS +21 -0
  129. annet-0.0.dist-info/LICENSE +21 -0
  130. annet-0.0.dist-info/METADATA +26 -0
  131. annet-0.0.dist-info/RECORD +137 -0
  132. annet-0.0.dist-info/WHEEL +5 -0
  133. annet-0.0.dist-info/entry_points.txt +5 -0
  134. annet-0.0.dist-info/top_level.txt +2 -0
  135. annet_generators/__init__.py +0 -0
  136. annet_generators/example/__init__.py +12 -0
  137. annet_generators/example/lldp.py +53 -0
annet/argparse.py ADDED
@@ -0,0 +1,415 @@
1
+ import abc
2
+ import argparse
3
+ import contextlib
4
+ import functools
5
+ import os
6
+ import sys
7
+ from functools import lru_cache
8
+ from typing import (
9
+ Callable,
10
+ ContextManager,
11
+ Dict,
12
+ Iterable,
13
+ Iterator,
14
+ List,
15
+ Optional,
16
+ Type,
17
+ TypeVar,
18
+ Union,
19
+ )
20
+
21
+ from annet.connectors import Connector
22
+
23
+
24
+ T = TypeVar("T")
25
+
26
+ _ARGS_ATTR_NAME = "_alan_opts"
27
+
28
+
29
+ # =====
30
+ class FuncMeta:
31
+ opts: List[Callable]
32
+ parent: Optional[Callable]
33
+ parser: Optional[argparse.ArgumentParser]
34
+
35
+ def __init__(self):
36
+ self.cmd_name = ""
37
+ self.opts = []
38
+ self.parent = None
39
+ self.parser = None
40
+ self.sub = None
41
+
42
+ def __repr__(self):
43
+ return f"{self.__class__.__name__}(" + ", ".join((
44
+ f"cmd_name={self.cmd_name}",
45
+ f"opt={self.opts}",
46
+ f"parent={self.parent}",
47
+ f"parser={self.parser}",
48
+ f"sub={self.sub}",
49
+ )) + ")"
50
+
51
+
52
+ def _get_meta(func: Callable) -> FuncMeta:
53
+ if not hasattr(func, _ARGS_ATTR_NAME):
54
+ _reset_meta(func)
55
+ return getattr(func, _ARGS_ATTR_NAME)
56
+
57
+
58
+ def _reset_meta(func: Callable):
59
+ setattr(func, _ARGS_ATTR_NAME, FuncMeta())
60
+
61
+
62
+ class ConvertibleDefault:
63
+ def __init__(self, value: str):
64
+ self.value = value
65
+
66
+ def convert(self, type_function: Callable[[str], T]) -> T:
67
+ return type_function(self.value)
68
+
69
+
70
+ class DefaultFromEnv(ConvertibleDefault):
71
+ def __init__(self, var_name: str, default_val: str):
72
+ super().__init__(os.environ.get(var_name, default_val))
73
+ self.var_name = var_name
74
+
75
+ def convert(self, type_function: Callable[[str], T]) -> T:
76
+ try:
77
+ return super().convert(type_function)
78
+ except Exception:
79
+ function_name = getattr(type_function, "__name__", repr(type_function))
80
+ raise ValueError(f"invalid {function_name} in the {self.var_name} environment variable: {repr(self.value)}")
81
+
82
+
83
+ class Arg:
84
+ """аргумент CLI"""
85
+
86
+ def __init__(self, *args, **kwargs):
87
+ """Конструктор повторяет прототип parser.add_argument()"""
88
+ if args and isinstance(args[0], type(self)):
89
+ # copy constructor
90
+ args = args[0].args
91
+ new_kwargs = args[0].kwargs.copy()
92
+ new_kwargs.update(kwargs)
93
+ kwargs = new_kwargs
94
+
95
+ self.args = args
96
+ self.kwargs = kwargs
97
+
98
+ self._prepared = False
99
+ self.default = None
100
+
101
+ self.dest = None # заполняется в attach()
102
+
103
+ def _prepare(self):
104
+ if self._prepared:
105
+ return
106
+ self._prepared = True
107
+ default = self.kwargs.get("default", None)
108
+ if isinstance(default, ConvertibleDefault) and "type" in self.kwargs:
109
+ default = self.kwargs["default"] = default.convert(self.kwargs["type"])
110
+ elif default is False and "action" not in self.kwargs:
111
+ self.kwargs["action"] = "store_true"
112
+ self.default = default
113
+
114
+ if default is not None and not (default is False and self.kwargs.get("action") == "store_true"):
115
+ if "help" not in self.kwargs:
116
+ self.kwargs["help"] = ""
117
+ self.kwargs["help"] += " (default: %r)" % default
118
+
119
+ def attach(self, parser: argparse.ArgumentParser):
120
+ self._prepare()
121
+ arg = parser.add_argument(*self.args, **self.kwargs)
122
+ self.dest = arg.dest
123
+
124
+
125
+ class ArgGroup:
126
+ """
127
+ Контейнер для нескольких аргументов.
128
+ Класс служит для описания набора аргументов, а экземляр - для хранения значений.
129
+
130
+ Пример:
131
+ class Group1(ArgGroup):
132
+ in = Arg("--in")
133
+ out = Arg("--out")
134
+
135
+ values = Group1(in="/dev/null", out="/dev/null")
136
+ open(values.in)
137
+ """
138
+ def __init__(self, *args, **kwargs):
139
+ """
140
+ В kwargs - пары ключ-значение. Соотвествующие опции должны быть объявлены в классе
141
+ """
142
+ keys = {arg_name: arg.default for arg_name, arg in self._enum_args().items()}
143
+ for src_obj in args:
144
+ for opt, val in vars(src_obj).items():
145
+ if opt in keys:
146
+ keys[opt] = val
147
+ for opt, val in kwargs.items():
148
+ if opt in keys:
149
+ keys[opt] = val
150
+ else:
151
+ raise TypeError("Unknown argument %r for %s" % (opt, type(self).__qualname__))
152
+ self.__dict__.update(keys)
153
+
154
+ @classmethod
155
+ @lru_cache()
156
+ def _enum_args(cls) -> Dict[str, Arg]:
157
+ ret = {}
158
+ for base in cls.__mro__:
159
+ for name, value in vars(base).items():
160
+ if not name.startswith("_") and isinstance(value, Arg):
161
+ ret[name] = value
162
+ return ret
163
+
164
+ @classmethod
165
+ def attach(cls, parser: argparse.ArgumentParser):
166
+ for arg in cls._enum_args().values():
167
+ arg.attach(parser)
168
+
169
+ @classmethod
170
+ def construct_from(cls, ns: argparse.Namespace) -> "ArgGroup":
171
+ kwargs = {}
172
+ for arg_name, arg in cls._enum_args().items():
173
+ kwargs[arg_name] = getattr(ns, arg.dest)
174
+ return cls(**kwargs)
175
+
176
+ @classmethod
177
+ def copy_from(cls, other: "ArgGroup", **more_kwargs) -> "ArgGroup":
178
+ kwargs = {}
179
+ for arg_name in cls._enum_args():
180
+ try:
181
+ kwargs[arg_name] = getattr(other, arg_name)
182
+ except AttributeError:
183
+ pass
184
+
185
+ kwargs.update(more_kwargs)
186
+ return cls(**kwargs)
187
+
188
+ def __repr__(self):
189
+ return "%(classname)s(%(params)s)" % dict(
190
+ classname=type(self).__qualname__,
191
+ params=", ".join(
192
+ "%s=%r" % (key, getattr(self, key))
193
+ for key in sorted(self._enum_args())
194
+ )
195
+ )
196
+
197
+ def stdin(self, **kwargs):
198
+ ret = {}
199
+ stdin_used = False
200
+ for arg, val in kwargs.items():
201
+ if val == "-":
202
+ if stdin_used:
203
+ raise ValueError("stdin can not be used twice")
204
+ self.validate_stdin(arg, val, **kwargs.copy())
205
+ ret[arg] = _read_stdin_once()
206
+ stdin_used = True
207
+ else:
208
+ ret[arg] = None
209
+ return ret
210
+
211
+ def validate_stdin(self, arg, val, **kwargs):
212
+ pass
213
+
214
+
215
+ class _HelpFormatter(argparse.HelpFormatter):
216
+ def __init__(self, *args, **kwargs):
217
+ kwargs["width"] = self._get_term_width()
218
+ super().__init__(*args, **kwargs)
219
+
220
+ @staticmethod
221
+ def _get_term_width():
222
+ try:
223
+ return int(os.environ["COLUMNS"])
224
+ except (KeyError, ValueError):
225
+ try:
226
+ return os.get_terminal_size(sys.stdout.fileno()).columns
227
+ except OSError:
228
+ return None
229
+
230
+
231
+ class ArgDispatcher(abc.ABC):
232
+ @abc.abstractmethod
233
+ def setup(self) -> ContextManager[None]:
234
+ pass
235
+
236
+ @abc.abstractmethod
237
+ def exec(self) -> ContextManager[None]:
238
+ pass
239
+
240
+ @abc.abstractmethod
241
+ def pre_call(self) -> ContextManager[None]:
242
+ pass
243
+
244
+ @contextlib.contextmanager
245
+ def func_opts(self, arg: Union[Arg, Type[ArgGroup]]) -> ContextManager[None]:
246
+ pass
247
+
248
+ @abc.abstractmethod
249
+ def func(self, ns: argparse.Namespace) -> ContextManager[None]:
250
+ pass
251
+
252
+
253
+ class NullArgDispatcher(ArgDispatcher):
254
+ @contextlib.contextmanager
255
+ def setup(self) -> Iterator[None]:
256
+ yield
257
+
258
+ @contextlib.contextmanager
259
+ def exec(self) -> ContextManager[None]:
260
+ yield
261
+
262
+ @contextlib.contextmanager
263
+ def pre_call(self) -> ContextManager[None]:
264
+ yield
265
+
266
+ @contextlib.contextmanager
267
+ def func_opts(self, arg: Union[Arg, Type[ArgGroup]]) -> ContextManager[None]:
268
+ yield
269
+
270
+ @contextlib.contextmanager
271
+ def func(self, ns: argparse.Namespace) -> ContextManager[None]:
272
+ yield
273
+
274
+
275
+ class _DispatcherConnector(Connector[ArgDispatcher]):
276
+ name = "Dispatcher"
277
+ ep_name = "dispatcher"
278
+
279
+ def _get_default(self) -> Type["ArgDispatcher"]:
280
+ return NullArgDispatcher
281
+
282
+
283
+ dispatcher_connector = _DispatcherConnector()
284
+
285
+
286
+ class ArgParser(argparse.ArgumentParser):
287
+ def __init__(self, *args, **kwargs):
288
+ if "formatter_class" not in kwargs:
289
+ kwargs["formatter_class"] = _HelpFormatter
290
+ super().__init__(*args, **kwargs)
291
+ self._dispatcher: ArgDispatcher = dispatcher_connector.get()
292
+
293
+ def argv(self):
294
+ return sys.argv[1:]
295
+
296
+ def add_commands(self, commands: Iterable[Callable]):
297
+ subparsers = self.add_subparsers()
298
+ commands = list(commands)
299
+ failcount = 0
300
+ while commands and failcount < len(commands):
301
+ func, commands = commands[0], commands[1:]
302
+ if not self.add_func(func, subparsers):
303
+ commands.append(func)
304
+ failcount += 1
305
+ else:
306
+ failcount = 0
307
+ if failcount:
308
+ raise RuntimeError("Failed to resolve subparsers")
309
+
310
+ def add_func(self, func, sub):
311
+ meta = _get_meta(func)
312
+ parent = meta.parent
313
+ if parent:
314
+ parent_meta = _get_meta(parent)
315
+ if not parent_meta.parser:
316
+ return False
317
+ if not parent_meta.sub:
318
+ parent_meta.sub = parent_meta.parser.add_subparsers()
319
+ sub = parent_meta.sub
320
+
321
+ sp = sub.add_parser(meta.cmd_name, help=func.__doc__)
322
+ sp.set_defaults(func=func)
323
+ meta.parser = sp
324
+
325
+ for arg in self.func_opts(func):
326
+ arg.attach(sp)
327
+ return True
328
+
329
+ def set_dispatcher(self, dispatcher: ArgDispatcher) -> None:
330
+ self._dispatcher = dispatcher
331
+
332
+ def dispatch(self, pre_call=None, add_help_command=False):
333
+ with self._dispatcher.setup():
334
+ with self._dispatcher.exec():
335
+ argv = self.argv()
336
+ if add_help_command and argv and argv[0] == "help":
337
+ argv.pop(0)
338
+ argv.append("--help")
339
+
340
+ # show usage in cache when no options specified
341
+ if not argv:
342
+ argv = [""]
343
+
344
+ ns = self.parse_args(argv)
345
+ assert ns.func
346
+ if pre_call:
347
+ with self._dispatcher.pre_call():
348
+ pre_call(ns)
349
+
350
+ values = []
351
+ for arg in self.func_opts(ns.func):
352
+ with self._dispatcher.func_opts(arg):
353
+ if isinstance(arg, Arg):
354
+ values.append(getattr(ns, arg.dest))
355
+ else:
356
+ # arg is a ArgGroup-derived type
357
+ values.append(arg.construct_from(ns))
358
+
359
+ with self._dispatcher.func(ns):
360
+ return ns.func(*values)
361
+
362
+ @staticmethod
363
+ def find_subcommands(variables):
364
+ for var in variables.values():
365
+ if callable(var) and hasattr(var, _ARGS_ATTR_NAME):
366
+ yield var
367
+
368
+ @classmethod
369
+ def func_opts(cls, func):
370
+ yield from _get_meta(func).opts
371
+
372
+
373
+ def subcommand(*arg_list: Union[Arg, Type[ArgGroup]], parent: Callable = None):
374
+ """
375
+ декоратор, задающий cli-аргументы подпрограммы
376
+
377
+ Пример:
378
+ @subcommand(Arg(), ArgGroup)
379
+ def cmd1(arg1, arg2):
380
+ pass
381
+
382
+ Связь аргументов происходит только по порядковому номеру, каждый аргумент subcommand становится аргументом функции.
383
+ Функция вызывается только с позиционными аргументами, всегда с одним и тем же количеством аргументов.
384
+
385
+ Для создания более одного уровня команд используeтся агрумент parent
386
+
387
+ Пример: 'ann some thing' - вызовет some_thing()
388
+
389
+ @subcommand()
390
+ def some():
391
+ pass
392
+
393
+
394
+ @subcommand(parent=some)
395
+ def some_thing():
396
+ pass
397
+ """
398
+ def _amend_func(func):
399
+ meta = _get_meta(func)
400
+ cmd_name = func.__name__
401
+ if parent:
402
+ parentprefix = parent.__name__ + "_"
403
+ if cmd_name.startswith(parentprefix):
404
+ cmd_name = cmd_name[len(parentprefix):]
405
+ meta.parent = parent
406
+
407
+ meta.cmd_name = cmd_name.replace("_", "-").lower()
408
+ meta.opts.extend(arg_list)
409
+ return func
410
+ return _amend_func
411
+
412
+
413
+ @functools.lru_cache()
414
+ def _read_stdin_once():
415
+ return sys.stdin.read()
annet/cli.py ADDED
@@ -0,0 +1,237 @@
1
+ import argparse
2
+ import operator
3
+ import os
4
+ import platform
5
+ import subprocess
6
+ import shutil
7
+ from contextlib import ExitStack, contextmanager
8
+ from typing import Tuple, Iterable
9
+
10
+ import yaml
11
+ from contextlog import get_logger
12
+ from valkit.python import valid_logging_level
13
+
14
+ from annet.deploy import driver_connector, fetcher_connector
15
+ from annet import api, cli_args, filtering
16
+ from annet.api import collapse_texts, Deployer
17
+ from annet.argparse import ArgParser, subcommand
18
+ from annet.diff import gen_sort_diff
19
+ from annet.gen import Loader, old_raw
20
+ from annet.lib import get_context_path, repair_context_file
21
+ from annet.output import output_driver_connector, OutputDriver
22
+ from annet.storage import storage_connector
23
+
24
+
25
+ def fill_base_args(parser: ArgParser, pkg_name: str, logging_config: str):
26
+ parser.add_argument("--log-level", default="WARN", type=valid_logging_level,
27
+ help="Уровень детализации логов (DEBUG, DEBUG2 (with comocutor debug), INFO, WARN, CRITICAL)")
28
+ parser.add_argument("--pkg_name", default=pkg_name, help=argparse.SUPPRESS)
29
+ parser.add_argument("--logging_config", default=logging_config, help=argparse.SUPPRESS)
30
+
31
+
32
+ def list_subcommands():
33
+ return globals().copy()
34
+
35
+
36
+ def _gen_current_items(
37
+ config,
38
+ stdin,
39
+ loader: Loader,
40
+ output_driver: OutputDriver,
41
+ gen_args: cli_args.GenOptions,
42
+ ) -> Iterable[Tuple[str, str, bool]]:
43
+ for device, result in old_raw(
44
+ args=gen_args,
45
+ loader=loader,
46
+ config=config,
47
+ stdin=stdin,
48
+ do_files_download=True,
49
+ use_mesh=False,
50
+ ):
51
+ if device.hw.vendor != "pc":
52
+ destname = output_driver.cfg_file_names(device)[0]
53
+ yield (destname, result, False)
54
+ else:
55
+ for entire_path, entire_data in sorted(result.items(), key=operator.itemgetter(0)):
56
+ if entire_data is None:
57
+ entire_data = ""
58
+ destname = output_driver.entire_config_dest_path(device, entire_path)
59
+ yield (destname, entire_data, False)
60
+
61
+
62
+ @contextmanager
63
+ def get_loader(gen_args: cli_args.GenOptions, args: cli_args.QueryOptions):
64
+ exit_stack = ExitStack()
65
+ connectors = storage_connector.get_all()
66
+ storages = []
67
+ with exit_stack:
68
+ for connector in connectors:
69
+ storage_opts = connector.opts().from_cli_opts(args)
70
+ storages.append(exit_stack.enter_context(connector.storage()(storage_opts)))
71
+ yield Loader(*storages, args=gen_args)
72
+
73
+
74
+ @subcommand(cli_args.QueryOptions, cli_args.opt_config, cli_args.FileOutOptions)
75
+ def show_current(args: cli_args.QueryOptions, config, arg_out: cli_args.FileOutOptions) -> None:
76
+ """ Показать текущий конфиг устройств """
77
+ gen_args = cli_args.GenOptions(args, no_acl=True)
78
+ output_driver = output_driver_connector.get()
79
+ with get_loader(gen_args, args) as loader:
80
+ if not loader.devices:
81
+ get_logger().error("No devices found for %s", args.query)
82
+
83
+ items = _gen_current_items(
84
+ loader=loader,
85
+ output_driver=output_driver,
86
+ gen_args=gen_args,
87
+ stdin=args.stdin(config=config),
88
+ config=config,
89
+ )
90
+ output_driver.write_output(arg_out, items, len(loader.devices))
91
+
92
+
93
+ @subcommand(cli_args.ShowGenOptions)
94
+ def gen(args: cli_args.ShowGenOptions):
95
+ """ Сгенерировать конфиг для устройств """
96
+ with get_loader(args, args) as loader:
97
+ (success, fail) = api.gen(args, loader)
98
+
99
+ out = [item for items in success.values() for item in items]
100
+ output_driver = output_driver_connector.get()
101
+ if args.dest is None:
102
+ text_mapping = {item[0]: item[1] for item in out}
103
+ out = [(",".join(key), value, False) for key, value in collapse_texts(text_mapping).items()]
104
+
105
+ out.extend(output_driver.format_fails(fail, loader.device_fqdns))
106
+ total = len(success) + len(fail)
107
+ if not total:
108
+ get_logger().error("No devices found for %s", args.query)
109
+ output_driver.write_output(args, out, total)
110
+
111
+
112
+ @subcommand(cli_args.ShowDiffOptions)
113
+ def diff(args: cli_args.ShowDiffOptions):
114
+ """ Сгенерировать конфиг для устройств и показать дифф по рулбуку с текущим """
115
+ with get_loader(args, args) as loader:
116
+ filterer = filtering.filterer_connector.get()
117
+ device_ids = loader.device_ids
118
+ output_driver_connector.get().write_output(
119
+ args,
120
+ gen_sort_diff(api.diff(args, loader, device_ids, filterer), args),
121
+ len(loader.device_ids)
122
+ )
123
+
124
+
125
+ @subcommand(cli_args.ShowPatchOptions)
126
+ def patch(args: cli_args.ShowPatchOptions):
127
+ """ Сгенерировать конфиг для устройств и сформировать патч """
128
+ with get_loader(args, args) as loader:
129
+ (success, fail) = api.patch(args, loader)
130
+
131
+ out = [item for items in success.values() for item in items]
132
+ output_driver = output_driver_connector.get()
133
+ out.extend(output_driver.format_fails(fail, loader.device_fqdns))
134
+ total = len(success) + len(fail)
135
+ if not total:
136
+ get_logger().error("No devices found for %s", args.query)
137
+ output_driver.write_output(args, out, total)
138
+
139
+
140
+ @subcommand(cli_args.DeployOptions)
141
+ def deploy(args: cli_args.DeployOptions):
142
+ """ Сгенерировать конфиг для устройств и задеплоить его """
143
+
144
+ deployer = Deployer(args)
145
+ filterer = filtering.filterer_connector.get()
146
+ fetcher = fetcher_connector.get()
147
+ deploy_driver = driver_connector.get()
148
+
149
+ with get_loader(args, args) as loader:
150
+ return api.deploy(
151
+ args=args, loader=loader, deployer=deployer,
152
+ deploy_driver=deploy_driver, filterer=filterer,
153
+ fetcher=fetcher,
154
+ )
155
+
156
+
157
+ @subcommand(cli_args.FileDiffOptions)
158
+ def file_diff(args: cli_args.FileDiffOptions):
159
+ """ Показать дифф по рулбуку между файлами или каталогами """
160
+ (success, fail) = api.file_diff(args)
161
+ out = []
162
+ output_driver = output_driver_connector.get()
163
+ if not args.fails_only:
164
+ out.extend(item for items in success.values() for item in items)
165
+ out.extend(output_driver.format_fails(fail))
166
+ # todo отрефакторить логику с отображением хоста в диффе: передавать в write_output явно критерий
167
+ output_driver.write_output(args, out, len(out) + 1)
168
+
169
+
170
+ @subcommand(cli_args.FilePatchOptions)
171
+ def file_patch(args: cli_args.FilePatchOptions):
172
+ """ Сформировать патч для файлов или каталогов """
173
+ (success, fail) = api.file_patch(args)
174
+ out = []
175
+ output_driver = output_driver_connector.get()
176
+ if not args.fails_only:
177
+ out.extend(item for items in success.values() for item in items)
178
+ out.extend(output_driver.format_fails(fail))
179
+ output_driver.write_output(args, out, len(out))
180
+
181
+
182
+ @subcommand()
183
+ def context():
184
+ """ Операции для управления файлом контекста.
185
+
186
+ По-умолчанию находится в '~/.annushka/context.yml', либо по пути в переменной окружения ANN_CONTEXT_CONFIG_PATH.
187
+ """
188
+ context_touch()
189
+
190
+
191
+ @subcommand(parent=context)
192
+ def context_touch():
193
+ """ Вывести путь к файлу контекста и, при отсутствии, создать его и наполнить данными (команда по-умолчанию) """
194
+ print(get_context_path(touch=True))
195
+
196
+
197
+ @subcommand(cli_args.SelectContext, parent=context)
198
+ def context_set_context(args: cli_args.SelectContext):
199
+ """ Задать текущий активный контекст по имени в конфигурации
200
+
201
+ Выбранный контекст будет использоваться по-умолчанию при незаданной переменной окружения ANN_SELECTED_CONTEXT
202
+ """
203
+ with open(path := get_context_path(touch=True)) as f:
204
+ data = yaml.safe_load(f)
205
+ if args.context_name not in data.get("context", {}):
206
+ raise KeyError(f"Cannot select context with name '{args.context_name}'. "
207
+ f"Available options are: {list(data.get('context', []))}")
208
+ data["selected_context"] = args.context_name
209
+ with open(path, "w") as f:
210
+ yaml.dump(data, f, sort_keys=False)
211
+
212
+
213
+ @subcommand(parent=context)
214
+ def context_edit():
215
+ """ Открыть файл конфигурации контекста в редакторе из переменной окружения EDITOR
216
+
217
+ Если переменная окружения EDITOR не задана,
218
+ для Windows пытаемся открыть файл средствами ОС, для остальных случаев пытаемся открыть в vi
219
+ """
220
+ editor = ""
221
+ if e := os.getenv("EDITOR"):
222
+ editor = e
223
+ elif platform.system() == "Windows":
224
+ editor = "notepad.exe"
225
+ elif shutil.which("vim"):
226
+ editor = "vim"
227
+ else:
228
+ editor = "vi"
229
+ path = get_context_path(touch=True)
230
+ proc = subprocess.Popen([editor, path])
231
+ proc.wait()
232
+
233
+
234
+ @subcommand(parent=context)
235
+ def context_repair():
236
+ """ Попытаться исправить расхождения в формате файла контекста после изменении версии """
237
+ repair_context_file()