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/executor.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import multiprocessing
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import resource
|
|
7
|
+
import signal
|
|
8
|
+
import statistics
|
|
9
|
+
import time
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from functools import partial
|
|
12
|
+
from operator import itemgetter
|
|
13
|
+
from queue import Empty
|
|
14
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
15
|
+
|
|
16
|
+
import colorama
|
|
17
|
+
import psutil
|
|
18
|
+
from annet.annlib.command import Command, CommandList, Question # noqa: F401
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from annet.storage import Device
|
|
22
|
+
except ImportError:
|
|
23
|
+
from noc.annushka.annet.storage import Device # noqa: F401
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_logger = logging.getLogger(__name__)
|
|
27
|
+
FIRST_EXCEPTION = 1
|
|
28
|
+
ALL_COMPLETED = 2
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CommandResult(ABC):
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def get_out(self) -> str:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Connector(ABC):
|
|
38
|
+
@abstractmethod
|
|
39
|
+
async def cmd(self, cmd: Union[Command, str]) -> CommandResult:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def download(self, files: List[str]) -> Dict[str, str]:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
async def upload(self, files: Dict[str, str]):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def get_conn_trace(self) -> str:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def aclose(self) -> str:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Executor(ABC):
|
|
60
|
+
# method for bulk config downloading TODO: remove in favor Connector.cmd
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def fetch(self,
|
|
63
|
+
devices: List[Device],
|
|
64
|
+
files_to_download: Dict[str, List[str]] = None) -> Tuple[Dict[Device, str], Dict[Device, Any]]:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
async def amake_connection(self, device: Device) -> Connector:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ExecutorException(Exception):
|
|
73
|
+
def __init__(self, *args: List[Any], auxiliary: Optional[Any] = None, **kwargs: object):
|
|
74
|
+
self.auxiliary = auxiliary
|
|
75
|
+
super().__init__(*args, **kwargs)
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return "%s(args=%r,auxiliary=%s)" % (self.__class__.__name__, self.args, self.auxiliary)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ExecException(ExecutorException):
|
|
82
|
+
def __init__(self, msg: str, cmd: str, res: str, **kwargs):
|
|
83
|
+
super().__init__(**kwargs)
|
|
84
|
+
self.args = msg, cmd, res
|
|
85
|
+
self.kwargs = kwargs
|
|
86
|
+
self.msg = msg
|
|
87
|
+
self.cmd = cmd
|
|
88
|
+
self.res = res
|
|
89
|
+
|
|
90
|
+
def __str__(self) -> str:
|
|
91
|
+
return str(self.msg)
|
|
92
|
+
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
return "%s<%s, %s>" % (self.__class__.__name__, self.msg, self.cmd)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class BadCommand(ExecException):
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class NonzeroRetcode(ExecException):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CommitException(ExecException):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def chunks_tuple(l, n): # noqa
|
|
109
|
+
return [tuple(l[i:i + n]) for i in range(0, len(l), n)]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def async_bulk(
|
|
113
|
+
executor: Executor,
|
|
114
|
+
devices: List[Device],
|
|
115
|
+
coro_gen: Callable[[Connector, Device], Any],
|
|
116
|
+
*args,
|
|
117
|
+
processes: int = 1,
|
|
118
|
+
show_report: bool = True,
|
|
119
|
+
do_log: bool = True,
|
|
120
|
+
log_dir: Optional[str] = None,
|
|
121
|
+
policy: int = ALL_COMPLETED,
|
|
122
|
+
**kwargs,
|
|
123
|
+
):
|
|
124
|
+
"""Connect to specified devices and work with their CLI.
|
|
125
|
+
|
|
126
|
+
Note: this function is not allowed to be run in parallel, since it's using global state (TODO: fixme)
|
|
127
|
+
|
|
128
|
+
:param devices: List of devices' fqdns to use their CLI.
|
|
129
|
+
:param coro_gen: Async function. It contains all logic about usage of CLI.
|
|
130
|
+
See docstring of "bind_coro_args" for allowed function signature and examples.
|
|
131
|
+
:param args: Positional arguments to "bulk" function.
|
|
132
|
+
:type processes: Amount of processes to fork for current work.
|
|
133
|
+
:param show_report: Set this flag to show report to stdout.
|
|
134
|
+
:param do_log: If True and log_dir is not set, then log_dir will be filled automatically.
|
|
135
|
+
:param log_dir: Specify path to log all response from devices.
|
|
136
|
+
:param policy: int flag. If FIRST_EXCEPTION, then work will be stopped after first error.
|
|
137
|
+
Otherwise all hosts will be processed.
|
|
138
|
+
TODO: fix that policy is not used if processes=1
|
|
139
|
+
:param kwargs: other arguments to pass to "bulk" function.
|
|
140
|
+
Note: it is not passed directly to "coro_gen" function!
|
|
141
|
+
kwargs should be {'kwargs': {'var1': value1}} to set "var1" with "value1" in "coro_gen" function.
|
|
142
|
+
|
|
143
|
+
TODOs:
|
|
144
|
+
* do not log if do_log=False and log_dir is set.
|
|
145
|
+
|
|
146
|
+
"""
|
|
147
|
+
res = {}
|
|
148
|
+
deploy_durations = {}
|
|
149
|
+
kwargs["log_dir"] = log_dir
|
|
150
|
+
kwargs["policy"] = policy
|
|
151
|
+
|
|
152
|
+
if processes == 1:
|
|
153
|
+
host_res, host_duration = asyncio.get_event_loop().run_until_complete(bulk(executor, devices, coro_gen, *args, **kwargs))
|
|
154
|
+
res.update(host_res)
|
|
155
|
+
deploy_durations.update(host_duration)
|
|
156
|
+
else:
|
|
157
|
+
# FIXME: show_report works per process
|
|
158
|
+
if len(devices) != len(set(devices)):
|
|
159
|
+
raise Exception("hostnames should be unique")
|
|
160
|
+
# warm up a cache
|
|
161
|
+
# asyncio.get_event_loop().run_until_complete(get_validator_rt_data(hostnames))
|
|
162
|
+
if isinstance(devices, dict):
|
|
163
|
+
devices = list(devices.keys())
|
|
164
|
+
hostnames_chunks = chunks_tuple(devices, int(len(devices) / processes) + 1)
|
|
165
|
+
pool = {}
|
|
166
|
+
for hostnames_chunk in hostnames_chunks:
|
|
167
|
+
res_q = multiprocessing.Queue()
|
|
168
|
+
p = multiprocessing.Process(target=_mp_async_bulk, args=[res_q, hostnames_chunk, coro_gen, *args], kwargs=kwargs)
|
|
169
|
+
pool[p] = [res_q, hostnames_chunk]
|
|
170
|
+
p.start()
|
|
171
|
+
_logger.info("process (id=%d) work with %d chunks", p.pid, len(hostnames_chunks))
|
|
172
|
+
|
|
173
|
+
seen_error = False
|
|
174
|
+
while True:
|
|
175
|
+
done = []
|
|
176
|
+
for p in pool:
|
|
177
|
+
host_res = None
|
|
178
|
+
try:
|
|
179
|
+
# proc wont be exited till q.get() call
|
|
180
|
+
host_res, host_duration = pool[p][0].get(timeout=0.2)
|
|
181
|
+
except Empty:
|
|
182
|
+
pass
|
|
183
|
+
else:
|
|
184
|
+
done.append(p)
|
|
185
|
+
|
|
186
|
+
if not p.is_alive() and not host_res:
|
|
187
|
+
_logger.error("process %s has died: hostnames: %s", p.pid, pool[p][1])
|
|
188
|
+
host_res = {hostname: Exception("died with exitcode %s" % p.exitcode) for hostname in pool[p][1]}
|
|
189
|
+
host_duration = {hostname: 0 for hostname in pool[p][1]} # FIXME:
|
|
190
|
+
done.append(p)
|
|
191
|
+
|
|
192
|
+
if host_res:
|
|
193
|
+
res.update(host_res)
|
|
194
|
+
deploy_durations.update(host_duration)
|
|
195
|
+
|
|
196
|
+
if p.exitcode:
|
|
197
|
+
_logger.error("process %s finished with bad exitcode %s", p.pid, p.exitcode)
|
|
198
|
+
seen_error = True
|
|
199
|
+
for p in done:
|
|
200
|
+
pool.pop(p)
|
|
201
|
+
if policy == FIRST_EXCEPTION and seen_error:
|
|
202
|
+
for p in pool:
|
|
203
|
+
p.terminate()
|
|
204
|
+
if p.is_alive():
|
|
205
|
+
time.sleep(0.4)
|
|
206
|
+
if p.is_alive():
|
|
207
|
+
os.kill(p.pid, signal.SIGKILL)
|
|
208
|
+
for hostname in pool[p][1]:
|
|
209
|
+
res[hostname] = Exception("force kill with exitcode %s" % p.exitcode)
|
|
210
|
+
deploy_durations[hostname] = 0 # FIXME:
|
|
211
|
+
if not pool:
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
if show_report:
|
|
215
|
+
show_bulk_report(devices, res, deploy_durations, do_log and log_dir)
|
|
216
|
+
|
|
217
|
+
return res
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _show_type_summary(caption, items, total, stat_items=None):
|
|
221
|
+
if items:
|
|
222
|
+
if not stat_items:
|
|
223
|
+
stat = ""
|
|
224
|
+
else:
|
|
225
|
+
avg = statistics.mean(stat_items)
|
|
226
|
+
stat = " %(min).1f/%(max).1f/%(avg).1f/%(stdev)s (min/max/avg/stdev)" % dict(
|
|
227
|
+
min=min(stat_items),
|
|
228
|
+
max=max(stat_items),
|
|
229
|
+
avg=avg,
|
|
230
|
+
stdev="-" if len(stat_items) < 2 else "%.1f" % statistics.stdev(stat_items, xbar=avg)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
print("%-8s %d of %d%s" % (caption, len(items), total, stat))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def show_bulk_report(hostnames, res, durations, log_dir):
|
|
237
|
+
total = len(hostnames)
|
|
238
|
+
if not total:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
colorama.init()
|
|
242
|
+
|
|
243
|
+
print("\n====== bulk deploy report ======")
|
|
244
|
+
|
|
245
|
+
done = [host for (host, hres) in res.items() if not isinstance(hres, Exception)]
|
|
246
|
+
cancelled = [host for (host, hres) in res.items() if isinstance(hres, asyncio.CancelledError)]
|
|
247
|
+
failed = [host for (host, hres) in res.items() if isinstance(hres, Exception) and host not in cancelled]
|
|
248
|
+
lost = [host for host in hostnames if host not in res]
|
|
249
|
+
limit = 30
|
|
250
|
+
|
|
251
|
+
_show_type_summary("Done :", done, total, [durations[h] for h in done])
|
|
252
|
+
_print_limit(done, partial(_print_hostname, style=colorama.Fore.GREEN), limit, total)
|
|
253
|
+
|
|
254
|
+
_show_type_summary("Failed :", failed, total, [durations[h] for h in failed])
|
|
255
|
+
|
|
256
|
+
_print_limit(failed, partial(_print_failed, res=res), limit, total)
|
|
257
|
+
|
|
258
|
+
_show_type_summary("Cancelled :", cancelled, total, [durations[h] for h in cancelled if durations[h] is not None])
|
|
259
|
+
_print_limit(cancelled, partial(_print_hostname, style=colorama.Fore.RED), limit, total)
|
|
260
|
+
|
|
261
|
+
_show_type_summary("Lost :", lost, total)
|
|
262
|
+
_print_limit(lost, _print_hostname, limit, total)
|
|
263
|
+
|
|
264
|
+
err_limit = 5
|
|
265
|
+
if failed:
|
|
266
|
+
errs = {}
|
|
267
|
+
for hostname in failed:
|
|
268
|
+
fmt_err = _format_exc(res[hostname])
|
|
269
|
+
if fmt_err in errs:
|
|
270
|
+
errs[fmt_err] += 1
|
|
271
|
+
else:
|
|
272
|
+
errs[fmt_err] = 1
|
|
273
|
+
print("Top errors :")
|
|
274
|
+
for fmt_err, n in sorted(errs.items(), key=itemgetter(1), reverse=True)[:err_limit]:
|
|
275
|
+
print(" %-4d %s" % (n, fmt_err))
|
|
276
|
+
print("\n", end="")
|
|
277
|
+
|
|
278
|
+
if log_dir:
|
|
279
|
+
print("See deploy logs in %s/\n" % os.path.relpath(log_dir))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _format_exc(exc):
|
|
283
|
+
if isinstance(exc, ExecException):
|
|
284
|
+
cmd = str(exc.cmd)
|
|
285
|
+
if len(cmd) > 50:
|
|
286
|
+
cmd = cmd[:50] + "~.."
|
|
287
|
+
return "'%s', cmd '%s'" % (exc.msg, cmd)
|
|
288
|
+
elif isinstance(exc, ExecutorException):
|
|
289
|
+
return "%s%r" % (exc.__class__.__name__, exc.args) # исключить многословный auxiliary
|
|
290
|
+
else:
|
|
291
|
+
return repr(exc)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _print_hostname(host, style=None):
|
|
295
|
+
if style:
|
|
296
|
+
host = style + host + colorama.Style.RESET_ALL
|
|
297
|
+
print(" %s" % host)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _print_limit(items, printer, limit, total, end="\n"):
|
|
301
|
+
if not items:
|
|
302
|
+
return
|
|
303
|
+
if len(items) > limit and len(items) > total * 0.7:
|
|
304
|
+
print(" ... %d hosts" % len(items))
|
|
305
|
+
for host in items[:limit]:
|
|
306
|
+
printer(host)
|
|
307
|
+
if len(items) > limit:
|
|
308
|
+
print(" ... %d more hosts" % (len(items) - limit))
|
|
309
|
+
|
|
310
|
+
print(end, end="")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _print_failed(host, res):
|
|
314
|
+
exc = res[host]
|
|
315
|
+
color = colorama.Fore.YELLOW if isinstance(exc, Warning) else colorama.Fore.RED
|
|
316
|
+
print(" %s - %s" % (color + host + colorama.Style.RESET_ALL, _format_exc(exc)))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _mp_async_bulk(res_q: multiprocessing.Queue, *args, **kwargs):
|
|
320
|
+
asyncio.get_event_loop().close()
|
|
321
|
+
loop = asyncio.new_event_loop()
|
|
322
|
+
asyncio.set_event_loop(loop)
|
|
323
|
+
res = loop.run_until_complete(bulk(*args, **kwargs))
|
|
324
|
+
res_q.put(res)
|
|
325
|
+
res_q.close()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
async def bulk(
|
|
329
|
+
executor: Executor,
|
|
330
|
+
devices: List[Device],
|
|
331
|
+
coro_gen: Callable[[Connector, Device, Optional[Dict[str, Any]]], Any],
|
|
332
|
+
max_parallel: float = 100,
|
|
333
|
+
policy: int = ALL_COMPLETED,
|
|
334
|
+
log_dir: str = True, # pylint: disable=unused-argument
|
|
335
|
+
kwargs: Optional[dict] = None,
|
|
336
|
+
console_log: bool = True
|
|
337
|
+
) -> Tuple[Dict[str, Any], Dict[str, float]]:
|
|
338
|
+
"""Connect to specified devices and work with their CLI.
|
|
339
|
+
|
|
340
|
+
:param hostnames: List of devices' fqdns to use their CLI.
|
|
341
|
+
:param coro_gen: Async function. It contains all logic about usage of CLI.
|
|
342
|
+
See docstring of "bind_coro_args" for allowed function signature and examples.
|
|
343
|
+
:param max_parallel: Upper border to CPU usage (in percentage 1 CPU = 100).
|
|
344
|
+
If cpu usage is over, then tasks are trottled.
|
|
345
|
+
:param policy: Flag to specify when tasks are completed.
|
|
346
|
+
:param log_dir: Specify path to log all response from devices.
|
|
347
|
+
TODO: fix default value.
|
|
348
|
+
:param kwargs: Device independent arguments to call function. See @bind_coro_args for details.
|
|
349
|
+
:param get_device: See "make_connection" for better understanding.
|
|
350
|
+
:param device_cls: See "make_connection" for better understanding.
|
|
351
|
+
:param streamer_cls: See "make_connection" for better understanding.
|
|
352
|
+
:param console_log: If True and there is no handlers for root logger, then stderr will be used for logging.
|
|
353
|
+
:return: two dicts with results per host and execution duration per host.
|
|
354
|
+
|
|
355
|
+
"""
|
|
356
|
+
if console_log:
|
|
357
|
+
init_log()
|
|
358
|
+
|
|
359
|
+
tasks = []
|
|
360
|
+
res = {}
|
|
361
|
+
pending = set()
|
|
362
|
+
tasks_to_hostname = {}
|
|
363
|
+
time_of_start = {}
|
|
364
|
+
deploy_durations = {}
|
|
365
|
+
now = None
|
|
366
|
+
if not kwargs:
|
|
367
|
+
kwargs = {}
|
|
368
|
+
|
|
369
|
+
def start_hook(device: Device):
|
|
370
|
+
time_of_start[device.hostname] = time.monotonic()
|
|
371
|
+
|
|
372
|
+
def end_hook(device: Device, task: asyncio.Task):
|
|
373
|
+
duration = now - time_of_start[device.hostname]
|
|
374
|
+
deploy_durations[device.hostname] = duration
|
|
375
|
+
|
|
376
|
+
coro_exc = task.exception()
|
|
377
|
+
if coro_exc:
|
|
378
|
+
if policy == FIRST_EXCEPTION:
|
|
379
|
+
_logger.error("%s %r", device.hostname, coro_exc, exc_info=coro_exc)
|
|
380
|
+
_logger.info("Terminating all running tasks according to FIRST_EXCEPTION policy")
|
|
381
|
+
res[device.hostname] = coro_exc
|
|
382
|
+
raise CancelAllTasks
|
|
383
|
+
else:
|
|
384
|
+
if isinstance(coro_exc, AssertionError):
|
|
385
|
+
_logger.error("%s %r", device.hostname, coro_exc, exc_info=coro_exc)
|
|
386
|
+
else:
|
|
387
|
+
_logger.error("%s %r", device.hostname, coro_exc)
|
|
388
|
+
return coro_exc
|
|
389
|
+
else:
|
|
390
|
+
_logger.info("Finished in %0.2fs, hostname=%s", duration, device.hostname)
|
|
391
|
+
return task.result()
|
|
392
|
+
|
|
393
|
+
for device in devices:
|
|
394
|
+
conn = await executor.amake_connection(device=device)
|
|
395
|
+
start_hook(device)
|
|
396
|
+
task = asyncio.create_task(coro_gen(conn=conn, device=device, **kwargs))
|
|
397
|
+
tasks_to_hostname[task] = device
|
|
398
|
+
tasks.append(task)
|
|
399
|
+
try:
|
|
400
|
+
ndone = 0
|
|
401
|
+
with CpuThrottler(asyncio.get_event_loop(), maximum=max_parallel) as throttler:
|
|
402
|
+
while pending or tasks:
|
|
403
|
+
left_fds = int(fd_left() / 2) # better to be safe than sorry
|
|
404
|
+
|
|
405
|
+
for _ in range(min(throttler.curr - len(pending), len(tasks), left_fds)):
|
|
406
|
+
pending.add(tasks.pop(0))
|
|
407
|
+
if len(pending) == 0:
|
|
408
|
+
_logger.debug("empty pending list. tasks=%s throttler curr=%s left_fds=%s. waiting", len(tasks),
|
|
409
|
+
throttler.curr, left_fds)
|
|
410
|
+
await asyncio.sleep(1)
|
|
411
|
+
continue
|
|
412
|
+
example_host = next(iter(pending))
|
|
413
|
+
_logger.debug("tasks status: %d pending, %d queued, pending example %s", len(pending), len(tasks), example_host)
|
|
414
|
+
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
|
|
415
|
+
|
|
416
|
+
now = time.monotonic()
|
|
417
|
+
for task in done:
|
|
418
|
+
hostname = tasks_to_hostname[task]
|
|
419
|
+
res[hostname] = end_hook(hostname, task)
|
|
420
|
+
ndone += 1
|
|
421
|
+
except CancelAllTasks:
|
|
422
|
+
exc = asyncio.CancelledError()
|
|
423
|
+
|
|
424
|
+
now = time.monotonic()
|
|
425
|
+
for hostname, task in _get_remaining(tasks, pending, tasks_to_hostname):
|
|
426
|
+
res[hostname] = exc
|
|
427
|
+
|
|
428
|
+
if hostname in time_of_start:
|
|
429
|
+
duration = now - time_of_start[hostname]
|
|
430
|
+
else:
|
|
431
|
+
duration = None
|
|
432
|
+
deploy_durations[hostname] = duration
|
|
433
|
+
|
|
434
|
+
if not asyncio.iscoroutine(task):
|
|
435
|
+
_logger.info("task %s", task)
|
|
436
|
+
task.cancel()
|
|
437
|
+
|
|
438
|
+
return res, deploy_durations
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def init_log():
|
|
442
|
+
streamer = logging.StreamHandler()
|
|
443
|
+
fmt = logging.Formatter("%(asctime)s - %(filename)s:%(lineno)d - %(funcName)s() - %(levelname)s - %(message)s",
|
|
444
|
+
"%Y-%m-%d %H:%M:%S")
|
|
445
|
+
streamer.setFormatter(fmt)
|
|
446
|
+
if not logging.root.handlers:
|
|
447
|
+
logging.root.addHandler(streamer)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class DeferredFileWrite:
|
|
451
|
+
def __init__(self, file, mode="r"):
|
|
452
|
+
self._file = file
|
|
453
|
+
wrapper = {"w": "a", "wb": "ab"}
|
|
454
|
+
if mode in wrapper:
|
|
455
|
+
self._mode = wrapper[mode]
|
|
456
|
+
else:
|
|
457
|
+
raise Exception()
|
|
458
|
+
|
|
459
|
+
def write(self, data):
|
|
460
|
+
with open(self._file, self._mode) as fh:
|
|
461
|
+
fh.write(data)
|
|
462
|
+
|
|
463
|
+
def close(self):
|
|
464
|
+
pass
|
|
465
|
+
|
|
466
|
+
def flush(self):
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class CancelAllTasks(Exception):
|
|
471
|
+
pass
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _get_remaining(tasks, pending, tasks_to_hostname):
|
|
475
|
+
for task in pending:
|
|
476
|
+
yield (tasks_to_hostname[task], task)
|
|
477
|
+
for task in tasks:
|
|
478
|
+
yield (tasks_to_hostname[task], task)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
_platform = platform.system()
|
|
482
|
+
_fd_available = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def fd_left():
|
|
486
|
+
res = _fd_available
|
|
487
|
+
if _platform == "Linux":
|
|
488
|
+
res = _fd_available - len(list(os.scandir(path="/proc/self/fd/")))
|
|
489
|
+
return res
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class CpuThrottler:
|
|
493
|
+
def __init__(self, loop, start=20, maximum=None, minimum=5, hz=1.0, target=80.0):
|
|
494
|
+
self.loop = loop
|
|
495
|
+
self.minimum = int(minimum)
|
|
496
|
+
self.maximum = int(maximum or 0)
|
|
497
|
+
self.hz = hz
|
|
498
|
+
self.target = target
|
|
499
|
+
self.timer_handle = None
|
|
500
|
+
self.last_usage = 0
|
|
501
|
+
self.curr = int(start)
|
|
502
|
+
self.proc = psutil.Process(os.getpid())
|
|
503
|
+
|
|
504
|
+
def __enter__(self):
|
|
505
|
+
if self.proc and self.maximum:
|
|
506
|
+
self.proc.cpu_percent() # initialize previous value
|
|
507
|
+
self.timer_handle = self.loop.call_later(self.hz, self.schedule)
|
|
508
|
+
return self
|
|
509
|
+
|
|
510
|
+
def __exit__(self, type_, value, tb):
|
|
511
|
+
if self.timer_handle:
|
|
512
|
+
self.timer_handle.cancel()
|
|
513
|
+
|
|
514
|
+
def schedule(self):
|
|
515
|
+
# re-schedule
|
|
516
|
+
self.timer_handle = self.loop.call_later(self.hz, self.schedule)
|
|
517
|
+
|
|
518
|
+
cpu_usage = self.proc.cpu_percent()
|
|
519
|
+
self.last_usage = cpu_usage
|
|
520
|
+
_logger.debug("current cpu_usage=%s", cpu_usage)
|
|
521
|
+
if cpu_usage > self.target:
|
|
522
|
+
self.change_by(0.5)
|
|
523
|
+
elif cpu_usage > self.target * 0.8:
|
|
524
|
+
pass
|
|
525
|
+
elif cpu_usage > self.target * 0.2:
|
|
526
|
+
self.change_by(1.2)
|
|
527
|
+
else:
|
|
528
|
+
self.change_by(1.5)
|
|
529
|
+
|
|
530
|
+
def change_by(self, rate):
|
|
531
|
+
new_curr = int(self.curr * rate)
|
|
532
|
+
# округлим в нужную сторону
|
|
533
|
+
if new_curr == self.curr:
|
|
534
|
+
if rate > 1:
|
|
535
|
+
new_curr += 1
|
|
536
|
+
elif rate < 1:
|
|
537
|
+
new_curr -= 1
|
|
538
|
+
# ограничим пределами
|
|
539
|
+
if new_curr < self.curr:
|
|
540
|
+
new_curr = max(self.minimum, new_curr)
|
|
541
|
+
else:
|
|
542
|
+
if self.maximum is not None:
|
|
543
|
+
new_curr = min(self.maximum, new_curr)
|
|
544
|
+
|
|
545
|
+
if new_curr < self.curr:
|
|
546
|
+
_logger.info("decreasing max_slots %d -> %d, cpu_usage=%.1f", self.curr, new_curr, self.last_usage)
|
|
547
|
+
elif new_curr > self.curr:
|
|
548
|
+
_logger.info("increasing max_slots %d -> %d, cpu_usage=%.1f", self.curr, new_curr, self.last_usage)
|
|
549
|
+
|
|
550
|
+
# new_curr не делаем меньше 0, иначе не сможем его увеличить
|
|
551
|
+
self.curr = max(new_curr, 1)
|
annet/filtering.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Type
|
|
3
|
+
|
|
4
|
+
from annet.connectors import Connector
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _FiltererConnector(Connector["Filterer"]):
|
|
8
|
+
name = "Filterer"
|
|
9
|
+
ep_name = "filterer"
|
|
10
|
+
|
|
11
|
+
def _get_default(self) -> Type["Filterer"]:
|
|
12
|
+
return NopFilterer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
filterer_connector = _FiltererConnector()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Filterer(abc.ABC):
|
|
19
|
+
@abc.abstractmethod
|
|
20
|
+
def for_ifaces(self, device, ifnames) -> str:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abc.abstractmethod
|
|
24
|
+
def for_peers(self, device, peers_allowed) -> str:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abc.abstractmethod
|
|
28
|
+
def for_policies(self, device, policies_allowed) -> str:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NopFilterer(Filterer):
|
|
33
|
+
def for_ifaces(self, device, ifnames) -> str:
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
def for_peers(self, device, peers_allowed) -> str:
|
|
37
|
+
return ""
|
|
38
|
+
|
|
39
|
+
def for_policies(self, device, policies_allowed) -> str:
|
|
40
|
+
return ""
|