From 36d5debe74162142ab44c865d680120f3aaf4490 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 4 Aug 2017 10:52:29 -0400 Subject: [PATCH 01/14] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 52154fbb4f4..00acc3e67d7 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ ) -VERSION = '2.1.2' +VERSION = '2.1.3-dev' # Import required configuration parameters ALLOWED_HOSTS = DATABASE = SECRET_KEY = None From b6afc688474d79b03c587c037f87e282db1c10b6 Mon Sep 17 00:00:00 2001 From: johnhu Date: Tue, 8 Aug 2017 11:44:50 +0000 Subject: [PATCH 02/14] Fixes #1400: Device interface shows twice on IP Addresses page --- netbox/ipam/tables.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 65ab5b2e407..af82042dd00 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -80,7 +80,6 @@ IPADDRESS_DEVICE = """ {% if record.interface %} {{ record.interface.device }} - ({{ record.interface.name }}) {% else %} — {% endif %} From 7557220d5d00ad551a06cdef8abe2819af3845a3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 8 Aug 2017 11:48:51 -0400 Subject: [PATCH 03/14] Fixes #1389: Avoid splitting carat/prefix on prefix list --- netbox/ipam/tables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index af82042dd00..96127aec5ea 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -45,9 +45,9 @@ PREFIX_LINK = """ {% if record.has_children %} - + {% else %} - + {% endif %} {{ record.prefix }} From babe42ef352d6135d23c326105fae02245ba121b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 9 Aug 2017 13:59:25 -0400 Subject: [PATCH 04/14] Closes #1414: Selecting a site from the rack filters automatically updates the available rack groups --- netbox/templates/dcim/rack_list.html | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/netbox/templates/dcim/rack_list.html b/netbox/templates/dcim/rack_list.html index 1de08d37a5f..88b5a2f9d0b 100644 --- a/netbox/templates/dcim/rack_list.html +++ b/netbox/templates/dcim/rack_list.html @@ -25,3 +25,36 @@

{% block title %}Racks{% endblock %}

{% endblock %} + +{% block javascript %} + +{% endblock %} + From 8fb504c963c5ea17387ee802fc4284c301d8946b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 9 Aug 2017 14:19:33 -0400 Subject: [PATCH 05/14] Tweaked installation docs to better accommodate Python 3 --- docs/installation/netbox.md | 2 +- docs/installation/web-server.md | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index 4befbeefcee..b0928c1b5ec 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -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 diff --git a/docs/installation/web-server.md b/docs/installation/web-server.md index 9da487f1350..0acedccc6c6 100644 --- a/docs/installation/web-server.md +++ b/docs/installation/web-server.md @@ -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 @@ -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 @@ -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 From e6a58b67003f1d869a47fbd255a3645e2a1d5c76 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 9 Aug 2017 15:46:40 -0400 Subject: [PATCH 06/14] Fixes #1415: Ignore leading/trailing semicolons in topology map device lists --- netbox/extras/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 9d0e636ff0a..4afc3afcf22 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -285,7 +285,7 @@ def render(self, img_format='png'): # Add each device to the graph devices = [] - for query in device_set.split(';'): # Split regexes on semicolons + for query in device_set.strip(';').split(';'): # Split regexes on semicolons devices += Device.objects.filter(name__regex=query).select_related('device_role') for d in devices: bg_color = '#{}'.format(d.device_role.color) From 63757af1a022dcce6acb356e65794bd910a5a60e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 9 Aug 2017 16:55:49 -0400 Subject: [PATCH 07/14] Expanded API overview documentation --- docs/api/overview.md | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/api/overview.md b/docs/api/overview.md index a9ad115f897..84e88a6d266 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -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 +* `UPDATE`: 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:///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/ @@ -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 From 3d92669df59179c7c97634a3a681729c4b66bb6f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 10 Aug 2017 11:41:25 -0400 Subject: [PATCH 08/14] Corrected HTTP verb in API docs --- docs/api/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/overview.md b/docs/api/overview.md index 84e88a6d266..bdf0a2f4c5b 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -6,7 +6,7 @@ REST stands for [representational state transfer](https://en.wikipedia.org/wiki/ * `GET`: Retrieve an object or list of objects * `POST`: Create an object -* `UPDATE`: Modify an existing 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.) From 117f33afc5a0d80e6fd58d69d921a0f7c3bd084e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 11 Aug 2017 10:47:06 -0400 Subject: [PATCH 09/14] Fixes #1419: Allow editing image attachments without re-uploading an image --- netbox/utilities/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 0fa402d5282..7004d42607e 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -459,7 +459,7 @@ def __init__(self, *args, **kwargs): if field.widget.__class__ not in exempt_widgets: css = field.widget.attrs.get('class', '') field.widget.attrs['class'] = ' '.join([css, 'form-control']).strip() - if field.required: + if field.required and not isinstance(field.widget, forms.FileInput): field.widget.attrs['required'] = 'required' if 'placeholder' not in field.widget.attrs: field.widget.attrs['placeholder'] = field.label From 04c300b8e2debf29af9cf3eebf96adcd464dc205 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 15 Aug 2017 11:07:26 -0400 Subject: [PATCH 10/14] Fixes #1420: Exclude virtual interfaces from device LLDP neighbors view --- netbox/dcim/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 78881c20097..d2c75fc24ce 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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' From c394985b1be3f2126363782018af9acd3e4865cf Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 15 Aug 2017 13:54:04 -0400 Subject: [PATCH 11/14] Fixes #1421: Improved model validation logic for API serializers --- netbox/circuits/api/serializers.py | 6 ++-- netbox/dcim/api/serializers.py | 44 +++++++++++++++--------------- netbox/extras/api/customfields.py | 13 ++------- netbox/extras/api/serializers.py | 4 +-- netbox/ipam/api/serializers.py | 8 +++--- netbox/secrets/api/serializers.py | 4 +-- netbox/tenancy/api/serializers.py | 4 +-- netbox/utilities/api.py | 28 +++++++++++-------- 8 files changed, 53 insertions(+), 58 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index cdab3427a37..d2432374f3d 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -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 # @@ -45,7 +45,7 @@ class Meta: # Circuit types # -class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer): +class CircuitTypeSerializer(ValidatedModelSerializer): class Meta: model = CircuitType @@ -111,7 +111,7 @@ class Meta: ] -class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableCircuitTerminationSerializer(ValidatedModelSerializer): class Meta: model = CircuitTermination diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 50bf756e3ee..ebfb781e01d 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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 # @@ -38,7 +38,7 @@ class Meta: fields = ['id', 'name', 'slug', 'parent'] -class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableRegionSerializer(ValidatedModelSerializer): class Meta: model = Region @@ -100,7 +100,7 @@ class Meta: fields = ['id', 'url', 'name', 'slug'] -class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableRackGroupSerializer(ValidatedModelSerializer): class Meta: model = RackGroup @@ -111,7 +111,7 @@ class Meta: # Rack roles # -class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer): +class RackRoleSerializer(ValidatedModelSerializer): class Meta: model = RackRole @@ -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 @@ -227,7 +227,7 @@ class Meta: # Manufacturers # -class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer): +class ManufacturerSerializer(ValidatedModelSerializer): class Meta: model = Manufacturer @@ -292,7 +292,7 @@ class Meta: fields = ['id', 'device_type', 'name'] -class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableConsolePortTemplateSerializer(ValidatedModelSerializer): class Meta: model = ConsolePortTemplate @@ -311,7 +311,7 @@ class Meta: fields = ['id', 'device_type', 'name'] -class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer): class Meta: model = ConsoleServerPortTemplate @@ -330,7 +330,7 @@ class Meta: fields = ['id', 'device_type', 'name'] -class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritablePowerPortTemplateSerializer(ValidatedModelSerializer): class Meta: model = PowerPortTemplate @@ -349,7 +349,7 @@ class Meta: fields = ['id', 'device_type', 'name'] -class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer): class Meta: model = PowerOutletTemplate @@ -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 @@ -388,7 +388,7 @@ class Meta: fields = ['id', 'device_type', 'name'] -class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer): class Meta: model = DeviceBayTemplate @@ -399,7 +399,7 @@ class Meta: # Device roles # -class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer): +class DeviceRoleSerializer(ValidatedModelSerializer): class Meta: model = DeviceRole @@ -418,7 +418,7 @@ class Meta: # Platforms # -class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer): +class PlatformSerializer(ValidatedModelSerializer): class Meta: model = Platform @@ -516,7 +516,7 @@ class Meta: read_only_fields = ['connected_console'] -class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableConsoleServerPortSerializer(ValidatedModelSerializer): class Meta: model = ConsoleServerPort @@ -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 @@ -556,7 +556,7 @@ class Meta: read_only_fields = ['connected_port'] -class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritablePowerOutletSerializer(ValidatedModelSerializer): class Meta: model = PowerOutlet @@ -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 @@ -664,7 +664,7 @@ class Meta: ] -class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableInterfaceSerializer(ValidatedModelSerializer): class Meta: model = Interface @@ -694,7 +694,7 @@ class Meta: fields = ['id', 'url', 'name'] -class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableDeviceBaySerializer(ValidatedModelSerializer): class Meta: model = DeviceBay @@ -717,7 +717,7 @@ class Meta: ] -class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableInventoryItemSerializer(ValidatedModelSerializer): class Meta: model = InventoryItem @@ -749,7 +749,7 @@ class Meta: fields = ['id', 'url', 'connection_status'] -class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableInterfaceConnectionSerializer(ValidatedModelSerializer): class Meta: model = InterfaceConnection diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 52f127a7d6b..fc83b33e575 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -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 # @@ -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. """ @@ -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) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 39ce63524c0..0eeab49ecc6 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -10,7 +10,7 @@ ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction, ) from users.api.serializers import NestedUserSerializer -from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ModelValidationMixin +from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer # @@ -104,7 +104,7 @@ def get_parent(self, obj): return serializer(obj.parent, context={'request': self.context['request']}).data -class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer): +class WritableImageAttachmentSerializer(ValidatedModelSerializer): content_type = ContentTypeFieldSerializer() class Meta: diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 1374d355275..3ef152ebe0d 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -11,7 +11,7 @@ PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF, ) from tenancy.api.serializers import NestedTenantSerializer -from utilities.api import ChoiceFieldSerializer, ModelValidationMixin +from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer # @@ -45,7 +45,7 @@ class Meta: # Roles # -class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer): +class RoleSerializer(ValidatedModelSerializer): class Meta: model = Role @@ -64,7 +64,7 @@ class Meta: # RIRs # -class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer): +class RIRSerializer(ValidatedModelSerializer): class Meta: model = RIR @@ -303,7 +303,7 @@ class Meta: fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description'] -# TODO: Figure out how to use ModelValidationMixin with ManyToManyFields. Calling clean() yields a ValueError. +# TODO: Figure out how to use model validation with ManyToManyFields. Calling clean() yields a ValueError. class WritableServiceSerializer(serializers.ModelSerializer): class Meta: diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index ff2eb1dfa55..b7c4bac9a49 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -5,14 +5,14 @@ from dcim.api.serializers import NestedDeviceSerializer from secrets.models import Secret, SecretRole -from utilities.api import ModelValidationMixin +from utilities.api import ValidatedModelSerializer # # SecretRoles # -class SecretRoleSerializer(ModelValidationMixin, serializers.ModelSerializer): +class SecretRoleSerializer(ValidatedModelSerializer): class Meta: model = SecretRole diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index ef5b15a169c..a52ac2c6015 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -4,14 +4,14 @@ from extras.api.customfields import CustomFieldModelSerializer from tenancy.models import Tenant, TenantGroup -from utilities.api import ModelValidationMixin +from utilities.api import ValidatedModelSerializer # # Tenant groups # -class TenantGroupSerializer(ModelValidationMixin, serializers.ModelSerializer): +class TenantGroupSerializer(ValidatedModelSerializer): class Meta: model = TenantGroup diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 6a515b21d68..2e827d503bf 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -8,7 +8,7 @@ from rest_framework.exceptions import APIException from rest_framework.pagination import LimitOffsetPagination from rest_framework.permissions import BasePermission, DjangoModelPermissions, SAFE_METHODS -from rest_framework.serializers import Field, ValidationError +from rest_framework.serializers import Field, ModelSerializer, ValidationError from users.models import Token @@ -80,6 +80,21 @@ def has_permission(self, request, view): # Serializers # +class ValidatedModelSerializer(ModelSerializer): + """ + Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation. + """ + def validate(self, attrs): + if self.instance is None: + instance = self.Meta.model(**attrs) + else: + instance = self.instance + for k, v in attrs.items(): + setattr(instance, k, v) + instance.clean() + return attrs + + class ChoiceFieldSerializer(Field): """ Represent a ChoiceField as {'value': , 'label': }. @@ -121,17 +136,6 @@ def to_internal_value(self, data): # Mixins # -class ModelValidationMixin(object): - """ - Enforce a model's validation through clean() when validating serializer data. This is necessary to ensure we're - employing the same validation logic via both forms and the API. - """ - def validate(self, attrs): - instance = self.Meta.model(**attrs) - instance.clean() - return attrs - - class WritableSerializerMixin(object): """ Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT). From 51b1da660aba8e7a97eab6eed6d569b41333ab86 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 15 Aug 2017 14:05:02 -0400 Subject: [PATCH 12/14] Fixes #1330: Raise validation error when assigning an unrelated IP as the primary IP for a device --- netbox/dcim/models.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 3719c7c2509..9f72fc83ef8 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -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) From c37cfeb74f1d5488faf05bebdf993ffe46355cbc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 15 Aug 2017 15:30:45 -0400 Subject: [PATCH 13/14] Fixed page titles in the browsable API --- netbox/netbox/settings.py | 3 ++- netbox/utilities/api.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 00acc3e67d7..5bb6f5c0f14 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -221,6 +221,7 @@ # Django REST framework (API) REST_FRAMEWORK_VERSION = VERSION[0:3] # Use major.minor as API version REST_FRAMEWORK = { + 'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION], 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', 'utilities.api.TokenAuthentication', @@ -233,9 +234,9 @@ 'utilities.api.TokenPermissions', ), 'DEFAULT_VERSION': REST_FRAMEWORK_VERSION, - 'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION], 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 'PAGE_SIZE': PAGINATE_COUNT, + 'VIEW_NAME_FUNCTION': 'utilities.api.get_view_name', } # Django debug toolbar diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 2e827d503bf..7a945c04530 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -9,6 +9,7 @@ from rest_framework.pagination import LimitOffsetPagination from rest_framework.permissions import BasePermission, DjangoModelPermissions, SAFE_METHODS from rest_framework.serializers import Field, ModelSerializer, ValidationError +from rest_framework.views import get_view_name as drf_get_view_name from users.models import Token @@ -196,3 +197,21 @@ def get_limit(self, request): pass return self.default_limit + + +# +# Miscellaneous +# + +def get_view_name(view_cls, suffix=None): + """ + Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name`. + """ + if hasattr(view_cls, 'queryset'): + name = view_cls.queryset.model._meta.verbose_name + name = ' '.join([w[0].upper() + w[1:] for w in name.split()]) # Capitalize each word + if suffix: + name = "{} {}".format(name, suffix) + return name + + return drf_get_view_name(view_cls, suffix) From 669ae104a4b020a6cd39b2d76281b4c6bec1af00 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 15 Aug 2017 15:50:51 -0400 Subject: [PATCH 14/14] Release v2.1.3 --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 5bb6f5c0f14..1c3f3688b26 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ ) -VERSION = '2.1.3-dev' +VERSION = '2.1.3' # Import required configuration parameters ALLOWED_HOSTS = DATABASE = SECRET_KEY = None