annet 2.4.0__py3-none-any.whl → 2.5.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.

@@ -1,321 +1,91 @@
1
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
2
 
7
3
  from adaptix import P
8
- from adaptix.conversion import impl_converter, link, link_constant
9
- from annetbox.v37 import models as api_models
10
- from annetbox.v37.client_sync import NetboxV37
4
+ from adaptix.conversion import get_converter, link_function, link_constant, link
5
+ from annetbox.v37 import models as api_models, client_sync
11
6
 
12
- from annet.adapters.netbox.common import models
13
- from annet.adapters.netbox.common.manufacturer import (
14
- get_hw, get_breed,
15
- )
16
- from annet.adapters.netbox.common.query import NetboxQuery, FIELD_VALUE_SEPARATOR
17
- from annet.adapters.netbox.common.storage_opts import NetboxStorageOpts
18
- from annet.annlib.netdev.views.hardware import HardwareView
19
- from annet.storage import Storage, Device, Interface
7
+ from annet.adapters.netbox.common.adapter import NetboxAdapter, get_device_breed, get_device_hw
8
+ from annet.adapters.netbox.common.storage_base import BaseNetboxStorage
9
+ from annet.storage import Storage
10
+ from .models import IpAddressV37, NetboxDeviceV37, InterfaceV37, PrefixV37
20
11
 
21
- logger = getLogger(__name__)
22
12
 
23
-
24
- @impl_converter(recipe=[
25
- link(P[api_models.Device].name, P[models.NetboxDevice].hostname),
26
- link(P[api_models.Device].name, P[models.NetboxDevice].fqdn),
27
- ])
28
- def extend_device_base(
29
- device: api_models.Device,
30
- interfaces: List[models.Interface],
31
- hw: Optional[HardwareView],
32
- breed: str,
33
- storage: Storage,
34
- ) -> models.NetboxDevice:
35
- ...
36
-
37
-
38
- def extend_device(
39
- device: api_models.Device,
40
- interfaces: List[models.Interface],
41
- storage: Storage,
42
- ) -> models.NetboxDevice:
43
- platform_name: str = ""
44
- breed: str = ""
45
- hw = HardwareView("", "")
46
- if device.platform:
47
- platform_name = device.platform.name
48
- if device.device_type and device.device_type.manufacturer:
49
- breed = get_breed(
50
- device.device_type.manufacturer.name,
51
- device.device_type.model,
13
+ class NetboxV37Adapter(NetboxAdapter[NetboxDeviceV37, InterfaceV37, IpAddressV37, PrefixV37]):
14
+ def __init__(
15
+ self,
16
+ storage: Storage,
17
+ url: str,
18
+ token: str,
19
+ ssl_context: ssl.SSLContext | None,
20
+ threads: int,
21
+ ):
22
+ self.netbox = client_sync.NetboxV37(url=url, token=token, ssl_context=ssl_context, threads=threads)
23
+ self.convert_device = get_converter(
24
+ api_models.Device,
25
+ NetboxDeviceV37,
26
+ recipe=[
27
+ link_function(get_device_breed, P[NetboxDeviceV37].breed),
28
+ link_function(get_device_hw, P[NetboxDeviceV37].hw),
29
+ link_constant(P[NetboxDeviceV37].interfaces, factory=list),
30
+ link_constant(P[NetboxDeviceV37].storage, value=storage),
31
+ link(P[api_models.Device].name, P[NetboxDeviceV37].hostname),
32
+ link(P[api_models.Device].name, P[NetboxDeviceV37].fqdn),
33
+ ]
52
34
  )
53
- hw = get_hw(
54
- device.device_type.manufacturer.name,
55
- device.device_type.model,
56
- platform_name,
35
+ self.convert_interfaces = get_converter(
36
+ list[api_models.Interface],
37
+ list[InterfaceV37],
38
+ recipe=[
39
+ link_constant(P[InterfaceV37].ip_addresses, factory=list),
40
+ link_constant(P[InterfaceV37].lag_min_links, value=None),
41
+ ]
57
42
  )
58
- res = extend_device_base(
59
- device=device,
60
- interfaces=interfaces,
61
- breed=breed,
62
- hw=hw,
63
- storage=storage,
64
- )
65
- return res
66
-
67
-
68
- @impl_converter(
69
- recipe=[link_constant(P[models.Interface].lag_min_links, value=None)],
70
- )
71
- def extend_interface(
72
- interface: api_models.Interface,
73
- ip_addresses: List[models.IpAddress],
74
- ) -> models.Interface:
75
- ...
76
-
77
-
78
- @impl_converter
79
- def extend_ip_address(
80
- ip_address: models.IpAddress, prefix: Optional[models.Prefix],
81
- ) -> models.IpAddress:
82
- ...
83
-
84
-
85
- class NetboxStorageV37(Storage):
86
- def __init__(self, opts: Optional[NetboxStorageOpts] = None):
87
- ctx: Optional[ssl.SSLContext] = None
88
- url = ""
89
- token = ""
90
- self.exact_host_filter = False
91
- threads = 1
92
- if opts:
93
- if opts.insecure:
94
- ctx = ssl.create_default_context()
95
- ctx.check_hostname = False
96
- ctx.verify_mode = ssl.CERT_NONE
97
- url = opts.url
98
- token = opts.token
99
- threads = opts.threads
100
- self.exact_host_filter = opts.exact_host_filter
101
-
102
- self.netbox = NetboxV37(url=url, token=token, ssl_context=ctx, threads=threads)
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] = {}
107
-
108
- def __enter__(self):
109
- return self
110
-
111
- def __exit__(self, _, __, ___):
112
- pass
113
-
114
- def resolve_object_ids_by_query(self, query: NetboxQuery):
115
- return [
116
- d.id for d in self._load_devices(query)
117
- ]
118
-
119
- def resolve_fdnds_by_query(self, query: NetboxQuery):
120
- return [
121
- d.name for d in self._load_devices(query)
122
- ]
123
-
124
- def resolve_all_fdnds(self) -> list[str]:
125
- if self._all_fqdns is None:
126
- self._all_fqdns = [
127
- d.name
128
- for d in self.netbox.dcim_all_devices_brief().results
43
+ self.convert_ip_addresses = get_converter(
44
+ list[api_models.IpAddress],
45
+ list[IpAddressV37],
46
+ recipe=[
47
+ link_constant(P[IpAddressV37].prefix, value=None),
129
48
  ]
130
- return self._all_fqdns
131
-
132
- def make_devices(
133
- self,
134
- query: Union[NetboxQuery, list],
135
- preload_neighbors=False,
136
- use_mesh=None,
137
- preload_extra_fields=False,
138
- **kwargs,
139
- ) -> List[models.NetboxDevice]:
140
- if isinstance(query, list):
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
49
+ )
50
+ self.convert_ip_prefixes = get_converter(
51
+ list[api_models.Prefix],
52
+ list[PrefixV37],
163
53
  )
164
54
 
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]:
173
- device_ids = {
174
- device.id: extend_device(
175
- device=device,
176
- interfaces=[],
177
- storage=self,
178
- )
179
- for device in self._load_devices(query)
180
- }
181
- if not device_ids:
182
- return []
183
-
184
- for device in device_ids.values():
185
- self._record_device(device)
186
-
187
- interfaces = self._load_interfaces(list(device_ids))
188
- for interface in interfaces:
189
- device_ids[interface.device.id].interfaces.append(interface)
190
-
191
- return list(device_ids.values())
192
-
193
- def _record_device(self, device: models.NetboxDevice):
194
- self._id_devices[device.id] = device
195
- self._short_name_devices[device.name] = device
196
- if not self.exact_host_filter:
197
- short_name = device.name.split(".")[0]
198
- self._short_name_devices[short_name] = device
199
-
200
- def _load_devices(self, query: NetboxQuery) -> List[api_models.Device]:
201
- if not query.globs:
202
- return []
203
- query_groups = parse_glob(self.exact_host_filter, query)
55
+ def list_all_fqdns(self) -> list[str]:
204
56
  return [
205
- device
206
- for device in self.netbox.dcim_all_devices(**query_groups).results
207
- if _match_query(self.exact_host_filter, query, device)
57
+ d.name
58
+ for d in self.netbox.dcim_all_devices_brief().results
208
59
  ]
209
60
 
210
- def _extend_interfaces(self, interfaces: List[models.Interface]) -> List[models.Interface]:
211
- extended_ifaces = {
212
- interface.id: extend_interface(interface, [])
213
- for interface in interfaces
214
- }
215
-
216
- ips = self.netbox.ipam_all_ip_addresses(interface_id=list(extended_ifaces))
217
- ip_to_cidrs: Dict[str, str] = {ip.address: str(ip_interface(ip.address).network) for ip in ips.results}
218
- prefixes = self.netbox.ipam_all_prefixes(prefix=list(ip_to_cidrs.values()))
219
- cidr_to_prefix: Dict[str, models.Prefix] = {x.prefix: x for x in prefixes.results}
220
-
221
- for ip in ips.results:
222
- cidr = ip_to_cidrs[ip.address]
223
- ip = extend_ip_address(ip, prefix=cidr_to_prefix.get(cidr))
224
- extended_ifaces[ip.assigned_object_id].ip_addresses.append(ip)
225
- return list(extended_ifaces.values())
226
-
227
- def _load_interfaces(self, device_ids: List[int]) -> List[models.Interface]:
228
- interfaces = self.netbox.dcim_all_interfaces(device_id=device_ids)
229
- return self._extend_interfaces(interfaces.results)
230
-
231
- def _load_interfaces_by_id(self, ids: List[int]) -> List[models.Interface]:
232
- interfaces = self.netbox.dcim_all_interfaces_by_id(id=ids)
233
- return self._extend_interfaces(interfaces.results)
234
-
235
- def get_device(
236
- self, obj_id, preload_neighbors=False, use_mesh=None,
237
- **kwargs,
238
- ) -> models.NetboxDevice:
239
- if obj_id in self._id_devices:
240
- return self._id_devices[obj_id]
241
-
242
- device = self.netbox.dcim_device(obj_id)
243
- interfaces = self._load_interfaces([device.id])
244
-
245
- res = extend_device(
246
- device=device,
247
- storage=self,
248
- interfaces=interfaces,
249
- )
250
- self._record_device(res)
251
- return res
252
-
253
- def flush_perf(self):
254
- pass
255
-
256
- def search_connections(self, device: Device, neighbor: Device) -> list[tuple[Interface, Interface]]:
257
- if device.storage is not self:
258
- raise ValueError("device does not belong to this storage")
259
- if neighbor.storage is not self:
260
- raise ValueError("neighbor does not belong to this storage")
261
- # both devices are NetboxDevice if they are loaded from this storage
262
- res = []
263
- for local_port in device.interfaces:
264
- if not local_port.connected_endpoints:
265
- continue
266
- for endpoint in local_port.connected_endpoints:
267
- if endpoint.device.id == neighbor.id:
268
- for remote_port in neighbor.interfaces:
269
- if remote_port.name == endpoint.name:
270
- res.append((local_port, remote_port))
271
- break
272
- return res
273
-
274
-
275
- def _match_query(exact_host_filter: bool, query: NetboxQuery, device_data: api_models.Device) -> bool:
276
- """
277
- Additional filtering after netbox due to limited backend logic.
278
- """
279
- if exact_host_filter:
280
- return True # nothing to check, all filtering is done by netbox
281
- hostnames = [subquery.strip() for subquery in query.globs if FIELD_VALUE_SEPARATOR not in subquery]
282
- if not hostnames:
283
- return True # no hostnames to check
61
+ def list_devices(self, query: dict[str, list[str]]) -> list[NetboxDeviceV37]:
62
+ return [
63
+ self.convert_device(dev)
64
+ for dev in self.netbox.dcim_all_devices(**query).results
65
+ ]
284
66
 
285
- short_name = device_data.name.split(".")[0]
286
- for hostname in hostnames:
287
- hostname = hostname.strip().rstrip(".")
288
- if short_name == hostname or device_data.name == hostname:
289
- return True
290
- return False
67
+ def get_device(self, device_id: int) -> NetboxDeviceV37:
68
+ return self.convert_device(self.netbox.dcim_device(device_id))
291
69
 
70
+ def list_interfaces_by_devices(self, device_ids: list[int]) -> list[InterfaceV37]:
71
+ return self.convert_interfaces(self.netbox.dcim_all_interfaces(device_id=device_ids).results)
292
72
 
293
- def _hostname_dot_hack(raw_query: str) -> str:
294
- # there is no proper way to lookup host by its hostname
295
- # ie find "host" with fqdn "host.example.com"
296
- # besides using name__ic (ie startswith)
297
- # since there is no direct analogue for this field in netbox
298
- # so we need to add a dot to hostnames (top-level fqdn part)
299
- # so we would not receive devices with a common name prefix
300
- def add_dot(raw_query: Any) -> Any:
301
- if isinstance(raw_query, str) and "." not in raw_query:
302
- raw_query = raw_query + "."
303
- return raw_query
73
+ def list_interfaces(self, ids: list[int]) -> list[InterfaceV37]:
74
+ return self.convert_interfaces(self.netbox.dcim_all_interfaces(id=ids).results)
304
75
 
305
- if isinstance(raw_query, list):
306
- for i, name in enumerate(raw_query):
307
- raw_query[i] = add_dot(name)
308
- elif isinstance(raw_query, str):
309
- raw_query = add_dot(raw_query)
76
+ def list_ipaddr_by_ifaces(self, iface_ids: list[int]) -> list[IpAddressV37]:
77
+ return self.convert_ip_addresses(self.netbox.ipam_all_ip_addresses(interface_id=iface_ids).results)
310
78
 
311
- return raw_query
79
+ def list_ipprefixes(self, prefixes: list[str]) -> list[PrefixV37]:
80
+ return self.convert_ip_prefixes(self.netbox.ipam_all_prefixes(prefix=prefixes).results)
312
81
 
313
82
 
314
- def parse_glob(exact_host_filter: bool, query: NetboxQuery) -> dict[str, list[str]]:
315
- query_groups = cast(dict[str, list[str]], query.parse_query())
316
- if names := query_groups.pop("name", None):
317
- if exact_host_filter:
318
- query_groups["name__ie"] = names
319
- else:
320
- query_groups["name__ic"] = [_hostname_dot_hack(name) for name in names]
321
- return query_groups
83
+ class NetboxStorageV37(BaseNetboxStorage[NetboxDeviceV37, InterfaceV37, IpAddressV37, PrefixV37]):
84
+ def _init_adapter(
85
+ self,
86
+ url: str,
87
+ token: str,
88
+ ssl_context: ssl.SSLContext | None,
89
+ threads: int,
90
+ ) -> NetboxAdapter[NetboxDeviceV37, InterfaceV37, IpAddressV37, PrefixV37]:
91
+ return NetboxV37Adapter(self, url, token, ssl_context, threads)
File without changes
@@ -0,0 +1,78 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+ import warnings
4
+ from datetime import datetime, timezone
5
+ from annet.adapters.netbox.common.models import Entity, Interface, InterfaceType, IpAddress, Label, NetboxDevice, DeviceIp, IpFamily, Prefix
6
+
7
+
8
+ @dataclass
9
+ class PrefixV41(Prefix):
10
+ site: Optional[Entity] = None
11
+
12
+
13
+ @dataclass
14
+ class IpAddressV41(IpAddress[PrefixV41]):
15
+ prefix: Optional[PrefixV41] = None
16
+
17
+
18
+ @dataclass
19
+ class InterfaceV41(Interface[IpAddressV41]):
20
+ def _add_new_addr(self, address_mask: str, vrf: Entity | None, family: IpFamily) -> None:
21
+ self.ip_addresses.append(IpAddressV41(
22
+ id=0,
23
+ display=address_mask,
24
+ address=address_mask,
25
+ vrf=vrf,
26
+ prefix=None,
27
+ family=family,
28
+ created=datetime.now(timezone.utc),
29
+ last_updated=datetime.now(timezone.utc),
30
+ tags=[],
31
+ status=Label(value="active", label="Active"),
32
+ assigned_object_id=self.id,
33
+ ))
34
+
35
+
36
+ @dataclass
37
+ class DeviceIpV41(DeviceIp):
38
+ id: int
39
+ display: str
40
+ address: str
41
+ family: IpFamily
42
+
43
+
44
+ @dataclass
45
+ class NetboxDeviceV41(NetboxDevice[InterfaceV41]):
46
+ role: Entity
47
+ primary_ip: Optional[DeviceIpV41]
48
+ primary_ip4: Optional[DeviceIpV41]
49
+ primary_ip6: Optional[DeviceIpV41]
50
+
51
+ @property
52
+ def device_role(self):
53
+ warnings.warn(
54
+ "'device_role' is deprecated, use 'role' instead.",
55
+ DeprecationWarning,
56
+ stacklevel=2
57
+ )
58
+ return self.role
59
+
60
+ def __hash__(self):
61
+ return hash((self.id, type(self)))
62
+
63
+ def _make_interface(self, name: str, type: InterfaceType) -> InterfaceV41:
64
+ return InterfaceV41(
65
+ name=name,
66
+ device=self,
67
+ enabled=True,
68
+ description="",
69
+ type=type,
70
+ id=0,
71
+ vrf=None,
72
+ display=name,
73
+ untagged_vlan=None,
74
+ tagged_vlans=[],
75
+ ip_addresses=[],
76
+ connected_endpoints=[],
77
+ mode=None,
78
+ )
@@ -0,0 +1,91 @@
1
+ import ssl
2
+ from adaptix import P
3
+ from adaptix.conversion import get_converter, link, link_constant, link_function
4
+ from annetbox.v41 import client_sync
5
+ from annetbox.v41 import models as api_models
6
+
7
+ from annet.adapters.netbox.common.adapter import NetboxAdapter, get_device_breed, get_device_hw
8
+ from annet.adapters.netbox.common.storage_base import BaseNetboxStorage
9
+ from annet.adapters.netbox.v41.models import InterfaceV41, IpAddressV41, NetboxDeviceV41, PrefixV41
10
+ from annet.storage import Storage
11
+
12
+
13
+ class NetboxV41Adapter(NetboxAdapter[NetboxDeviceV41, InterfaceV41, IpAddressV41, PrefixV41]):
14
+ def __init__(
15
+ self,
16
+ storage: Storage,
17
+ url: str,
18
+ token: str,
19
+ ssl_context: ssl.SSLContext | None,
20
+ threads: int,
21
+ ):
22
+ self.netbox = client_sync.NetboxV41(url=url, token=token, ssl_context=ssl_context, threads=threads)
23
+ self.convert_device = get_converter(
24
+ api_models.Device,
25
+ NetboxDeviceV41,
26
+ recipe=[
27
+ link_function(get_device_breed, P[NetboxDeviceV41].breed),
28
+ link_function(get_device_hw, P[NetboxDeviceV41].hw),
29
+ link_constant(P[NetboxDeviceV41].interfaces, factory=list),
30
+ link_constant(P[NetboxDeviceV41].storage, value=storage),
31
+ link(P[api_models.Device].name, P[NetboxDeviceV41].hostname),
32
+ link(P[api_models.Device].name, P[NetboxDeviceV41].fqdn),
33
+ ]
34
+ )
35
+ self.convert_interfaces = get_converter(
36
+ list[api_models.Interface],
37
+ list[InterfaceV41],
38
+ recipe=[
39
+ link_constant(P[InterfaceV41].ip_addresses, factory=list),
40
+ link_constant(P[InterfaceV41].lag_min_links, value=None),
41
+ ]
42
+ )
43
+ self.convert_ip_addresses = get_converter(
44
+ list[api_models.IpAddress],
45
+ list[IpAddressV41],
46
+ recipe=[
47
+ link_constant(P[IpAddressV41].prefix, value=None),
48
+ ]
49
+ )
50
+ self.convert_ip_prefixes = get_converter(
51
+ list[api_models.Prefix],
52
+ list[PrefixV41],
53
+ )
54
+
55
+ def list_all_fqdns(self) -> list[str]:
56
+ return [
57
+ d.name
58
+ for d in self.netbox.dcim_all_devices_brief().results
59
+ ]
60
+
61
+ def list_devices(self, query: dict[str, list[str]]) -> list[NetboxDeviceV41]:
62
+ return [
63
+ self.convert_device(dev)
64
+ for dev in self.netbox.dcim_all_devices(**query).results
65
+ ]
66
+
67
+ def get_device(self, device_id: int) -> NetboxDeviceV41:
68
+ return self.convert_device(self.netbox.dcim_device(device_id))
69
+
70
+ def list_interfaces_by_devices(self, device_ids: list[int]) -> list[InterfaceV41]:
71
+ return self.convert_interfaces(self.netbox.dcim_all_interfaces(device_id=device_ids).results)
72
+
73
+ def list_interfaces(self, ids: list[int]) -> list[InterfaceV41]:
74
+ return self.convert_interfaces(self.netbox.dcim_all_interfaces(id=ids).results)
75
+
76
+ def list_ipaddr_by_ifaces(self, iface_ids: list[int]) -> list[IpAddressV41]:
77
+ return self.convert_ip_addresses(self.netbox.ipam_all_ip_addresses(interface_id=iface_ids).results)
78
+
79
+ def list_ipprefixes(self, prefixes: list[str]) -> list[PrefixV41]:
80
+ return self.convert_ip_prefixes(self.netbox.ipam_all_prefixes(prefix=prefixes).results)
81
+
82
+
83
+ class NetboxStorageV41(BaseNetboxStorage[NetboxDeviceV41, InterfaceV41, IpAddressV41, PrefixV41]):
84
+ def _init_adapter(
85
+ self,
86
+ url: str,
87
+ token: str,
88
+ ssl_context: ssl.SSLContext | None,
89
+ threads: int,
90
+ ) -> NetboxAdapter[NetboxDeviceV41, InterfaceV41, IpAddressV41, PrefixV41]:
91
+ return NetboxV41Adapter(self, url, token, ssl_context, threads)
File without changes
@@ -0,0 +1,63 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime, timezone
3
+ from typing import Optional
4
+ from annet.adapters.netbox.common.models import InterfaceType, IpFamily, Label, Prefix, Entity
5
+ from annet.adapters.netbox.v41.models import InterfaceV41, IpAddressV41, NetboxDeviceV41
6
+
7
+
8
+ @dataclass
9
+ class PrefixV42(Prefix):
10
+ scope: Optional[Entity] = None
11
+ scope_type: str | None = None
12
+
13
+ @property
14
+ def site(self) -> Optional[Entity]:
15
+ if self.scope_type == "dcim.site":
16
+ return self.scope
17
+ return None
18
+
19
+
20
+ @dataclass
21
+ class IpAddressV42(IpAddressV41):
22
+ prefix: Optional[PrefixV42] = None
23
+
24
+
25
+ @dataclass
26
+ class InterfaceV42(InterfaceV41):
27
+ def _add_new_addr(self, address_mask: str, vrf: Entity | None, family: IpFamily) -> None:
28
+ self.ip_addresses.append(IpAddressV42(
29
+ id=0,
30
+ display=address_mask,
31
+ address=address_mask,
32
+ vrf=vrf,
33
+ prefix=None,
34
+ family=family,
35
+ created=datetime.now(timezone.utc),
36
+ last_updated=datetime.now(timezone.utc),
37
+ tags=[],
38
+ status=Label(value="active", label="Active"),
39
+ assigned_object_id=self.id,
40
+ ))
41
+
42
+
43
+ @dataclass
44
+ class NetboxDeviceV42(NetboxDeviceV41):
45
+ def __hash__(self):
46
+ return hash((self.id, type(self)))
47
+
48
+ def _make_interface(self, name: str, type: InterfaceType) -> InterfaceV42:
49
+ return InterfaceV42(
50
+ name=name,
51
+ device=self,
52
+ enabled=True,
53
+ description="",
54
+ type=type,
55
+ id=0,
56
+ vrf=None,
57
+ display=name,
58
+ untagged_vlan=None,
59
+ tagged_vlans=[],
60
+ ip_addresses=[],
61
+ connected_endpoints=[],
62
+ mode=None,
63
+ )