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.

@@ -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, devices: List[Device],
13
- processes: int = 1, max_slots: int = 0):
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, devices: List[Device],
17
- files_to_download: Dict[str, List[str]] = None,
18
- processes: int = 1, max_slots: int = 0):
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
- devices = []
169
- device_ids = set()
170
- query_groups = parse_glob(query.globs)
171
- if self.exact_host_filter:
172
- name_ies = [_hostname_dot_hack(query) for query in query_groups.pop("name__ic", [])]
173
- query_groups["name__ie"].extend(name_ies)
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
- for subquery in query.globs:
277
- if subquery.strip() in device_data.name:
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(globs: list[str]) -> dict[str, list[str]]:
302
- query_groups: dict[str, list[str]] = {"tag": [], "site": [], "name__ic": []}
303
- for q in globs:
304
- if ":" in q:
305
- glob_type, param = q.split(":", 2)
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"].append(q)
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.deploy.AskConfirm(
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.deploy.AskConfirm(
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.deploy.AskConfirm) -> str:
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
- result = annet.lib.do_async(deploy_driver.bulk_deploy(deploy_cmds, args))
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
- annet.lib.do_async(deploy_driver.bulk_deploy(rollback_cmds, args))
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)