Skip to content

Commit 98625ed

Browse files
abhi1693nautics889
andauthored
Adds support for NetBox v3.5+ (#564)
* Fix unused `PowerPorts` model (#535) Updated models/mapper.py: set `PowerPorts` for "dcim.powerport" in the map-dict. * migrate from pkg_resources to importlib * adds core app * adds endpoints added in 3.5 * adds support for 3.5 * lint fixes * updates openapi tests * adds testing for 3.4 and 3.5 * updates pytest.skip * updates docker tags for testing * fixes superuser account creation for testing * fixed requirements * updates the docstring for render-config endpoint * removes extra semicolon from content type value * migrate from pkg_resources to importlib * adds core app * adds endpoints added in 3.5 * adds support for 3.5 * lint fixes * updates openapi tests * adds testing for 3.4 and 3.5 * updates pytest.skip * updates docker tags for testing * fixes superuser account creation for testing * fixed requirements * updates the docstring for render-config endpoint * removes extra semicolon from content type value --------- Co-authored-by: nautics889 <cyberukr@gmail.com>
1 parent f32ed2f commit 98625ed

File tree

17 files changed

+126
-33
lines changed

17 files changed

+126
-33
lines changed

.github/workflows/py3.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
matrix:
1515
python: ["3.8", "3.9", "3.10"]
16-
netbox: ["3.3"]
16+
netbox: ["3.3", "3.4", "3.5"]
1717

1818
steps:
1919
- uses: actions/checkout@v2

pynetbox/__init__.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
from pkg_resources import get_distribution, DistributionNotFound
1+
from importlib.metadata import metadata
22

33
from pynetbox.core.query import RequestError, AllocationError, ContentError
44
from pynetbox.core.api import Api as api
55

6-
try:
7-
__version__ = get_distribution(__name__).version
8-
except DistributionNotFound:
9-
pass
6+
__version__ = metadata(__name__).get("Version")

pynetbox/core/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class Api:
2727
you can specify which app and endpoint you wish to interact with.
2828
2929
Valid attributes currently are:
30+
* core (NetBox 3.5+)
3031
* dcim
3132
* ipam
3233
* circuits
@@ -74,6 +75,7 @@ def __init__(
7475
self.base_url = base_url
7576
self.http_session = requests.Session()
7677
self.threading = threading
78+
self.core = App(self, "core")
7779
self.dcim = App(self, "dcim")
7880
self.ipam = App(self, "ipam")
7981
self.circuits = App(self, "circuits")

pynetbox/core/query.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""
1616
import concurrent.futures as cf
1717
import json
18+
from packaging import version
1819

1920

2021
def calc_pages(limit, count):
@@ -153,12 +154,22 @@ def __init__(
153154
def get_openapi(self):
154155
"""Gets the OpenAPI Spec"""
155156
headers = {
156-
"Content-Type": "application/json;",
157+
"Accept": "application/json",
158+
"Content-Type": "application/json",
157159
}
158-
req = self.http_session.get(
159-
"{}docs/?format=openapi".format(self.normalize_url(self.base)),
160-
headers=headers,
161-
)
160+
161+
current_version = version.parse(self.get_version())
162+
if current_version >= version.parse("3.5"):
163+
req = self.http_session.get(
164+
"{}schema/".format(self.normalize_url(self.base)),
165+
headers=headers,
166+
)
167+
else:
168+
req = self.http_session.get(
169+
"{}docs/?format=openapi".format(self.normalize_url(self.base)),
170+
headers=headers,
171+
)
172+
162173
if req.ok:
163174
return req.json()
164175
else:
@@ -175,7 +186,7 @@ def get_version(self):
175186
present in the headers.
176187
"""
177188
headers = {
178-
"Content-Type": "application/json;",
189+
"Content-Type": "application/json",
179190
}
180191
req = self.http_session.get(
181192
self.normalize_url(self.base),
@@ -192,7 +203,7 @@ def get_status(self):
192203
:Returns: Dictionary as returned by NetBox.
193204
:Raises: RequestError if request is not successful.
194205
"""
195-
headers = {"Content-Type": "application/json;"}
206+
headers = {"Content-Type": "application/json"}
196207
if self.token:
197208
headers["authorization"] = "Token {}".format(self.token)
198209
req = self.http_session.get(
@@ -213,9 +224,9 @@ def normalize_url(self, url):
213224

214225
def _make_call(self, verb="get", url_override=None, add_params=None, data=None):
215226
if verb in ("post", "put") or verb == "delete" and data:
216-
headers = {"Content-Type": "application/json;"}
227+
headers = {"Content-Type": "application/json"}
217228
else:
218-
headers = {"accept": "application/json;"}
229+
headers = {"accept": "application/json"}
219230

220231
if self.token:
221232
headers["authorization"] = "Token {}".format(self.token)

pynetbox/models/dcim.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from pynetbox.core.query import Request
1919
from pynetbox.core.response import Record, JsonField
20-
from pynetbox.core.endpoint import RODetailEndpoint
20+
from pynetbox.core.endpoint import RODetailEndpoint, DetailEndpoint
2121
from pynetbox.models.ipam import IpAddresses
2222
from pynetbox.models.circuits import Circuits
2323

@@ -121,6 +121,23 @@ def napalm(self):
121121
"""
122122
return RODetailEndpoint(self, "napalm")
123123

124+
@property
125+
def render_config(self):
126+
"""
127+
Represents the ``render-config`` detail endpoint.
128+
129+
Returns a DetailEndpoint object that is the interface for
130+
viewing response from the render-config endpoint.
131+
132+
:returns: :py:class:`.DetailEndpoint`
133+
134+
:Examples:
135+
136+
>>> device = nb.ipam.devices.get(123)
137+
>>> device.render_config.create()
138+
"""
139+
return DetailEndpoint(self, "render-config")
140+
124141

125142
class InterfaceConnections(Record):
126143
def __str__(self):

pynetbox/models/ipam.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,33 @@ def available_vlans(self):
162162
NewVLAN (10)
163163
"""
164164
return DetailEndpoint(self, "available-vlans", custom_return=Vlans)
165+
166+
167+
class AsnRanges(Record):
168+
@property
169+
def available_asns(self):
170+
"""
171+
Represents the ``available-asns`` detail endpoint.
172+
173+
Returns a DetailEndpoint object that is the interface for
174+
viewing and creating ASNs inside an ASN range.
175+
176+
:returns: :py:class:`.DetailEndpoint`
177+
178+
:Examples:
179+
180+
>>> asn_range = nb.ipam.asn_ranges.get(1)
181+
>>> asn_range.available_asns.list()
182+
[64512, 64513, 64514]
183+
184+
To create a new ASN:
185+
186+
>>> asn_range.available_asns.create()
187+
64512
188+
189+
To create multiple ASNs:
190+
191+
>>> asn_range.available_asns.create([{} for i in range(2)])
192+
[64513, 64514]
193+
"""
194+
return DetailEndpoint(self, "available-asns")

requirements-dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
black~=22.10
22
pytest==7.1.*
33
pytest-docker==1.0.*
4-
PyYAML==6.0
4+
PyYAML==6.0.1

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
requests>=2.20.0,<3.0
2+
packaging<24.0

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
packages=find_packages(exclude=["tests", "tests.*"]),
1414
install_requires=[
1515
"requests>=2.20.0,<3.0",
16+
"packaging<24.0"
1617
],
1718
zip_safe=False,
1819
keywords=["netbox"],

tests/integration/conftest.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ def get_netbox_docker_version_tag(netbox_version):
2828
major, minor = netbox_version.major, netbox_version.minor
2929

3030
if (major, minor) == (3, 3):
31-
tag = "2.2.0"
31+
tag = "2.3.0"
32+
elif (major, minor) == (3, 4):
33+
tag = "2.5.3"
34+
elif (major, minor) == (3, 5):
35+
tag = "2.6.1"
3236
else:
3337
raise NotImplementedError(
3438
"Version %s is not currently supported" % netbox_version
@@ -48,7 +52,7 @@ def git_toplevel():
4852
try:
4953
subp.check_call(["which", "git"])
5054
except subp.CalledProcessError:
51-
pytest.skip(msg="git executable was not found on the host")
55+
pytest.skip(reason="git executable was not found on the host")
5256
return (
5357
subp.check_output(["git", "rev-parse", "--show-toplevel"])
5458
.decode("utf-8")
@@ -73,7 +77,7 @@ def netbox_docker_repo_dirpaths(pytestconfig, git_toplevel):
7377
try:
7478
subp.check_call(["which", "docker"])
7579
except subp.CalledProcessError:
76-
pytest.skip(msg="docker executable was not found on the host")
80+
pytest.skip(reason="docker executable was not found on the host")
7781
netbox_versions_by_repo_dirpaths = {}
7882
for netbox_version in pytestconfig.option.netbox_versions:
7983
repo_version_tag = get_netbox_docker_version_tag(netbox_version=netbox_version)
@@ -248,6 +252,14 @@ def docker_compose_file(pytestconfig, netbox_docker_repo_dirpaths):
248252
"netboxcommunity/netbox:v%s" % netbox_version
249253
)
250254

255+
new_services[new_service_name]["environment"] = {
256+
"SKIP_SUPERUSER": "false",
257+
"SUPERUSER_API_TOKEN": "0123456789abcdef0123456789abcdef01234567",
258+
"SUPERUSER_EMAIL": "admin@example.com",
259+
"SUPERUSER_NAME": "admin",
260+
"SUPERUSER_PASSWORD": "admin",
261+
}
262+
251263
if service_name == "netbox":
252264
# ensure the netbox container listens on a random port
253265
new_services[new_service_name]["ports"] = ["8080"]
@@ -341,7 +353,7 @@ def docker_compose_file(pytestconfig, netbox_docker_repo_dirpaths):
341353

342354

343355
def netbox_is_responsive(url):
344-
"""Chack if the HTTP service is up and responsive."""
356+
"""Check if the HTTP service is up and responsive."""
345357
try:
346358
response = requests.get(url)
347359
if response.status_code == 200:

0 commit comments

Comments
 (0)