annet 0.16.34__py3-none-any.whl → 1.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.
- annet/adapters/fetchers/stub/fetcher.py +11 -5
- annet/adapters/netbox/common/query.py +36 -1
- annet/adapters/netbox/v37/storage.py +79 -42
- annet/api/__init__.py +32 -8
- annet/deploy.py +37 -343
- annet/deploy_ui.py +774 -0
- annet/gen.py +5 -5
- annet/lib.py +19 -3
- annet/rulebook/texts/huawei.deploy +1 -1
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/METADATA +1 -1
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/RECORD +16 -15
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/AUTHORS +0 -0
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/LICENSE +0 -0
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/WHEEL +0 -0
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/entry_points.txt +0 -0
- {annet-0.16.34.dist-info → annet-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -9,11 +9,17 @@ class StubFetcher(Fetcher, AdapterWithConfig):
|
|
|
9
9
|
def with_config(cls, **kwargs: Dict[str, Any]) -> Fetcher:
|
|
10
10
|
return cls(**kwargs)
|
|
11
11
|
|
|
12
|
-
def fetch_packages(self,
|
|
13
|
-
|
|
12
|
+
async def fetch_packages(self,
|
|
13
|
+
devices: list[Device],
|
|
14
|
+
processes: int = 1,
|
|
15
|
+
max_slots: int = 0,
|
|
16
|
+
) -> tuple[dict[Device, str], dict[Device, Any]]:
|
|
14
17
|
raise NotImplementedError()
|
|
15
18
|
|
|
16
|
-
def fetch(self,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
async def fetch(self,
|
|
20
|
+
devices: list[Device],
|
|
21
|
+
files_to_download: dict[str, list[str]] | None = None,
|
|
22
|
+
processes: int = 1,
|
|
23
|
+
max_slots: int = 0,
|
|
24
|
+
):
|
|
19
25
|
raise NotImplementedError()
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
1
2
|
from dataclasses import dataclass
|
|
2
|
-
from typing import List, Union, Iterable, Optional
|
|
3
|
+
from typing import cast, List, Union, Iterable, Optional, TypedDict
|
|
3
4
|
|
|
4
5
|
from annet.storage import Query
|
|
5
6
|
|
|
7
|
+
FIELD_VALUE_SEPARATOR = ":"
|
|
8
|
+
ALLOWED_GLOB_GROUPS = ["site", "tag", "role"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Filter(TypedDict, total=False):
|
|
12
|
+
site: list[str]
|
|
13
|
+
tag: list[str]
|
|
14
|
+
role: list[str]
|
|
15
|
+
name: list[str]
|
|
16
|
+
|
|
6
17
|
|
|
7
18
|
@dataclass
|
|
8
19
|
class NetboxQuery(Query):
|
|
@@ -22,5 +33,29 @@ class NetboxQuery(Query):
|
|
|
22
33
|
# We process every query host as a glob
|
|
23
34
|
return self.query
|
|
24
35
|
|
|
36
|
+
def parse_query(self) -> Filter:
|
|
37
|
+
query_groups = defaultdict(list)
|
|
38
|
+
for q in self.globs:
|
|
39
|
+
if FIELD_VALUE_SEPARATOR in q:
|
|
40
|
+
glob_type, param = q.split(FIELD_VALUE_SEPARATOR, 2)
|
|
41
|
+
if glob_type not in ALLOWED_GLOB_GROUPS:
|
|
42
|
+
raise Exception(f"unknown query type: '{glob_type}'")
|
|
43
|
+
if not param:
|
|
44
|
+
raise Exception(f"empty param for '{glob_type}'")
|
|
45
|
+
query_groups[glob_type].append(param)
|
|
46
|
+
else:
|
|
47
|
+
query_groups["name"].append(q)
|
|
48
|
+
|
|
49
|
+
query_groups.default_factory = None
|
|
50
|
+
return cast(Filter, query_groups)
|
|
51
|
+
|
|
25
52
|
def is_empty(self) -> bool:
|
|
26
53
|
return len(self.query) == 0
|
|
54
|
+
|
|
55
|
+
def is_host_query(self) -> bool:
|
|
56
|
+
if not self.globs:
|
|
57
|
+
return False
|
|
58
|
+
for q in self.globs:
|
|
59
|
+
if FIELD_VALUE_SEPARATOR in q:
|
|
60
|
+
return False
|
|
61
|
+
return True
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from logging import getLogger
|
|
2
|
-
from typing import Any, Optional, List, Union, Dict
|
|
3
|
-
from ipaddress import ip_interface
|
|
4
|
-
from collections import defaultdict
|
|
5
1
|
import ssl
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from ipaddress import ip_interface
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
from typing import Any, Optional, List, Union, Dict, cast
|
|
6
6
|
|
|
7
7
|
from adaptix import P
|
|
8
8
|
from adaptix.conversion import impl_converter, link, link_constant
|
|
@@ -13,7 +13,7 @@ from annet.adapters.netbox.common import models
|
|
|
13
13
|
from annet.adapters.netbox.common.manufacturer import (
|
|
14
14
|
get_hw, get_breed,
|
|
15
15
|
)
|
|
16
|
-
from annet.adapters.netbox.common.query import NetboxQuery
|
|
16
|
+
from annet.adapters.netbox.common.query import NetboxQuery, FIELD_VALUE_SEPARATOR
|
|
17
17
|
from annet.adapters.netbox.common.storage_opts import NetboxStorageOpts
|
|
18
18
|
from annet.annlib.netdev.views.hardware import HardwareView
|
|
19
19
|
from annet.storage import Storage, Device, Interface
|
|
@@ -101,6 +101,9 @@ class NetboxStorageV37(Storage):
|
|
|
101
101
|
self.exact_host_filter = opts.exact_host_filter
|
|
102
102
|
self.netbox = NetboxV37(url=url, token=token, ssl_context=ctx)
|
|
103
103
|
self._all_fqdns: Optional[list[str]] = None
|
|
104
|
+
self._id_devices: dict[int, models.NetboxDevice] = {}
|
|
105
|
+
self._name_devices: dict[str, models.NetboxDevice] = {}
|
|
106
|
+
self._short_name_devices: dict[str, models.NetboxDevice] = {}
|
|
104
107
|
|
|
105
108
|
def __enter__(self):
|
|
106
109
|
return self
|
|
@@ -136,6 +139,37 @@ class NetboxStorageV37(Storage):
|
|
|
136
139
|
) -> List[models.NetboxDevice]:
|
|
137
140
|
if isinstance(query, list):
|
|
138
141
|
query = NetboxQuery.new(query)
|
|
142
|
+
|
|
143
|
+
devices = []
|
|
144
|
+
if query.is_host_query():
|
|
145
|
+
globs = []
|
|
146
|
+
for glob in query.globs:
|
|
147
|
+
if glob in self._name_devices:
|
|
148
|
+
devices.append(self._name_devices[glob])
|
|
149
|
+
if glob in self._short_name_devices:
|
|
150
|
+
devices.append(self._short_name_devices[glob])
|
|
151
|
+
else:
|
|
152
|
+
globs.append(glob)
|
|
153
|
+
if not globs:
|
|
154
|
+
return devices
|
|
155
|
+
query = NetboxQuery.new(globs)
|
|
156
|
+
|
|
157
|
+
return devices + self._make_devices(
|
|
158
|
+
query=query,
|
|
159
|
+
preload_neighbors=preload_neighbors,
|
|
160
|
+
use_mesh=use_mesh,
|
|
161
|
+
preload_extra_fields=preload_extra_fields,
|
|
162
|
+
**kwargs
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _make_devices(
|
|
166
|
+
self,
|
|
167
|
+
query: NetboxQuery,
|
|
168
|
+
preload_neighbors=False,
|
|
169
|
+
use_mesh=None,
|
|
170
|
+
preload_extra_fields=False,
|
|
171
|
+
**kwargs,
|
|
172
|
+
) -> List[models.NetboxDevice]:
|
|
139
173
|
device_ids = {
|
|
140
174
|
device.id: extend_device(
|
|
141
175
|
device=device,
|
|
@@ -148,6 +182,9 @@ class NetboxStorageV37(Storage):
|
|
|
148
182
|
if not device_ids:
|
|
149
183
|
return []
|
|
150
184
|
|
|
185
|
+
for device in device_ids.values():
|
|
186
|
+
self._record_device(device)
|
|
187
|
+
|
|
151
188
|
interfaces = self._load_interfaces(list(device_ids))
|
|
152
189
|
neighbours = {x.id: x for x in self._load_neighbours(interfaces)}
|
|
153
190
|
neighbours_seen: dict[str, set] = defaultdict(set)
|
|
@@ -162,32 +199,22 @@ class NetboxStorageV37(Storage):
|
|
|
162
199
|
|
|
163
200
|
return list(device_ids.values())
|
|
164
201
|
|
|
202
|
+
def _record_device(self, device: models.NetboxDevice):
|
|
203
|
+
self._id_devices[device.id] = device
|
|
204
|
+
self._short_name_devices[device.name] = device
|
|
205
|
+
if not self.exact_host_filter:
|
|
206
|
+
short_name = device.name.split(".")[0]
|
|
207
|
+
self._short_name_devices[short_name] = device
|
|
208
|
+
|
|
165
209
|
def _load_devices(self, query: NetboxQuery) -> List[api_models.Device]:
|
|
166
210
|
if not query.globs:
|
|
167
211
|
return []
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
for grp, params in query_groups.items():
|
|
175
|
-
if not params:
|
|
176
|
-
continue
|
|
177
|
-
try:
|
|
178
|
-
new_devices = self.netbox.dcim_all_devices(**{grp: params}).results
|
|
179
|
-
except Exception as e:
|
|
180
|
-
# tag and site lookup returns 400 in case of unknown tag or site
|
|
181
|
-
if "is not one of the available choices" in str(e):
|
|
182
|
-
continue
|
|
183
|
-
raise
|
|
184
|
-
if grp == "name__ic":
|
|
185
|
-
new_devices = [device for device in new_devices if _match_query(query, device)]
|
|
186
|
-
for device in new_devices:
|
|
187
|
-
if device.id not in device_ids:
|
|
188
|
-
device_ids.add(device.id)
|
|
189
|
-
devices.extend(new_devices)
|
|
190
|
-
return devices
|
|
212
|
+
query_groups = parse_glob(self.exact_host_filter, query)
|
|
213
|
+
return [
|
|
214
|
+
device
|
|
215
|
+
for device in self.netbox.dcim_all_devices(**query_groups).results
|
|
216
|
+
if _match_query(self.exact_host_filter, query, device)
|
|
217
|
+
]
|
|
191
218
|
|
|
192
219
|
def _extend_interfaces(self, interfaces: List[models.Interface]) -> List[models.Interface]:
|
|
193
220
|
extended_ifaces = {
|
|
@@ -238,6 +265,9 @@ class NetboxStorageV37(Storage):
|
|
|
238
265
|
self, obj_id, preload_neighbors=False, use_mesh=None,
|
|
239
266
|
**kwargs,
|
|
240
267
|
) -> models.NetboxDevice:
|
|
268
|
+
if obj_id in self._id_devices:
|
|
269
|
+
return self._id_devices[obj_id]
|
|
270
|
+
|
|
241
271
|
device = self.netbox.dcim_device(obj_id)
|
|
242
272
|
interfaces = self._load_interfaces([device.id])
|
|
243
273
|
neighbours = self._load_neighbours(interfaces)
|
|
@@ -248,6 +278,7 @@ class NetboxStorageV37(Storage):
|
|
|
248
278
|
interfaces=interfaces,
|
|
249
279
|
neighbours=neighbours,
|
|
250
280
|
)
|
|
281
|
+
self._record_device(res)
|
|
251
282
|
return res
|
|
252
283
|
|
|
253
284
|
def flush_perf(self):
|
|
@@ -272,9 +303,18 @@ class NetboxStorageV37(Storage):
|
|
|
272
303
|
return res
|
|
273
304
|
|
|
274
305
|
|
|
275
|
-
def _match_query(query: NetboxQuery, device_data: api_models.Device) -> bool:
|
|
276
|
-
|
|
277
|
-
|
|
306
|
+
def _match_query(exact_host_filter: bool, query: NetboxQuery, device_data: api_models.Device) -> bool:
|
|
307
|
+
"""
|
|
308
|
+
Additional filtering after netbox due to limited backend logic.
|
|
309
|
+
"""
|
|
310
|
+
if exact_host_filter:
|
|
311
|
+
return True # nothing to check, all filtering is done by netbox
|
|
312
|
+
hostnames = [subquery.strip() for subquery in query.globs if FIELD_VALUE_SEPARATOR not in subquery]
|
|
313
|
+
if not hostnames:
|
|
314
|
+
return True # no hostnames to check
|
|
315
|
+
short_name = device_data.name.split(".")[0]
|
|
316
|
+
for hostname in hostnames:
|
|
317
|
+
if short_name == hostname or device_data.name == hostname:
|
|
278
318
|
return True
|
|
279
319
|
return False
|
|
280
320
|
|
|
@@ -294,20 +334,17 @@ def _hostname_dot_hack(raw_query: str) -> str:
|
|
|
294
334
|
if isinstance(raw_query, list):
|
|
295
335
|
for i, name in enumerate(raw_query):
|
|
296
336
|
raw_query[i] = add_dot(name)
|
|
337
|
+
elif isinstance(raw_query, str):
|
|
338
|
+
raw_query = add_dot(raw_query)
|
|
297
339
|
|
|
298
340
|
return raw_query
|
|
299
341
|
|
|
300
342
|
|
|
301
|
-
def parse_glob(
|
|
302
|
-
query_groups
|
|
303
|
-
|
|
304
|
-
if
|
|
305
|
-
|
|
306
|
-
if glob_type not in query_groups:
|
|
307
|
-
raise Exception(f"unknown query type: '{glob_type}'")
|
|
308
|
-
if not param:
|
|
309
|
-
raise Exception(f"empty param for '{glob_type}'")
|
|
310
|
-
query_groups[glob_type].append(param)
|
|
343
|
+
def parse_glob(exact_host_filter: bool, query: NetboxQuery) -> dict[str, list[str]]:
|
|
344
|
+
query_groups = cast(dict[str, list[str]], query.parse_query())
|
|
345
|
+
if names := query_groups.pop("name", None):
|
|
346
|
+
if exact_host_filter:
|
|
347
|
+
query_groups["name__ie"] = names
|
|
311
348
|
else:
|
|
312
|
-
query_groups["name__ic"]
|
|
349
|
+
query_groups["name__ic"] = [_hostname_dot_hack(name) for name in names]
|
|
313
350
|
return query_groups
|
annet/api/__init__.py
CHANGED
|
@@ -22,6 +22,8 @@ from typing import (
|
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
import colorama
|
|
25
|
+
import annet.deploy
|
|
26
|
+
import annet.deploy_ui
|
|
25
27
|
import annet.lib
|
|
26
28
|
from annet.annlib import jsontools
|
|
27
29
|
from annet.annlib.netdev.views.hardware import HardwareView
|
|
@@ -262,7 +264,7 @@ def patch(args: cli_args.ShowPatchOptions, loader: ann_gen.Loader):
|
|
|
262
264
|
global live_configs # pylint: disable=global-statement
|
|
263
265
|
if args.config == "running":
|
|
264
266
|
fetcher = annet.deploy.get_fetcher()
|
|
265
|
-
live_configs = fetcher.fetch(loader.devices, processes=args.parallel)
|
|
267
|
+
live_configs = annet.lib.do_async(fetcher.fetch(loader.devices, processes=args.parallel))
|
|
266
268
|
stdin = args.stdin(filter_acl=args.filter_acl, config=args.config)
|
|
267
269
|
|
|
268
270
|
filterer = filtering.filterer_connector.get()
|
|
@@ -567,7 +569,7 @@ class Deployer:
|
|
|
567
569
|
if not diff_obj:
|
|
568
570
|
self.empty_diff_hostnames.update(dev.hostname for dev in devices)
|
|
569
571
|
if not self.args.no_ask_deploy:
|
|
570
|
-
#
|
|
572
|
+
# разобьём список устройств на несколько линий
|
|
571
573
|
dest_name = ""
|
|
572
574
|
try:
|
|
573
575
|
_, term_columns_str = os.popen("stty size", "r").read().split()
|
|
@@ -596,18 +598,18 @@ class Deployer:
|
|
|
596
598
|
return diff_lines
|
|
597
599
|
|
|
598
600
|
def ask_deploy(self) -> str:
|
|
599
|
-
return self._ask("y", annet.
|
|
601
|
+
return self._ask("y", annet.deploy_ui.AskConfirm(
|
|
600
602
|
text="\n".join(self.diff_lines()),
|
|
601
603
|
alternative_text="\n".join(self.cmd_lines),
|
|
602
604
|
))
|
|
603
605
|
|
|
604
606
|
def ask_rollback(self) -> str:
|
|
605
|
-
return self._ask("n", annet.
|
|
607
|
+
return self._ask("n", annet.deploy_ui.AskConfirm(
|
|
606
608
|
text="Execute rollback?\n",
|
|
607
609
|
alternative_text="",
|
|
608
610
|
))
|
|
609
611
|
|
|
610
|
-
def _ask(self, default_ans: str, ask: annet.
|
|
612
|
+
def _ask(self, default_ans: str, ask: annet.deploy_ui.AskConfirm) -> str:
|
|
611
613
|
# если filter_acl из stdin то с ним уже не получится работать как с терминалом
|
|
612
614
|
ans = default_ans
|
|
613
615
|
if not self.args.no_ask_deploy:
|
|
@@ -666,11 +668,22 @@ def deploy(
|
|
|
666
668
|
filterer: Filterer,
|
|
667
669
|
fetcher: Fetcher,
|
|
668
670
|
deploy_driver: DeployDriver,
|
|
671
|
+
) -> ExitCode:
|
|
672
|
+
return annet.lib.do_async(adeploy(args, loader, deployer, filterer, fetcher, deploy_driver))
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
async def adeploy(
|
|
676
|
+
args: cli_args.DeployOptions,
|
|
677
|
+
loader: ann_gen.Loader,
|
|
678
|
+
deployer: Deployer,
|
|
679
|
+
filterer: Filterer,
|
|
680
|
+
fetcher: Fetcher,
|
|
681
|
+
deploy_driver: DeployDriver,
|
|
669
682
|
) -> ExitCode:
|
|
670
683
|
""" Сгенерировать конфиг для устройств и задеплоить его """
|
|
671
684
|
ret: ExitCode = 0
|
|
672
685
|
global live_configs # pylint: disable=global-statement
|
|
673
|
-
live_configs = fetcher.fetch(devices=loader.devices, processes=args.parallel)
|
|
686
|
+
live_configs = await fetcher.fetch(devices=loader.devices, processes=args.parallel)
|
|
674
687
|
pool = ann_gen.OldNewParallel(args, loader, filterer)
|
|
675
688
|
|
|
676
689
|
for res in pool.generated_configs(loader.devices):
|
|
@@ -687,7 +700,18 @@ def deploy(
|
|
|
687
700
|
ans = deployer.ask_deploy()
|
|
688
701
|
if ans != "y":
|
|
689
702
|
return 2 ** 2
|
|
690
|
-
|
|
703
|
+
progress_bar = None
|
|
704
|
+
if sys.stdout.isatty() and not args.no_progress:
|
|
705
|
+
progress_bar = annet.deploy_ui.ProgressBars(odict([(device.fqdn, {}) for device in deploy_cmds]))
|
|
706
|
+
progress_bar.init()
|
|
707
|
+
with progress_bar:
|
|
708
|
+
progress_bar.start_terminal_refresher()
|
|
709
|
+
result = await deploy_driver.bulk_deploy(deploy_cmds, args, progress_bar=progress_bar)
|
|
710
|
+
await progress_bar.wait_for_exit()
|
|
711
|
+
progress_bar.screen.clear()
|
|
712
|
+
progress_bar.stop_terminal_refresher()
|
|
713
|
+
else:
|
|
714
|
+
result = await deploy_driver.bulk_deploy(deploy_cmds, args)
|
|
691
715
|
|
|
692
716
|
rolled_back = False
|
|
693
717
|
rollback_cmds = {deployer.fqdn_to_device[x]: cc for x, cc in result.original_states.items() if cc}
|
|
@@ -695,7 +719,7 @@ def deploy(
|
|
|
695
719
|
ans = deployer.ask_rollback()
|
|
696
720
|
if rollback_cmds and ans == "y":
|
|
697
721
|
rolled_back = True
|
|
698
|
-
|
|
722
|
+
await deploy_driver.bulk_deploy(rollback_cmds, args)
|
|
699
723
|
|
|
700
724
|
if not args.no_check_diff and not rolled_back:
|
|
701
725
|
deployer.check_diff(result, loader)
|