From 82e34a50eec4398844d91c9075aae9394b9db9de Mon Sep 17 00:00:00 2001 From: nautics889 Date: Tue, 21 Mar 2023 20:00:24 +0200 Subject: [PATCH 01/27] Fix unused `PowerPorts` model (#535) Updated models/mapper.py: set `PowerPorts` for "dcim.powerport" in the map-dict. --- pynetbox/models/mapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pynetbox/models/mapper.py b/pynetbox/models/mapper.py index a57c07f4..bbd4ed27 100644 --- a/pynetbox/models/mapper.py +++ b/pynetbox/models/mapper.py @@ -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, From 690bd03ddeef411a35643354e8ef63445e50e9ed Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:02:56 +0530 Subject: [PATCH 02/27] migrate from pkg_resources to importlib --- pynetbox/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pynetbox/__init__.py b/pynetbox/__init__.py index 0e1f5100..ac03699e 100644 --- a/pynetbox/__init__.py +++ b/pynetbox/__init__.py @@ -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') From 1565b9c5aea442d17e366c4c26a768326c368bf8 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:03:12 +0530 Subject: [PATCH 03/27] adds core app --- pynetbox/core/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pynetbox/core/api.py b/pynetbox/core/api.py index 3b9b79d3..5f673931 100644 --- a/pynetbox/core/api.py +++ b/pynetbox/core/api.py @@ -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 @@ -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") From 208542208abc33c9dc8876a4da5056818513d67f Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:03:31 +0530 Subject: [PATCH 04/27] adds endpoints added in 3.5 --- pynetbox/models/dcim.py | 6 +++++- pynetbox/models/ipam.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index 5f21bdd3..70e92d99 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -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 @@ -121,6 +121,10 @@ def napalm(self): """ return RODetailEndpoint(self, "napalm") + @property + def render_config(self): + return DetailEndpoint(self, "render-config") + class InterfaceConnections(Record): def __str__(self): diff --git a/pynetbox/models/ipam.py b/pynetbox/models/ipam.py index d97d32cd..64488541 100644 --- a/pynetbox/models/ipam.py +++ b/pynetbox/models/ipam.py @@ -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") From 46ede22dfd3fd0f33d39d7e1d433d1f52ae7af9a Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:03:47 +0530 Subject: [PATCH 05/27] adds support for 3.5 --- pynetbox/core/query.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pynetbox/core/query.py b/pynetbox/core/query.py index 84a823b8..06c1d17d 100644 --- a/pynetbox/core/query.py +++ b/pynetbox/core/query.py @@ -15,6 +15,7 @@ """ import concurrent.futures as cf import json +from packaging import version def calc_pages(limit, count): @@ -153,12 +154,22 @@ def __init__( def get_openapi(self): """Gets the OpenAPI Spec""" headers = { + "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: From 0c87b10dd94e6eb94181cff73173c97ce483bcc7 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:06:23 +0530 Subject: [PATCH 06/27] lint fixes --- pynetbox/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pynetbox/__init__.py b/pynetbox/__init__.py index ac03699e..024701e3 100644 --- a/pynetbox/__init__.py +++ b/pynetbox/__init__.py @@ -3,4 +3,4 @@ from pynetbox.core.query import RequestError, AllocationError, ContentError from pynetbox.core.api import Api as api -__version__ = metadata(__name__).get('Version') +__version__ = metadata(__name__).get("Version") From 723141b5e3637a4012f87c9d963070368d4050c7 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:22:24 +0530 Subject: [PATCH 07/27] updates openapi tests --- tests/unit/test_request.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_request.py b/tests/unit/test_request.py index 3a577f2f..a4cb9bf7 100644 --- a/tests/unit/test_request.py +++ b/tests/unit/test_request.py @@ -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;"}, ) From e3bf5cc2fccc2892da867717bfaeed0d6512d98c Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:26:41 +0530 Subject: [PATCH 08/27] adds testing for 3.4 and 3.5 --- .github/workflows/py3.yml | 2 +- tests/integration/conftest.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/py3.yml b/.github/workflows/py3.yml index 5a7f7322..d78151f2 100644 --- a/.github/workflows/py3.yml +++ b/.github/workflows/py3.yml @@ -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 diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 61eef2d0..cf338bd8 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -29,6 +29,10 @@ def get_netbox_docker_version_tag(netbox_version): if (major, minor) == (3, 3): tag = "2.2.0" + elif (major, minor) == (3, 4): + tag = "2.4.0" + elif (major, minor) == (3, 5): + tag = "2.6.0" else: raise NotImplementedError( "Version %s is not currently supported" % netbox_version From 4bb0ae4be9228bb8f42ef0caec1d7ea9f0a260e7 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:28:29 +0530 Subject: [PATCH 09/27] updates pytest.skip --- tests/integration/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index cf338bd8..0566d75c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -52,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") @@ -77,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) From e3d3f5f835a2da6b17cdd2dbb4089c8e9bea06c1 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:35:42 +0530 Subject: [PATCH 10/27] updates docker tags for testing --- tests/integration/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 0566d75c..249dc547 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -28,11 +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.4.0" + tag = "2.5.3" elif (major, minor) == (3, 5): - tag = "2.6.0" + tag = "2.6.1" else: raise NotImplementedError( "Version %s is not currently supported" % netbox_version From 7d1ea3d85bd2748b4db4fc30bd423746cdbdc0e8 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:59:37 +0530 Subject: [PATCH 11/27] fixes superuser account creation for testing --- tests/integration/conftest.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 249dc547..cee972c1 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -252,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": "admin@example.com", + "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"] @@ -345,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: From d6743544190e766c76758310ec6df55d5cb50247 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 13:19:25 +0530 Subject: [PATCH 12/27] fixed requirements --- requirements-dev.txt | 2 +- requirements.txt | 1 + setup.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ce2b2e5f..50604511 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ black~=22.10 pytest==7.1.* pytest-docker==1.0.* -PyYAML==6.0 +PyYAML==6.0.1 diff --git a/requirements.txt b/requirements.txt index 44d90730..f0b78821 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ requests>=2.20.0,<3.0 +packaging<24.0 \ No newline at end of file diff --git a/setup.py b/setup.py index c18c2dce..70c1a8e1 100644 --- a/setup.py +++ b/setup.py @@ -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"], From 2fdd20ae56c809f26afe43c5549262a018558520 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 13:28:09 +0530 Subject: [PATCH 13/27] updates the docstring for render-config endpoint --- pynetbox/models/dcim.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index 70e92d99..7dd32d45 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -123,6 +123,19 @@ def napalm(self): @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") From 878147616ab5a45f4e8b1a87bf66701b358c13d0 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 13:36:13 +0530 Subject: [PATCH 14/27] removes extra semicolon from content type value --- pynetbox/core/query.py | 10 +++++----- tests/test_circuits.py | 2 +- tests/test_tenancy.py | 2 +- tests/test_users.py | 2 +- tests/test_virtualization.py | 2 +- tests/test_wireless.py | 2 +- tests/unit/test_query.py | 10 +++++----- tests/unit/test_request.py | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pynetbox/core/query.py b/pynetbox/core/query.py index 06c1d17d..5b9b40f9 100644 --- a/pynetbox/core/query.py +++ b/pynetbox/core/query.py @@ -155,7 +155,7 @@ def get_openapi(self): """Gets the OpenAPI Spec""" headers = { "Accept": "application/json", - "Content-Type": "application/json;", + "Content-Type": "application/json", } current_version = version.parse(self.get_version()) @@ -186,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), @@ -203,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( @@ -224,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) diff --git a/tests/test_circuits.py b/tests/test_circuits.py index cb07b55f..809178b4 100644 --- a/tests/test_circuits.py +++ b/tests/test_circuits.py @@ -11,7 +11,7 @@ nb = api.circuits -HEADERS = {"accept": "application/json;"} +HEADERS = {"accept": "application/json"} class Generic: diff --git a/tests/test_tenancy.py b/tests/test_tenancy.py index 80d4938d..211940b3 100644 --- a/tests/test_tenancy.py +++ b/tests/test_tenancy.py @@ -11,7 +11,7 @@ nb = api.tenancy -HEADERS = {"accept": "application/json;"} +HEADERS = {"accept": "application/json"} class Generic: diff --git a/tests/test_users.py b/tests/test_users.py index 1a325673..9b3eb6c8 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -11,7 +11,7 @@ nb = api.users -HEADERS = {"accept": "application/json;"} +HEADERS = {"accept": "application/json"} class Generic: diff --git a/tests/test_virtualization.py b/tests/test_virtualization.py index 7e27a5d3..3b5fe136 100644 --- a/tests/test_virtualization.py +++ b/tests/test_virtualization.py @@ -11,7 +11,7 @@ nb = api.virtualization -HEADERS = {"accept": "application/json;"} +HEADERS = {"accept": "application/json"} class Generic: diff --git a/tests/test_wireless.py b/tests/test_wireless.py index b819a652..47a7009a 100644 --- a/tests/test_wireless.py +++ b/tests/test_wireless.py @@ -9,7 +9,7 @@ nb_app = api.wireless -HEADERS = {"accept": "application/json;"} +HEADERS = {"accept": "application/json"} class Generic: diff --git a/tests/unit/test_query.py b/tests/unit/test_query.py index 93f50322..7877c76b 100644 --- a/tests/unit/test_query.py +++ b/tests/unit/test_query.py @@ -20,7 +20,7 @@ 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() @@ -28,7 +28,7 @@ def test_get_count(self): 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, ) @@ -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, ) @@ -69,7 +69,7 @@ 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() @@ -77,6 +77,6 @@ def test_get_manual_pagination(self): 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, ) diff --git a/tests/unit/test_request.py b/tests/unit/test_request.py index a4cb9bf7..682cd5f0 100644 --- a/tests/unit/test_request.py +++ b/tests/unit/test_request.py @@ -17,7 +17,7 @@ def test_get_openapi_version_less_than_3_5(self): test.get_openapi() test.http_session.get.assert_called_with( "http://localhost:8080/api/docs/?format=openapi", - headers={"Accept": "application/json", "Content-Type": "application/json;"}, + headers={"Accept": "application/json", "Content-Type": "application/json"}, ) def test_get_openapi_version_3_5_or_greater(self): @@ -32,5 +32,5 @@ def test_get_openapi_version_3_5_or_greater(self): test.get_openapi() test.http_session.get.assert_called_with( "http://localhost:8080/api/schema/", - headers={"Accept": "application/json", "Content-Type": "application/json;"}, + headers={"Accept": "application/json", "Content-Type": "application/json"}, ) From 2f8607a719cde4dbd277d93e44b3934826697595 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:02:56 +0530 Subject: [PATCH 15/27] migrate from pkg_resources to importlib --- pynetbox/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pynetbox/__init__.py b/pynetbox/__init__.py index 0e1f5100..ac03699e 100644 --- a/pynetbox/__init__.py +++ b/pynetbox/__init__.py @@ -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') From 1c07649882a2a87da727c7ee9cd1235da9f1c194 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:03:12 +0530 Subject: [PATCH 16/27] adds core app --- pynetbox/core/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pynetbox/core/api.py b/pynetbox/core/api.py index 3b9b79d3..5f673931 100644 --- a/pynetbox/core/api.py +++ b/pynetbox/core/api.py @@ -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 @@ -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") From 2216701620eaa76884fdcd4f6b21128c450f76e9 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:03:31 +0530 Subject: [PATCH 17/27] adds endpoints added in 3.5 --- pynetbox/models/dcim.py | 6 +++++- pynetbox/models/ipam.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index 5f21bdd3..70e92d99 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -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 @@ -121,6 +121,10 @@ def napalm(self): """ return RODetailEndpoint(self, "napalm") + @property + def render_config(self): + return DetailEndpoint(self, "render-config") + class InterfaceConnections(Record): def __str__(self): diff --git a/pynetbox/models/ipam.py b/pynetbox/models/ipam.py index d97d32cd..64488541 100644 --- a/pynetbox/models/ipam.py +++ b/pynetbox/models/ipam.py @@ -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") From c68c3b1658570f129609feaf596574da103c3feb Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:03:47 +0530 Subject: [PATCH 18/27] adds support for 3.5 --- pynetbox/core/query.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pynetbox/core/query.py b/pynetbox/core/query.py index 84a823b8..06c1d17d 100644 --- a/pynetbox/core/query.py +++ b/pynetbox/core/query.py @@ -15,6 +15,7 @@ """ import concurrent.futures as cf import json +from packaging import version def calc_pages(limit, count): @@ -153,12 +154,22 @@ def __init__( def get_openapi(self): """Gets the OpenAPI Spec""" headers = { + "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: From 1f2cc83173731fc17c1ebff6e3f49c2027a4f349 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:06:23 +0530 Subject: [PATCH 19/27] lint fixes --- pynetbox/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pynetbox/__init__.py b/pynetbox/__init__.py index ac03699e..024701e3 100644 --- a/pynetbox/__init__.py +++ b/pynetbox/__init__.py @@ -3,4 +3,4 @@ from pynetbox.core.query import RequestError, AllocationError, ContentError from pynetbox.core.api import Api as api -__version__ = metadata(__name__).get('Version') +__version__ = metadata(__name__).get("Version") From 16d766804334e8f955c263e87881dda52f83cb06 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:22:24 +0530 Subject: [PATCH 20/27] updates openapi tests --- tests/unit/test_request.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_request.py b/tests/unit/test_request.py index 3a577f2f..a4cb9bf7 100644 --- a/tests/unit/test_request.py +++ b/tests/unit/test_request.py @@ -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;"}, ) From ac8a47879d42bc05ebe21e88303e0b5e850b5f48 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:26:41 +0530 Subject: [PATCH 21/27] adds testing for 3.4 and 3.5 --- .github/workflows/py3.yml | 2 +- tests/integration/conftest.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/py3.yml b/.github/workflows/py3.yml index 5a7f7322..d78151f2 100644 --- a/.github/workflows/py3.yml +++ b/.github/workflows/py3.yml @@ -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 diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 61eef2d0..cf338bd8 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -29,6 +29,10 @@ def get_netbox_docker_version_tag(netbox_version): if (major, minor) == (3, 3): tag = "2.2.0" + elif (major, minor) == (3, 4): + tag = "2.4.0" + elif (major, minor) == (3, 5): + tag = "2.6.0" else: raise NotImplementedError( "Version %s is not currently supported" % netbox_version From 8f5696c786b404016c0b9b9bdb7b25a605237e63 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:28:29 +0530 Subject: [PATCH 22/27] updates pytest.skip --- tests/integration/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index cf338bd8..0566d75c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -52,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") @@ -77,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) From 191ebb55e46075ed7e149647b9baa11881da9f9b Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:35:42 +0530 Subject: [PATCH 23/27] updates docker tags for testing --- tests/integration/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 0566d75c..249dc547 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -28,11 +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.4.0" + tag = "2.5.3" elif (major, minor) == (3, 5): - tag = "2.6.0" + tag = "2.6.1" else: raise NotImplementedError( "Version %s is not currently supported" % netbox_version From 0cb704023a3b98c8ed30cb6d7d036f58c7a2c69f Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 01:59:37 +0530 Subject: [PATCH 24/27] fixes superuser account creation for testing --- tests/integration/conftest.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 249dc547..cee972c1 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -252,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": "admin@example.com", + "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"] @@ -345,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: From 232367e34a51264686bb4141c2378d109ca176b1 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 13:19:25 +0530 Subject: [PATCH 25/27] fixed requirements --- requirements-dev.txt | 2 +- requirements.txt | 1 + setup.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ce2b2e5f..50604511 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ black~=22.10 pytest==7.1.* pytest-docker==1.0.* -PyYAML==6.0 +PyYAML==6.0.1 diff --git a/requirements.txt b/requirements.txt index 44d90730..f0b78821 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ requests>=2.20.0,<3.0 +packaging<24.0 \ No newline at end of file diff --git a/setup.py b/setup.py index c18c2dce..70c1a8e1 100644 --- a/setup.py +++ b/setup.py @@ -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"], From 2db0c8994ec6afdb21e37d0bc89774bca8e46273 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 13:28:09 +0530 Subject: [PATCH 26/27] updates the docstring for render-config endpoint --- pynetbox/models/dcim.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index 70e92d99..7dd32d45 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -123,6 +123,19 @@ def napalm(self): @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") From 433c951fd27f2323f40976b89af34f1febf703c5 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 25 Aug 2023 13:36:13 +0530 Subject: [PATCH 27/27] removes extra semicolon from content type value --- pynetbox/core/query.py | 10 +++++----- tests/test_circuits.py | 2 +- tests/test_tenancy.py | 2 +- tests/test_users.py | 2 +- tests/test_virtualization.py | 2 +- tests/test_wireless.py | 2 +- tests/unit/test_query.py | 10 +++++----- tests/unit/test_request.py | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pynetbox/core/query.py b/pynetbox/core/query.py index 06c1d17d..5b9b40f9 100644 --- a/pynetbox/core/query.py +++ b/pynetbox/core/query.py @@ -155,7 +155,7 @@ def get_openapi(self): """Gets the OpenAPI Spec""" headers = { "Accept": "application/json", - "Content-Type": "application/json;", + "Content-Type": "application/json", } current_version = version.parse(self.get_version()) @@ -186,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), @@ -203,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( @@ -224,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) diff --git a/tests/test_circuits.py b/tests/test_circuits.py index cb07b55f..809178b4 100644 --- a/tests/test_circuits.py +++ b/tests/test_circuits.py @@ -11,7 +11,7 @@ nb = api.circuits -HEADERS = {"accept": "application/json;"} +HEADERS = {"accept": "application/json"} class Generic: diff --git a/tests/test_tenancy.py b/tests/test_tenancy.py index 80d4938d..211940b3 100644 --- a/tests/test_tenancy.py +++ b/tests/test_tenancy.py @@ -11,7 +11,7 @@ nb = api.tenancy -HEADERS = {"accept": "application/json;"} +HEADERS = {"accept": "application/json"} class Generic: diff --git a/tests/test_users.py b/tests/test_users.py index 1a325673..9b3eb6c8 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -11,7 +11,7 @@ nb = api.users -HEADERS = {"accept": "application/json;"} +HEADERS = {"accept": "application/json"} class Generic: diff --git a/tests/test_virtualization.py b/tests/test_virtualization.py index 7e27a5d3..3b5fe136 100644 --- a/tests/test_virtualization.py +++ b/tests/test_virtualization.py @@ -11,7 +11,7 @@ nb = api.virtualization -HEADERS = {"accept": "application/json;"} +HEADERS = {"accept": "application/json"} class Generic: diff --git a/tests/test_wireless.py b/tests/test_wireless.py index b819a652..47a7009a 100644 --- a/tests/test_wireless.py +++ b/tests/test_wireless.py @@ -9,7 +9,7 @@ nb_app = api.wireless -HEADERS = {"accept": "application/json;"} +HEADERS = {"accept": "application/json"} class Generic: diff --git a/tests/unit/test_query.py b/tests/unit/test_query.py index 93f50322..7877c76b 100644 --- a/tests/unit/test_query.py +++ b/tests/unit/test_query.py @@ -20,7 +20,7 @@ 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() @@ -28,7 +28,7 @@ def test_get_count(self): 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, ) @@ -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, ) @@ -69,7 +69,7 @@ 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() @@ -77,6 +77,6 @@ def test_get_manual_pagination(self): 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, ) diff --git a/tests/unit/test_request.py b/tests/unit/test_request.py index a4cb9bf7..682cd5f0 100644 --- a/tests/unit/test_request.py +++ b/tests/unit/test_request.py @@ -17,7 +17,7 @@ def test_get_openapi_version_less_than_3_5(self): test.get_openapi() test.http_session.get.assert_called_with( "http://localhost:8080/api/docs/?format=openapi", - headers={"Accept": "application/json", "Content-Type": "application/json;"}, + headers={"Accept": "application/json", "Content-Type": "application/json"}, ) def test_get_openapi_version_3_5_or_greater(self): @@ -32,5 +32,5 @@ def test_get_openapi_version_3_5_or_greater(self): test.get_openapi() test.http_session.get.assert_called_with( "http://localhost:8080/api/schema/", - headers={"Accept": "application/json", "Content-Type": "application/json;"}, + headers={"Accept": "application/json", "Content-Type": "application/json"}, )