diff --git a/.travis.yml b/.travis.yml index 6de3d1ca..488614e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,9 +7,10 @@ install: python: - "2.7" + - "3.6" script: - - nosetests + - python -m "nose" - pep8 pynetbox deploy: diff --git a/README.md b/README.md index 40785e21..4a97662f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.org/digitalocean/pynetbox.svg?branch=master)](https://travis-ci.org/digitalocean/pynetbox) + # Pynetbox Python API client library for [NetBox](https://github.com/digitalocean/netbox). diff --git a/docs/IPAM.rst b/docs/IPAM.rst new file mode 100644 index 00000000..352c4273 --- /dev/null +++ b/docs/IPAM.rst @@ -0,0 +1,5 @@ +IPAM +======== + +.. autoclass:: pynetbox.ipam.Prefixes + :members: \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 4fe9a603..4375a609 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,6 +18,7 @@ # import os import sys +import pkg_resources sys.path.insert(0, os.path.abspath('../')) @@ -54,9 +55,9 @@ # built documents. # # The short X.Y version. -version = u'1.0.0' +version = pkg_resources.get_distribution("pynetbox").version # The full version, including alpha/beta/rc tags. -release = u'1.0.0' +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -93,8 +94,15 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - +# html_static_path = ['_static'] + +html_sidebars = {'**': [ + 'globaltoc.html', + 'relations.html', + 'sourcelink.html', + 'searchbox.html' + ] +} # -- Options for HTMLHelp output ------------------------------------------ diff --git a/docs/endpoint.rst b/docs/endpoint.rst new file mode 100644 index 00000000..bbc0e5d0 --- /dev/null +++ b/docs/endpoint.rst @@ -0,0 +1,8 @@ +Endpoint +======== + +.. autoclass:: pynetbox.lib.endpoint.Endpoint + :members: + +.. autoclass:: pynetbox.lib.endpoint.DetailEndpoint + :members: \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 1b7580fd..2c722868 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,6 +2,11 @@ :maxdepth: 2 :caption: Contents: + endpoint + response + request + IPAM + API === @@ -14,30 +19,6 @@ App .. autoclass:: pynetbox.api.App -Endpoint -======== - -.. autoclass:: pynetbox.lib.endpoint.Endpoint - :members: - -Response -======== - -.. autoclass:: pynetbox.lib.response.Record - :members: - -.. autoclass:: pynetbox.lib.response.IPRecord - :members: - -.. autoclass:: pynetbox.lib.response.BoolRecord - :members: - -Request -======== - -.. autoclass:: pynetbox.lib.query.RequestError - :members: - Indices and tables ================== diff --git a/docs/request.rst b/docs/request.rst new file mode 100644 index 00000000..9140d754 --- /dev/null +++ b/docs/request.rst @@ -0,0 +1,5 @@ +Request +======== + +.. autoclass:: pynetbox.lib.query.RequestError + :members: \ No newline at end of file diff --git a/docs/response.rst b/docs/response.rst new file mode 100644 index 00000000..cc91a50d --- /dev/null +++ b/docs/response.rst @@ -0,0 +1,8 @@ +Response +======== + +.. autoclass:: pynetbox.lib.response.Record + :members: + +.. autoclass:: pynetbox.lib.response.IPRecord + :members: \ No newline at end of file diff --git a/pynetbox/api.py b/pynetbox/api.py index 63c5da3f..546390e6 100644 --- a/pynetbox/api.py +++ b/pynetbox/api.py @@ -51,6 +51,7 @@ class Api(object): * secrets * tenancy * extras + * virtualization Calling any of these attributes will return :py:class:`.App` which exposes endpoints as attributes. @@ -115,3 +116,4 @@ def __init__(self, url, token=None, private_key=None, self.secrets = App('secrets', api_kwargs=self.api_kwargs) self.tenancy = App('tenancy', api_kwargs=self.api_kwargs) self.extras = App('extras', api_kwargs=self.api_kwargs) + self.virtualization = App('virtualization', api_kwargs=self.api_kwargs) diff --git a/pynetbox/ipam.py b/pynetbox/ipam.py index e00b305f..17a68244 100644 --- a/pynetbox/ipam.py +++ b/pynetbox/ipam.py @@ -14,6 +14,7 @@ limitations under the License. ''' from pynetbox.lib.response import IPRecord +from pynetbox.lib.endpoint import DetailEndpoint class IpAddresses(IPRecord): @@ -27,6 +28,74 @@ class Prefixes(IPRecord): def __str__(self): return str(self.prefix) + @property + def available_ips(self): + """ Represents the ``available-ips`` detail endpoint. + + Returns a DetailEndpoint object that is the interface for + viewing and creating IP addresses inside a prefix. + + :returns: :py:class:`.DetailEndpoint` + + :Examples: + + >>> prefix = nb.ipam.prefixes.get(24) + >>> prefix.available_ips.list() + [{u'vrf': None, u'family': 4, u'address': u'10.1.1.49/30'}...] + + To create a single IP: + + >>> prefix = nb.ipam.prefixes.get(24) + >>> prefix.available_ips.create() + {u'status': 1, u'description': u'', u'nat_inside': None...} + + + To create multiple IPs: + + >>> prefix = nb.ipam.prefixes.get(24) + >>> create = prefix.available_ips.create([{} for i in range(2)]) + >>> len(create) + 2 + """ + return DetailEndpoint( + 'available-ips', + parent_obj=self, + ) + + @property + def available_prefixes(self): + ''' Represents the ``available-prefixes`` detail endpoint. + + Returns a DetailEndpoint object that is the interface for + viewing and creating prefixes inside a parent prefix. + + Very similar to :py:meth:`~pynetbox.ipam.Prefixes.available_ips` + , except that dict (or list of dicts) passed to ``.create()`` + needs to have a ``prefex_length`` key/value specifed. + + :returns: :py:class:`.DetailEndpoint` + + :Examples: + + >>> prefix.available_prefixes.list() + [{u'prefix': u'10.1.1.44/30', u'vrf': None, u'family': 4}] + + + Creating a single child prefix: + + >>> prefix = nb.ipam.prefixes.get(1) + >>> new_prefix = prefix.available_prefixes.create( + ... {'prefix_length': 29} + ...) + >>> new_prefix['prefix'] + u'10.1.1.56/29' + + ''' + return DetailEndpoint( + 'available-prefixes', + parent_obj=self, + ) + class Aggregates(IPRecord): diff --git a/pynetbox/lib/endpoint.py b/pynetbox/lib/endpoint.py index 87ef2a2c..85e1a780 100644 --- a/pynetbox/lib/endpoint.py +++ b/pynetbox/lib/endpoint.py @@ -245,34 +245,25 @@ def filter(self, *args, **kwargs): CACHE[self.endpoint_name].extend(ret) return ret - def _create(self, data): - """Base create method + def create(self, *args, **kwargs): + """Creates an object on an endpoint. - Used in .create() and .bulk_create() - """ + Allows for the creation of new objects on an endpoint. Named + arguments are converted to json properties, and a single object + is created. NetBox's bulk creation capabilities can be used by + passing a list of dictionaries as the first argument. - req = Request( - base=self.url, - token=self.token, - session_key=self.session_key, - version=self.version, - ) - post = req.post(data) - if post: - return post['id'] - else: - return False + .. note: - def create(self, **kwargs): - """Creates an object on an endpoint. - - Allows for the creation of new objects on an endpoint. - Named arguments are converted to json properties. + Any positional arguments will supercede named ones. + :arg list \*args: A list of dictionaries containing the + properties of the objects to be created. :arg str \**kwargs: key/value strings representing properties on a json object. - :returns: Integer of created object's id. + :returns: A response from NetBox as a dictionary or list of + dictionaries. :Examples: @@ -284,14 +275,79 @@ def create(self, **kwargs): ... device_role=1, ... ) >>> + + Use bulk creation by passing a list of dictionaries: + + >>> nb.dcim.devices.create([ + ... { + ... "name": "test1-core3", + ... "device_role": 3, + ... "site": 1, + ... "device_type": 1, + ... "status": 1 + ... }, + ... { + ... "name": "test1-core4", + ... "device_role": 3, + ... "site": 1, + ... "device_type": 1, + ... "status": 1 + ... } + ... ]) """ - return self._create(kwargs) - def bulk_create(self, create_list): - """Bulk creation method. + return Request( + base=self.url, + token=self.token, + session_key=self.session_key, + version=self.version, + ).post(args[0] if len(args) > 0 else kwargs) + + +class DetailEndpoint(object): + '''Enables read/write Operations on detail endpoints. + + Endpoints like ``available-ips`` that are detail routes off + traditional endpoints are handled with this class. + ''' + + def __init__(self, name, parent_obj=None): + self.token = parent_obj.api_kwargs.get('token') + self.version = parent_obj.api_kwargs.get('version') + self.session_key = parent_obj.api_kwargs.get('session_key') + self.url = "{}/{}/{}/".format( + parent_obj.endpoint_meta.get('url'), + parent_obj.id, + name + ) + self.request_kwargs = dict( + base=self.url, + token=self.token, + session_key=self.session_key, + version=self.version, + ) + + def list(self): + """The view operation for a detail endpoint + + Returns the response from NetBox for a detail endpoint. + + :returns: A dictionary or list of dictionaries its retrieved + from NetBox. + """ + return Request(**self.request_kwargs).get() + + def create(self, data={}): + """The write operation for a detail endpoint. + + Creates objects on a detail endpoint in NetBox. + + :arg dict/list,optional data: A dictionary containing the + key/value pair of the items you're creating on the parent + object. Defaults to empty dict which will create a single + item with default values. - Does the same as .create() only accepting a list of dictionaries - with the the same key/value pairs as kwargs. + :returns: A dictionary or list of dictionaries its created in + NetBox. """ - for i in create_list: - self._create(i) + return Request(**self.request_kwargs).post(data) diff --git a/pynetbox/lib/query.py b/pynetbox/lib/query.py index 08b51051..4cf02dd3 100644 --- a/pynetbox/lib/query.py +++ b/pynetbox/lib/query.py @@ -154,6 +154,14 @@ def construct_url(input): else: return self.base + def normalize_url(self, url): + """ Builds a url for POST actions. + """ + if url[-1] != '/': + return "{}/".format(url) + + return url + def get(self): """Makes a GET request. @@ -187,7 +195,7 @@ def make_request(url): def req_all(url): req = make_request(url) - if req.get('results') is not None: + if isinstance(req, dict) and req.get('results') is not None: ret = req['results'] first_run = True while req['next']: @@ -255,7 +263,7 @@ def post(self, data): {'X-Session-Key': self.session_key} ) req = requests.post( - "{}/".format(self.url), + self.normalize_url(self.url), headers=headers, data=json.dumps(data) ) diff --git a/pynetbox/lib/response.py b/pynetbox/lib/response.py index 9aa49eda..3885f208 100644 --- a/pynetbox/lib/response.py +++ b/pynetbox/lib/response.py @@ -14,6 +14,7 @@ limitations under the License. ''' import netaddr +import six from pynetbox.lib.query import Request @@ -294,7 +295,7 @@ def _parse_values(self, values): v = lookup(v, api_kwargs=self.api_kwargs) else: v = self.default_ret(v, api_kwargs=self.api_kwargs) - if isinstance(v, (str, unicode)): + if isinstance(v, six.string_types): try: v = netaddr.IPNetwork(v) except netaddr.AddrFormatError: diff --git a/requirements.txt b/requirements.txt index 002968d4..041422fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ netaddr==0.7.18 -requests==2.10.0 \ No newline at end of file +requests==2.10.0 +six==1.11.0 \ No newline at end of file diff --git a/setup.py b/setup.py index c52ad40b..67b7146d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='pynetbox', - version='2.1.0', + version='3.0.0', description='NetBox API client library', url='https://github.com/digitalocean/pynetbox', author='Zach Moody', @@ -13,8 +13,9 @@ 'pynetbox.lib' ], install_requires=[ - 'netaddr', - 'requests' + 'netaddr==0.7.18', + 'requests==2.10.0', + 'six==1.11.0' ], zip_safe=False, keywords=['netbox'], @@ -23,6 +24,8 @@ 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', ], ) diff --git a/tests/fixtures/dcim/device_bulk_create.json b/tests/fixtures/dcim/device_bulk_create.json new file mode 100644 index 00000000..62672d9a --- /dev/null +++ b/tests/fixtures/dcim/device_bulk_create.json @@ -0,0 +1,40 @@ +[ + { + "id": 1, + "name": "test1-core3", + "device_type": 1, + "device_role": 3, + "tenant": null, + "platform": null, + "serial": "", + "asset_tag": null, + "site": 1, + "rack": null, + "position": null, + "face": null, + "status": 1, + "primary_ip4": null, + "primary_ip6": null, + "cluster": null, + "comments": "" + }, + { + "id": 2, + "name": "test1-core4", + "device_type": 1, + "device_role": 3, + "tenant": null, + "platform": null, + "serial": "", + "asset_tag": null, + "site": 1, + "rack": null, + "position": null, + "face": null, + "status": 1, + "primary_ip4": null, + "primary_ip6": null, + "cluster": null, + "comments": "" + } +] \ No newline at end of file diff --git a/tests/fixtures/ipam/available-ips-post.json b/tests/fixtures/ipam/available-ips-post.json new file mode 100644 index 00000000..2684952d --- /dev/null +++ b/tests/fixtures/ipam/available-ips-post.json @@ -0,0 +1,11 @@ +{ + "id": 1, + "address": "10.1.1.1/32", + "vrf": null, + "tenant": null, + "status": 1, + "role": null, + "interface": null, + "description": "", + "nat_inside": null +} \ No newline at end of file diff --git a/tests/fixtures/ipam/available-ips.json b/tests/fixtures/ipam/available-ips.json new file mode 100644 index 00000000..50ccad2e --- /dev/null +++ b/tests/fixtures/ipam/available-ips.json @@ -0,0 +1,17 @@ +[ + { + "family": 4, + "address": "10.1.1.2/27", + "vrf": null + }, + { + "family": 4, + "address": "10.1.1.3/27", + "vrf": null + }, + { + "family": 4, + "address": "10.1.1.7/27", + "vrf": null + } +] \ No newline at end of file diff --git a/tests/fixtures/ipam/available-prefixes-post.json b/tests/fixtures/ipam/available-prefixes-post.json new file mode 100644 index 00000000..cf329c6a --- /dev/null +++ b/tests/fixtures/ipam/available-prefixes-post.json @@ -0,0 +1,14 @@ +[ + { + "id": 4, + "prefix": "10.1.1.0/30", + "site": null, + "vrf": null, + "tenant": null, + "vlan": null, + "status": 1, + "role": null, + "is_pool": false, + "description": "" + } +] \ No newline at end of file diff --git a/tests/fixtures/ipam/available-prefixes.json b/tests/fixtures/ipam/available-prefixes.json new file mode 100644 index 00000000..01372fe3 --- /dev/null +++ b/tests/fixtures/ipam/available-prefixes.json @@ -0,0 +1,7 @@ +[ + { + "family": 4, + "prefix": "10.1.1.0/24", + "vrf": null + } +] \ No newline at end of file diff --git a/tests/fixtures/virtualization/cluster.json b/tests/fixtures/virtualization/cluster.json new file mode 100644 index 00000000..b8efe8f0 --- /dev/null +++ b/tests/fixtures/virtualization/cluster.json @@ -0,0 +1,24 @@ +{ + "id": 1, + "name": "vm-test-cluster", + "type": { + "id": 1, + "url": "http://localhost:8000/api/virtualization/cluster-types/1/", + "name": "vm-test-type", + "slug": "vm-test-type" + }, + "group": { + "id": 1, + "url": "http://localhost:8000/api/virtualization/cluster-groups/1/", + "name": "vm-test-group", + "slug": "vm-test-group" + }, + "site": { + "id": 1, + "url": "http://localhost:8000/api/dcim/sites/1/", + "name": "TEST1", + "slug": "test1" + }, + "comments": "", + "custom_fields": {} +} \ No newline at end of file diff --git a/tests/fixtures/virtualization/cluster_group.json b/tests/fixtures/virtualization/cluster_group.json new file mode 100644 index 00000000..e0ff03f9 --- /dev/null +++ b/tests/fixtures/virtualization/cluster_group.json @@ -0,0 +1,5 @@ +{ + "id": 1, + "name": "vm-test-group", + "slug": "vm-test-group" +} \ No newline at end of file diff --git a/tests/fixtures/virtualization/cluster_groups.json b/tests/fixtures/virtualization/cluster_groups.json new file mode 100644 index 00000000..3bbca160 --- /dev/null +++ b/tests/fixtures/virtualization/cluster_groups.json @@ -0,0 +1,12 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "name": "vm-test-group", + "slug": "vm-test-group" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/virtualization/cluster_type.json b/tests/fixtures/virtualization/cluster_type.json new file mode 100644 index 00000000..8b3d70a3 --- /dev/null +++ b/tests/fixtures/virtualization/cluster_type.json @@ -0,0 +1,5 @@ +{ + "id": 1, + "name": "vm-test-type", + "slug": "vm-test-type" +} \ No newline at end of file diff --git a/tests/fixtures/virtualization/cluster_types.json b/tests/fixtures/virtualization/cluster_types.json new file mode 100644 index 00000000..47401f18 --- /dev/null +++ b/tests/fixtures/virtualization/cluster_types.json @@ -0,0 +1,12 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "name": "vm-test-type", + "slug": "vm-test-type" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/virtualization/clusters.json b/tests/fixtures/virtualization/clusters.json new file mode 100644 index 00000000..56e9a1d0 --- /dev/null +++ b/tests/fixtures/virtualization/clusters.json @@ -0,0 +1,31 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "name": "vm-test-cluster", + "type": { + "id": 1, + "url": "http://localhost:8000/api/virtualization/cluster-types/1/", + "name": "vm-test-type", + "slug": "vm-test-type" + }, + "group": { + "id": 1, + "url": "http://localhost:8000/api/virtualization/cluster-groups/1/", + "name": "vm-test-group", + "slug": "vm-test-group" + }, + "site": { + "id": 1, + "url": "http://localhost:8000/api/dcim/sites/1/", + "name": "TEST1", + "slug": "test1" + }, + "comments": "", + "custom_fields": {} + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/virtualization/interface.json b/tests/fixtures/virtualization/interface.json new file mode 100644 index 00000000..6e3fc7e8 --- /dev/null +++ b/tests/fixtures/virtualization/interface.json @@ -0,0 +1,13 @@ +{ + "id": 223, + "name": "eth0", + "virtual_machine": { + "id": 1, + "url": "http://localhost:8000/api/virtualization/virtual-machines/1/", + "name": "vm-test01" + }, + "enabled": true, + "mac_address": null, + "mtu": null, + "description": "" +} \ No newline at end of file diff --git a/tests/fixtures/virtualization/interfaces.json b/tests/fixtures/virtualization/interfaces.json new file mode 100644 index 00000000..00b0be1b --- /dev/null +++ b/tests/fixtures/virtualization/interfaces.json @@ -0,0 +1,33 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "id": 223, + "name": "eth0", + "virtual_machine": { + "id": 1, + "url": "http://localhost:8000/api/virtualization/virtual-machines/1/", + "name": "vm-test01" + }, + "enabled": true, + "mac_address": null, + "mtu": null, + "description": "" + }, + { + "id": 224, + "name": "eth1", + "virtual_machine": { + "id": 1, + "url": "http://localhost:8000/api/virtualization/virtual-machines/1/", + "name": "vm-test01" + }, + "enabled": true, + "mac_address": null, + "mtu": null, + "description": "" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/virtualization/virtual_machine.json b/tests/fixtures/virtualization/virtual_machine.json new file mode 100644 index 00000000..7a0ce158 --- /dev/null +++ b/tests/fixtures/virtualization/virtual_machine.json @@ -0,0 +1,23 @@ +{ + "id": 1, + "name": "vm-test01", + "status": { + "value": 1, + "label": "Active" + }, + "cluster": { + "id": 1, + "url": "http://localhost:8000/api/virtualization/clusters/1/", + "name": "vm-test-cluster" + }, + "role": null, + "tenant": null, + "platform": null, + "primary_ip4": null, + "primary_ip6": null, + "vcpus": 2, + "memory": 1024, + "disk": 500, + "comments": "", + "custom_fields": {} +} diff --git a/tests/fixtures/virtualization/virtual_machines.json b/tests/fixtures/virtualization/virtual_machines.json new file mode 100644 index 00000000..45b4e3b0 --- /dev/null +++ b/tests/fixtures/virtualization/virtual_machines.json @@ -0,0 +1,30 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "name": "vm-test01", + "status": { + "value": 1, + "label": "Active" + }, + "cluster": { + "id": 1, + "url": "http://localhost:8000/api/virtualization/clusters/1/", + "name": "vm-test-cluster" + }, + "role": null, + "tenant": null, + "platform": null, + "primary_ip4": null, + "primary_ip6": null, + "vcpus": 2, + "memory": 1024, + "disk": 500, + "comments": "", + "custom_fields": {} + } + ] +} \ No newline at end of file diff --git a/tests/test_circuits.py b/tests/test_circuits.py index 5279e62a..44e4295c 100644 --- a/tests/test_circuits.py +++ b/tests/test_circuits.py @@ -1,9 +1,14 @@ import unittest +import six -from mock import patch -from util import Response +from .util import Response import pynetbox +if six.PY3: + from unittest.mock import patch +else: + from mock import patch + api = pynetbox.api( "http://localhost:8000", version='2.0' diff --git a/tests/test_dcim.py b/tests/test_dcim.py index 0744ea24..82b07205 100644 --- a/tests/test_dcim.py +++ b/tests/test_dcim.py @@ -1,9 +1,13 @@ import unittest +import six -from mock import patch -from util import Response +from .util import Response import pynetbox -import netaddr + +if six.PY3: + from unittest.mock import patch +else: + from mock import patch api = pynetbox.api( @@ -203,6 +207,29 @@ def test_create(self, mock): ret = nb.devices.create(**data) self.assertTrue(ret) + @patch( + 'pynetbox.lib.query.requests.post', + return_value=Response(fixture='dcim/device_bulk_create.json') + ) + def test_create_device_bulk(self, mock): + data = [ + { + 'name': 'test-device', + 'site': 1, + 'device_type': 1, + 'device_role': 1, + }, + { + 'name': 'test-device1', + 'site': 1, + 'device_type': 1, + 'device_role': 1, + }, + ] + ret = nb.devices.create(data) + self.assertTrue(ret) + self.assertTrue(len(ret), 2) + @patch( 'pynetbox.lib.query.requests.get', side_effect=[ diff --git a/tests/test_ipam.py b/tests/test_ipam.py index b3242108..b752fa20 100644 --- a/tests/test_ipam.py +++ b/tests/test_ipam.py @@ -1,11 +1,17 @@ import unittest +import json -from mock import patch -from util import Response import netaddr +import six +from .util import Response import pynetbox +if six.PY3: + from unittest.mock import patch +else: + from mock import patch + api = pynetbox.api( "http://localhost:8000", @@ -18,6 +24,11 @@ 'accept': 'application/json; version=2.0;' } +POST_HEADERS = { + 'Content-Type': 'application/json; version=2.0;', + 'authorization': 'Token None', +} + class GenericTest(object): name = None @@ -112,6 +123,94 @@ def test_modify(self, mock): self.assertEqual(ret_serialized['prefix'], '10.1.2.0/24') self.assertTrue(netaddr.IPNetwork(ret_serialized['prefix'])) + @patch( + 'pynetbox.lib.query.requests.get', + side_effect=[ + Response(fixture='ipam/prefix.json'), + Response(fixture='ipam/available-ips.json'), + ] + ) + def test_get_available_ips(self, mock): + pfx = nb.prefixes.get(1) + ret = pfx.available_ips.list() + mock.assert_called_with( + 'http://localhost:8000/api/ipam/prefixes/1/available-ips/'.format(self.name), + headers=HEADERS + ) + self.assertTrue(ret) + self.assertEqual(len(ret), 3) + + @patch( + 'pynetbox.lib.query.requests.post', + return_value=Response(fixture='ipam/available-ips-post.json') + ) + @patch( + 'pynetbox.lib.query.requests.get', + return_value=Response(fixture='ipam/prefix.json'), + ) + def test_create_available_ips(self, get, post): + expected_result = { + 'status': 1, + 'description': '', + 'nat_inside': None, + 'role': None, + 'vrf': None, + 'address': + '10.1.1.1/32', + 'interface': None, + 'id': 1, + 'tenant': None + } + create_parms = dict( + status=2, + ) + pfx = nb.prefixes.get(1) + ret = pfx.available_ips.create(create_parms) + post.assert_called_with( + 'http://localhost:8000/api/ipam/prefixes/1/available-ips/'.format(self.name), + headers=POST_HEADERS, + data=json.dumps(create_parms), + ) + self.assertTrue(ret) + self.assertEqual(ret, expected_result) + + @patch( + 'pynetbox.lib.query.requests.get', + side_effect=[ + Response(fixture='ipam/prefix.json'), + Response(fixture='ipam/available-prefixes.json'), + ] + ) + def test_get_available_prefixes(self, mock): + pfx = nb.prefixes.get(1) + ret = pfx.available_prefixes.list() + mock.assert_called_with( + 'http://localhost:8000/api/ipam/prefixes/1/available-prefixes/'.format(self.name), + headers=HEADERS + ) + self.assertTrue(ret) + + @patch( + 'pynetbox.lib.query.requests.post', + return_value=Response(fixture='ipam/available-prefixes-post.json') + ) + @patch( + 'pynetbox.lib.query.requests.get', + return_value=Response(fixture='ipam/prefix.json'), + ) + def test_create_available_prefixes(self, get, post): + create_parms = dict( + prefix_length=30, + ) + pfx = nb.prefixes.get(1) + ret = pfx.available_prefixes.create(create_parms) + post.assert_called_with( + 'http://localhost:8000/api/ipam/prefixes/1/available-prefixes/'.format(self.name), + headers=POST_HEADERS, + data=json.dumps(create_parms), + ) + self.assertTrue(ret) + class IPAddressTestCase(unittest.TestCase, GenericTest): name = 'ip_addresses' diff --git a/tests/test_tenancy.py b/tests/test_tenancy.py index 53d07edf..1f2ce2f8 100644 --- a/tests/test_tenancy.py +++ b/tests/test_tenancy.py @@ -1,9 +1,14 @@ import unittest +import six -from mock import patch -from util import Response +from .util import Response import pynetbox +if six.PY3: + from unittest.mock import patch +else: + from mock import patch + api = pynetbox.api( "http://localhost:8000", diff --git a/tests/test_virtualization.py b/tests/test_virtualization.py new file mode 100644 index 00000000..5dc6d81a --- /dev/null +++ b/tests/test_virtualization.py @@ -0,0 +1,107 @@ +import unittest +import six + +from .util import Response +import pynetbox + +if six.PY3: + from unittest.mock import patch +else: + from mock import patch + + +api = pynetbox.api( + "http://localhost:8000", + version='2.0' +) + +nb = api.virtualization + +HEADERS = { + 'accept': 'application/json; version=2.0;' +} + + +class GenericTest(object): + name = None + ret = pynetbox.lib.response.Record + app = 'virtualization' + + def test_get_all(self): + with patch( + 'pynetbox.lib.query.requests.get', + return_value=Response(fixture='{}/{}.json'.format( + self.app, + self.name + )) + ) as mock: + ret = getattr(nb, self.name).all() + self.assertTrue(ret) + self.assertTrue(isinstance(ret, list)) + self.assertTrue(isinstance(ret[0], self.ret)) + mock.assert_called_with( + 'http://localhost:8000/api/{}/{}/'.format( + self.app, + self.name.replace('_', '-') + ), + headers=HEADERS + ) + + def test_filter(self): + with patch( + 'pynetbox.lib.query.requests.get', + return_value=Response(fixture='{}/{}.json'.format( + self.app, + self.name + )) + ) as mock: + ret = getattr(nb, self.name).filter(pk=1) + self.assertTrue(ret) + self.assertTrue(isinstance(ret, list)) + self.assertTrue(isinstance(ret[0], self.ret)) + mock.assert_called_with( + 'http://localhost:8000/api/{}/{}/?pk=1'.format( + self.app, + self.name.replace('_', '-') + ), + headers=HEADERS + ) + + def test_get(self): + with patch( + 'pynetbox.lib.query.requests.get', + return_value=Response(fixture='{}/{}.json'.format( + self.app, + self.name[:-1] + )) + ) as mock: + ret = getattr(nb, self.name).get(1) + self.assertTrue(ret) + self.assertTrue(isinstance(ret, self.ret)) + mock.assert_called_with( + 'http://localhost:8000/api/{}/{}/1/'.format( + self.app, + self.name.replace('_', '-') + ), + headers=HEADERS + ) + + +class ClusterTypesTestCase(unittest.TestCase, GenericTest): + name = 'cluster_types' + + +class ClusterGroupsTestCase(unittest.TestCase, GenericTest): + name = 'cluster_groups' + + +class ClustersTestCase(unittest.TestCase, GenericTest): + name = 'clusters' + + +class VirtualMachinesTestCase(unittest.TestCase, GenericTest): + name = 'virtual_machines' + + +class InterfacesTestCase(unittest.TestCase, GenericTest): + name = 'interfaces'