Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for NetBox v3.5+ #564

Merged
merged 29 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
82e34a5
Fix unused `PowerPorts` model (#535)
nautics889 Mar 21, 2023
690bd03
migrate from pkg_resources to importlib
abhi1693 Aug 24, 2023
1565b9c
adds core app
abhi1693 Aug 24, 2023
2085422
adds endpoints added in 3.5
abhi1693 Aug 24, 2023
46ede22
adds support for 3.5
abhi1693 Aug 24, 2023
0c87b10
lint fixes
abhi1693 Aug 24, 2023
723141b
updates openapi tests
abhi1693 Aug 24, 2023
e3bf5cc
adds testing for 3.4 and 3.5
abhi1693 Aug 24, 2023
4bb0ae4
updates pytest.skip
abhi1693 Aug 24, 2023
e3d3f5f
updates docker tags for testing
abhi1693 Aug 24, 2023
7d1ea3d
fixes superuser account creation for testing
abhi1693 Aug 24, 2023
d674354
fixed requirements
abhi1693 Aug 25, 2023
2fdd20a
updates the docstring for render-config endpoint
abhi1693 Aug 25, 2023
8781476
removes extra semicolon from content type value
abhi1693 Aug 25, 2023
f0866a6
Merge pull request #536 from nautics889/master
abhi1693 Aug 25, 2023
2f8607a
migrate from pkg_resources to importlib
abhi1693 Aug 24, 2023
1c07649
adds core app
abhi1693 Aug 24, 2023
2216701
adds endpoints added in 3.5
abhi1693 Aug 24, 2023
c68c3b1
adds support for 3.5
abhi1693 Aug 24, 2023
1f2cc83
lint fixes
abhi1693 Aug 24, 2023
16d7668
updates openapi tests
abhi1693 Aug 24, 2023
ac8a478
adds testing for 3.4 and 3.5
abhi1693 Aug 24, 2023
8f5696c
updates pytest.skip
abhi1693 Aug 24, 2023
191ebb5
updates docker tags for testing
abhi1693 Aug 24, 2023
0cb7040
fixes superuser account creation for testing
abhi1693 Aug 24, 2023
232367e
fixed requirements
abhi1693 Aug 25, 2023
2db0c89
updates the docstring for render-config endpoint
abhi1693 Aug 25, 2023
433c951
removes extra semicolon from content type value
abhi1693 Aug 25, 2023
edcbb4f
Merge remote-tracking branch 'origin/feat/3.5-support' into feat/3.5-…
abhi1693 Aug 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/py3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
python: ["3.8", "3.9", "3.10"]
netbox: ["3.3"]
netbox: ["3.3", "3.4", "3.5"]

steps:
- uses: actions/checkout@v2
Expand Down
7 changes: 2 additions & 5 deletions pynetbox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
from pkg_resources import get_distribution, DistributionNotFound
from importlib.metadata import metadata

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

try:
__version__ = get_distribution(__name__).version
except DistributionNotFound:
pass
__version__ = metadata(__name__).get("Version")
2 changes: 2 additions & 0 deletions pynetbox/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Api:
you can specify which app and endpoint you wish to interact with.

Valid attributes currently are:
* core (NetBox 3.5+)
* dcim
* ipam
* circuits
Expand Down Expand Up @@ -74,6 +75,7 @@ def __init__(
self.base_url = base_url
self.http_session = requests.Session()
self.threading = threading
self.core = App(self, "core")
self.dcim = App(self, "dcim")
self.ipam = App(self, "ipam")
self.circuits = App(self, "circuits")
Expand Down
29 changes: 20 additions & 9 deletions pynetbox/core/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""
import concurrent.futures as cf
import json
from packaging import version


def calc_pages(limit, count):
Expand Down Expand Up @@ -153,12 +154,22 @@ def __init__(
def get_openapi(self):
"""Gets the OpenAPI Spec"""
headers = {
"Content-Type": "application/json;",
"Accept": "application/json",
"Content-Type": "application/json",
}
req = self.http_session.get(
"{}docs/?format=openapi".format(self.normalize_url(self.base)),
headers=headers,
)

current_version = version.parse(self.get_version())
if current_version >= version.parse("3.5"):
req = self.http_session.get(
"{}schema/".format(self.normalize_url(self.base)),
headers=headers,
)
else:
req = self.http_session.get(
"{}docs/?format=openapi".format(self.normalize_url(self.base)),
headers=headers,
)

if req.ok:
return req.json()
else:
Expand All @@ -175,7 +186,7 @@ def get_version(self):
present in the headers.
"""
headers = {
"Content-Type": "application/json;",
"Content-Type": "application/json",
}
req = self.http_session.get(
self.normalize_url(self.base),
Expand All @@ -192,7 +203,7 @@ def get_status(self):
:Returns: Dictionary as returned by NetBox.
:Raises: RequestError if request is not successful.
"""
headers = {"Content-Type": "application/json;"}
headers = {"Content-Type": "application/json"}
if self.token:
headers["authorization"] = "Token {}".format(self.token)
req = self.http_session.get(
Expand All @@ -213,9 +224,9 @@ def normalize_url(self, url):

def _make_call(self, verb="get", url_override=None, add_params=None, data=None):
if verb in ("post", "put") or verb == "delete" and data:
headers = {"Content-Type": "application/json;"}
headers = {"Content-Type": "application/json"}
else:
headers = {"accept": "application/json;"}
headers = {"accept": "application/json"}

if self.token:
headers["authorization"] = "Token {}".format(self.token)
Expand Down
19 changes: 18 additions & 1 deletion pynetbox/models/dcim.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from pynetbox.core.query import Request
from pynetbox.core.response import Record, JsonField
from pynetbox.core.endpoint import RODetailEndpoint
from pynetbox.core.endpoint import RODetailEndpoint, DetailEndpoint
from pynetbox.models.ipam import IpAddresses
from pynetbox.models.circuits import Circuits

Expand Down Expand Up @@ -121,6 +121,23 @@ def napalm(self):
"""
return RODetailEndpoint(self, "napalm")

@property
def render_config(self):
"""
Represents the ``render-config`` detail endpoint.

Returns a DetailEndpoint object that is the interface for
viewing response from the render-config endpoint.

:returns: :py:class:`.DetailEndpoint`

:Examples:

>>> device = nb.ipam.devices.get(123)
>>> device.render_config.create()
"""
return DetailEndpoint(self, "render-config")


class InterfaceConnections(Record):
def __str__(self):
Expand Down
30 changes: 30 additions & 0 deletions pynetbox/models/ipam.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,33 @@ def available_vlans(self):
NewVLAN (10)
"""
return DetailEndpoint(self, "available-vlans", custom_return=Vlans)


class AsnRanges(Record):
@property
def available_asns(self):
"""
Represents the ``available-asns`` detail endpoint.

Returns a DetailEndpoint object that is the interface for
viewing and creating ASNs inside an ASN range.

:returns: :py:class:`.DetailEndpoint`

:Examples:

>>> asn_range = nb.ipam.asn_ranges.get(1)
>>> asn_range.available_asns.list()
[64512, 64513, 64514]

To create a new ASN:

>>> asn_range.available_asns.create()
64512

To create multiple ASNs:

>>> asn_range.available_asns.create([{} for i in range(2)])
[64513, 64514]
"""
return DetailEndpoint(self, "available-asns")
2 changes: 1 addition & 1 deletion pynetbox/models/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"dcim.poweroutlet": PowerOutlets,
"dcim.poweroutlettemplate": None,
"dcim.powerpanel": None,
"dcim.powerport": ConsolePorts,
"dcim.powerport": PowerPorts,
"dcim.powerporttemplate": None,
"dcim.rack": Racks,
"dcim.rackreservation": RackReservations,
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
black~=22.10
pytest==7.1.*
pytest-docker==1.0.*
PyYAML==6.0
PyYAML==6.0.1
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
requests>=2.20.0,<3.0
packaging<24.0
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
packages=find_packages(exclude=["tests", "tests.*"]),
install_requires=[
"requests>=2.20.0,<3.0",
"packaging<24.0"
],
zip_safe=False,
keywords=["netbox"],
Expand Down
20 changes: 16 additions & 4 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ def get_netbox_docker_version_tag(netbox_version):
major, minor = netbox_version.major, netbox_version.minor

if (major, minor) == (3, 3):
tag = "2.2.0"
tag = "2.3.0"
elif (major, minor) == (3, 4):
tag = "2.5.3"
elif (major, minor) == (3, 5):
tag = "2.6.1"
else:
raise NotImplementedError(
"Version %s is not currently supported" % netbox_version
Expand All @@ -48,7 +52,7 @@ def git_toplevel():
try:
subp.check_call(["which", "git"])
except subp.CalledProcessError:
pytest.skip(msg="git executable was not found on the host")
pytest.skip(reason="git executable was not found on the host")
return (
subp.check_output(["git", "rev-parse", "--show-toplevel"])
.decode("utf-8")
Expand All @@ -73,7 +77,7 @@ def netbox_docker_repo_dirpaths(pytestconfig, git_toplevel):
try:
subp.check_call(["which", "docker"])
except subp.CalledProcessError:
pytest.skip(msg="docker executable was not found on the host")
pytest.skip(reason="docker executable was not found on the host")
netbox_versions_by_repo_dirpaths = {}
for netbox_version in pytestconfig.option.netbox_versions:
repo_version_tag = get_netbox_docker_version_tag(netbox_version=netbox_version)
Expand Down Expand Up @@ -248,6 +252,14 @@ def docker_compose_file(pytestconfig, netbox_docker_repo_dirpaths):
"netboxcommunity/netbox:v%s" % netbox_version
)

new_services[new_service_name]["environment"] = {
"SKIP_SUPERUSER": "false",
"SUPERUSER_API_TOKEN": "0123456789abcdef0123456789abcdef01234567",
"SUPERUSER_EMAIL": "[email protected]",
"SUPERUSER_NAME": "admin",
"SUPERUSER_PASSWORD": "admin",
}

if service_name == "netbox":
# ensure the netbox container listens on a random port
new_services[new_service_name]["ports"] = ["8080"]
Expand Down Expand Up @@ -341,7 +353,7 @@ def docker_compose_file(pytestconfig, netbox_docker_repo_dirpaths):


def netbox_is_responsive(url):
"""Chack if the HTTP service is up and responsive."""
"""Check if the HTTP service is up and responsive."""
try:
response = requests.get(url)
if response.status_code == 200:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

nb = api.circuits

HEADERS = {"accept": "application/json;"}
HEADERS = {"accept": "application/json"}


class Generic:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_tenancy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

nb = api.tenancy

HEADERS = {"accept": "application/json;"}
HEADERS = {"accept": "application/json"}


class Generic:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

nb = api.users

HEADERS = {"accept": "application/json;"}
HEADERS = {"accept": "application/json"}


class Generic:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_virtualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

nb = api.virtualization

HEADERS = {"accept": "application/json;"}
HEADERS = {"accept": "application/json"}


class Generic:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_wireless.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

nb_app = api.wireless

HEADERS = {"accept": "application/json;"}
HEADERS = {"accept": "application/json"}


class Generic:
Expand Down
10 changes: 5 additions & 5 deletions tests/unit/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ def test_get_count(self):
expected = call(
"http://localhost:8001/api/dcim/devices/",
params={"q": "abcd", "limit": 1},
headers={"accept": "application/json;"},
headers={"accept": "application/json"},
)
test_obj.http_session.get.ok = True
test = test_obj.get_count()
self.assertEqual(test, 42)
test_obj.http_session.get.assert_called_with(
"http://localhost:8001/api/dcim/devices/",
params={"q": "abcd", "limit": 1},
headers={"accept": "application/json;"},
headers={"accept": "application/json"},
json=None,
)

Expand All @@ -49,7 +49,7 @@ def test_get_count_no_filters(self):
test_obj.http_session.get.assert_called_with(
"http://localhost:8001/api/dcim/devices/",
params={"limit": 1},
headers={"accept": "application/json;"},
headers={"accept": "application/json"},
json=None,
)

Expand All @@ -69,14 +69,14 @@ def test_get_manual_pagination(self):
expected = call(
"http://localhost:8001/api/dcim/devices/",
params={"offset": 20, "limit": 10},
headers={"accept": "application/json;"},
headers={"accept": "application/json"},
)
test_obj.http_session.get.ok = True
generator = test_obj.get()
self.assertEqual(len(list(generator)), 4)
test_obj.http_session.get.assert_called_with(
"http://localhost:8001/api/dcim/devices/",
params={"offset": 20, "limit": 10},
headers={"accept": "application/json;"},
headers={"accept": "application/json"},
json=None,
)
26 changes: 24 additions & 2 deletions tests/unit/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,32 @@


class RequestTestCase(unittest.TestCase):
def test_get_openapi(self):
def test_get_openapi_version_less_than_3_5(self):
test = Request("http://localhost:8080/api", Mock())
test.get_version = Mock(return_value="3.4")

# Mock the HTTP response
response_mock = Mock()
response_mock.ok = True
test.http_session.get.return_value = response_mock

test.get_openapi()
test.http_session.get.assert_called_with(
"http://localhost:8080/api/docs/?format=openapi",
headers={"Content-Type": "application/json;"},
headers={"Accept": "application/json", "Content-Type": "application/json"},
)

def test_get_openapi_version_3_5_or_greater(self):
test = Request("http://localhost:8080/api", Mock())
test.get_version = Mock(return_value="3.5")

# Mock the HTTP response
response_mock = Mock()
response_mock.ok = True
test.http_session.get.return_value = response_mock

test.get_openapi()
test.http_session.get.assert_called_with(
"http://localhost:8080/api/schema/",
headers={"Accept": "application/json", "Content-Type": "application/json"},
)
Loading