mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-30 09:07:46 -06:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa8f734bd1 | ||
|
|
0015484679 | ||
|
|
aa5aa798e6 | ||
|
|
63a4a70420 | ||
|
|
2badb04a03 | ||
|
|
a328e12642 | ||
|
|
ce9f1eb201 | ||
|
|
b454ac9ce5 | ||
|
|
a010f7439d | ||
|
|
d7b0ba57e0 | ||
|
|
89194c067b | ||
|
|
126a5e5e4e | ||
|
|
466ab0b404 | ||
|
|
a52a871a94 | ||
|
|
4f9379a5e2 | ||
|
|
e31d2c9857 | ||
|
|
b252f52f8e | ||
|
|
eba30c4d79 | ||
|
|
ae1662e433 | ||
|
|
81df837a33 | ||
|
|
b84b526a43 | ||
|
|
17c1a1e465 | ||
|
|
b6df0209ba | ||
|
|
6b53d263fe | ||
|
|
372e9335b1 | ||
|
|
f6d1163ddd | ||
|
|
e6b6082a2b | ||
|
|
e0ee0b9254 | ||
|
|
fde47133da | ||
|
|
643f64df3f | ||
|
|
a6ec1ba23b | ||
|
|
ae231b1d1b | ||
|
|
ecdf66c454 | ||
|
|
aaca28c1a6 | ||
|
|
f0d8fdf2d8 | ||
|
|
c9288a052a | ||
|
|
4b3a425888 | ||
|
|
1308839a41 | ||
|
|
6d849ad5f6 | ||
|
|
3e4b66675b | ||
|
|
6f8d3f7a57 | ||
|
|
eb994a691a | ||
|
|
ae73e6f04d | ||
|
|
7843a6bf77 | ||
|
|
fa6d933544 | ||
|
|
83085d08ca | ||
|
|
70a6b171d0 | ||
|
|
9a25b5458d | ||
|
|
416aeb4072 | ||
|
|
0c1594b8f8 | ||
|
|
5be30bd278 | ||
|
|
669ae104a4 | ||
|
|
c37cfeb74f | ||
|
|
51b1da660a | ||
|
|
c394985b1b | ||
|
|
04c300b8e2 | ||
|
|
117f33afc5 | ||
|
|
3d92669df5 | ||
|
|
63757af1a0 | ||
|
|
e6a58b6700 | ||
|
|
8fb504c963 | ||
|
|
babe42ef35 | ||
|
|
7557220d5d | ||
|
|
50ccccc3f4 | ||
|
|
b6afc68847 | ||
|
|
36d5debe74 |
47
.github/ISSUE_TEMPLATE.md
vendored
47
.github/ISSUE_TEMPLATE.md
vendored
@@ -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
|
||||
|
||||
@@ -4,7 +4,8 @@ Supported HTTP methods:
|
||||
|
||||
* `GET`: Retrieve an object or list of objects
|
||||
* `POST`: Create a new object
|
||||
* `PUT`: Update an existing object
|
||||
* `PUT`: Update an existing object, all mandatory fields must be specified
|
||||
* `PATCH`: Updates an existing object, only specifiying the field to be changed
|
||||
* `DELETE`: Delete an existing object
|
||||
|
||||
To authenticate a request, attach your token in an `Authorization` header:
|
||||
@@ -104,12 +105,19 @@ $ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0
|
||||
|
||||
### Modify an existing site
|
||||
|
||||
Make an authenticated `PUT` request to the site detail endpoint. As with a create (POST) request, all mandatory fields must be included.
|
||||
Make an authenticated `PUT` request to the site detail endpoint. As with a create (`POST`) request, all mandatory fields must be included.
|
||||
|
||||
```
|
||||
$ curl -X PUT -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/ --data '{"name": "Renamed Site", "slug": "renamed-site"}'
|
||||
```
|
||||
|
||||
### Modify an object by changing a field
|
||||
|
||||
Make an authenticated `PATCH` request to the device endpoint. With `PATCH`, unlike `POST` and `PUT`, we only specify the field that is being changed. In this example, we add a serial number to a device.
|
||||
```
|
||||
$ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/devices/2549/ --data '{"serial": "FTX1123A090"}'
|
||||
```
|
||||
|
||||
### Delete an existing site
|
||||
|
||||
Send an authenticated `DELETE` request to the site detail endpoint.
|
||||
|
||||
@@ -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. `PUT` requires all mandatory fields to be specified, while `PATCH` only expects the field that is being modified to be specified.
|
||||
* `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
|
||||
|
||||
@@ -17,7 +17,7 @@ ADMINS = [
|
||||
|
||||
## BANNER_BOTTOM
|
||||
|
||||
Setting these variables will display content in a banner at the top and/or bottom of the page, respectively. To replicate the content of the top banner in the bottom banner, set:
|
||||
Setting these variables will display content in a banner at the top and/or bottom of the page, respectively. HTML is allowed. To replicate the content of the top banner in the bottom banner, set:
|
||||
|
||||
```
|
||||
BANNER_TOP = 'Your banner text'
|
||||
@@ -26,6 +26,12 @@ BANNER_BOTTOM = BANNER_TOP
|
||||
|
||||
---
|
||||
|
||||
## BANNER_LOGIN
|
||||
|
||||
The value of this variable will be displayed on the login page above the login form. HTML is allowed.
|
||||
|
||||
---
|
||||
|
||||
## BASE_PATH
|
||||
|
||||
Default: None
|
||||
@@ -135,6 +141,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
|
||||
|
||||
@@ -78,6 +78,8 @@ AUTH_LDAP_USER_ATTR_MAP = {
|
||||
```
|
||||
|
||||
# User Groups for Permissions
|
||||
!!! Info
|
||||
When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for AUTH_LDAP_GROUP_TYPE.
|
||||
|
||||
```python
|
||||
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
|
||||
|
||||
@@ -20,7 +20,7 @@ Python 3:
|
||||
|
||||
```no-highlight
|
||||
# yum install -y epel-release
|
||||
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
|
||||
# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
|
||||
# easy_install-3.4 pip
|
||||
# ln -s -f python3.4 /usr/bin/python
|
||||
```
|
||||
@@ -29,7 +29,7 @@ Python 2:
|
||||
|
||||
```no-highlight
|
||||
# yum install -y epel-release
|
||||
# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
|
||||
# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
|
||||
```
|
||||
|
||||
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
|
||||
@@ -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)',
|
||||
)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from ipam.models import IPAddress
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||
APISelect, ArrayFieldSelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField,
|
||||
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||
FilterTreeNodeMultipleChoiceField,
|
||||
@@ -28,12 +28,6 @@ from .models import (
|
||||
)
|
||||
|
||||
|
||||
FORM_STATUS_CHOICES = [
|
||||
['', '---------'],
|
||||
]
|
||||
|
||||
FORM_STATUS_CHOICES += STATUS_CHOICES
|
||||
|
||||
DEVICE_BY_PK_RE = '{\d+\}'
|
||||
|
||||
|
||||
@@ -642,13 +636,28 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
# Compile list of choices for primary IPv4 and IPv6 addresses
|
||||
for family in [4, 6]:
|
||||
ip_choices = []
|
||||
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
|
||||
ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
|
||||
.select_related('nat_inside__interface')
|
||||
ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
||||
self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
|
||||
ip_choices = [(None, '---------')]
|
||||
# Collect interface IPs
|
||||
interface_ips = IPAddress.objects.select_related('interface').filter(
|
||||
family=family, interface__device=self.instance
|
||||
)
|
||||
if interface_ips:
|
||||
ip_choices.append(
|
||||
('Interface IPs', [
|
||||
(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
|
||||
])
|
||||
)
|
||||
# Collect NAT IPs
|
||||
nat_ips = IPAddress.objects.select_related('nat_inside').filter(
|
||||
family=family, nat_inside__interface__device=self.instance
|
||||
)
|
||||
if nat_ips:
|
||||
ip_choices.append(
|
||||
('NAT IPs', [
|
||||
(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips
|
||||
])
|
||||
)
|
||||
self.fields['primary_ip{}'.format(family)].choices = ip_choices
|
||||
|
||||
# If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
|
||||
# can be flipped from one face to another.
|
||||
@@ -848,7 +857,7 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
|
||||
status = forms.ChoiceField(choices=add_blank_choice(STATUS_CHOICES), required=False, initial='')
|
||||
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
||||
|
||||
class Meta:
|
||||
|
||||
25
netbox/dcim/migrations/0042_interface_ff_10ge_cx4.py
Normal file
25
netbox/dcim/migrations/0042_interface_ff_10ge_cx4.py
Normal 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),
|
||||
),
|
||||
]
|
||||
70
netbox/dcim/migrations/0043_device_component_name_lengths.py
Normal file
70
netbox/dcim/migrations/0043_device_component_name_lengths.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -13,6 +13,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
@@ -574,7 +575,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 +591,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 +607,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 +623,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']
|
||||
@@ -642,15 +643,16 @@ class InterfaceQuerySet(models.QuerySet):
|
||||
To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
|
||||
slot, subslot, position, channel, and virtual circuit:
|
||||
|
||||
{type}{slot}/{subslot}/{position}:{channel}.{vc}
|
||||
{type}{slot}/{subslot}/{position}/{subposition}:{channel}.{vc}
|
||||
|
||||
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
|
||||
be parsed as follows:
|
||||
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet1/2/3
|
||||
would be parsed as follows:
|
||||
|
||||
name = 'GigabitEthernet'
|
||||
slot = None
|
||||
subslot = 0
|
||||
position = 1
|
||||
slot = 1
|
||||
subslot = 2
|
||||
position = 3
|
||||
subposition = 0
|
||||
channel = None
|
||||
vc = 0
|
||||
|
||||
@@ -659,17 +661,35 @@ class InterfaceQuerySet(models.QuerySet):
|
||||
"""
|
||||
sql_col = '{}.name'.format(self.model._meta.db_table)
|
||||
ordering = {
|
||||
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
|
||||
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
|
||||
IFACE_ORDERING_POSITION: (
|
||||
'_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_type', '_id', 'name',
|
||||
),
|
||||
IFACE_ORDERING_NAME: (
|
||||
'_type', '_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_id', 'name',
|
||||
),
|
||||
}[method]
|
||||
return self.extra(select={
|
||||
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
||||
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_channel': "COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col),
|
||||
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
|
||||
}).order_by(*ordering)
|
||||
|
||||
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
|
||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
|
||||
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)\/') AS integer)"
|
||||
SUBSLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/)([0-9]+)') AS integer)"
|
||||
POSITION_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer)"
|
||||
SUBPOSITION_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer)"
|
||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
|
||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
|
||||
|
||||
fields = {
|
||||
'_type': RawSQL(TYPE_RE.format(sql_col), []),
|
||||
'_id': RawSQL(ID_RE.format(sql_col), []),
|
||||
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
|
||||
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
|
||||
'_position': RawSQL(POSITION_RE.format(sql_col), []),
|
||||
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
|
||||
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
|
||||
'_vc': RawSQL(VC_RE.format(sql_col), []),
|
||||
}
|
||||
|
||||
return self.annotate(**fields).order_by(*ordering)
|
||||
|
||||
def connectable(self):
|
||||
"""
|
||||
@@ -685,7 +705,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 +725,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 +910,30 @@ 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
|
||||
) and (
|
||||
self.primary_ip4.nat_inside.interface is None or
|
||||
self.primary_ip4.nat_inside.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
|
||||
) and (
|
||||
self.primary_ip6.nat_inside.interface is None or
|
||||
self.primary_ip6.nat_inside.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 +1044,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 +1094,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 +1115,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 +1159,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 +1189,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')
|
||||
|
||||
@@ -98,3 +98,112 @@ class RackTestCase(TestCase):
|
||||
face=None,
|
||||
)
|
||||
self.assertTrue(pdu)
|
||||
|
||||
|
||||
class InterfaceTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.site = Site.objects.create(
|
||||
name='TestSite1',
|
||||
slug='my-test-site'
|
||||
)
|
||||
self.rack = Rack.objects.create(
|
||||
name='TestRack1',
|
||||
facility_id='A101',
|
||||
site=self.site,
|
||||
u_height=42
|
||||
)
|
||||
self.manufacturer = Manufacturer.objects.create(
|
||||
name='Acme',
|
||||
slug='acme'
|
||||
)
|
||||
|
||||
self.device_type = DeviceType.objects.create(
|
||||
manufacturer=self.manufacturer,
|
||||
model='FrameForwarder 2048',
|
||||
slug='ff2048'
|
||||
)
|
||||
self.role = DeviceRole.objects.create(
|
||||
name='Switch',
|
||||
slug='switch',
|
||||
)
|
||||
|
||||
def test_interface_order_natural(self):
|
||||
device1 = Device.objects.create(
|
||||
name='TestSwitch1',
|
||||
device_type=self.device_type,
|
||||
device_role=self.role,
|
||||
site=self.site,
|
||||
rack=self.rack,
|
||||
position=10,
|
||||
face=RACK_FACE_REAR,
|
||||
)
|
||||
interface1 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/3/1'
|
||||
)
|
||||
interface2 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/5/1'
|
||||
)
|
||||
interface3 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/4'
|
||||
)
|
||||
interface4 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/3/2/4'
|
||||
)
|
||||
interface5 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/3/2/1'
|
||||
)
|
||||
interface6 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Loopback1'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
list(Interface.objects.all().order_naturally()),
|
||||
[interface1, interface5, interface4, interface3, interface2, interface6]
|
||||
)
|
||||
|
||||
def test_interface_order_natural_subinterfaces(self):
|
||||
device1 = Device.objects.create(
|
||||
name='TestSwitch1',
|
||||
device_type=self.device_type,
|
||||
device_role=self.role,
|
||||
site=self.site,
|
||||
rack=self.rack,
|
||||
position=10,
|
||||
face=RACK_FACE_REAR,
|
||||
)
|
||||
interface1 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/3'
|
||||
)
|
||||
interface2 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/2.2'
|
||||
)
|
||||
interface3 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/0.120'
|
||||
)
|
||||
interface4 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/0'
|
||||
)
|
||||
interface5 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/1.117'
|
||||
)
|
||||
interface6 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0'
|
||||
)
|
||||
self.assertEqual(
|
||||
list(Interface.objects.all().order_naturally()),
|
||||
[interface4, interface3, interface5, interface2, interface1, interface6]
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
#
|
||||
@@ -28,34 +29,47 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
|
||||
for field_name, value in data.items():
|
||||
|
||||
cf = custom_fields[field_name]
|
||||
try:
|
||||
cf = custom_fields[field_name]
|
||||
except KeyError:
|
||||
raise ValidationError(
|
||||
"Invalid custom field for {} objects: {}".format(content_type, field_name)
|
||||
)
|
||||
|
||||
# Validate custom field name
|
||||
if field_name not in custom_fields:
|
||||
raise ValidationError("Invalid custom field for {} objects: {}".format(content_type, field_name))
|
||||
# Data validation
|
||||
if value not in [None, '']:
|
||||
|
||||
# Validate boolean
|
||||
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
raise ValidationError("Invalid value for boolean field {}: {}".format(field_name, value))
|
||||
# Validate boolean
|
||||
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
raise ValidationError(
|
||||
"Invalid value for boolean field {}: {}".format(field_name, value)
|
||||
)
|
||||
|
||||
# Validate date
|
||||
if cf.type == CF_TYPE_DATE:
|
||||
try:
|
||||
datetime.strptime(value, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise ValidationError("Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(
|
||||
field_name, value
|
||||
))
|
||||
# Validate date
|
||||
if cf.type == CF_TYPE_DATE:
|
||||
try:
|
||||
datetime.strptime(value, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
"Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(field_name, value)
|
||||
)
|
||||
|
||||
# Validate selected choice
|
||||
if cf.type == CF_TYPE_SELECT:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
raise ValidationError("{}: Choice selections must be passed as integers.".format(field_name))
|
||||
valid_choices = [c.pk for c in cf.choices.all()]
|
||||
if value not in valid_choices:
|
||||
raise ValidationError("Invalid choice for field {}: {}".format(field_name, value))
|
||||
# Validate selected choice
|
||||
if cf.type == CF_TYPE_SELECT:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
"{}: Choice selections must be passed as integers.".format(field_name)
|
||||
)
|
||||
valid_choices = [c.pk for c in cf.choices.all()]
|
||||
if value not in valid_choices:
|
||||
raise ValidationError(
|
||||
"Invalid choice for field {}: {}".format(field_name, value)
|
||||
)
|
||||
|
||||
elif cf.required:
|
||||
raise ValidationError("Required field {} cannot be empty.".format(field_name))
|
||||
|
||||
# Check for missing required fields
|
||||
missing_fields = []
|
||||
@@ -68,7 +82,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 +125,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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -98,7 +98,7 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
# Create the new IP address
|
||||
data = request.data.copy()
|
||||
data['address'] = '{}/{}'.format(ipaddress, prefix.prefix.prefixlen)
|
||||
data['vrf'] = prefix.vrf
|
||||
data['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
||||
serializer = serializers.WritableIPAddressSerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
@@ -25,11 +25,8 @@ IP_FAMILY_CHOICES = [
|
||||
(6, 'IPv6'),
|
||||
]
|
||||
|
||||
PREFIX_MASK_LENGTH_CHOICES = [
|
||||
('', '---------'),
|
||||
] + [(i, i) for i in range(1, 128)]
|
||||
|
||||
IPADDRESS_MASK_LENGTH_CHOICES = PREFIX_MASK_LENGTH_CHOICES + [(128, 128)]
|
||||
PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 128)])
|
||||
IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 129)])
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -512,6 +512,16 @@ class VLANGroup(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||
|
||||
def get_next_available_vid(self):
|
||||
"""
|
||||
Return the first available VLAN ID (1-4094) in the group.
|
||||
"""
|
||||
vids = [vlan['vid'] for vlan in self.vlans.order_by('vid').values('vid')]
|
||||
for i in range(1, 4095):
|
||||
if i not in vids:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
@@ -34,7 +34,7 @@ RIR_ACTIONS = """
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% if record.pk %}{% utilization_graph value %}{% else %}—{% endif %}
|
||||
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}—{% endif %}
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
@@ -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 %}
|
||||
—
|
||||
{% endif %}
|
||||
@@ -121,6 +120,13 @@ VLAN_ROLE_LINK = """
|
||||
"""
|
||||
|
||||
VLANGROUP_ACTIONS = """
|
||||
{% with next_vid=record.get_next_available_vid %}
|
||||
{% if next_vid and perms.ipam.add_vlan %}
|
||||
<a href="{% url 'ipam:vlan_add' %}?site={{ record.site_id }}&group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if perms.ipam.change_vlangroup %}
|
||||
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
@@ -204,10 +210,10 @@ class AggregateTable(BaseTable):
|
||||
|
||||
class AggregateDetailTable(AggregateTable):
|
||||
child_count = tables.Column(verbose_name='Prefixes')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(AggregateTable.Meta):
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description')
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
|
||||
|
||||
|
||||
#
|
||||
@@ -250,10 +256,10 @@ class PrefixTable(BaseTable):
|
||||
|
||||
|
||||
class PrefixDetailTable(PrefixTable):
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False)
|
||||
|
||||
class Meta(PrefixTable.Meta):
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -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,
|
||||
@@ -285,11 +286,12 @@ class AggregateListView(ObjectListView):
|
||||
ipv4_total = 0
|
||||
ipv6_total = 0
|
||||
|
||||
for a in self.queryset:
|
||||
if a.prefix.version == 4:
|
||||
ipv4_total += a.prefix.size
|
||||
elif a.prefix.version == 6:
|
||||
ipv6_total += a.prefix.size / 2 ** 64
|
||||
for aggregate in self.queryset:
|
||||
if aggregate.prefix.version == 6:
|
||||
# Report equivalent /64s for IPv6 to keep things sane
|
||||
ipv6_total += int(aggregate.prefix.size / 2 ** 64)
|
||||
else:
|
||||
ipv4_total += aggregate.prefix.size
|
||||
|
||||
return {
|
||||
'ipv4_total': ipv4_total,
|
||||
@@ -313,7 +315,7 @@ class AggregateView(View):
|
||||
)
|
||||
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
|
||||
|
||||
prefix_table = tables.PrefixTable(child_prefixes)
|
||||
prefix_table = tables.PrefixDetailTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
prefix_table.base_columns['pk'].visible = True
|
||||
|
||||
@@ -472,11 +474,11 @@ class PrefixView(View):
|
||||
child_prefixes = Prefix.objects.filter(
|
||||
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
|
||||
).select_related(
|
||||
'site', 'role'
|
||||
'site', 'vlan', 'role',
|
||||
).annotate_depth(limit=0)
|
||||
if child_prefixes:
|
||||
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
||||
child_prefix_table = tables.PrefixTable(child_prefixes)
|
||||
child_prefix_table = tables.PrefixDetailTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
child_prefix_table.base_columns['pk'].visible = True
|
||||
|
||||
@@ -624,6 +626,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
|
||||
|
||||
@@ -38,11 +38,14 @@ ADMINS = [
|
||||
# ['John Doe', 'jdoe@example.com'],
|
||||
]
|
||||
|
||||
# Optionally display a persistent banner at the top and/or bottom of every page. To display the same content in both
|
||||
# banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
||||
# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same
|
||||
# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
||||
BANNER_TOP = ''
|
||||
BANNER_BOTTOM = ''
|
||||
|
||||
# Text to include on the login page above the login form. HTML is allowed.
|
||||
BANNER_LOGIN = ''
|
||||
|
||||
# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
|
||||
# BASE_PATH = 'netbox/'
|
||||
BASE_PATH = ''
|
||||
@@ -93,6 +96,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 = ''
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -13,7 +13,9 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.1.2'
|
||||
VERSION = '2.1.5'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Import required configuration parameters
|
||||
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
|
||||
@@ -27,8 +29,9 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
|
||||
# Import optional configuration parameters
|
||||
ADMINS = getattr(configuration, 'ADMINS', [])
|
||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', '')
|
||||
BANNER_LOGIN = getattr(configuration, 'BANNER_LOGIN', '')
|
||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', '')
|
||||
BASE_PATH = getattr(configuration, 'BASE_PATH', '')
|
||||
if BASE_PATH:
|
||||
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
|
||||
@@ -44,14 +47,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 +108,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 +203,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 +222,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',
|
||||
@@ -232,10 +234,14 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'utilities.api.TokenPermissions',
|
||||
),
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'utilities.api.FormlessBrowsableAPIRenderer',
|
||||
),
|
||||
'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
|
||||
|
||||
@@ -329,13 +329,14 @@ li.occupied + li.available {
|
||||
}
|
||||
|
||||
/* Devices */
|
||||
table.component-list tr.ipaddress td {
|
||||
background-color: #eeffff;
|
||||
padding-bottom: 4px;
|
||||
padding-top: 4px;
|
||||
table.component-list td.subtable {
|
||||
padding: 0;
|
||||
padding-left: 16px;
|
||||
}
|
||||
table.component-list tr.ipaddress:hover td {
|
||||
background-color: #e6f7f7;
|
||||
table.component-list td.subtable td {
|
||||
border: none;
|
||||
padding-bottom: 6px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
/* AJAX loader */
|
||||
|
||||
@@ -43,7 +43,7 @@ $(document).ready(function() {
|
||||
success: function (response, status) {
|
||||
if (response.plaintext) {
|
||||
console.log("Secret retrieved successfully");
|
||||
$('#secret_' + secret_id).html(response.plaintext);
|
||||
$('#secret_' + secret_id).text(response.plaintext);
|
||||
$('button.unlock-secret[secret-id=' + secret_id + ']').hide();
|
||||
$('button.lock-secret[secret-id=' + secret_id + ']').show();
|
||||
} else {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 }}
|
||||
<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 }}
|
||||
<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>
|
||||
|
||||
17
netbox/templates/circuits/inc/speed_widget.html
Normal file
17
netbox/templates/circuits/inc/speed_widget.html
Normal 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>
|
||||
@@ -53,7 +53,7 @@ $(document).ready(function() {
|
||||
success: function(json) {
|
||||
$.each(json['get_lldp_neighbors'], function(iface, neighbors) {
|
||||
var neighbor = neighbors[0];
|
||||
var row = $('#' + iface.replace(/(\/)/g, "\\$1"));
|
||||
var row = $('#' + iface.split(".")[0].replace(/(\/)/g, "\\$1"));
|
||||
var configured_device = row.children('td.configured_device').attr('data');
|
||||
var configured_interface = row.children('td.configured_interface').attr('data');
|
||||
// Add LLDP neighbors to table
|
||||
@@ -62,7 +62,7 @@ $(document).ready(function() {
|
||||
// Apply colors to rows
|
||||
if (!configured_device && neighbor['hostname']) {
|
||||
row.addClass('info');
|
||||
} else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port']) {
|
||||
} else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port'].split(".")[0]) {
|
||||
row.addClass('success');
|
||||
} else {
|
||||
row.addClass('danger');
|
||||
|
||||
@@ -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']) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status %} success{% elif iface.connection and not iface.connection.connection_status %} info{% endif %}">
|
||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% endif %}">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||
@@ -113,41 +113,55 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% for ip in iface.ip_addresses.all %}
|
||||
<tr class="ipaddress">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td colspan="3">
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
{% if ip.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
||||
{% endif %}
|
||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||
<span class="label label-success">Primary</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if ip.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
|
||||
{% with iface.ip_addresses.all as ipaddresses %}
|
||||
{% if ipaddresses %}
|
||||
<tr class="ipaddress">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td></td>
|
||||
<td colspan="6" class="subtable">
|
||||
{% else %}
|
||||
<span class="text-muted">Global</span>
|
||||
<td colspan="7" class="subtable">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<table class="table table-hover">
|
||||
{% for ip in ipaddresses %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
{% if ip.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||
<span class="label label-success">Primary</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if ip.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Global table</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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">— {{ table.empty_text }} —</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -20,11 +20,18 @@
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:aggregate_bulk_edit' bulk_delete_url='ipam:aggregate_bulk_delete' %}
|
||||
<p class="text-right">IPv4 total: <strong>{{ ipv4_total|intcomma }} /32s</strong></p>
|
||||
<p class="text-right">IPv6 total: <strong>{{ ipv6_total|intcomma }} /64s</strong></p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong><i class="fa fa-bar-chart"></i> Statistics</strong>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">Total IPv4 IPs <span class="badge">{{ ipv4_total|intcomma }}</span></li>
|
||||
<li class="list-group-item">Total IPv6 /64s <span class="badge">{{ ipv6_total|intcomma }}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row" style="margin-top: 150px;">
|
||||
<div class="row" style="margin-top: {% if settings.BANNER_LOGIN %}100{% else %}150{% endif %}px;">
|
||||
<div class="col-sm-4 col-sm-offset-4">
|
||||
{% if settings.BANNER_LOGIN %}
|
||||
<div style="margin-bottom: 25px">
|
||||
{{ settings.BANNER_LOGIN|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,9 @@ 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.renderers import BrowsableAPIRenderer
|
||||
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 +82,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 +145,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 +205,33 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
||||
pass
|
||||
|
||||
return self.default_limit
|
||||
|
||||
|
||||
#
|
||||
# Renderers
|
||||
#
|
||||
|
||||
class FormlessBrowsableAPIRenderer(BrowsableAPIRenderer):
|
||||
"""
|
||||
Override the built-in BrowsableAPIRenderer to disable HTML forms.
|
||||
"""
|
||||
def show_form_for_method(self, *args, **kwargs):
|
||||
return False
|
||||
|
||||
|
||||
#
|
||||
# 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
30
netbox/utilities/validators.py
Normal file
30
netbox/utilities/validators.py
Normal 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()
|
||||
@@ -478,7 +478,7 @@ class BulkEditView(View):
|
||||
template_name = 'utilities/obj_bulk_edit.html'
|
||||
default_return_url = 'home'
|
||||
|
||||
def get(self):
|
||||
def get(self, request):
|
||||
return redirect(self.default_return_url)
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
@@ -626,6 +626,9 @@ class BulkDeleteView(View):
|
||||
template_name = 'utilities/obj_bulk_delete.html'
|
||||
default_return_url = 'home'
|
||||
|
||||
def get(self, request):
|
||||
return redirect(self.default_return_url)
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
|
||||
# Attempt to derive parent object if a parent class has been given
|
||||
|
||||
Reference in New Issue
Block a user