annet 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of annet might be problematic. Click here for more details.
- annet/__init__.py +61 -0
- annet/annet.py +25 -0
- annet/annlib/__init__.py +7 -0
- annet/annlib/command.py +49 -0
- annet/annlib/diff.py +158 -0
- annet/annlib/errors.py +8 -0
- annet/annlib/filter_acl.py +196 -0
- annet/annlib/jsontools.py +89 -0
- annet/annlib/lib.py +495 -0
- annet/annlib/netdev/__init__.py +0 -0
- annet/annlib/netdev/db.py +62 -0
- annet/annlib/netdev/devdb/__init__.py +28 -0
- annet/annlib/netdev/devdb/data/devdb.json +137 -0
- annet/annlib/netdev/views/__init__.py +0 -0
- annet/annlib/netdev/views/dump.py +121 -0
- annet/annlib/netdev/views/hardware.py +112 -0
- annet/annlib/output.py +246 -0
- annet/annlib/patching.py +533 -0
- annet/annlib/rbparser/__init__.py +0 -0
- annet/annlib/rbparser/acl.py +120 -0
- annet/annlib/rbparser/deploying.py +55 -0
- annet/annlib/rbparser/ordering.py +52 -0
- annet/annlib/rbparser/platform.py +51 -0
- annet/annlib/rbparser/syntax.py +115 -0
- annet/annlib/rulebook/__init__.py +0 -0
- annet/annlib/rulebook/common.py +350 -0
- annet/annlib/tabparser.py +648 -0
- annet/annlib/types.py +35 -0
- annet/api/__init__.py +807 -0
- annet/argparse.py +415 -0
- annet/cli.py +192 -0
- annet/cli_args.py +493 -0
- annet/configs/context.yml +18 -0
- annet/configs/logging.yaml +39 -0
- annet/connectors.py +64 -0
- annet/deploy.py +441 -0
- annet/diff.py +85 -0
- annet/executor.py +551 -0
- annet/filtering.py +40 -0
- annet/gen.py +828 -0
- annet/generators/__init__.py +987 -0
- annet/generators/common/__init__.py +0 -0
- annet/generators/common/initial.py +33 -0
- annet/hardware.py +45 -0
- annet/implicit.py +139 -0
- annet/lib.py +128 -0
- annet/output.py +170 -0
- annet/parallel.py +448 -0
- annet/patching.py +25 -0
- annet/reference.py +148 -0
- annet/rulebook/__init__.py +114 -0
- annet/rulebook/arista/__init__.py +0 -0
- annet/rulebook/arista/iface.py +16 -0
- annet/rulebook/aruba/__init__.py +16 -0
- annet/rulebook/aruba/ap_env.py +146 -0
- annet/rulebook/aruba/misc.py +8 -0
- annet/rulebook/cisco/__init__.py +0 -0
- annet/rulebook/cisco/iface.py +68 -0
- annet/rulebook/cisco/misc.py +57 -0
- annet/rulebook/cisco/vlandb.py +90 -0
- annet/rulebook/common.py +19 -0
- annet/rulebook/deploying.py +87 -0
- annet/rulebook/huawei/__init__.py +0 -0
- annet/rulebook/huawei/aaa.py +75 -0
- annet/rulebook/huawei/bgp.py +97 -0
- annet/rulebook/huawei/iface.py +33 -0
- annet/rulebook/huawei/misc.py +337 -0
- annet/rulebook/huawei/vlandb.py +115 -0
- annet/rulebook/juniper/__init__.py +107 -0
- annet/rulebook/nexus/__init__.py +0 -0
- annet/rulebook/nexus/iface.py +92 -0
- annet/rulebook/patching.py +143 -0
- annet/rulebook/ribbon/__init__.py +12 -0
- annet/rulebook/texts/arista.deploy +20 -0
- annet/rulebook/texts/arista.order +125 -0
- annet/rulebook/texts/arista.rul +59 -0
- annet/rulebook/texts/aruba.deploy +20 -0
- annet/rulebook/texts/aruba.order +83 -0
- annet/rulebook/texts/aruba.rul +87 -0
- annet/rulebook/texts/cisco.deploy +27 -0
- annet/rulebook/texts/cisco.order +82 -0
- annet/rulebook/texts/cisco.rul +105 -0
- annet/rulebook/texts/huawei.deploy +188 -0
- annet/rulebook/texts/huawei.order +388 -0
- annet/rulebook/texts/huawei.rul +471 -0
- annet/rulebook/texts/juniper.rul +120 -0
- annet/rulebook/texts/nexus.deploy +24 -0
- annet/rulebook/texts/nexus.order +85 -0
- annet/rulebook/texts/nexus.rul +83 -0
- annet/rulebook/texts/nokia.rul +31 -0
- annet/rulebook/texts/pc.order +5 -0
- annet/rulebook/texts/pc.rul +9 -0
- annet/rulebook/texts/ribbon.deploy +22 -0
- annet/rulebook/texts/ribbon.rul +77 -0
- annet/rulebook/texts/routeros.order +38 -0
- annet/rulebook/texts/routeros.rul +45 -0
- annet/storage.py +121 -0
- annet/tabparser.py +36 -0
- annet/text_term_format.py +95 -0
- annet/tracing.py +170 -0
- annet/types.py +223 -0
- annet-0.1.dist-info/AUTHORS +21 -0
- annet-0.1.dist-info/LICENSE +21 -0
- annet-0.1.dist-info/METADATA +24 -0
- annet-0.1.dist-info/RECORD +113 -0
- annet-0.1.dist-info/WHEEL +5 -0
- annet-0.1.dist-info/entry_points.txt +6 -0
- annet-0.1.dist-info/top_level.txt +3 -0
- annet_generators/__init__.py +0 -0
- annet_generators/example/__init__.py +12 -0
- annet_generators/example/lldp.py +52 -0
- annet_nbexport/__init__.py +220 -0
- annet_nbexport/main.py +46 -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,192 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import operator
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
import shutil
|
|
7
|
+
from typing import Generator, Tuple
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
from contextlog import get_logger
|
|
11
|
+
from valkit.python import valid_logging_level
|
|
12
|
+
|
|
13
|
+
from annet import api, cli_args, filtering
|
|
14
|
+
from annet.api import collapse_texts
|
|
15
|
+
from annet.argparse import ArgParser, subcommand
|
|
16
|
+
from annet.diff import gen_sort_diff
|
|
17
|
+
from annet.gen import Loader, old_raw
|
|
18
|
+
from annet.lib import get_context_path, repair_context_file
|
|
19
|
+
from annet.output import output_driver_connector
|
|
20
|
+
from annet.storage import Storage, storage_connector
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fill_base_args(parser: ArgParser, pkg_name: str, logging_config: str):
|
|
24
|
+
parser.add_argument("--log-level", default="WARN", type=valid_logging_level,
|
|
25
|
+
help="Уровень детализации логов (DEBUG, DEBUG2 (with comocutor debug), INFO, WARN, CRITICAL)")
|
|
26
|
+
parser.add_argument("--pkg_name", default=pkg_name, help=argparse.SUPPRESS)
|
|
27
|
+
parser.add_argument("--logging_config", default=logging_config, help=argparse.SUPPRESS)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def list_subcommands():
|
|
31
|
+
return globals().copy()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@subcommand(cli_args.QueryOptions, cli_args.opt_config, cli_args.FileOutOptions)
|
|
35
|
+
def show_current(args: cli_args.QueryOptions, config, arg_out: cli_args.FileOutOptions) -> None:
|
|
36
|
+
""" Показать текущий конфиг устройств """
|
|
37
|
+
|
|
38
|
+
def _gen_items(storage: Storage) -> Generator[Tuple[str, str, bool], None, None]:
|
|
39
|
+
for device, result in old_raw(
|
|
40
|
+
cli_args.GenOptions(args, no_acl=True),
|
|
41
|
+
storage,
|
|
42
|
+
config,
|
|
43
|
+
stdin=args.stdin(storage=storage, config=config),
|
|
44
|
+
do_files_download=True,
|
|
45
|
+
use_mesh=False,
|
|
46
|
+
):
|
|
47
|
+
output_driver = output_driver_connector.get()
|
|
48
|
+
destname = output_driver.cfg_file_names(device)[0]
|
|
49
|
+
if device.hw.vendor != "pc":
|
|
50
|
+
yield (destname, result, False)
|
|
51
|
+
else:
|
|
52
|
+
for entire_path, entire_data in sorted(result.items(), key=operator.itemgetter(0)):
|
|
53
|
+
if entire_data is None:
|
|
54
|
+
entire_data = ""
|
|
55
|
+
yield (output_driver.entire_config_dest_path(device, entire_path), entire_data, False)
|
|
56
|
+
|
|
57
|
+
with storage_connector.get().storage()(args) as storage:
|
|
58
|
+
ids = storage.resolve_object_ids_by_query(args.query)
|
|
59
|
+
if not ids:
|
|
60
|
+
get_logger().error("No devices found for %s", args.query)
|
|
61
|
+
output_driver_connector.get().write_output(arg_out, _gen_items(storage), len(ids))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@subcommand(cli_args.ShowGenOptions)
|
|
65
|
+
def gen(args: cli_args.ShowGenOptions):
|
|
66
|
+
""" Сгенерировать конфиг для устройств """
|
|
67
|
+
(success, fail) = api.gen(args)
|
|
68
|
+
out = [item for items in success.values() for item in items]
|
|
69
|
+
output_driver = output_driver_connector.get()
|
|
70
|
+
if args.dest is None:
|
|
71
|
+
text_mapping = {item[0]: item[1] for item in out}
|
|
72
|
+
out = [(",".join(key), value, False) for key, value in collapse_texts(text_mapping).items()]
|
|
73
|
+
out.extend(output_driver.format_fails(fail, args))
|
|
74
|
+
total = len(success) + len(fail)
|
|
75
|
+
if not total:
|
|
76
|
+
get_logger().error("No devices found for %s", args.query)
|
|
77
|
+
output_driver.write_output(args, out, total)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@subcommand(cli_args.ShowDiffOptions)
|
|
81
|
+
def diff(args: cli_args.ShowDiffOptions):
|
|
82
|
+
""" Сгенерировать конфиг для устройств и показать дифф по рулбуку с текущим """
|
|
83
|
+
with storage_connector.get().storage()(args) as storage:
|
|
84
|
+
filterer = filtering.filterer_connector.get()
|
|
85
|
+
loader = Loader(storage, args)
|
|
86
|
+
output_driver_connector.get().write_output(
|
|
87
|
+
args,
|
|
88
|
+
gen_sort_diff(api.diff(args, loader, filterer), args),
|
|
89
|
+
len(loader.device_ids)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@subcommand(cli_args.ShowPatchOptions)
|
|
94
|
+
def patch(args: cli_args.ShowPatchOptions):
|
|
95
|
+
""" Сгенерировать конфиг для устройств и сформировать патч """
|
|
96
|
+
(success, fail) = api.patch(args)
|
|
97
|
+
out = [item for items in success.values() for item in items]
|
|
98
|
+
output_driver = output_driver_connector.get()
|
|
99
|
+
out.extend(output_driver.format_fails(fail, args))
|
|
100
|
+
total = len(success) + len(fail)
|
|
101
|
+
if not total:
|
|
102
|
+
get_logger().error("No devices found for %s", args.query)
|
|
103
|
+
output_driver.write_output(args, out, total)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@subcommand(cli_args.DeployOptions)
|
|
107
|
+
def deploy(args: cli_args.DeployOptions):
|
|
108
|
+
""" Сгенерировать конфиг для устройств и задеплоить его """
|
|
109
|
+
return api.deploy(args)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@subcommand(cli_args.FileDiffOptions)
|
|
113
|
+
def file_diff(args: cli_args.FileDiffOptions):
|
|
114
|
+
""" Показать дифф по рулбуку между файлами или каталогами """
|
|
115
|
+
(success, fail) = api.file_diff(args)
|
|
116
|
+
out = []
|
|
117
|
+
output_driver = output_driver_connector.get()
|
|
118
|
+
if not args.fails_only:
|
|
119
|
+
out.extend(item for items in success.values() for item in items)
|
|
120
|
+
out.extend(output_driver.format_fails(fail))
|
|
121
|
+
# todo отрефакторить логику с отображением хоста в диффе: передавать в write_output явно критерий
|
|
122
|
+
output_driver.write_output(args, out, len(out) + 1)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@subcommand(cli_args.FilePatchOptions)
|
|
126
|
+
def file_patch(args: cli_args.FilePatchOptions):
|
|
127
|
+
""" Сформировать патч для файлов или каталогов """
|
|
128
|
+
(success, fail) = api.file_patch(args)
|
|
129
|
+
out = []
|
|
130
|
+
output_driver = output_driver_connector.get()
|
|
131
|
+
if not args.fails_only:
|
|
132
|
+
out.extend(item for items in success.values() for item in items)
|
|
133
|
+
out.extend(output_driver.format_fails(fail))
|
|
134
|
+
output_driver.write_output(args, out, len(out))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@subcommand()
|
|
138
|
+
def context():
|
|
139
|
+
""" Операции для управления файлом контекста.
|
|
140
|
+
|
|
141
|
+
По-умолчанию находится в '~/.annushka/context.yml', либо по пути в переменной окружения ANN_CONTEXT_CONFIG_PATH.
|
|
142
|
+
"""
|
|
143
|
+
context_touch()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@subcommand(parent=context)
|
|
147
|
+
def context_touch():
|
|
148
|
+
""" Вывести путь к файлу контекста и, при отсутствии, создать его и наполнить данными (команда по-умолчанию) """
|
|
149
|
+
print(get_context_path(touch=True))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@subcommand(cli_args.SelectContext, parent=context)
|
|
153
|
+
def context_set_context(args: cli_args.SelectContext):
|
|
154
|
+
""" Задать текущий активный контекст по имени в конфигурации
|
|
155
|
+
|
|
156
|
+
Выбранный контекст будет использоваться по-умолчанию при незаданной переменной окружения ANN_SELECTED_CONTEXT
|
|
157
|
+
"""
|
|
158
|
+
with open(path := get_context_path(touch=True)) as f:
|
|
159
|
+
data = yaml.safe_load(f)
|
|
160
|
+
if args.context_name not in data.get("context", {}):
|
|
161
|
+
raise KeyError(f"Cannot select context with name '{args.context_name}'. "
|
|
162
|
+
f"Available options are: {list(data.get('context', []))}")
|
|
163
|
+
data["selected_context"] = args.context_name
|
|
164
|
+
with open(path, "w") as f:
|
|
165
|
+
yaml.dump(data, f, sort_keys=False)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@subcommand(parent=context)
|
|
169
|
+
def context_edit():
|
|
170
|
+
""" Открыть файл конфигурации контекста в редакторе из переменной окружения EDITOR
|
|
171
|
+
|
|
172
|
+
Если переменная окружения EDITOR не задана,
|
|
173
|
+
для Windows пытаемся открыть файл средствами ОС, для остальных случаев пытаемся открыть в vi
|
|
174
|
+
"""
|
|
175
|
+
editor = ""
|
|
176
|
+
if e := os.getenv("EDITOR"):
|
|
177
|
+
editor = e
|
|
178
|
+
elif platform.system() == "Windows":
|
|
179
|
+
editor = "notepad.exe"
|
|
180
|
+
elif shutil.which("vim"):
|
|
181
|
+
editor = "vim"
|
|
182
|
+
else:
|
|
183
|
+
editor = "vi"
|
|
184
|
+
path = get_context_path(touch=True)
|
|
185
|
+
proc = subprocess.Popen([editor, path])
|
|
186
|
+
proc.wait()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@subcommand(parent=context)
|
|
190
|
+
def context_repair():
|
|
191
|
+
""" Попытаться исправить расхождения в формате файла контекста после изменении версии """
|
|
192
|
+
repair_context_file()
|