annet 0.16.34__py3-none-any.whl → 0.16.35__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.

@@ -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
@@ -32,7 +32,7 @@ undo route-distinguisher *
32
32
  dialog: Warning: All VPN targets of this EVPN instance and all EVPN address family-related configurations under BGP will be deleted. Continue? [Y/N]: ::: Y
33
33
 
34
34
  save
35
- dialog: Warning: The current configuration will be written to the device. Continue? [Y/N] ::: Y
35
+ dialog: /Warning: The current configuration will be written to the device. Continue\? \[Y/N\]:?/ ::: Y
36
36
  dialog: Warning: The current configuration will be written to the device. Are you sure to continue? [Y/N]: ::: Y
37
37
  dialog: Are you sure to continue?[Y/N] ::: Y
38
38
  dialog: Are you sure to continue? [Y/N] ::: Y
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: annet
3
- Version: 0.16.34
3
+ Version: 0.16.35
4
4
  Summary: annet
5
5
  Home-page: https://github.com/annetutil/annet
6
6
  License: MIT
@@ -34,13 +34,13 @@ annet/adapters/netbox/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
34
34
  annet/adapters/netbox/common/client.py,sha256=PaxHG4W9H8_uunIwMBNYkLq4eQJYoO6p6gY-ciQs7Nc,2563
35
35
  annet/adapters/netbox/common/manufacturer.py,sha256=LAPT6OlV_ew96GhtwNrCpeiT0IGrg2_9__MMdZk431U,1733
36
36
  annet/adapters/netbox/common/models.py,sha256=Xq6Dc3kY9_QyvS9DiKEq1AxjTxiF4qEKhs1EMtBw-k4,7384
37
- annet/adapters/netbox/common/query.py,sha256=ziUFM7cUEbEIf3k1szTll4aO-OCUa-2Ogxbebi7Tegs,646
37
+ annet/adapters/netbox/common/query.py,sha256=mv5WlU6gGge8gUYwloXDXEABmfP5teYyq8DyBtGdFkw,1761
38
38
  annet/adapters/netbox/common/status_client.py,sha256=XXx0glomaBaglmkUEy6YtFOxQQkHb59CDA0h1I-IhxM,592
39
39
  annet/adapters/netbox/common/storage_opts.py,sha256=5tt6wxUUJTIzNbOVXMnYBwZedNAIqYlve3YWl6GdbZM,1197
40
40
  annet/adapters/netbox/v24/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  annet/adapters/netbox/v24/storage.py,sha256=THI592VLx3ehixsPZQ1Ko3mYIAZQbnDY-toIziqBbL8,6005
42
42
  annet/adapters/netbox/v37/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- annet/adapters/netbox/v37/storage.py,sha256=fBs7bUyk-3KnGpuygf8skPwHZfzxPJmCVCpfzifRrYo,11458
43
+ annet/adapters/netbox/v37/storage.py,sha256=noG-Lt1ZRiXm_uJE8xTRBsEvnEWcB1p2Uszuz6zECiI,12721
44
44
  annet/annlib/__init__.py,sha256=fT1l4xV5fqqg8HPw9HqmZVN2qwS8i6X1aIm2zGDjxKY,252
45
45
  annet/annlib/command.py,sha256=uuBddMQphtn8P5MO5kzIa8_QrtMns-k05VeKv1bcAuA,1043
46
46
  annet/annlib/diff.py,sha256=MZ6eQAU3cadQp8KaSE6uAYFtcfMDCIe_eNuVROnYkCk,4496
@@ -147,7 +147,7 @@ annet/rulebook/texts/b4com.rul,sha256=OiQroBySk33he1YNN3jOn5sGL_WDBJEejDI6F7jIWo
147
147
  annet/rulebook/texts/cisco.deploy,sha256=Hu0NkcGv3f1CWUrnbzI3eQOPXJxtH4NNOPRV68IrW4U,1226
148
148
  annet/rulebook/texts/cisco.order,sha256=OvNHMNqkCc-DN2dEjLCTKv_7ZhiaHt4q2X4Y4Z8dvR4,1901
149
149
  annet/rulebook/texts/cisco.rul,sha256=jgL5_xnSwd_H4E8cx4gcneSvJC5W1zz6_BWSb64iuxI,3017
150
- annet/rulebook/texts/huawei.deploy,sha256=azEC6_jQRzwnTSrNgag0hHh6L7hezS_eMk6ZDZfWyXI,10444
150
+ annet/rulebook/texts/huawei.deploy,sha256=uUsZCHUrC5Zyb_MePrR5svnE1QyGQlg7UxcKf00sJyg,10451
151
151
  annet/rulebook/texts/huawei.order,sha256=ENllPX4kO6xNw2mUQcx11yhxo3tKStZ5mUyc0C6s3d0,10657
152
152
  annet/rulebook/texts/huawei.rul,sha256=02Fi1RG4YYea2clHCluBuJDKNbT0hS9jtsk6_h6GK8k,12958
153
153
  annet/rulebook/texts/juniper.rul,sha256=EmtrEJZesnmc2nXjURRD2G0WOq4zLluI_PNupKhSOJs,2654
@@ -176,10 +176,10 @@ annet_generators/rpl_example/generator.py,sha256=zndIGfV4ZlTxPgAGYs7bMQvTc_tYScO
176
176
  annet_generators/rpl_example/items.py,sha256=Ez1RF5YhcXNCusBmeApIjRL3rBlMazNZd29Gpw1_IsA,766
177
177
  annet_generators/rpl_example/mesh.py,sha256=z_WgfDZZ4xnyh3cSf75igyH09hGvtexEVwy1gCD_DzA,288
178
178
  annet_generators/rpl_example/route_policy.py,sha256=z6nPb0VDeQtKD1NIg9sFvmUxBD5tVs2frfNIuKdM-5c,2318
179
- annet-0.16.34.dist-info/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
180
- annet-0.16.34.dist-info/LICENSE,sha256=yPxl7dno02Pw7gAcFPIFONzx_gapwDoPXsIsh6Y7lC0,1079
181
- annet-0.16.34.dist-info/METADATA,sha256=C2fqE9miXLSOMaYpyOLpD0LfEOsFpbFvndCqLqdOEhw,854
182
- annet-0.16.34.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
183
- annet-0.16.34.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
184
- annet-0.16.34.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
185
- annet-0.16.34.dist-info/RECORD,,
179
+ annet-0.16.35.dist-info/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
180
+ annet-0.16.35.dist-info/LICENSE,sha256=yPxl7dno02Pw7gAcFPIFONzx_gapwDoPXsIsh6Y7lC0,1079
181
+ annet-0.16.35.dist-info/METADATA,sha256=XiL8MGmXK4eW_mXEUkn6P6AoDJYlKpqdzAeom1JFr8Q,854
182
+ annet-0.16.35.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
183
+ annet-0.16.35.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
184
+ annet-0.16.35.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
185
+ annet-0.16.35.dist-info/RECORD,,