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.
- annet/adapters/netbox/common/adapter.py +60 -0
- annet/adapters/netbox/common/models.py +47 -55
- annet/adapters/netbox/common/status_client.py +7 -0
- annet/adapters/netbox/common/storage_base.py +239 -0
- annet/adapters/netbox/provider.py +22 -7
- annet/adapters/netbox/v24/models.py +295 -0
- annet/adapters/netbox/v24/storage.py +8 -2
- annet/adapters/netbox/v37/models.py +60 -0
- annet/adapters/netbox/v37/storage.py +70 -300
- annet/adapters/netbox/v41/__init__.py +0 -0
- annet/adapters/netbox/v41/models.py +78 -0
- annet/adapters/netbox/v41/storage.py +91 -0
- annet/adapters/netbox/v42/__init__.py +0 -0
- annet/adapters/netbox/v42/models.py +63 -0
- annet/adapters/netbox/v42/storage.py +91 -0
- annet/rpl_generators/cumulus_frr.py +5 -5
- {annet-2.4.0.dist-info → annet-2.5.1.dist-info}/METADATA +2 -2
- {annet-2.4.0.dist-info → annet-2.5.1.dist-info}/RECORD +23 -13
- {annet-2.4.0.dist-info → annet-2.5.1.dist-info}/WHEEL +1 -1
- {annet-2.4.0.dist-info → annet-2.5.1.dist-info}/entry_points.txt +0 -0
- {annet-2.4.0.dist-info → annet-2.5.1.dist-info}/licenses/AUTHORS +0 -0
- {annet-2.4.0.dist-info → annet-2.5.1.dist-info}/licenses/LICENSE +0 -0
- {annet-2.4.0.dist-info → annet-2.5.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from abc import abstractmethod, ABC
|
|
2
|
+
from typing import Protocol, Generic, TypeVar
|
|
3
|
+
|
|
4
|
+
from annet.annlib.netdev.views.hardware import HardwareView
|
|
5
|
+
from .manufacturer import get_breed, get_hw
|
|
6
|
+
from .models import NetboxDevice, Interface, IpAddress, Prefix
|
|
7
|
+
|
|
8
|
+
NetboxDeviceT = TypeVar("NetboxDeviceT", bound=NetboxDevice)
|
|
9
|
+
InterfaceT = TypeVar("InterfaceT", bound=Interface)
|
|
10
|
+
IpAddressT = TypeVar("IpAddressT", bound=IpAddress)
|
|
11
|
+
PrefixT = TypeVar("PrefixT", bound=Prefix)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_device_breed(device: NetboxDeviceT) -> str:
|
|
15
|
+
if device.device_type and device.device_type.manufacturer:
|
|
16
|
+
return get_breed(
|
|
17
|
+
device.device_type.manufacturer.name,
|
|
18
|
+
device.device_type.model,
|
|
19
|
+
)
|
|
20
|
+
return ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_device_hw(device: NetboxDeviceT) -> HardwareView:
|
|
24
|
+
if device.device_type and device.device_type.manufacturer:
|
|
25
|
+
return get_hw(
|
|
26
|
+
device.device_type.manufacturer.name,
|
|
27
|
+
device.device_type.model,
|
|
28
|
+
device.platform.name if device.platform else "",
|
|
29
|
+
)
|
|
30
|
+
return HardwareView("", "")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NetboxAdapter(ABC, Generic[NetboxDeviceT, InterfaceT, IpAddressT, PrefixT]):
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def list_all_fqdns(self) -> list[str]:
|
|
36
|
+
raise NotImplementedError()
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def list_devices(self, query: dict[str, list[str]]) -> list[NetboxDeviceT]:
|
|
40
|
+
raise NotImplementedError()
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def get_device(self, device_id: int) -> NetboxDeviceT:
|
|
44
|
+
raise NotImplementedError()
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def list_interfaces_by_devices(self, device_ids: list[int]) -> list[InterfaceT]:
|
|
48
|
+
raise NotImplementedError()
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def list_interfaces(self, ids: list[int]) -> list[InterfaceT]:
|
|
52
|
+
raise NotImplementedError()
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def list_ipaddr_by_ifaces(self, iface_ids: list[int]) -> list[IpAddressT]:
|
|
56
|
+
raise NotImplementedError()
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def list_ipprefixes(self, prefixes: list[str]) -> list[PrefixT]:
|
|
60
|
+
raise NotImplementedError()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
1
2
|
from dataclasses import dataclass, field
|
|
2
|
-
from datetime import datetime
|
|
3
|
+
from datetime import datetime
|
|
3
4
|
from ipaddress import ip_interface, IPv6Interface
|
|
4
|
-
from typing import List, Optional, Any, Dict, Sequence,
|
|
5
|
+
from typing import List, Optional, Any, Dict, Sequence, TypeVar, Generic
|
|
5
6
|
|
|
6
7
|
from annet.annlib.netdev.views.dump import DumpableView
|
|
7
8
|
from annet.annlib.netdev.views.hardware import HardwareView, lag_name, svi_name
|
|
@@ -53,7 +54,7 @@ class DeviceIp(DumpableView):
|
|
|
53
54
|
class Prefix(DumpableView):
|
|
54
55
|
id: int
|
|
55
56
|
prefix: str
|
|
56
|
-
site
|
|
57
|
+
# `site` deprecated since v4.2, replace in derived classes.
|
|
57
58
|
vrf: Optional[Entity]
|
|
58
59
|
tenant: Optional[Entity]
|
|
59
60
|
vlan: Optional[Entity]
|
|
@@ -70,10 +71,13 @@ class Prefix(DumpableView):
|
|
|
70
71
|
return self.prefix
|
|
71
72
|
|
|
72
73
|
|
|
74
|
+
_PrefixT = TypeVar("_PrefixT", bound=Prefix)
|
|
75
|
+
|
|
76
|
+
|
|
73
77
|
@dataclass
|
|
74
|
-
class IpAddress(DumpableView):
|
|
78
|
+
class IpAddress(DumpableView, Generic[_PrefixT]):
|
|
75
79
|
id: int
|
|
76
|
-
assigned_object_id: int
|
|
80
|
+
assigned_object_id: int | None
|
|
77
81
|
display: str
|
|
78
82
|
family: IpFamily
|
|
79
83
|
address: str
|
|
@@ -81,7 +85,7 @@ class IpAddress(DumpableView):
|
|
|
81
85
|
tags: List[Entity]
|
|
82
86
|
created: datetime
|
|
83
87
|
last_updated: datetime
|
|
84
|
-
prefix: Optional[
|
|
88
|
+
prefix: Optional[_PrefixT] = None
|
|
85
89
|
vrf: Optional[Entity] = None
|
|
86
90
|
|
|
87
91
|
@property
|
|
@@ -111,8 +115,18 @@ class InterfaceVlan(Entity):
|
|
|
111
115
|
vid: int
|
|
112
116
|
|
|
113
117
|
|
|
118
|
+
def vrf_object(vrf: str | None) -> Entity | None:
|
|
119
|
+
if vrf is None:
|
|
120
|
+
return None
|
|
121
|
+
else:
|
|
122
|
+
return Entity(id=0, name=vrf)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
_IpAddressT = TypeVar("_IpAddressT", bound=IpAddress)
|
|
126
|
+
|
|
127
|
+
|
|
114
128
|
@dataclass
|
|
115
|
-
class Interface(Entity):
|
|
129
|
+
class Interface(Entity, Generic[_IpAddressT]):
|
|
116
130
|
device: Entity
|
|
117
131
|
enabled: bool
|
|
118
132
|
description: str
|
|
@@ -122,53 +136,44 @@ class Interface(Entity):
|
|
|
122
136
|
untagged_vlan: Optional[InterfaceVlan]
|
|
123
137
|
tagged_vlans: Optional[List[InterfaceVlan]]
|
|
124
138
|
display: str = ""
|
|
125
|
-
ip_addresses: List[
|
|
139
|
+
ip_addresses: List[_IpAddressT] = field(default_factory=list)
|
|
126
140
|
vrf: Optional[Entity] = None
|
|
127
141
|
mtu: int | None = None
|
|
128
142
|
lag: Entity | None = None
|
|
129
143
|
lag_min_links: int | None = None
|
|
130
144
|
|
|
131
145
|
def add_addr(self, address_mask: str, vrf: str | None) -> None:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
for existing_addr in self.ip_addresses:
|
|
147
|
+
if existing_addr.address == address_mask and (
|
|
148
|
+
(existing_addr.vrf is None and vrf is None) or
|
|
149
|
+
(existing_addr.vrf is not None and existing_addr.vrf.name == vrf)
|
|
150
|
+
):
|
|
151
|
+
return
|
|
137
152
|
|
|
153
|
+
addr = ip_interface(address_mask)
|
|
154
|
+
vrf_obj = vrf_object(vrf)
|
|
138
155
|
if isinstance(addr, IPv6Interface):
|
|
139
156
|
family = IpFamily(value=6, label="IPv6")
|
|
140
157
|
else:
|
|
141
158
|
family = IpFamily(value=4, label="IPv4")
|
|
159
|
+
self._add_new_addr(address_mask, vrf_obj, family)
|
|
142
160
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
self.ip_addresses.append(IpAddress(
|
|
150
|
-
id=0,
|
|
151
|
-
display=address_mask,
|
|
152
|
-
address=address_mask,
|
|
153
|
-
vrf=vrf_obj,
|
|
154
|
-
prefix=None,
|
|
155
|
-
family=family,
|
|
156
|
-
created=datetime.now(timezone.utc),
|
|
157
|
-
last_updated=datetime.now(timezone.utc),
|
|
158
|
-
tags=[],
|
|
159
|
-
status=Label(value="active", label="Active"),
|
|
160
|
-
assigned_object_id=self.id,
|
|
161
|
-
))
|
|
161
|
+
@abstractmethod
|
|
162
|
+
def _add_new_addr(self, address_mask: str, vrf: Entity | None, family: IpFamily):
|
|
163
|
+
raise NotImplementedError
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
_InterfaceT = TypeVar("_InterfaceT", bound=Interface)
|
|
162
167
|
|
|
163
168
|
|
|
164
169
|
@dataclass
|
|
165
|
-
class NetboxDevice(Entity):
|
|
170
|
+
class NetboxDevice(Entity, Generic[_InterfaceT]):
|
|
166
171
|
url: str
|
|
167
172
|
storage: Storage
|
|
168
173
|
|
|
169
174
|
display: str
|
|
170
175
|
device_type: DeviceType
|
|
171
|
-
device_role
|
|
176
|
+
# `device_role` deprecated since v4.0, replace in derived classes.
|
|
172
177
|
tenant: Optional[Entity]
|
|
173
178
|
platform: Optional[Entity]
|
|
174
179
|
serial: str
|
|
@@ -191,7 +196,7 @@ class NetboxDevice(Entity):
|
|
|
191
196
|
hw: Optional[HardwareView]
|
|
192
197
|
breed: str
|
|
193
198
|
|
|
194
|
-
interfaces: List[
|
|
199
|
+
interfaces: List[_InterfaceT]
|
|
195
200
|
|
|
196
201
|
@property
|
|
197
202
|
def neighbors(self) -> List["Entity"]:
|
|
@@ -222,27 +227,14 @@ class NetboxDevice(Entity):
|
|
|
222
227
|
custom_breed_pc = ("Mellanox", "NVIDIA", "Moxa", "Nebius")
|
|
223
228
|
return self.device_type.manufacturer.name in custom_breed_pc or self.breed == "pc"
|
|
224
229
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
device=self,
|
|
229
|
-
enabled=True,
|
|
230
|
-
description="",
|
|
231
|
-
type=type,
|
|
232
|
-
id=0,
|
|
233
|
-
vrf=None,
|
|
234
|
-
display=name,
|
|
235
|
-
untagged_vlan=None,
|
|
236
|
-
tagged_vlans=[],
|
|
237
|
-
ip_addresses=[],
|
|
238
|
-
connected_endpoints=[],
|
|
239
|
-
mode=None,
|
|
240
|
-
)
|
|
230
|
+
@abstractmethod
|
|
231
|
+
def _make_interface(self, name: str, type: InterfaceType) -> _InterfaceT:
|
|
232
|
+
raise NotImplementedError
|
|
241
233
|
|
|
242
234
|
def _lag_name(self, lag: int) -> str:
|
|
243
235
|
return lag_name(self.hw, lag)
|
|
244
236
|
|
|
245
|
-
def make_lag(self, lag: int, ports: Sequence[str], lag_min_links: int | None) ->
|
|
237
|
+
def make_lag(self, lag: int, ports: Sequence[str], lag_min_links: int | None) -> _InterfaceT:
|
|
246
238
|
new_name = self._lag_name(lag)
|
|
247
239
|
for target_interface in self.interfaces:
|
|
248
240
|
if target_interface.name == new_name:
|
|
@@ -261,7 +253,7 @@ class NetboxDevice(Entity):
|
|
|
261
253
|
def _svi_name(self, svi: int) -> str:
|
|
262
254
|
return svi_name(self.hw, svi)
|
|
263
255
|
|
|
264
|
-
def add_svi(self, svi: int) ->
|
|
256
|
+
def add_svi(self, svi: int) -> _InterfaceT:
|
|
265
257
|
name = self._svi_name(svi)
|
|
266
258
|
for interface in self.interfaces:
|
|
267
259
|
if interface.name == name:
|
|
@@ -276,7 +268,7 @@ class NetboxDevice(Entity):
|
|
|
276
268
|
def _subif_name(self, interface: str, subif: int) -> str:
|
|
277
269
|
return f"{interface}.{subif}"
|
|
278
270
|
|
|
279
|
-
def add_subif(self, interface: str, subif: int) ->
|
|
271
|
+
def add_subif(self, interface: str, subif: int) -> _InterfaceT:
|
|
280
272
|
name = self._subif_name(interface, subif)
|
|
281
273
|
for target_port in self.interfaces:
|
|
282
274
|
if target_port.name == name:
|
|
@@ -288,7 +280,7 @@ class NetboxDevice(Entity):
|
|
|
288
280
|
self.interfaces.append(target_port)
|
|
289
281
|
return target_port
|
|
290
282
|
|
|
291
|
-
def find_interface(self, name: str) -> Optional[
|
|
283
|
+
def find_interface(self, name: str) -> Optional[_InterfaceT]:
|
|
292
284
|
for iface in self.interfaces:
|
|
293
285
|
if iface.name == name:
|
|
294
286
|
return iface
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from typing import Dict
|
|
3
|
+
import re
|
|
3
4
|
|
|
4
5
|
from adaptix import Retort, name_mapping, NameStyle
|
|
5
6
|
from dataclass_rest import get
|
|
@@ -13,6 +14,12 @@ class Status:
|
|
|
13
14
|
netbox_version: str
|
|
14
15
|
plugins: Dict[str, str]
|
|
15
16
|
|
|
17
|
+
@property
|
|
18
|
+
def minor_version(self) -> str:
|
|
19
|
+
if match := re.match(r"\d+\.\d+", self.netbox_version):
|
|
20
|
+
return match.group(0)
|
|
21
|
+
return ""
|
|
22
|
+
|
|
16
23
|
|
|
17
24
|
class NetboxStatusClient(BaseNetboxClient):
|
|
18
25
|
def _init_response_body_factory(self) -> FactoryProtocol:
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import ssl
|
|
2
|
+
from ipaddress import ip_interface
|
|
3
|
+
from logging import getLogger
|
|
4
|
+
from typing import Any, Optional, List, Union, Dict, cast, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
from annetbox.v37 import models as api_models
|
|
7
|
+
|
|
8
|
+
from annet.adapters.netbox.common.query import NetboxQuery, FIELD_VALUE_SEPARATOR
|
|
9
|
+
from annet.adapters.netbox.common.storage_opts import NetboxStorageOpts
|
|
10
|
+
from annet.storage import Storage
|
|
11
|
+
from .adapter import NetboxAdapter
|
|
12
|
+
from .models import IpAddress, Interface, NetboxDevice, Prefix
|
|
13
|
+
|
|
14
|
+
logger = getLogger(__name__)
|
|
15
|
+
NetboxDeviceT = TypeVar("NetboxDeviceT", bound=NetboxDevice)
|
|
16
|
+
InterfaceT = TypeVar("InterfaceT", bound=Interface)
|
|
17
|
+
IpAddressT = TypeVar("IpAddressT", bound=IpAddress)
|
|
18
|
+
PrefixT = TypeVar("PrefixT", bound=Prefix)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseNetboxStorage(
|
|
22
|
+
Storage,
|
|
23
|
+
Generic[
|
|
24
|
+
NetboxDeviceT,
|
|
25
|
+
InterfaceT,
|
|
26
|
+
IpAddressT,
|
|
27
|
+
PrefixT,
|
|
28
|
+
],
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Base class for Netbox storage
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, opts: Optional[NetboxStorageOpts] = None):
|
|
35
|
+
ctx: Optional[ssl.SSLContext] = None
|
|
36
|
+
url = ""
|
|
37
|
+
token = ""
|
|
38
|
+
self.exact_host_filter = False
|
|
39
|
+
threads = 1
|
|
40
|
+
if opts:
|
|
41
|
+
if opts.insecure:
|
|
42
|
+
ctx = ssl.create_default_context()
|
|
43
|
+
ctx.check_hostname = False
|
|
44
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
45
|
+
url = opts.url
|
|
46
|
+
token = opts.token
|
|
47
|
+
threads = opts.threads
|
|
48
|
+
self.exact_host_filter = opts.exact_host_filter
|
|
49
|
+
self.netbox = self._init_adapter(url=url, token=token, ssl_context=ctx, threads=threads)
|
|
50
|
+
self._all_fqdns: Optional[list[str]] = None
|
|
51
|
+
self._id_devices: dict[int, NetboxDeviceT] = {}
|
|
52
|
+
self._name_devices: dict[str, NetboxDeviceT] = {}
|
|
53
|
+
self._short_name_devices: dict[str, NetboxDeviceT] = {}
|
|
54
|
+
|
|
55
|
+
def _init_adapter(
|
|
56
|
+
self,
|
|
57
|
+
url: str,
|
|
58
|
+
token: str,
|
|
59
|
+
ssl_context: Optional[ssl.SSLContext],
|
|
60
|
+
threads: int,
|
|
61
|
+
) -> NetboxAdapter[NetboxDeviceT, InterfaceT, IpAddressT, PrefixT]:
|
|
62
|
+
raise NotImplementedError()
|
|
63
|
+
|
|
64
|
+
def __enter__(self):
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def __exit__(self, _, __, ___):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def resolve_object_ids_by_query(self, query: NetboxQuery):
|
|
71
|
+
return [
|
|
72
|
+
d.id for d in self._load_devices(query)
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
def resolve_fdnds_by_query(self, query: NetboxQuery):
|
|
76
|
+
return [
|
|
77
|
+
d.name for d in self._load_devices(query)
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def resolve_all_fdnds(self) -> list[str]:
|
|
81
|
+
if self._all_fqdns is None:
|
|
82
|
+
self._all_fqdns = self.netbox.list_all_fqdns()
|
|
83
|
+
return self._all_fqdns
|
|
84
|
+
|
|
85
|
+
def make_devices(
|
|
86
|
+
self,
|
|
87
|
+
query: Union[NetboxQuery, list],
|
|
88
|
+
preload_neighbors=False,
|
|
89
|
+
use_mesh=None,
|
|
90
|
+
preload_extra_fields=False,
|
|
91
|
+
**kwargs,
|
|
92
|
+
) -> List[NetboxDeviceT]:
|
|
93
|
+
if isinstance(query, list):
|
|
94
|
+
query = NetboxQuery.new(query)
|
|
95
|
+
|
|
96
|
+
devices = []
|
|
97
|
+
if query.is_host_query():
|
|
98
|
+
globs = []
|
|
99
|
+
for glob in query.globs:
|
|
100
|
+
if glob in self._name_devices:
|
|
101
|
+
devices.append(self._name_devices[glob])
|
|
102
|
+
if glob in self._short_name_devices:
|
|
103
|
+
devices.append(self._short_name_devices[glob])
|
|
104
|
+
else:
|
|
105
|
+
globs.append(glob)
|
|
106
|
+
if not globs:
|
|
107
|
+
return devices
|
|
108
|
+
query = NetboxQuery.new(globs)
|
|
109
|
+
|
|
110
|
+
new_devices = self._load_devices(query)
|
|
111
|
+
self._fill_device_interfaces(new_devices)
|
|
112
|
+
for device in new_devices:
|
|
113
|
+
self._record_device(device)
|
|
114
|
+
return devices + new_devices
|
|
115
|
+
|
|
116
|
+
def _load_devices(self, query: NetboxQuery) -> List[NetboxDeviceT]:
|
|
117
|
+
if not query.globs:
|
|
118
|
+
return []
|
|
119
|
+
query_groups = parse_glob(self.exact_host_filter, query)
|
|
120
|
+
devices = [
|
|
121
|
+
device
|
|
122
|
+
for device in self.netbox.list_devices(query_groups)
|
|
123
|
+
if _match_query(self.exact_host_filter, query, device)
|
|
124
|
+
]
|
|
125
|
+
return devices
|
|
126
|
+
|
|
127
|
+
def _fill_device_interfaces(self, devices: list[NetboxDeviceT]) -> None:
|
|
128
|
+
device_mapping = {d.id: d for d in devices}
|
|
129
|
+
interfaces = self.netbox.list_interfaces_by_devices(list(device_mapping))
|
|
130
|
+
for interface in interfaces:
|
|
131
|
+
device_mapping[interface.device.id].interfaces.append(interface)
|
|
132
|
+
self._fill_interface_ipaddress(interfaces)
|
|
133
|
+
|
|
134
|
+
def _fill_interface_ipaddress(self, interfaces: list[InterfaceT]) -> None:
|
|
135
|
+
interface_mapping = {i.id: i for i in interfaces}
|
|
136
|
+
ips = self.netbox.list_ipaddr_by_ifaces(list(interface_mapping))
|
|
137
|
+
for ip in ips:
|
|
138
|
+
interface_mapping[ip.assigned_object_id].ip_addresses.append(ip)
|
|
139
|
+
self._fill_ipaddr_prefixes(ips)
|
|
140
|
+
|
|
141
|
+
def _fill_ipaddr_prefixes(self, ips: list[IpAddressT]) -> None:
|
|
142
|
+
ip_to_cidrs: Dict[str, str] = {ip.address: str(ip_interface(ip.address).network) for ip in ips}
|
|
143
|
+
prefixes = self.netbox.list_ipprefixes(list(ip_to_cidrs.values()))
|
|
144
|
+
cidr_to_prefix: Dict[str, PrefixT] = {x.prefix: x for x in prefixes}
|
|
145
|
+
for ip in ips:
|
|
146
|
+
cidr = ip_to_cidrs[ip.address]
|
|
147
|
+
ip.prefix = cidr_to_prefix.get(cidr)
|
|
148
|
+
|
|
149
|
+
def _record_device(self, device: NetboxDeviceT):
|
|
150
|
+
self._id_devices[device.id] = device
|
|
151
|
+
self._short_name_devices[device.name] = device
|
|
152
|
+
if not self.exact_host_filter:
|
|
153
|
+
short_name = device.name.split(".")[0]
|
|
154
|
+
self._short_name_devices[short_name] = device
|
|
155
|
+
|
|
156
|
+
def get_device(
|
|
157
|
+
self, obj_id, preload_neighbors=False, use_mesh=None,
|
|
158
|
+
**kwargs,
|
|
159
|
+
) -> NetboxDeviceT:
|
|
160
|
+
if obj_id in self._id_devices:
|
|
161
|
+
return self._id_devices[obj_id]
|
|
162
|
+
|
|
163
|
+
device = self.netbox.get_device(obj_id)
|
|
164
|
+
self._record_device(device)
|
|
165
|
+
return device
|
|
166
|
+
|
|
167
|
+
def flush_perf(self):
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
def search_connections(
|
|
171
|
+
self,
|
|
172
|
+
device: NetboxDeviceT,
|
|
173
|
+
neighbor: NetboxDeviceT,
|
|
174
|
+
) -> list[tuple[InterfaceT, InterfaceT]]:
|
|
175
|
+
if device.storage is not self:
|
|
176
|
+
raise ValueError("device does not belong to this storage")
|
|
177
|
+
if neighbor.storage is not self:
|
|
178
|
+
raise ValueError("neighbor does not belong to this storage")
|
|
179
|
+
# both devices are NetboxDevice if they are loaded from this storage
|
|
180
|
+
res = []
|
|
181
|
+
for local_port in device.interfaces:
|
|
182
|
+
if not local_port.connected_endpoints:
|
|
183
|
+
continue
|
|
184
|
+
for endpoint in local_port.connected_endpoints:
|
|
185
|
+
if endpoint.device.id == neighbor.id:
|
|
186
|
+
for remote_port in neighbor.interfaces:
|
|
187
|
+
if remote_port.name == endpoint.name:
|
|
188
|
+
res.append((local_port, remote_port))
|
|
189
|
+
break
|
|
190
|
+
return res
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _match_query(exact_host_filter: bool, query: NetboxQuery, device_data: api_models.Device) -> bool:
|
|
194
|
+
"""
|
|
195
|
+
Additional filtering after netbox due to limited backend logic.
|
|
196
|
+
"""
|
|
197
|
+
if exact_host_filter:
|
|
198
|
+
return True # nothing to check, all filtering is done by netbox
|
|
199
|
+
hostnames = [subquery.strip() for subquery in query.globs if FIELD_VALUE_SEPARATOR not in subquery]
|
|
200
|
+
if not hostnames:
|
|
201
|
+
return True # no hostnames to check
|
|
202
|
+
|
|
203
|
+
short_name = device_data.name.split(".")[0]
|
|
204
|
+
for hostname in hostnames:
|
|
205
|
+
hostname = hostname.strip().rstrip(".")
|
|
206
|
+
if short_name == hostname or device_data.name == hostname:
|
|
207
|
+
return True
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _hostname_dot_hack(raw_query: str) -> str:
|
|
212
|
+
# there is no proper way to lookup host by its hostname
|
|
213
|
+
# ie find "host" with fqdn "host.example.com"
|
|
214
|
+
# besides using name__ic (ie startswith)
|
|
215
|
+
# since there is no direct analogue for this field in netbox
|
|
216
|
+
# so we need to add a dot to hostnames (top-level fqdn part)
|
|
217
|
+
# so we would not receive devices with a common name prefix
|
|
218
|
+
def add_dot(raw_query: Any) -> Any:
|
|
219
|
+
if isinstance(raw_query, str) and "." not in raw_query:
|
|
220
|
+
raw_query = raw_query + "."
|
|
221
|
+
return raw_query
|
|
222
|
+
|
|
223
|
+
if isinstance(raw_query, list):
|
|
224
|
+
for i, name in enumerate(raw_query):
|
|
225
|
+
raw_query[i] = add_dot(name)
|
|
226
|
+
elif isinstance(raw_query, str):
|
|
227
|
+
raw_query = add_dot(raw_query)
|
|
228
|
+
|
|
229
|
+
return raw_query
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def parse_glob(exact_host_filter: bool, query: NetboxQuery) -> dict[str, list[str]]:
|
|
233
|
+
query_groups = cast(dict[str, list[str]], query.parse_query())
|
|
234
|
+
if names := query_groups.pop("name", None):
|
|
235
|
+
if exact_host_filter:
|
|
236
|
+
query_groups["name__ie"] = names
|
|
237
|
+
else:
|
|
238
|
+
query_groups["name__ic"] = [_hostname_dot_hack(name) for name in names]
|
|
239
|
+
return query_groups
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Dict, Any, Optional
|
|
2
2
|
|
|
3
|
-
from dataclass_rest.exceptions import ClientError
|
|
3
|
+
from dataclass_rest.exceptions import ClientError, ClientLibraryError
|
|
4
4
|
|
|
5
5
|
from annet.storage import StorageProvider, Storage
|
|
6
6
|
from annet.connectors import AdapterWithName, AdapterWithConfig, T
|
|
@@ -9,21 +9,36 @@ from .common.storage_opts import NetboxStorageOpts
|
|
|
9
9
|
from .common.query import NetboxQuery
|
|
10
10
|
from .v24.storage import NetboxStorageV24
|
|
11
11
|
from .v37.storage import NetboxStorageV37
|
|
12
|
+
from .v41.storage import NetboxStorageV41
|
|
13
|
+
from .v42.storage import NetboxStorageV42
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def storage_factory(opts: NetboxStorageOpts) -> Storage:
|
|
15
17
|
client = NetboxStatusClient(opts.url, opts.token, opts.insecure)
|
|
18
|
+
version_class_map = {
|
|
19
|
+
"3.4": NetboxStorageV37,
|
|
20
|
+
"3.7": NetboxStorageV37,
|
|
21
|
+
"4.0": NetboxStorageV41,
|
|
22
|
+
"4.1": NetboxStorageV41,
|
|
23
|
+
"4.2": NetboxStorageV42,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
status = None
|
|
27
|
+
|
|
16
28
|
try:
|
|
17
29
|
status = client.status()
|
|
30
|
+
for version_prefix, storage_class in version_class_map.items():
|
|
31
|
+
if version_prefix == status.minor_version:
|
|
32
|
+
return storage_class(opts)
|
|
33
|
+
|
|
18
34
|
except ClientError as e:
|
|
19
35
|
if e.status_code == 404:
|
|
20
|
-
# old version do not support status reqeust
|
|
21
36
|
return NetboxStorageV24(opts)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError(f"Unsupported version: {status.netbox_version}")
|
|
39
|
+
except ClientLibraryError:
|
|
40
|
+
raise ValueError(f"Connection error: Unable to reach Netbox at URL: {opts.url}")
|
|
41
|
+
raise Exception(f"Unsupported version: {status.netbox_version}")
|
|
27
42
|
|
|
28
43
|
|
|
29
44
|
class NetboxProvider(StorageProvider, AdapterWithName, AdapterWithConfig):
|