Compare commits

..

41 Commits

Author SHA1 Message Date
Jeremy Stretch
f6d1163ddd Merge pull request #1461 from digitalocean/develop
Release v2.1.4
2017-08-30 14:43:01 -04:00
Jeremy Stretch
e6b6082a2b Release v2.1.4 2017-08-30 14:40:52 -04:00
Jeremy Stretch
e0ee0b9254 Closes #1460: Hostnames with no domain are now acceptable in custom URL fields 2017-08-30 14:08:39 -04:00
Jeremy Stretch
fde47133da Fix global search placeholder text 2017-08-30 12:07:11 -04:00
Jeremy Stretch
643f64df3f Closes #1341: Added a MEDIA_ROOT configuration setting to specify where uploaded files are stored on disk 2017-08-30 11:11:59 -04:00
Jeremy Stretch
a6ec1ba23b Closes #1376: Ignore anycast when detecting duplicate IPs 2017-08-30 10:46:30 -04:00
Jeremy Stretch
ae231b1d1b Moved circuit speed humanization to a template tag 2017-08-29 22:42:06 -04:00
Jeremy Stretch
ecdf66c454 Closes #1326: Added dropdown widget with common values for circuit speed fields 2017-08-29 22:29:30 -04:00
Jeremy Stretch
aaca28c1a6 Closes #1402: Increased max length of name field for device components 2017-08-29 17:27:30 -04:00
Jeremy Stretch
f0d8fdf2d8 Closes #1431: Added interface form factor for 10GBASE-CX4 2017-08-29 17:02:09 -04:00
Jeremy Stretch
c9288a052a Closes #1432: Added a commit_rate field to the circuits list search form 2017-08-29 16:48:11 -04:00
Jeremy Stretch
4b3a425888 Fixed role link on IP address view 2017-08-29 16:14:32 -04:00
Jeremy Stretch
1308839a41 Fixes #1458: Corrected permission name on prefix/VLAN roles list 2017-08-29 15:36:51 -04:00
Jeremy Stretch
6d849ad5f6 Deleted obsolete templates 2017-08-29 15:31:11 -04:00
Jeremy Stretch
3e4b66675b Rearranged circuits navigation dropdown 2017-08-24 12:51:50 -04:00
Jeremy Stretch
6f8d3f7a57 Fixes #1443: Fixed API validation error involving custom field data 2017-08-22 11:31:33 -04:00
Jeremy Stretch
eb994a691a Added style to BaseTable's empty_text 2017-08-21 17:38:27 -04:00
Jeremy Stretch
ae73e6f04d Removed task list formatting 2017-08-21 11:10:07 -04:00
Jeremy Stretch
7843a6bf77 Fixed devicetype_id filter for DeviceType components 2017-08-18 10:27:43 -04:00
Jeremy Stretch
fa6d933544 Converted list of issue types to a task list 2017-08-17 15:14:20 -04:00
Jeremy Stretch
83085d08ca Revised the GitHub issue template 2017-08-17 15:11:55 -04:00
Jeremy Stretch
70a6b171d0 Fixes #1429: Fixed uptime formatting on device status page 2017-08-16 11:20:52 -04:00
Jeremy Stretch
9a25b5458d Merge pull request #1430 from xmudrii/fix-docs
docs: web-server link to new ssl tutorials for 16.04
2017-08-15 21:39:59 -04:00
Marko Mudrinić
416aeb4072 docs: web-server link to new ssl tutorials for 16.04 2017-08-15 23:39:10 +02:00
Jeremy Stretch
0c1594b8f8 Post-release version bump 2017-08-15 15:53:10 -04:00
Jeremy Stretch
5be30bd278 Merge pull request #1428 from digitalocean/develop
Release v2.1.3
2017-08-15 15:52:34 -04:00
Jeremy Stretch
669ae104a4 Release v2.1.3 2017-08-15 15:50:51 -04:00
Jeremy Stretch
c37cfeb74f Fixed page titles in the browsable API 2017-08-15 15:30:45 -04:00
Jeremy Stretch
51b1da660a Fixes #1330: Raise validation error when assigning an unrelated IP as the primary IP for a device 2017-08-15 14:05:02 -04:00
Jeremy Stretch
c394985b1b Fixes #1421: Improved model validation logic for API serializers 2017-08-15 13:54:04 -04:00
Jeremy Stretch
04c300b8e2 Fixes #1420: Exclude virtual interfaces from device LLDP neighbors view 2017-08-15 11:07:26 -04:00
Jeremy Stretch
117f33afc5 Fixes #1419: Allow editing image attachments without re-uploading an image 2017-08-11 10:47:06 -04:00
Jeremy Stretch
3d92669df5 Corrected HTTP verb in API docs 2017-08-10 11:41:25 -04:00
Jeremy Stretch
63757af1a0 Expanded API overview documentation 2017-08-09 16:55:49 -04:00
Jeremy Stretch
e6a58b6700 Fixes #1415: Ignore leading/trailing semicolons in topology map device lists 2017-08-09 15:46:40 -04:00
Jeremy Stretch
8fb504c963 Tweaked installation docs to better accommodate Python 3 2017-08-09 14:19:33 -04:00
Jeremy Stretch
babe42ef35 Closes #1414: Selecting a site from the rack filters automatically updates the available rack groups 2017-08-09 13:59:25 -04:00
Jeremy Stretch
7557220d5d Fixes #1389: Avoid splitting carat/prefix on prefix list 2017-08-08 11:48:51 -04:00
Jeremy Stretch
50ccccc3f4 Merge pull request #1403 from huzichunjohn/issue_1400
Fixes #1400: Device interface shows twice on IP Addresses page
2017-08-08 08:23:14 -04:00
johnhu
b6afc68847 Fixes #1400: Device interface shows twice on IP Addresses page 2017-08-08 11:44:50 +00:00
Jeremy Stretch
36d5debe74 Post-release version bump 2017-08-04 10:52:29 -04:00
44 changed files with 495 additions and 250 deletions

View File

@@ -1,28 +1,41 @@
<!--
Please note: GitHub issues are to be used only for feature requests
and bug reports. For installation assistance or general discussion,
please join us on the mailing list:
Before opening a new issue, please search through the existing issues to
see if your topic has already been addressed. Note that you may need to
remove the "is:open" filter from the search bar to include closed issues.
Check the appropriate type for your issue below by placing an x between the
brackets. If none of the below apply, please raise your issue for
discussion on our mailing list:
https://groups.google.com/forum/#!forum/netbox-discuss
Please indicate "bug report" or "feature request" below. Be sure to
search the existing set of issues (both open and closed) to see if
a similar issue has already been raised.
-->
### Issue type:
Please note that issues which do not fall under any of the below categories
will be closed.
--->
### Issue type
[ ] Feature request <!-- Requesting the implementation of a new feature -->
[ ] Bug report <!-- Reporting unexpected or erroneous behavior -->
[ ] Documentation <!-- Proposing a modification to the documentation -->
<!--
If filing a bug, please indicate the version of Python and NetBox
you are running. (This is not necessary for feature requests.)
Please describe the environment in which you are running NetBox. (Be sure
to verify that you are running the latest stable release of NetBox before
submitting a bug report.)
-->
**Python version:**
**NetBox version:**
### Environment
* Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.1.3 -->
<!--
If filing a bug, please record the exact steps taken to reproduce
the bug and any errors messages that are generated.
BUG REPORTS must include:
* A list of the steps needed to reproduce the bug
* A description of the expected behavior
* Any relevant error messages (screenshots may also help)
If filing a feature request, please precisely describe the data
model or workflow you would like to see implemented, and provide a
use case.
FEATURE REQUESTS must include:
* A detailed description of the proposed functionality
* A use case for the new feature
* A rough description of any necessary changes to the database schema
* Any relevant third-party libraries which would be needed
-->
### Description

View File

@@ -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/
@@ -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

View File

@@ -135,6 +135,14 @@ An API consumer can request an arbitrary number of objects by appending the "lim
---
## MEDIA_ROOT
Default: $BASE_DIR/netbox/media/
The file path to the location where media files (such as image attachments) are stored. By default, this is the `netbox/media` directory within the base NetBox installation path.
---
## NAPALM_USERNAME
## NAPALM_PASSWORD

View File

@@ -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

View File

@@ -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
@@ -55,7 +51,7 @@ Restart the nginx service to use the new configuration.
# service nginx restart
```
To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04).
To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04).
## Option B: Apache
@@ -100,10 +96,16 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c
# service apache2 restart
```
To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-14-04).
To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-16-04).
# 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

View File

@@ -6,7 +6,7 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
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 WritableProviderSerializer(CustomFieldModelSerializer):
# Circuit types
#
class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
class CircuitTypeSerializer(ValidatedModelSerializer):
class Meta:
model = CircuitType
@@ -111,7 +111,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
]
class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
class Meta:
model = CircuitTermination

View File

@@ -170,6 +170,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
to_field_name='slug'
)
commit_rate = forms.IntegerField(required=False, min_value=0, label='Commit rate (Kbps)')
#

View File

@@ -13,22 +13,6 @@ from utilities.models import CreatedUpdatedModel
from .constants import *
def humanize_speed(speed):
"""
Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps')
"""
if speed >= 1000000000 and speed % 1000000000 == 0:
return '{} Tbps'.format(speed / 1000000000)
elif speed >= 1000000 and speed % 1000000 == 0:
return '{} Gbps'.format(speed / 1000000)
elif speed >= 1000 and speed % 1000 == 0:
return '{} Mbps'.format(speed / 1000)
elif speed >= 1000:
return '{} Mbps'.format(float(speed) / 1000)
else:
return '{} Kbps'.format(speed)
@python_2_unicode_compatible
class Provider(CreatedUpdatedModel, CustomFieldModel):
"""
@@ -139,10 +123,6 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
def termination_z(self):
return self._get_termination('Z')
def commit_rate_human(self):
return '' if not self.commit_rate else humanize_speed(self.commit_rate)
commit_rate_human.admin_order_field = 'commit_rate'
@python_2_unicode_compatible
class CircuitTermination(models.Model):
@@ -173,11 +153,3 @@ class CircuitTermination(models.Model):
return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side)
except CircuitTermination.DoesNotExist:
return None
def port_speed_human(self):
return humanize_speed(self.port_speed)
port_speed_human.admin_order_field = 'port_speed'
def upstream_speed_human(self):
return '' if not self.upstream_speed else humanize_speed(self.upstream_speed)
upstream_speed_human.admin_order_field = 'upstream_speed'

View File

@@ -15,7 +15,7 @@ from dcim.models import (
)
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 RegionSerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'slug', 'parent']
class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRegionSerializer(ValidatedModelSerializer):
class Meta:
model = Region
@@ -100,7 +100,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name', 'slug']
class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRackGroupSerializer(ValidatedModelSerializer):
class Meta:
model = RackGroup
@@ -111,7 +111,7 @@ class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSeriali
# Rack roles
#
class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class RackRoleSerializer(ValidatedModelSerializer):
class Meta:
model = RackRole
@@ -216,7 +216,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRackReservationSerializer(ValidatedModelSerializer):
class Meta:
model = RackReservation
@@ -227,7 +227,7 @@ class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelS
# Manufacturers
#
class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
class ManufacturerSerializer(ValidatedModelSerializer):
class Meta:
model = Manufacturer
@@ -292,7 +292,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsolePortTemplate
@@ -311,7 +311,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsoleServerPortTemplate
@@ -330,7 +330,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerPortTemplate
@@ -349,7 +349,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerOutletTemplate
@@ -369,7 +369,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
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 DeviceBayTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = DeviceBayTemplate
@@ -399,7 +399,7 @@ class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.Mode
# Device roles
#
class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class DeviceRoleSerializer(ValidatedModelSerializer):
class Meta:
model = DeviceRole
@@ -418,7 +418,7 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
# Platforms
#
class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
class PlatformSerializer(ValidatedModelSerializer):
class Meta:
model = Platform
@@ -516,7 +516,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_console']
class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsoleServerPortSerializer(ValidatedModelSerializer):
class Meta:
model = ConsoleServerPort
@@ -536,7 +536,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
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 PowerOutletSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_port']
class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerOutletSerializer(ValidatedModelSerializer):
class Meta:
model = PowerOutlet
@@ -576,7 +576,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
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 PeerInterfaceSerializer(serializers.ModelSerializer):
]
class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInterfaceSerializer(ValidatedModelSerializer):
class Meta:
model = Interface
@@ -694,7 +694,7 @@ class NestedDeviceBaySerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name']
class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableDeviceBaySerializer(ValidatedModelSerializer):
class Meta:
model = DeviceBay
@@ -717,7 +717,7 @@ class InventoryItemSerializer(serializers.ModelSerializer):
]
class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInventoryItemSerializer(ValidatedModelSerializer):
class Meta:
model = InventoryItem
@@ -749,7 +749,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'connection_status']
class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
class Meta:
model = InterfaceConnection

View File

@@ -58,6 +58,7 @@ IFACE_FF_1GE_FIXED = 1000
IFACE_FF_1GE_GBIC = 1050
IFACE_FF_1GE_SFP = 1100
IFACE_FF_10GE_FIXED = 1150
IFACE_FF_10GE_CX4 = 1170
IFACE_FF_10GE_SFP_PLUS = 1200
IFACE_FF_10GE_XFP = 1300
IFACE_FF_10GE_XENPAK = 1310
@@ -106,6 +107,7 @@ IFACE_FF_CHOICES = [
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
[IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'],
]
],
[

View File

@@ -273,6 +273,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
name='device_type_id',
label='Device type (ID)',
)

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-29 21:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0041_napalm_integration'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
]

View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-29 21:26
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0042_interface_ff_10ge_cx4'),
]
operations = [
migrations.AlterField(
model_name='consoleport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleporttemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleserverport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='consoleserverporttemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='devicebaytemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='interface',
name='name',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='interfacetemplate',
name='name',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='poweroutlet',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='powerport',
name='name',
field=models.CharField(max_length=50),
),
migrations.AlterField(
model_name='powerporttemplate',
name='name',
field=models.CharField(max_length=50),
),
]

View File

@@ -574,7 +574,7 @@ class ConsolePortTemplate(models.Model):
A template for a ConsolePort to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -590,7 +590,7 @@ class ConsoleServerPortTemplate(models.Model):
A template for a ConsoleServerPort to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -606,7 +606,7 @@ class PowerPortTemplate(models.Model):
A template for a PowerPort to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -622,7 +622,7 @@ class PowerOutletTemplate(models.Model):
A template for a PowerOutlet to be created for a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -685,7 +685,7 @@ class InterfaceTemplate(models.Model):
A template for a physical data interface on a new Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=64)
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
@@ -705,7 +705,7 @@ class DeviceBayTemplate(models.Model):
A template for a DeviceBay to be created for a new parent Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
class Meta:
ordering = ['device_type', 'name']
@@ -890,6 +890,18 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
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)
@@ -1000,7 +1012,7 @@ class ConsolePort(models.Model):
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
"""
device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL,
verbose_name='Console server port', blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
@@ -1050,7 +1062,7 @@ class ConsoleServerPort(models.Model):
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
"""
device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
objects = ConsoleServerPortManager()
@@ -1071,7 +1083,7 @@ class PowerPort(models.Model):
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
"""
device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL,
blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
@@ -1115,7 +1127,7 @@ class PowerOutlet(models.Model):
A physical power outlet (output) within a Device which provides power to a PowerPort.
"""
device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
name = models.CharField(max_length=50)
objects = PowerOutletManager()
@@ -1145,7 +1157,7 @@ class Interface(models.Model):
blank=True,
verbose_name='Parent LAG'
)
name = models.CharField(max_length=30)
name = models.CharField(max_length=64)
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
enabled = models.BooleanField(default=True)
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')

View File

@@ -936,7 +936,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
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'

View File

@@ -10,6 +10,7 @@ from django.db import transaction
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 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
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 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
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)

View File

@@ -10,7 +10,7 @@ from extras.models import (
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 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
return serializer(obj.parent, context={'request': self.context['request']}).data
class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableImageAttachmentSerializer(ValidatedModelSerializer):
content_type = ContentTypeFieldSerializer()
class Meta:

View File

@@ -285,7 +285,7 @@ class TopologyMap(models.Model):
# 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)

View File

@@ -11,7 +11,7 @@ from ipam.models import (
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 WritableVRFSerializer(CustomFieldModelSerializer):
# Roles
#
class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class RoleSerializer(ValidatedModelSerializer):
class Meta:
model = Role
@@ -64,7 +64,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
# RIRs
#
class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
class RIRSerializer(ValidatedModelSerializer):
class Meta:
model = RIR
@@ -303,7 +303,7 @@ class ServiceSerializer(serializers.ModelSerializer):
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:

View File

@@ -45,9 +45,9 @@ ROLE_ACTIONS = """
PREFIX_LINK = """
{% if record.has_children %}
<span style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
<span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
{% else %}
<span style="padding-left: {{ record.depth }}9px">
<span class="text-nowrap" style="padding-left: {{ record.depth }}9px">
{% endif %}
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
</span>
@@ -80,7 +80,6 @@ IPADDRESS_LINK = """
IPADDRESS_DEVICE = """
{% if record.interface %}
<a href="{{ record.interface.device.get_absolute_url }}">{{ record.interface.device }}</a>
({{ record.interface.name }})
{% else %}
&mdash;
{% endif %}

View File

@@ -16,6 +16,7 @@ from utilities.views import (
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .constants import IPADDRESS_ROLE_ANYCAST
from .models import (
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
Service, VLAN, VLANGroup, VRF,
@@ -624,6 +625,9 @@ class IPAddressView(View):
).select_related(
'interface__device', 'nat_inside'
)
# Exclude anycast IPs if this IP is anycast
if ipaddress.role == IPADDRESS_ROLE_ANYCAST:
duplicate_ips = duplicate_ips.exclude(role=IPADDRESS_ROLE_ANYCAST)
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
# Related IP table

View File

@@ -93,6 +93,10 @@ MAINTENANCE_MODE = False
# all objects by specifying "?limit=0".
MAX_PAGE_SIZE = 1000
# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that
# the default value of this setting is derived from the installed location.
# MEDIA_ROOT = '/opt/netbox/netbox/media'
# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
NAPALM_USERNAME = ''
NAPALM_PASSWORD = ''

View File

@@ -35,7 +35,7 @@ OBJ_TYPE_CHOICES = (
class SearchForm(BootstrapMixin, forms.Form):
q = forms.CharField(
label='Query', widget=forms.TextInput(attrs={'style': 'width: 350px'})
label='Search', widget=forms.TextInput(attrs={'style': 'width: 350px'})
)
obj_type = forms.ChoiceField(
choices=OBJ_TYPE_CHOICES, required=False, label='Type'

View File

@@ -13,7 +13,9 @@ except ImportError:
)
VERSION = '2.1.2'
VERSION = '2.1.4'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Import required configuration parameters
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
@@ -44,14 +46,15 @@ LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30)
NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '') # Deprecated
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '') # Deprecated
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
@@ -104,8 +107,6 @@ if LDAP_CONFIGURED:
"netbox/ldap_config.py to disable LDAP."
)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Database
configuration.DATABASE.update({'ENGINE': 'django.db.backends.postgresql'})
DATABASES = {
@@ -201,7 +202,6 @@ STATICFILES_DIRS = (
)
# Media
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/{}media/'.format(BASE_PATH)
# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
@@ -221,6 +221,7 @@ SECRETS_MIN_PUBKEY_SIZE = 2048
# 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 @@ REST_FRAMEWORK = {
'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

View File

@@ -5,14 +5,14 @@ from rest_framework.validators import UniqueTogetherValidator
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

View File

@@ -205,20 +205,18 @@
<li class="dropdown{% if request.path|contains:'/circuits/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'circuits:provider_list' %}"><strong>Providers</strong></a></li>
{% if perms.circuits.add_provider %}
<li class="subnav"><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus"></i> Add a Provider</a></li>
<li class="subnav"><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download"></i> Import Providers</a></li>
{% endif %}
{% if perms.circuits.add_circuit or perms.circuits.add_provider %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'circuits:circuit_list' %}"><strong>Circuits</strong></a></li>
{% if perms.circuits.add_circuit %}
<li class="subnav"><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus"></i> Add a Circuit</a></li>
<li class="subnav"><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download"></i> Import Circuits</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'circuits:provider_list' %}"><strong>Providers</strong></a></li>
{% if perms.circuits.add_provider %}
<li class="subnav"><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus"></i> Add a Provider</a></li>
<li class="subnav"><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download"></i> Import Providers</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'circuits:circuittype_list' %}"><strong>Circuit Types</strong></a></li>
{% if perms.circuits.add_circuittype %}
<li class="subnav"><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus"></i> Add a Circuit Type</a></li>

View File

@@ -88,7 +88,7 @@
<td>Commit Rate</td>
<td>
{% if circuit.commit_rate %}
{{ circuit.commit_rate_human }}
{{ circuit.commit_rate|humanize_speed }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}

View File

@@ -9,7 +9,16 @@
{% render_field form.cid %}
{% render_field form.type %}
{% render_field form.install_date %}
{% render_field form.commit_rate %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_commit_rate">{{ form.commit_rate.label }}</label>
<div class="col-md-9">
<div class="input-group">
{{ form.commit_rate }}
{% include 'circuits/inc/speed_widget.html' with target_field='commit_rate' %}
</div>
<span class="help-block">{{ form.commit_rate.help_text }}</span>
</div>
</div>
{% render_field form.description %}
</div>
</div>
@@ -35,3 +44,12 @@
</div>
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$("a.set_speed").click(function(e) {
e.preventDefault();
$("#id_" + $(this).attr("target")).val($(this).attr("data"));
});
</script>
{% endblock %}

View File

@@ -49,8 +49,26 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>Termination Details</strong></div>
<div class="panel-body">
{% render_field form.port_speed %}
{% render_field form.upstream_speed %}
<div class="form-group">
<label class="col-md-3 control-label required" for="id_port_speed">{{ form.port_speed.label }}</label>
<div class="col-md-9">
<div class="input-group">
{{ form.port_speed }}
{% include 'circuits/inc/speed_widget.html' with target_field='port_speed' %}
</div>
<span class="help-block">{{ form.port_speed.help_text }}</span>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="id_upstream_speed">{{ form.upstream_speed.label }}</label>
<div class="col-md-9">
<div class="input-group">
{{ form.upstream_speed }}
{% include 'circuits/inc/speed_widget.html' with target_field='upstream_speed' %}
</div>
<span class="help-block">{{ form.upstream_speed.help_text }}</span>
</div>
</div>
{% render_field form.xconnect_id %}
{% render_field form.pp_info %}
</div>
@@ -72,4 +90,10 @@
{% block javascript %}
<script src="{% static 'js/livesearch.js' %}?v{{ settings.VERSION }}"></script>
<script type="text/javascript">
$("a.set_speed").click(function(e) {
e.preventDefault();
$("#id_" + $(this).attr("target")).val($(this).attr("data"));
});
</script>
{% endblock %}

View File

@@ -1,3 +1,5 @@
{% load helpers %}
<div class="panel panel-default">
<div class="panel-heading">
<div class="pull-right">
@@ -49,10 +51,10 @@
<td>Speed</td>
<td>
{% if termination.upstream_speed %}
<i class="fa fa-arrow-down" title="Downstream"></i> {{ termination.port_speed_human }} &nbsp;
<i class="fa fa-arrow-up" title="Upstream"></i> {{ termination.upstream_speed_human }}
<i class="fa fa-arrow-down" title="Downstream"></i> {{ termination.port_speed|humanize_speed }} &nbsp;
<i class="fa fa-arrow-up" title="Upstream"></i> {{ termination.upstream_speed|humanize_speed }}
{% else %}
{{ termination.port_speed_human }}
{{ termination.port_speed|humanize_speed }}
{% endif %}
</td>
</tr>

View File

@@ -0,0 +1,17 @@
<span class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="#" target="{{ target_field }}" data="10000" class="set_speed">10 Mbps</a></li>
<li><a href="#" target="{{ target_field }}" data="100000" class="set_speed">100 Mbps</a></li>
<li><a href="#" target="{{ target_field }}" data="1000000" class="set_speed">1 Gbps</a></li>
<li><a href="#" target="{{ target_field }}" data="10000000" class="set_speed">10 Gbps</a></li>
<li><a href="#" target="{{ target_field }}" data="25000000" class="set_speed">25 Gbps</a></li>
<li><a href="#" target="{{ target_field }}" data="40000000" class="set_speed">40 Gbps</a></li>
<li><a href="#" target="{{ target_field }}" data="100000000" class="set_speed">100 Gbps</a></li>
<li class="divider"></li>
<li><a href="#" target="{{ target_field }}" data="1544" class="set_speed">T1 (1.544 Mbps)</a></li>
<li><a href="#" target="{{ target_field }}" data="2048" class="set_speed">E1 (2.048 Mbps)</a></li>
</ul>
</span>

View File

@@ -80,13 +80,19 @@ $(document).ready(function() {
$('#model').html(json['get_facts']['model']);
$('#serial_number').html(json['get_facts']['serial_number']);
$('#os_version').html(json['get_facts']['os_version']);
$('#uptime').html(json['get_facts']['uptime']);
// Calculate uptime
var uptime = json['get_facts']['uptime'];
console.log(uptime);
var uptime_days = Math.floor(uptime / 86400);
var uptime_hours = Math.floor(uptime % 86400 / 3600);
var uptime_minutes = Math.floor(uptime % 3600 / 60);
$('#uptime').html(uptime_days + "d " + uptime_hours + "h " + uptime_minutes + "m");
$.each(json['get_environment']['cpu'], function(name, obj) {
var row="<tr><td>" + name + "</td><td>" + obj['%usage'] + "%</td></tr>";
$("#cpu").after(row)
});
$('#memory').after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "MB</td></tr>");
$('#memory').after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "MB</td></tr>");
$('#memory').after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "</td></tr>");
$('#memory').after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "</td></tr>");
$.each(json['get_environment']['temperature'], function(name, obj) {
var style = "success";
if (obj['is_alert']) {

View File

@@ -25,3 +25,36 @@
</div>
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
var site_list = $('#id_site');
var rack_group_list = $('#id_group_id');
// Update rack group and rack options based on selected site
site_list.change(function() {
var selected_sites = $(this).val();
if (selected_sites) {
// Update rack group options
rack_group_list.empty();
$.ajax({
url: netbox_api_path + 'dcim/rack-groups/?limit=500&site=' + selected_sites.join('&site='),
dataType: 'json',
success: function (response, status) {
$.each(response["results"], function (index, group) {
var option = $("<option></option>").attr("value", group.id).text(group.name);
rack_group_list.append(option);
});
}
});
}
});
});
</script>
{% endblock %}

View File

@@ -24,7 +24,7 @@
{% empty %}
{% if table.empty_text %}
<tr>
<td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td>
<td colspan="{{ table.columns|length }}" class="text-center text-muted">&mdash; {{ table.empty_text }} &mdash;</td>
</tr>
{% endif %}
{% endfor %}

View File

@@ -83,7 +83,11 @@
<tr>
<td>Role</td>
<td>
<a href="{% url 'ipam:ipaddress_list' %}?role={{ ipaddress.role }}">{{ ipaddress.get_role_display }}</a>
{% if ipaddress.role %}
<a href="{% url 'ipam:ipaddress_list' %}?role={{ ipaddress.role }}">{{ ipaddress.get_role_display }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>

View File

@@ -1,70 +0,0 @@
{% extends '_base.html' %}
{% load static from staticfiles %}
{% load form_helpers %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.non_field_errors }}
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>{% block title %}Assign an IP Address{% endblock %}</strong>
</div>
<div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label">IP Address</label>
<div class="col-md-9">
<p class="form-control-static">{{ ipaddress }}</p>
</div>
<label class="col-md-3 control-label">VRF</label>
<div class="col-md-9">
<p class="form-control-static">
{% if ipaddress.vrf %}
<a href="{% url 'ipam:vrf' pk=ipaddress.vrf.pk %}">{{ ipaddress.vrf }}</a> ({{ ipaddress.vrf.rd }})
{% else %}
<span>Global</span>
{% endif %}
</p>
</div>
</div>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
<li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="search">
{% render_field form.livesearch %}
</div>
<div class="tab-pane" id="select">
{% render_field form.site %}
{% render_field form.rack %}
{% render_field form.device %}
</div>
</div>
{% render_field form.interface %}
{% render_field form.set_as_primary %}
</div>
</div>
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_assign" class="btn btn-primary">Assign</button>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>
</div>
</form>
{% endblock %}
{% block javascript %}
<script src="{% static 'js/livesearch.js' %}?v{{ settings.VERSION }}"></script>
{% endblock %}

View File

@@ -1,8 +0,0 @@
{% extends 'utilities/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Remove {{ ipaddress }} from {{ ipaddress.interface }}?{% endblock %}
{% block message %}
<p>Are you sure you want to remove this IP address from <strong>{{ ipaddress.interface.device }} {{ ipaddress.interface }}</strong>?</p>
{% endblock %}

View File

@@ -3,7 +3,7 @@
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_devicerole %}
{% if perms.ipam.add_role %}
<a href="{% url 'ipam:role_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a role

View File

@@ -4,14 +4,14 @@ from rest_framework import serializers
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

View File

@@ -8,7 +8,8 @@ from rest_framework.compat import is_authenticated
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 rest_framework.views import get_view_name as drf_get_view_name
from users.models import Token
@@ -80,6 +81,28 @@ class IsAuthenticatedOrLoginNotRequired(BasePermission):
# Serializers
#
class ValidatedModelSerializer(ModelSerializer):
"""
Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation.
"""
def validate(self, data):
# Remove custom field data (if any) prior to model validation
attrs = data.copy()
attrs.pop('custom_fields', None)
# Run clean() on an instance of the model
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 data
class ChoiceFieldSerializer(Field):
"""
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
@@ -121,17 +144,6 @@ class ContentTypeFieldSerializer(Field):
# 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).
@@ -192,3 +204,21 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
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)

View File

@@ -7,9 +7,10 @@ from mptt.forms import TreeNodeMultipleChoiceField
from django import forms
from django.conf import settings
from django.core.validators import URLValidator
from django.urls import reverse_lazy
from .validators import EnhancedURLValidator
COLOR_CHOICES = (
('aa1409', 'Dark red'),
@@ -431,17 +432,11 @@ class FilterTreeNodeMultipleChoiceField(FilterChoiceFieldMixin, TreeNodeMultiple
class LaxURLField(forms.URLField):
"""
Custom URLField which allows any valid URL scheme
Modifies Django's built-in URLField in two ways:
1) Allow any valid scheme per RFC 3986 section 3.1
2) Remove the requirement for fully-qualified domain names (e.g. http://myserver/ is valid)
"""
class AnyURLScheme(object):
# A fake URL list which "contains" all scheme names abiding by the syntax defined in RFC 3986 section 3.1
def __contains__(self, item):
if not item or not re.match('^[a-z][0-9a-z+\-.]*$', item.lower()):
return False
return True
default_validators = [URLValidator(schemes=AnyURLScheme())]
default_validators = [EnhancedURLValidator()]
#
@@ -459,7 +454,7 @@ class BootstrapMixin(forms.BaseForm):
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

View File

@@ -14,7 +14,7 @@ class BaseTable(tables.Table):
# Set default empty_text if none was provided
if self.empty_text is None:
self.empty_text = 'No {} found.'.format(self._meta.model._meta.verbose_name_plural)
self.empty_text = 'No {} found'.format(self._meta.model._meta.verbose_name_plural)
class Meta:
attrs = {

View File

@@ -62,6 +62,27 @@ def bettertitle(value):
return ' '.join([w[0].upper() + w[1:] for w in value.split()])
@register.filter()
def humanize_speed(speed):
"""
Humanize speeds given in Kbps. Examples:
1544 => "1.544 Mbps"
100000 => "100 Mbps"
10000000 => "10 Gbps"
"""
if speed >= 1000000000 and speed % 1000000000 == 0:
return '{} Tbps'.format(int(speed / 1000000000))
elif speed >= 1000000 and speed % 1000000 == 0:
return '{} Gbps'.format(int(speed / 1000000))
elif speed >= 1000 and speed % 1000 == 0:
return '{} Mbps'.format(int(speed / 1000))
elif speed >= 1000:
return '{} Mbps'.format(float(speed) / 1000)
else:
return '{} Kbps'.format(speed)
@register.filter()
def example_choices(field, arg=3):
"""

View File

@@ -0,0 +1,30 @@
from __future__ import unicode_literals
import re
from django.core.validators import _lazy_re_compile, URLValidator
class EnhancedURLValidator(URLValidator):
"""
Extends Django's built-in URLValidator to permit the use of hostnames with no domain extension.
"""
class AnyURLScheme(object):
"""
A fake URL list which "contains" all scheme names abiding by the syntax defined in RFC 3986 section 3.1
"""
def __contains__(self, item):
if not item or not re.match('^[a-z][0-9a-z+\-.]*$', item.lower()):
return False
return True
fqdn_re = URLValidator.hostname_re + URLValidator.domain_re + URLValidator.tld_re
host_res = [URLValidator.ipv4_re, URLValidator.ipv6_re, fqdn_re, URLValidator.hostname_re]
regex = _lazy_re_compile(
r'^(?:[a-z0-9\.\-\+]*)://' # Scheme (previously enforced by AnyURLScheme or schemes kwarg)
r'(?:\S+(?::\S*)?@)?' # HTTP basic authentication
r'(?:' + '|'.join(host_res) + ')' # IPv4, IPv6, FQDN, or hostname
r'(?::\d{2,5})?' # Port number
r'(?:[/?#][^\s]*)?' # Path
r'\Z', re.IGNORECASE)
schemes = AnyURLScheme()