├── .gitignore ├── LICENSE ├── README.md ├── examples ├── With-pynetbox-client.ipynb ├── holding.txt ├── setup.env ├── testdrive.py └── testdrive_pynb.py ├── netbox_pyswagger ├── __init__.py ├── client.py └── patch_2_2.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # PyCharm 71 | .idea 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jeremy Schulman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Swagger Client for Netbox 2 | 3 | If you love [Netbox](https://github.com/digitalocean/netbox), you want to automate it 4 | via the REST API, and you want a client that supports an interactive enviornment like [ipython](https://ipython.org/) 5 | and [jupyter notesbooks](http://jupyter.org/), then this client is for you! 6 | 7 | The gist of this client is that it consumes the Netbox Swagger 2.0 spec and uses that information 8 | to dynamically create a client with all API capabilities, model parameters as objects, and perform data validataton 9 | on all request parameters and responses. You never have to worry about the client being "out of sync" 10 | with the Netbox API. This client is specifically designed so that you can *introspect*, that is show in a "help" like manner, 11 | everything about each API, parameter, and data-type so that you do not need to have any API docs. You can use 12 | this client in an interactive Python shell, such as ipython and jupyter notebooks, in order to create a *CLI-like* user 13 | experience and not require **hard-core** programming to automate the Netbox system. For these purpose, this 14 | client was built with the halutz package - see these [tutorials](https://github.com/jeremyschulman/halutz/tree/master/docs) 15 | for usage. 16 | 17 | --- 18 | **NOTE:**    If you are already using the pynetbox 19 | client you can use both together. See this example [tutorial](examples/With-pynetbox-client.ipynb). 20 | 21 | --- 22 | 23 | # Installation 24 | 25 | ```bash 26 | pip install netbox-pyswagger 27 | ``` 28 | 29 | # Quickstart 30 | 31 | This example shows you how to create a client, and create a VLAN. 32 | ```python 33 | from netbox_pyswagger.client import Client 34 | 35 | server_url = "localhost:32768" 36 | api_token = "0123456789abcdef0123456789abcdef01234567" 37 | 38 | netbox = Client(server_url, api_token=api_token) 39 | 40 | # Use the `request` attribute to access a specific API based on the Swagger tag value 41 | # (ipam) and the Swagger operationId value (ipam_vlans_create) 42 | 43 | new_vlan = netbox.request.ipam.ipam_vlans_create 44 | 45 | # the command body parameter is called 'data' and here we set the required values 46 | # if you set an invalid value, like setting `vid` to "foobaz", you will get a validation 47 | # exception 48 | 49 | new_vlan.data.vid = 10 50 | new_vlan.data.name = 'Blue' 51 | 52 | # finally execute the command, which provides us the response data and the indication 53 | # if the command executed ok or not. If the command had addition (non-body) parameters 54 | # you would provide them as key-value pairs to the call. 55 | 56 | resp, ok = new_vlan() 57 | ``` 58 | 59 | The variable `ok` is True when the command is successful, and here is the example output 60 | of the response data, `resp`: 61 | 62 | ```json 63 | { 64 | "status": 1, 65 | "group": null, 66 | "name": "Blue", 67 | "vid": 10, 68 | "site": null, 69 | "role": null, 70 | "id": 8, 71 | "tenant": null, 72 | "description": "" 73 | } 74 | ``` 75 | 76 | ## Built With 77 | 78 | - [bravado](https://github.com/Yelp/bravado) for consuming the Swagger spec 79 | - [jsonschemas](https://github.com/Julian/jsonschema) for validating Swagger json-schema 80 | - [python-jsonschema-objects](https://github.com/cwacek/python-jsonschema-objects) for classbuilding 81 | the json-schema objects 82 | - [halutz](https://github.com/jeremyschulman/halutz) for wrapping all the above together 83 | 84 | ## Other Links 85 | 86 | - [Swagger 2.0 Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md) 87 | - [JSON-schema](http://json-schema.org/) 88 | 89 | ## Acknowledgements 90 | 91 | The Netbox probject is quite amazing, with a vibrant community of users and contributors! Many 92 | thanks to [@jeremystretch](https://github.com/jeremystretch]) and [@digitalocean](https://github.com/digitalocean)! 93 | 94 | ## Questions / Contributes? 95 | 96 | Please use the issues link to ask questions. Contributions are welcome and apprecaited. -------------------------------------------------------------------------------- /examples/With-pynetbox-client.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Using both netbox-pyswagger and pynetbox\n", 8 | "\n", 9 | "This tutorial illustrates how you can use both the `netbox-pyswagger` client when you have, or want to have, a `pynetbox` client as well. The `pynetbox` client is very powerful as a standalone and offers many excellent features and capabilities. The `netbox-pyswagger` client adds the \"make it like a CLI\" experience. Let's get started, using\n", 10 | "a locally hosted Netbox:" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 11, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "server_url = 'http://localhost:32768'\n", 20 | "token = '0123456789abcdef0123456789abcdef01234567'" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "Now let's create the netbox-pyswagger client. There is a classmethod specifically designed to bolt into the pynetbox client. This example simulatenously creates the pynetbox client and passes it directly to create the netbox-pyswagger client." 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 12, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "import pynetbox\n", 37 | "from netbox_pyswagger.client import Client\n", 38 | "\n", 39 | "netbox = Client.from_pynetbox(pynetbox.api(server_url, token=token))" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "The netbox-pyswagger client retains the instance of the pynetbox client in an attribute called `remote`. For example:" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 13, 52 | "metadata": {}, 53 | "outputs": [ 54 | { 55 | "data": { 56 | "text/plain": [ 57 | "" 58 | ] 59 | }, 60 | "execution_count": 13, 61 | "metadata": {}, 62 | "output_type": "execute_result" 63 | } 64 | ], 65 | "source": [ 66 | "netbox.remote" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "So you can then use this attribute to perform any pynetbox API call. Let's use it to create a VLAN:" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 14, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "resp = netbox.remote.ipam.vlans.create(name='Green', vid=1001)" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": {}, 88 | "source": [ 89 | "And we can introspect the `resp` value:" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 15, 95 | "metadata": {}, 96 | "outputs": [ 97 | { 98 | "data": { 99 | "text/plain": [ 100 | "{u'description': u'',\n", 101 | " u'group': None,\n", 102 | " u'id': 9,\n", 103 | " u'name': u'Green',\n", 104 | " u'role': None,\n", 105 | " u'site': None,\n", 106 | " u'status': 1,\n", 107 | " u'tenant': None,\n", 108 | " u'vid': 1001}" 109 | ] 110 | }, 111 | "execution_count": 15, 112 | "metadata": {}, 113 | "output_type": "execute_result" 114 | } 115 | ], 116 | "source": [ 117 | "resp" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "# Next Steps\n", 125 | "\n", 126 | "Please review the [halutz tutorials](https://github.com/jeremyschulman/halutz/tree/master/docs) to learn how to best utilize the netbox-pyswagger client." 127 | ] 128 | } 129 | ], 130 | "metadata": { 131 | "kernelspec": { 132 | "display_name": "Python 2", 133 | "language": "python", 134 | "name": "python2" 135 | }, 136 | "language_info": { 137 | "codemirror_mode": { 138 | "name": "ipython", 139 | "version": 2 140 | }, 141 | "file_extension": ".py", 142 | "mimetype": "text/x-python", 143 | "name": "python", 144 | "nbconvert_exporter": "python", 145 | "pygments_lexer": "ipython2", 146 | "version": "2.7.10" 147 | }, 148 | "toc": { 149 | "nav_menu": {}, 150 | "number_sections": true, 151 | "sideBar": true, 152 | "skip_h1_title": false, 153 | "toc_cell": false, 154 | "toc_position": {}, 155 | "toc_section_display": "block", 156 | "toc_window_display": false 157 | } 158 | }, 159 | "nbformat": 4, 160 | "nbformat_minor": 2 161 | } 162 | -------------------------------------------------------------------------------- /examples/holding.txt: -------------------------------------------------------------------------------- 1 | The more interesting examples are when you are doing comamnds that have body parameters 2 | such as POST and PATCH. See [this tutorial](https://github.com/jeremyschulman/halutz/blob/master/docs/Request-Body.ipynb) for a good example. 3 | 4 | # Example - with pynetbox 5 | 6 | Here is the same example of using netbox-pyswagger and pynetbox 7 | client. When you use this approach, the netbox-pyswagger instance 8 | maintains an attribute called `remote` which is set to the pynetbox 9 | instance you provided. 10 | 11 | ```python 12 | import os 13 | import sys 14 | 15 | import pynetbox 16 | from netbox_pyswagger import Client 17 | 18 | base_url = os.getenv('NETBOX_SERVER') 19 | if not base_url: 20 | sys.exit('NETBOX_SERVER not found in enviornment') 21 | 22 | netbox_token = os.getenv('NETBOX_TOKEN') 23 | if not netbox_token: 24 | sys.exit('NETBOX_TOKEN not found in environment') 25 | 26 | # create the netbox-pyswagger instance, passing in the 27 | # pynetbox client instance 28 | 29 | netbox = Client(pynb=pynetbox.api(url=base_url, token=netbox_token)) 30 | 31 | # run the command to get all VLANs using the pynetbox method 32 | 33 | resp = netbox.remote.ipam.vlans.all() 34 | ``` 35 | 36 | The `resp` contains the pynetbox VLAN instance values. For the 37 | same example, `resp` would contain the list `[green, blue]`. 38 | For example, dumping out the *green* instance: 39 | 40 | ```python 41 | for name, value in resp[0]: 42 | print(name, value) 43 | ``` 44 | 45 | results in: 46 | ```bash 47 | status {u'value': 1, u'label': u'Active'} 48 | group None 49 | name green 50 | vid 99 51 | site None 52 | role None 53 | tenant None 54 | display_name 99 (green) 55 | id 4 56 | custom_fields {} 57 | description 58 | ``` 59 | -------------------------------------------------------------------------------- /examples/setup.env: -------------------------------------------------------------------------------- 1 | export NETBOX_TOKEN=0123456789abcdef0123456789abcdef01234567 2 | export NETBOX_SERVER=http://localhost:32768 3 | -------------------------------------------------------------------------------- /examples/testdrive.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import sys 5 | 6 | 7 | from netbox_pyswagger.client import Client 8 | 9 | 10 | server_url = os.getenv('NETBOX_SERVER') 11 | if not server_url: 12 | sys.exit('NETBOX_SERVER not found in enviornment') 13 | 14 | 15 | netbox_token = os.getenv('NETBOX_TOKEN') 16 | if not netbox_token: 17 | sys.exit('NETBOX_TOKEN not found in environment') 18 | 19 | 20 | 21 | netbox = Client(server_url, api_token=netbox_token) 22 | -------------------------------------------------------------------------------- /examples/testdrive_pynb.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import sys 5 | 6 | import pynetbox 7 | from netbox_pyswagger.client import Client 8 | 9 | 10 | server_url = os.getenv('NETBOX_SERVER') 11 | if not server_url: 12 | sys.exit('NETBOX_SERVER not found in enviornment') 13 | 14 | 15 | netbox_token = os.getenv('NETBOX_TOKEN') 16 | if not netbox_token: 17 | sys.exit('NETBOX_TOKEN not found in environment') 18 | 19 | 20 | netbox = Client.from_pynetbox( 21 | pynetbox.api(url=server_url, token=netbox_token)) 22 | -------------------------------------------------------------------------------- /netbox_pyswagger/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.0' 2 | __author__ = 'Jeremy Schulman' 3 | 4 | -------------------------------------------------------------------------------- /netbox_pyswagger/client.py: -------------------------------------------------------------------------------- 1 | 2 | from requests import Session 3 | from halutz.client import Client as HalutzClient 4 | 5 | __all__ = ['Client'] 6 | 7 | 8 | class Client(HalutzClient): 9 | apidocs_url = "/api/docs?format=openapi" 10 | 11 | def __init__(self, api_token, **halutz_kwargs): 12 | # the api version will get set when the spec is loaded from the server 13 | self.api_version = None 14 | 15 | kwargs = halutz_kwargs or {} 16 | 17 | if 'session' not in kwargs: 18 | kwargs['session'] = Session() 19 | 20 | # setup the token in the session headers 21 | kwargs['session'].headers['Authorization'] = "Token %s" % api_token 22 | 23 | # invoke the halutz initializer 24 | super(Client, self).__init__(**kwargs) 25 | 26 | @classmethod 27 | def from_pynetbox(cls, pynb): 28 | kwargs = dict() 29 | kwargs['api_token'] = pynb.api_kwargs['token'] 30 | kwargs['server_url'] = pynb.api_kwargs['base_url'].split('/api')[0] 31 | kwargs['remote'] = pynb 32 | 33 | return cls(**kwargs) 34 | 35 | def fetch_swagger_spec(self): 36 | url = "%s%s" % (self.server_url, self.apidocs_url) 37 | resp = self.session.get(url) 38 | 39 | if not resp.ok: 40 | raise RuntimeError( 41 | 'unable to fetch Swagger spec:', resp) 42 | 43 | spec = resp.json() 44 | if 'paths' not in spec: 45 | if spec.get('details', '') == 'Invalid token': 46 | raise RuntimeError('Invalid API token provided') 47 | 48 | self.api_version = resp.headers['API-Version'] 49 | 50 | if self.api_version == "2.2": 51 | from .patch_2_2 import patch_spec 52 | patch_spec(spec) 53 | 54 | return spec 55 | -------------------------------------------------------------------------------- /netbox_pyswagger/patch_2_2.py: -------------------------------------------------------------------------------- 1 | 2 | def patch_spec(spec): 3 | # Fix for Netbox #1853 https://github.com/digitalocean/netbox/issues/1853 4 | for path in spec['paths'].keys(): 5 | for method in spec['paths'][path].keys(): 6 | 7 | if 'parameters' not in spec['paths'][path][method].keys(): 8 | continue 9 | 10 | for param in spec['paths'][path][method]['parameters']: 11 | if param['in'] != 'body': 12 | continue 13 | 14 | if 'custom_fields' not in param['schema']['properties'].keys(): 15 | continue 16 | 17 | if param['schema']['properties']['custom_fields']['type'] == 'string': 18 | param['schema']['properties']['custom_fields']['type'] = 'object' 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | import netbox_pyswagger 6 | 7 | 8 | def read(fname): 9 | with open(fname) as fp: 10 | content = fp.read() 11 | return content 12 | 13 | 14 | setup( 15 | name='netbox-pyswagger', 16 | version=netbox_pyswagger.__version__, 17 | description='Swagger client for Netbox', 18 | author='Jeremy Schulman', 19 | author_email='nwkautomaniac@gmail.com', 20 | url='https://github.com/jeremyschulman/netbox-pyswagger', 21 | packages=['netbox_pyswagger'], 22 | license='MIT', 23 | zip_safe=False, 24 | install_requires=[ 25 | 'halutz>=0.3.0', 26 | 'requests', 27 | 'six' 28 | ], 29 | keywords=('netbox', 'rest', 'json', 'api', 30 | 'network', 'automation'), 31 | classifiers=[ 32 | 'Development Status :: 4 - Beta', 33 | 'Intended Audience :: Developers', 34 | 'License :: OSI Approved :: MIT License', 35 | 'Programming Language :: Python :: 2.7', 36 | ] 37 | ) 38 | --------------------------------------------------------------------------------