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

Files changed (36) hide show
  1. annet/adapters/__init__.py +0 -0
  2. annet/adapters/netbox/__init__.py +0 -0
  3. annet/adapters/netbox/common/__init__.py +0 -0
  4. annet/adapters/netbox/common/client.py +87 -0
  5. annet/adapters/netbox/common/manufacturer.py +62 -0
  6. annet/adapters/netbox/common/models.py +98 -0
  7. annet/adapters/netbox/common/query.py +23 -0
  8. annet/adapters/netbox/common/status_client.py +24 -0
  9. annet/adapters/netbox/common/storage_opts.py +14 -0
  10. annet/adapters/netbox/provider.py +34 -0
  11. annet/adapters/netbox/v24/__init__.py +0 -0
  12. annet/adapters/netbox/v24/api_models.py +72 -0
  13. annet/adapters/netbox/v24/client.py +59 -0
  14. annet/adapters/netbox/v24/storage.py +190 -0
  15. annet/adapters/netbox/v37/__init__.py +0 -0
  16. annet/adapters/netbox/v37/api_models.py +37 -0
  17. annet/adapters/netbox/v37/client.py +62 -0
  18. annet/adapters/netbox/v37/storage.py +143 -0
  19. annet/annlib/jsontools.py +23 -0
  20. annet/api/__init__.py +18 -6
  21. annet/cli.py +6 -2
  22. annet/cli_args.py +10 -0
  23. annet/diff.py +1 -2
  24. annet/gen.py +34 -4
  25. annet/generators/__init__.py +78 -67
  26. annet/output.py +3 -1
  27. {annet-0.6.dist-info → annet-0.8.dist-info}/METADATA +3 -1
  28. {annet-0.6.dist-info → annet-0.8.dist-info}/RECORD +33 -17
  29. {annet-0.6.dist-info → annet-0.8.dist-info}/WHEEL +1 -1
  30. annet-0.8.dist-info/entry_points.txt +5 -0
  31. {annet-0.6.dist-info → annet-0.8.dist-info}/top_level.txt +0 -1
  32. annet-0.6.dist-info/entry_points.txt +0 -6
  33. annet_nbexport/__init__.py +0 -220
  34. annet_nbexport/main.py +0 -46
  35. {annet-0.6.dist-info → annet-0.8.dist-info}/AUTHORS +0 -0
  36. {annet-0.6.dist-info → annet-0.8.dist-info}/LICENSE +0 -0
@@ -0,0 +1,37 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import List, Optional, Any
4
+
5
+ from annet.adapters.netbox.common.models import (
6
+ Entity, Label, DeviceType, DeviceIp,
7
+ )
8
+
9
+
10
+ @dataclass
11
+ class Interface(Entity):
12
+ device: Entity
13
+ enabled: bool
14
+ display: str = "" # added in 3.x
15
+
16
+
17
+ @dataclass
18
+ class Device(Entity):
19
+ display: str # renamed in 3.x from display_name
20
+ device_type: DeviceType
21
+ device_role: Entity
22
+ tenant: Optional[Entity]
23
+ platform: Optional[Entity]
24
+ serial: str
25
+ asset_tag: Optional[str]
26
+ site: Entity
27
+ rack: Optional[Entity]
28
+ position: Optional[float]
29
+ face: Optional[Label]
30
+ status: Label
31
+ primary_ip: Optional[DeviceIp]
32
+ primary_ip4: Optional[DeviceIp]
33
+ primary_ip6: Optional[DeviceIp]
34
+ tags: List[Entity]
35
+ custom_fields: dict[str, Any]
36
+ created: datetime
37
+ last_updated: datetime
@@ -0,0 +1,62 @@
1
+ from datetime import datetime
2
+ from typing import List, Optional
3
+
4
+ import dateutil.parser
5
+ from adaptix import Retort, loader
6
+ from dataclass_rest import get
7
+ from dataclass_rest.client_protocol import FactoryProtocol
8
+
9
+ from annet.adapters.netbox.common.client import (
10
+ BaseNetboxClient, collect, PagingResponse,
11
+ )
12
+ from annet.adapters.netbox.common.models import IpAddress
13
+ from .api_models import Device, Interface
14
+
15
+
16
+ class NetboxV37(BaseNetboxClient):
17
+ def _init_response_body_factory(self) -> FactoryProtocol:
18
+ return Retort(recipe=[
19
+ loader(datetime, dateutil.parser.parse)
20
+ ])
21
+
22
+ @get("dcim/interfaces")
23
+ def interfaces(
24
+ self,
25
+ device_id: Optional[List[int]] = None,
26
+ limit: int = 20,
27
+ offset: int = 0,
28
+ ) -> PagingResponse[Interface]:
29
+ pass
30
+
31
+ all_interfaces = collect(interfaces, field="device_id")
32
+
33
+ @get("ipam/ip-addresses")
34
+ def ip_addresses(
35
+ self,
36
+ interface_id: Optional[List[int]] = None,
37
+ limit: int = 20,
38
+ offset: int = 0,
39
+ ) -> PagingResponse[IpAddress]:
40
+ pass
41
+
42
+ all_ip_addresses = collect(ip_addresses, field="interface_id")
43
+
44
+ @get("dcim/devices")
45
+ def devices(
46
+ self,
47
+ name: Optional[List[str]] = None,
48
+ name__ic: Optional[List[str]] = None,
49
+ tag: Optional[List[str]] = None,
50
+ limit: int = 20,
51
+ offset: int = 0,
52
+ ) -> PagingResponse[Device]:
53
+ pass
54
+
55
+ all_devices = collect(devices)
56
+
57
+ @get("dcim/devices/{device_id}")
58
+ def get_device(
59
+ self,
60
+ device_id: int,
61
+ ) -> Device:
62
+ pass
@@ -0,0 +1,143 @@
1
+ from logging import getLogger
2
+ from typing import Optional, List
3
+
4
+ from adaptix import P
5
+ from adaptix.conversion import impl_converter, link
6
+
7
+ from annet.adapters.netbox.common import models
8
+ from annet.adapters.netbox.common.manufacturer import (
9
+ is_supported, get_hw, get_breed,
10
+ )
11
+ from annet.adapters.netbox.common.query import NetboxQuery
12
+ from annet.adapters.netbox.common.storage_opts import NetboxStorageOpts
13
+ from annet.annlib.netdev.views.hardware import HardwareView
14
+ from annet.storage import Storage
15
+ from . import api_models
16
+ from .client import NetboxV37
17
+
18
+ logger = getLogger(__name__)
19
+
20
+
21
+ @impl_converter(recipe=[
22
+ link(P[api_models.Device].name, P[models.NetboxDevice].hostname),
23
+ link(P[api_models.Device].name, P[models.NetboxDevice].fqdn),
24
+ ])
25
+ def extend_device_base(
26
+ device: api_models.Device,
27
+ interfaces: List[models.Interface],
28
+ hw: Optional[HardwareView],
29
+ breed: str,
30
+ neighbours_ids: List[int],
31
+ ) -> models.NetboxDevice:
32
+ ...
33
+
34
+
35
+ def extend_device(
36
+ device: api_models.Device,
37
+ ) -> models.NetboxDevice:
38
+ return extend_device_base(
39
+ device=device,
40
+ interfaces=[],
41
+ breed=get_breed(
42
+ device.device_type.manufacturer.name,
43
+ device.device_type.model,
44
+ ),
45
+ hw=get_hw(
46
+ device.device_type.manufacturer.name,
47
+ device.device_type.model,
48
+ ),
49
+ neighbours_ids=[],
50
+ )
51
+
52
+
53
+ @impl_converter
54
+ def extend_interface(
55
+ interface: api_models.Interface, ip_addresses: List[models.IpAddress],
56
+ ) -> models.Interface:
57
+ ...
58
+
59
+
60
+ class NetboxStorageV37(Storage):
61
+ def __init__(self, opts: Optional[NetboxStorageOpts] = None):
62
+ self.netbox = NetboxV37(
63
+ url=opts.url,
64
+ token=opts.token,
65
+ )
66
+
67
+ def __enter__(self):
68
+ return self
69
+
70
+ def __exit__(self, _, __, ___):
71
+ pass
72
+
73
+ def resolve_object_ids_by_query(self, query: NetboxQuery):
74
+ return [
75
+ d.id for d in self._load_devices(query)
76
+ ]
77
+
78
+ def resolve_fdnds_by_query(self, query: NetboxQuery):
79
+ return [
80
+ d.name for d in self._load_devices(query)
81
+ ]
82
+
83
+ def make_devices(
84
+ self,
85
+ query: NetboxQuery,
86
+ preload_neighbors=False,
87
+ use_mesh=None,
88
+ preload_extra_fields=False,
89
+ **kwargs,
90
+ ) -> List[models.NetboxDevice]:
91
+ device_ids = {
92
+ device.id: extend_device(device=device)
93
+ for device in self._load_devices(query)
94
+ }
95
+ if not device_ids:
96
+ return []
97
+
98
+ interfaces = self._load_interfaces(list(device_ids))
99
+ for interface in interfaces:
100
+ device_ids[interface.device.id].interfaces.append(interface)
101
+ return list(device_ids.values())
102
+
103
+ def _load_devices(self, query: NetboxQuery) -> List[api_models.Device]:
104
+ return [
105
+ device
106
+ for device in self.netbox.all_devices(
107
+ name__ic=query.globs,
108
+ ).results
109
+ if _match_query(query, device)
110
+ if is_supported(device.device_type.manufacturer.name)
111
+ ]
112
+
113
+ def _load_interfaces(self, device_ids: List[int]) -> List[
114
+ models.Interface]:
115
+ interfaces = self.netbox.all_interfaces(device_id=device_ids)
116
+ extended_ifaces = {
117
+ interface.id: extend_interface(interface, [])
118
+ for interface in interfaces.results
119
+ }
120
+
121
+ ips = self.netbox.all_ip_addresses(interface_id=list(extended_ifaces))
122
+ for ip in ips.results:
123
+ extended_ifaces[ip.assigned_object_id].ip_addresses.append(ip)
124
+ return list(extended_ifaces.values())
125
+
126
+ def get_device(
127
+ self, obj_id, preload_neighbors=False, use_mesh=None,
128
+ **kwargs,
129
+ ) -> models.NetboxDevice:
130
+ device = self.netbox.get_device(obj_id)
131
+ res = extend_device(device=device)
132
+ res.interfaces = self._load_interfaces([device.id])
133
+ return res
134
+
135
+ def flush_perf(self):
136
+ pass
137
+
138
+
139
+ def _match_query(query: NetboxQuery, device_data: api_models.Device) -> bool:
140
+ for subquery in query.globs:
141
+ if subquery.strip() in device_data.name:
142
+ return True
143
+ return False
annet/annlib/jsontools.py CHANGED
@@ -87,3 +87,26 @@ def apply_patch(content: Optional[bytes], patch_bytes: bytes) -> bytes:
87
87
 
88
88
  new_contents = format_json(new_doc, stable=True).encode()
89
89
  return new_contents
90
+
91
+
92
+ def apply_acl_filters(content: Dict[str, Any], filters: List[str]) -> Dict[str, Any]:
93
+ result = {}
94
+ for f in filters:
95
+ pointer = jsonpointer.JsonPointer(f)
96
+
97
+ try:
98
+ part = pointer.get(copy.deepcopy(content))
99
+
100
+ sub_tree = result
101
+ for i in pointer.get_parts():
102
+ if i not in sub_tree:
103
+ sub_tree[i] = {}
104
+ sub_tree = sub_tree[i]
105
+
106
+ patch = jsonpatch.JsonPatch([{"op": "add", "path": f, "value": part}])
107
+ result = patch.apply(result)
108
+ except jsonpointer.JsonPointerException:
109
+ # no value found in new_fragment by the pointer, skip the ACL item
110
+ continue
111
+
112
+ return result
annet/api/__init__.py CHANGED
@@ -188,7 +188,9 @@ def _print_pre_as_diff(pre, show_rules, indent, file=None, _level=0):
188
188
  def log_host_progress_cb(pool: Parallel, task_result: TaskResult):
189
189
  progress_logger = get_logger("progress")
190
190
  args = cast(cli_args.QueryOptions, pool.args[0])
191
- with storage_connector.get().storage()(args) as storage:
191
+ connector = storage_connector.get()
192
+ storage_opts = connector.opts().from_cli_opts(args)
193
+ with connector.storage()(storage_opts) as storage:
192
194
  hosts = storage.resolve_fdnds_by_query(args.query)
193
195
  perc = int(pool.tasks_done / len(hosts) * 100)
194
196
  fqdn = hosts[task_result.device_id]
@@ -210,7 +212,9 @@ def log_host_progress_cb(pool: Parallel, task_result: TaskResult):
210
212
  # =====
211
213
  def gen(args: cli_args.ShowGenOptions):
212
214
  """ Сгенерировать конфиг для устройств """
213
- with storage_connector.get().storage()(args) as storage:
215
+ connector = storage_connector.get()
216
+ storage_opts = connector.opts().from_cli_opts(args)
217
+ with connector.storage()(storage_opts) as storage:
214
218
  loader = ann_gen.Loader(storage, args)
215
219
  stdin = args.stdin(storage=storage, filter_acl=args.filter_acl, config=None)
216
220
 
@@ -243,7 +247,9 @@ def _diff_files(old_files, new_files, context=3):
243
247
  def patch(args: cli_args.ShowPatchOptions):
244
248
  """ Сгенерировать патч для устройств """
245
249
  global live_configs # pylint: disable=global-statement
246
- with storage_connector.get().storage()(args) as storage:
250
+ connector = storage_connector.get()
251
+ storage_opts = connector.opts().from_cli_opts(args)
252
+ with connector.storage()(storage_opts) as storage:
247
253
  loader = ann_gen.Loader(storage, args)
248
254
  if args.config == "running":
249
255
  fetcher = annet.deploy.fetcher_connector.get()
@@ -286,7 +292,9 @@ def _patch_worker(device_id, args: cli_args.ShowPatchOptions, stdin, loader: ann
286
292
  # =====
287
293
  def res_diff_patch(device_id, args: cli_args.ShowPatchOptions, stdin, loader: ann_gen.Loader, filterer: filtering.Filterer) -> Iterable[
288
294
  Tuple[OldNewResult, Dict, Dict]]:
289
- with storage_connector.get().storage()(args) as storage:
295
+ connector = storage_connector.get()
296
+ storage_opts = connector.opts().from_cli_opts(args)
297
+ with connector.storage()(storage_opts) as storage:
290
298
  for res in ann_gen.old_new(
291
299
  args,
292
300
  storage,
@@ -314,7 +322,9 @@ def res_diff_patch(device_id, args: cli_args.ShowPatchOptions, stdin, loader: an
314
322
 
315
323
  def diff(args: cli_args.DiffOptions, loader: ann_gen.Loader, filterer: filtering.Filterer) -> Mapping[Device, Union[Diff, PCDiff]]:
316
324
  ret = {}
317
- with storage_connector.get().storage()(args) as storage:
325
+ connector = storage_connector.get()
326
+ storage_opts = connector.opts().from_cli_opts(args)
327
+ with connector.storage()(storage_opts) as storage:
318
328
  for res in ann_gen.old_new(
319
329
  args,
320
330
  storage,
@@ -624,7 +634,9 @@ def deploy(args: cli_args.DeployOptions) -> ExitCode:
624
634
  """ Сгенерировать конфиг для устройств и задеплоить его """
625
635
  ret: ExitCode = 0
626
636
  deployer = Deployer(args)
627
- with storage_connector.get().storage()(args) as storage:
637
+ connector = storage_connector.get()
638
+ storage_opts = connector.opts().from_cli_opts(args)
639
+ with connector.storage()(storage_opts) as storage:
628
640
  global live_configs # pylint: disable=global-statement
629
641
  loader = ann_gen.Loader(storage, args)
630
642
  filterer = filtering.filterer_connector.get()
annet/cli.py CHANGED
@@ -54,7 +54,9 @@ def show_current(args: cli_args.QueryOptions, config, arg_out: cli_args.FileOutO
54
54
  entire_data = ""
55
55
  yield (output_driver.entire_config_dest_path(device, entire_path), entire_data, False)
56
56
 
57
- with storage_connector.get().storage()(args) as storage:
57
+ connector = storage_connector.get()
58
+ storage_opts = connector.opts().from_cli_opts(args)
59
+ with connector.storage()(storage_opts) as storage:
58
60
  ids = storage.resolve_object_ids_by_query(args.query)
59
61
  if not ids:
60
62
  get_logger().error("No devices found for %s", args.query)
@@ -80,7 +82,9 @@ def gen(args: cli_args.ShowGenOptions):
80
82
  @subcommand(cli_args.ShowDiffOptions)
81
83
  def diff(args: cli_args.ShowDiffOptions):
82
84
  """ Сгенерировать конфиг для устройств и показать дифф по рулбуку с текущим """
83
- with storage_connector.get().storage()(args) as storage:
85
+ connector = storage_connector.get()
86
+ storage_opts = connector.opts().from_cli_opts(args)
87
+ with connector.storage()(storage_opts) as storage:
84
88
  filterer = filtering.filterer_connector.get()
85
89
  loader = Loader(storage, args)
86
90
  output_driver_connector.get().write_output(
annet/cli_args.py CHANGED
@@ -433,6 +433,11 @@ class FileOutOptions(ArgGroup):
433
433
  no_label = opt_no_label
434
434
  no_color = opt_no_color
435
435
 
436
+ def __init__(self, *args, **kwargs):
437
+ super().__init__(*args, **kwargs)
438
+ if self.dest:
439
+ self.no_color = True
440
+
436
441
 
437
442
  class DiffOptions(GenOptions, ComocutorOptions):
438
443
  clear = opt_clear
@@ -467,6 +472,11 @@ class ShowDiffOptions(DiffOptions, FileOutOptions):
467
472
  show_rules = opt_show_rules
468
473
  no_collapse = opt_no_collapse
469
474
 
475
+ def __init__(self, *args, **kwargs):
476
+ super().__init__(*args, **kwargs)
477
+ if self.dest:
478
+ self.no_collapse = True
479
+
470
480
 
471
481
  class ShowPatchOptions(PatchOptions, FileOutOptions):
472
482
  indent = opt_indent
annet/diff.py CHANGED
@@ -30,8 +30,7 @@ def gen_sort_diff(
30
30
  :param diffs: Маппинг устройства в дифф
31
31
  :param args: Параметры коммандной строки
32
32
  """
33
- # NOCDEV-2201 non-null --dest implies --no-collapse
34
- if args.no_collapse or args.dest:
33
+ if args.no_collapse:
35
34
  devices_to_diff = {(dev,): diff for dev, diff in diffs.items()}
36
35
  else:
37
36
  non_pc_diffs = {dev: diff for dev, diff in diffs.items() if not isinstance(diff, PCDiff)}
annet/gen.py CHANGED
@@ -182,6 +182,7 @@ def _old_new_per_device(ctx: OldNewDeviceContext, device: Device, filterer: Filt
182
182
  if not ctx.args.no_acl:
183
183
  acl_rules = generators.compile_acl_text(res.acl_text(), device.hw.vendor)
184
184
  old = (old and patching.apply_acl(old, acl_rules))
185
+
185
186
  new = patching.apply_acl(
186
187
  new,
187
188
  acl_rules,
@@ -232,11 +233,29 @@ def _old_new_per_device(ctx: OldNewDeviceContext, device: Device, filterer: Filt
232
233
 
233
234
  entire_results = res.entire_results
234
235
  json_fragment_results = res.json_fragment_results
236
+ old_json_fragment_files = old_files.json_fragment_files
237
+
235
238
  new_files = res.new_files()
236
- new_json_fragment_files = res.new_json_fragment_files(old_files.json_fragment_files)
239
+ new_json_fragment_files = res.new_json_fragment_files(old_json_fragment_files)
240
+
237
241
  if ctx.args.acl_safe:
238
242
  safe_new_files = res.new_files(safe=True)
239
243
 
244
+ filters = build_filter_text(filterer, device, ctx.stdin, ctx.args, ctx.config).split("\n")
245
+
246
+ for file_name in new_json_fragment_files:
247
+ new_json_fragment_files = _update_json_config(
248
+ new_json_fragment_files,
249
+ file_name,
250
+ jsontools.apply_acl_filters(new_json_fragment_files[file_name][0], filters)
251
+ )
252
+ for file_name in old_json_fragment_files:
253
+ old_json_fragment_files = _update_json_config(
254
+ old_json_fragment_files,
255
+ file_name,
256
+ jsontools.apply_acl_filters(old_json_fragment_files[file_name][0], filters)
257
+ )
258
+
240
259
  if ctx.args.profile:
241
260
  perf = res.perf_mesures()
242
261
  combined_perf[ALL_GENS] = {"total": time.monotonic() - start}
@@ -253,7 +272,7 @@ def _old_new_per_device(ctx: OldNewDeviceContext, device: Device, filterer: Filt
253
272
  new_files=new_files,
254
273
  partial_result=partial_results,
255
274
  entire_result=entire_results,
256
- old_json_fragment_files=old_files.json_fragment_files,
275
+ old_json_fragment_files=old_json_fragment_files,
257
276
  new_json_fragment_files=new_json_fragment_files,
258
277
  json_fragment_result=json_fragment_results,
259
278
  implicit_rules=implicit_rules,
@@ -266,6 +285,13 @@ def _old_new_per_device(ctx: OldNewDeviceContext, device: Device, filterer: Filt
266
285
  )
267
286
 
268
287
 
288
+ def _update_json_config(json_files, file_name, new_config):
289
+ file = list(json_files[file_name])
290
+ file[0] = new_config
291
+ json_files[file_name] = tuple(file)
292
+ return json_files
293
+
294
+
269
295
  @dataclasses.dataclass
270
296
  class DeviceDownloadedFiles:
271
297
  # map file path to file content for entire generators
@@ -425,7 +451,9 @@ def worker(device_id, args: ShowGenOptions, stdin, loader: "Loader", filterer: F
425
451
  if span:
426
452
  span.set_attribute("device.id", device_id)
427
453
 
428
- with storage_connector.get().storage()(args) as storage:
454
+ connector = storage_connector.get()
455
+ storage_opts = connector.opts().from_cli_opts(args)
456
+ with connector.storage()(storage_opts) as storage:
429
457
  for res in old_new(
430
458
  args,
431
459
  storage,
@@ -465,7 +493,9 @@ def worker(device_id, args: ShowGenOptions, stdin, loader: "Loader", filterer: F
465
493
 
466
494
 
467
495
  def old_new_worker(device_id, args: DeployOptions, config, stdin, loader: "Loader", filterer: Filterer):
468
- with storage_connector.get().storage()(args) as storage:
496
+ connector = storage_connector.get()
497
+ storage_opts = connector.opts().from_cli_opts(args)
498
+ with connector.storage()(storage_opts) as storage:
469
499
  yield from old_new(
470
500
  args,
471
501
  storage,