Skip to content

Commit

Permalink
Merge pull request #1428 from digitalocean/develop
Browse files Browse the repository at this point in the history
Release v2.1.3
  • Loading branch information
jeremystretch authored Aug 15, 2017
2 parents fa7b728 + 669ae10 commit 5be30bd
Show file tree
Hide file tree
Showing 18 changed files with 176 additions and 75 deletions.
40 changes: 37 additions & 3 deletions docs/api/overview.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,42 @@
NetBox v2.0 and later includes a full-featured REST API that allows its data model to be read and manipulated externally.

# What is a REST API?

REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP to create, retrieve, update, and delete objects from a database. (This set of operations is commonly referred to as CRUD.) Each type of operation is associated with a particular HTTP verb:

* `GET`: Retrieve an object or list of objects
* `POST`: Create an object
* `PUT` / `PATCH`: Modify an existing object
* `DELETE`: Delete an existing object

The NetBox API represents all objects in [JavaScript Object Notation (JSON)](http://www.json.org/). This makes it very easy to interact with NetBox data on the command line with common tools. For example, we can request an IP address from NetBox and output the JSON using `curl` and `jq`. (Piping the output through `jq` isn't strictly required but makes it much easier to read.)

```
$ curl -s http://localhost:8000/api/ipam/ip-addresses/2954/ | jq '.'
{
"custom_fields": {},
"nat_outside": null,
"nat_inside": null,
"description": "An example IP address",
"id": 2954,
"family": 4,
"address": "5.101.108.132/26",
"vrf": null,
"tenant": null,
"status": {
"label": "Active",
"value": 1
},
"role": null,
"interface": null
}
```

Each attribute of the NetBox object is expressed as a field in the dictionary. Fields may include their own nested objects, as in the case of the `status` field above. Every object includes a primary key named `id` which uniquely identifies it in the database.

# URL Hierarchy

NetBox's entire REST API is housed under the API root, `/api/`. The API's URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, secrets, and tenancy. Within each application, each model has its own path. For example, the provider and circuit objects are located under the "circuits" application:
NetBox's entire API is housed under the API root at `https://<hostname>/api/`. The URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, secrets, and tenancy. Within each application, each model has its own path. For example, the provider and circuit objects are located under the "circuits" application:

* /api/circuits/providers/
* /api/circuits/circuits/
Expand All @@ -13,9 +47,9 @@ Likewise, the site, rack, and device objects are located under the "DCIM" applic
* /api/dcim/racks/
* /api/dcim/devices/

The full hierarchy of available endpoints can be viewed by navigating to the API root (e.g. /api/) in a web browser.
The full hierarchy of available endpoints can be viewed by navigating to the API root in a web browser.

Each model generally has two URLs associated with it: a list URL and a detail URL. The list URL is used to request a list of multiple objects or to create a new object. The detail URL is used to retrieve, update, or delete an existing object. All objects are referenced by their numeric primary key (ID).
Each model generally has two views associated with it: a list view and a detail view. The list view is used to request a list of multiple objects or to create a new object. The detail view is used to retrieve, update, or delete an existing object. All objects are referenced by their numeric primary key (`id`).

* /api/dcim/devices/ - List devices or create a new device
* /api/dcim/devices/123/ - Retrieve, update, or delete the device with ID 123
Expand Down
2 changes: 1 addition & 1 deletion docs/installation/netbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Python 2:
As of v2.1.0, NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3:

```no-highlight
# pip install napalm
# pip3 install napalm
```

# Configuration
Expand Down
18 changes: 13 additions & 5 deletions docs/installation/web-server.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
# Web Server Installation

We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.

!!! info
For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.

```no-highlight
# apt-get install -y gunicorn supervisor
```
# Web Server Installation

## Option A: nginx

Expand Down Expand Up @@ -104,6 +100,12 @@ To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https

# gunicorn Installation

Install gunicorn using `pip3` (Python 3) or `pip` (Python 2):

```no-highlight
# pip3 install gunicorn
```

Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.

```no-highlight
Expand All @@ -116,6 +118,12 @@ user = 'www-data'

# supervisord Installation

Install supervisor:

```no-highlight
# apt-get install -y supervisor
```

Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.

```no-highlight
Expand Down
6 changes: 3 additions & 3 deletions netbox/circuits/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ModelValidationMixin
from utilities.api import ValidatedModelSerializer


#
Expand Down Expand Up @@ -45,7 +45,7 @@ class Meta:
# Circuit types
#

class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
class CircuitTypeSerializer(ValidatedModelSerializer):

class Meta:
model = CircuitType
Expand Down Expand Up @@ -111,7 +111,7 @@ class Meta:
]


class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableCircuitTerminationSerializer(ValidatedModelSerializer):

class Meta:
model = CircuitTermination
Expand Down
44 changes: 22 additions & 22 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer


#
Expand All @@ -38,7 +38,7 @@ class Meta:
fields = ['id', 'name', 'slug', 'parent']


class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRegionSerializer(ValidatedModelSerializer):

class Meta:
model = Region
Expand Down Expand Up @@ -100,7 +100,7 @@ class Meta:
fields = ['id', 'url', 'name', 'slug']


class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRackGroupSerializer(ValidatedModelSerializer):

class Meta:
model = RackGroup
Expand All @@ -111,7 +111,7 @@ class Meta:
# Rack roles
#

class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class RackRoleSerializer(ValidatedModelSerializer):

class Meta:
model = RackRole
Expand Down Expand Up @@ -216,7 +216,7 @@ class Meta:
fields = ['id', 'rack', 'units', 'created', 'user', 'description']


class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRackReservationSerializer(ValidatedModelSerializer):

class Meta:
model = RackReservation
Expand All @@ -227,7 +227,7 @@ class Meta:
# Manufacturers
#

class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
class ManufacturerSerializer(ValidatedModelSerializer):

class Meta:
model = Manufacturer
Expand Down Expand Up @@ -292,7 +292,7 @@ class Meta:
fields = ['id', 'device_type', 'name']


class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):

class Meta:
model = ConsolePortTemplate
Expand All @@ -311,7 +311,7 @@ class Meta:
fields = ['id', 'device_type', 'name']


class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):

class Meta:
model = ConsoleServerPortTemplate
Expand All @@ -330,7 +330,7 @@ class Meta:
fields = ['id', 'device_type', 'name']


class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):

class Meta:
model = PowerPortTemplate
Expand All @@ -349,7 +349,7 @@ class Meta:
fields = ['id', 'device_type', 'name']


class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):

class Meta:
model = PowerOutletTemplate
Expand All @@ -369,7 +369,7 @@ class Meta:
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']


class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):

class Meta:
model = InterfaceTemplate
Expand All @@ -388,7 +388,7 @@ class Meta:
fields = ['id', 'device_type', 'name']


class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):

class Meta:
model = DeviceBayTemplate
Expand All @@ -399,7 +399,7 @@ class Meta:
# Device roles
#

class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class DeviceRoleSerializer(ValidatedModelSerializer):

class Meta:
model = DeviceRole
Expand All @@ -418,7 +418,7 @@ class Meta:
# Platforms
#

class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
class PlatformSerializer(ValidatedModelSerializer):

class Meta:
model = Platform
Expand Down Expand Up @@ -516,7 +516,7 @@ class Meta:
read_only_fields = ['connected_console']


class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsoleServerPortSerializer(ValidatedModelSerializer):

class Meta:
model = ConsoleServerPort
Expand All @@ -536,7 +536,7 @@ class Meta:
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']


class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsolePortSerializer(ValidatedModelSerializer):

class Meta:
model = ConsolePort
Expand All @@ -556,7 +556,7 @@ class Meta:
read_only_fields = ['connected_port']


class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerOutletSerializer(ValidatedModelSerializer):

class Meta:
model = PowerOutlet
Expand All @@ -576,7 +576,7 @@ class Meta:
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']


class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerPortSerializer(ValidatedModelSerializer):

class Meta:
model = PowerPort
Expand Down Expand Up @@ -664,7 +664,7 @@ class Meta:
]


class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInterfaceSerializer(ValidatedModelSerializer):

class Meta:
model = Interface
Expand Down Expand Up @@ -694,7 +694,7 @@ class Meta:
fields = ['id', 'url', 'name']


class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableDeviceBaySerializer(ValidatedModelSerializer):

class Meta:
model = DeviceBay
Expand All @@ -717,7 +717,7 @@ class Meta:
]


class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInventoryItemSerializer(ValidatedModelSerializer):

class Meta:
model = InventoryItem
Expand Down Expand Up @@ -749,7 +749,7 @@ class Meta:
fields = ['id', 'url', 'connection_status']


class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):

class Meta:
model = InterfaceConnection
Expand Down
12 changes: 12 additions & 0 deletions netbox/dcim/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,18 @@ def clean(self):
except DeviceType.DoesNotExist:
pass

# Validate primary IPv4 address
if self.primary_ip4 and (self.primary_ip4.interface is None or self.primary_ip4.interface.device != self):
raise ValidationError({
'primary_ip4': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip4),
})

# Validate primary IPv6 address
if self.primary_ip6 and (self.primary_ip6.interface is None or self.primary_ip6.interface.device != self):
raise ValidationError({
'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip6),
})

def save(self, *args, **kwargs):

is_new = not bool(self.pk)
Expand Down
2 changes: 1 addition & 1 deletion netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ def get(self, request, pk):
device = get_object_or_404(Device, pk=pk)
interfaces = Interface.objects.order_naturally(
device.device_type.interface_ordering
).filter(
).connectable().filter(
device=device
).select_related(
'connected_as_a', 'connected_as_b'
Expand Down
13 changes: 2 additions & 11 deletions netbox/extras/api/customfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from extras.models import (
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
)
from utilities.api import ValidatedModelSerializer


#
Expand Down Expand Up @@ -68,7 +69,7 @@ def to_internal_value(self, data):
return data


class CustomFieldModelSerializer(serializers.ModelSerializer):
class CustomFieldModelSerializer(ValidatedModelSerializer):
"""
Extends ModelSerializer to render any CustomFields and their values associated with an object.
"""
Expand Down Expand Up @@ -111,16 +112,6 @@ def _save_custom_fields(self, instance, custom_fields):
defaults={'serialized_value': custom_field.serialize_value(value)},
)

def validate(self, data):
"""
Enforce model validation (see utilities.api.ModelValidationMixin)
"""
model_data = data.copy()
model_data.pop('custom_fields', None)
instance = self.Meta.model(**model_data)
instance.clean()
return data

def create(self, validated_data):

custom_fields = validated_data.pop('custom_fields', None)
Expand Down
Loading

0 comments on commit 5be30bd

Please sign in to comment.