Compare commits

...

35 Commits

Author SHA1 Message Date
Jeremy Stretch
8eb140fd65 Merge pull request #736 from digitalocean/develop
Release v1.7.3
2016-12-08 12:34:53 -05:00
Jeremy Stretch
a68e82575f Release v1.7.3 2016-12-08 12:33:36 -05:00
Jeremy Stretch
5035a9567b Fixes #729: Corrected cancellation links when editing secondary objects 2016-12-08 12:20:45 -05:00
Jeremy Stretch
d5095362d7 Fixes #734: Corrected display of device type when editing a device 2016-12-08 09:59:21 -05:00
Jeremy Stretch
3a6d7a1f7f #733: Fixed MAC address device filter 2016-12-07 15:53:19 -05:00
Jeremy Stretch
cc6ae8ebe4 Merge pull request #733 from linuxsimba/remove_mac_addr_required_from_filter
FIX: filtering devices fails because mac address filter is a required
2016-12-07 15:42:50 -05:00
stanley karunditu
b4940a64be FIX: filtering devices fails because mac address filter is a required
field
2016-12-07 15:30:57 -05:00
Jeremy Stretch
fca812928e #724: Exempt API views from LoginRequiredMiddleware to enable basic HTTP authentication when LOGIN_REQUIRED is true 2016-12-07 15:14:22 -05:00
Jeremy Stretch
4a9b4c5387 Fixes #732: Allow custom select field values to be deselected if the field is not required 2016-12-07 14:00:52 -05:00
Jeremy Stretch
1f09f3d096 Merge pull request #728 from digitalocean/develop
Release v1.7.2-r1
2016-12-06 15:38:52 -05:00
Jeremy Stretch
efb95937fc Reverting GitHub test 2016-12-06 15:32:11 -05:00
Jeremy Stretch
ce7ee1771a Testing GitHub 2016-12-06 15:31:31 -05:00
Jeremy Stretch
da216e2c22 Fixes #727: Corrected error in rack elevation display 2016-12-06 15:27:35 -05:00
Jeremy Stretch
e58ee4e0e3 Post-release version bump 2016-12-06 14:55:45 -05:00
Jeremy Stretch
66be85a41f Merge pull request #726 from digitalocean/develop
Release v1.7.2
2016-12-06 14:55:19 -05:00
Jeremy Stretch
2171dcee7f Release v1.7.2 2016-12-06 14:53:43 -05:00
Jeremy Stretch
3262262a8a Closes #663: Added MAC address search field to device list 2016-12-06 14:45:01 -05:00
Jeremy Stretch
28b586aca7 Fixes #723: API documentation is now accessible when using BASE_PATH 2016-12-06 14:08:25 -05:00
Jeremy Stretch
f007b0dbde Closes #695: Added is_private field to RIR 2016-12-06 13:59:13 -05:00
Jeremy Stretch
6e5950be77 Fixes #720: Display user action links properly in admin UI 2016-12-06 13:16:42 -05:00
Jeremy Stretch
eb4cd0e723 Fixes #672: Expanded color selection for rack and device roles 2016-12-06 12:28:29 -05:00
Jeremy Stretch
300ee820fa #672: Cleaned up rack elevation CSS 2016-12-05 18:11:07 -05:00
Jeremy Stretch
7d6d7942d9 Rewrote get_connected_interface() to return first connection if more than one exist (erroneously) 2016-12-02 16:09:07 -05:00
Jeremy Stretch
05debf7e40 Updated CONTRIBUTING to discourage the use of "+1" comments on issues 2016-12-01 15:16:04 -05:00
Jeremy Stretch
dc88cb5ac7 Fixes #718: Restore is_primary field on IP assignment form 2016-12-01 14:54:20 -05:00
Jeremy Stretch
b275009544 Fixed missing on command block 2016-11-30 12:10:46 -05:00
Jeremy Stretch
d960481adb Ditched syntax highlighting for shell commands 2016-11-30 12:07:51 -05:00
Jeremy Stretch
2986840755 Specified syntax for code blocks 2016-11-30 12:01:45 -05:00
Jeremy Stretch
9b8bae501b Fixes #677: Add cffi as an explicit dependency to avoid setuptools error on Debian 2016-11-30 11:19:28 -05:00
Jeremy Stretch
9ea3383fde #702: Fix lingering Unicode incompatibility 2016-11-29 17:33:22 -05:00
Jeremy Stretch
77ac79f32c Fixes #713: Include a label for the comments field when editing circuits, providers, or racks in bulk 2016-11-29 17:29:56 -05:00
Jeremy Stretch
e31fae5ec5 Fixes #712: Corrected export of tenants which are not assigned to a group 2016-11-29 13:45:31 -05:00
Jeremy Stretch
8bff8bcbe2 Fixes #702: Improved Unicode support for custom fields 2016-11-29 13:34:22 -05:00
Jeremy Stretch
cc79b1136b Fixes #696: Corrected link to VRF in prefix and IP address breadcrumbs 2016-11-18 09:49:04 -05:00
Jeremy Stretch
1af9ea9e2d Post-release version bump 2016-11-15 12:36:17 -05:00
55 changed files with 449 additions and 283 deletions

View File

@@ -43,8 +43,9 @@ take some time for someone to address your issue.
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
requesting is already listed. (Be sure to search closed issues as well, since some feature requests are rejected.) If
the feature you'd like to see has already been requested, click "add a reaction" in the top right corner of the issue
and add a thumbs up (+1). This ensures that the issue has a better chance of making it onto the roadmap. Also feel free
to add a comment with any additional justification for the feature.
and add a thumbs up. This ensures that the issue has a better chance of making it onto the roadmap. Also feel free
to add a comment with any additional justification for the feature. (However, note that comments with no substance
other than a "+1" will be deleted as spam. Please use GitHub's reactions feature to indicate your support.)
* While suggestions for new features are welcome, it's important to limit the scope of NetBox's feature set to avoid
feature creep. For example, the following features would be firmly out of scope for NetBox:

View File

@@ -4,10 +4,10 @@ This guide demonstrates how to build and run NetBox as a Docker container. It as
To get NetBox up and running:
```
git clone -b master https://github.com/digitalocean/netbox.git
cd netbox
docker-compose up -d
```no-highlight
# git clone -b master https://github.com/digitalocean/netbox.git
# cd netbox
# docker-compose up -d
```
The application will be available on http://localhost/ after a few minutes.

View File

@@ -7,19 +7,19 @@ built-in Django users in the event of a failure.
On Ubuntu:
```
```no-highlight
sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev
```
On CentOS:
```
```no-highlight
sudo yum install -y python-devel openldap-devel
```
## Install django-auth-ldap
```
```no-highlight
sudo pip install django-auth-ldap
```

View File

@@ -2,13 +2,13 @@
**Debian/Ubuntu**
```
```no-highlight
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
```
**CentOS/RHEL**
```
```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
```
@@ -19,7 +19,7 @@ You may opt to install NetBox either from a numbered release or by cloning the m
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox`.
```
```no-highlight
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
# tar -xzf vX.Y.Z.tar.gz -C /opt
# cd /opt/
@@ -31,28 +31,27 @@ Download the [latest stable release](https://github.com/digitalocean/netbox/rele
Create the base directory for the NetBox installation. For this guide, we'll use `/opt/netbox`.
```
# mkdir -p /opt/netbox/
# cd /opt/netbox/
```no-highlight
# mkdir -p /opt/netbox/ && cd /opt/netbox/
```
If `git` is not already installed, install it:
**Debian/Ubuntu**
```
```no-highlight
# apt-get install -y git
```
**CentOS/RHEL**
```
```no-highlight
# yum install -y git
```
Next, clone the **master** branch of the NetBox GitHub repository into the current directory:
```
```no-highlight
# git clone -b master https://github.com/digitalocean/netbox.git .
Cloning into '.'...
remote: Counting objects: 1994, done.
@@ -67,7 +66,7 @@ Checking connectivity... done.
Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
```
```no-highlight
# pip install -r requirements.txt
```
@@ -75,7 +74,7 @@ Install the required Python packages using pip. (If you encounter any compilatio
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
```
```no-highlight
# cd netbox/netbox/
# cp configuration.example.py configuration.py
```
@@ -92,7 +91,7 @@ This is a list of the valid hostnames by which this server can be reached. You m
Example:
```
```python
ALLOWED_HOSTS = ['netbox.example.com', '192.0.2.123']
```
@@ -102,7 +101,7 @@ This parameter holds the database configuration details. You must define the use
Example:
```
```python
DATABASE = {
'NAME': 'netbox', # Database name
'USER': 'netbox', # PostgreSQL username
@@ -125,7 +124,7 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
```
```no-highlight
# cd /opt/netbox/netbox/
# ./manage.py migrate
Operations to perform:
@@ -144,7 +143,7 @@ If this step results in a PostgreSQL authentication error, ensure that the usern
NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox:
```
```no-highlight
# ./manage.py createsuperuser
Username: admin
Email address: admin@example.com
@@ -155,7 +154,7 @@ Superuser created successfully.
# Collect Static Files
```
```no-highlight
# ./manage.py collectstatic
You have requested to collect static files at the destination
@@ -176,7 +175,7 @@ NetBox ships with some initial data to help you get started: RIR definitions, co
!!! note
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
```
```no-highlight
# ./manage.py loaddata initial_data
Installed 43 object(s) from 4 fixture(s)
```
@@ -185,7 +184,7 @@ Installed 43 object(s) from 4 fixture(s)
At this point, NetBox should be able to run. We can verify this by starting a development instance:
```
```no-highlight
# ./manage.py runserver 0.0.0.0:8000 --insecure
Performing system checks...

View File

@@ -4,27 +4,27 @@ NetBox requires a PostgreSQL database to store data. MySQL is not supported, as
**Debian/Ubuntu**
```
```no-highlight
# apt-get install -y postgresql libpq-dev python-psycopg2
```
**CentOS/RHEL**
```
```no-highlight
# yum install -y postgresql postgresql-server postgresql-devel python-psycopg2
# postgresql-setup initdb
```
If using CentOS, modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/data/pg_hba.conf`. For example:
```
```no-highlight
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
```
Then, start the service:
```
```no-highlight
# systemctl start postgresql
```
@@ -35,7 +35,7 @@ At a minimum, we need to create a database for NetBox and assign it a username a
!!! danger
DO NOT USE THE PASSWORD FROM THE EXAMPLE.
```
```no-highlight
# sudo -u postgres psql
psql (9.3.13)
Type "help" for help.
@@ -51,7 +51,7 @@ postgres=# \q
You can verify that authentication works issuing the following command and providing the configured password:
```
```no-highlight
# psql -U netbox -h localhost -W
```

View File

@@ -8,7 +8,7 @@ Download the [latest stable release](https://github.com/digitalocean/netbox/rele
Download and extract the latest version:
```
```no-highlight
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
# tar -xzf vX.Y.Z.tar.gz -C /opt
# cd /opt/
@@ -17,13 +17,13 @@ Download and extract the latest version:
Copy the 'configuration.py' you created when first installing to the new version:
```
```no-highlight
# cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py
```
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
```
```no-highlight
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
```
@@ -31,7 +31,7 @@ If you followed the original installation guide to set up gunicorn, be sure to c
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
```
```no-highlight
# cd /opt/netbox
# git checkout master
# git pull origin master
@@ -42,7 +42,7 @@ This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most
Once the new code is in place, run the upgrade script (which may need to be run as root depending on how your environment is configured).
```
```no-highlight
# ./upgrade.sh
```
@@ -56,6 +56,6 @@ This script:
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:
```
```no-highlight
# sudo supervisorctl restart netbox
```

View File

@@ -5,7 +5,7 @@ We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for
!!! info
Only Debian/Ubuntu instructions are provided here, but the installation process for CentOS/RHEL does not differ much. Please consult the documentation for those distributions for details.
```
```no-highlight
# apt-get install -y gunicorn supervisor
```
@@ -13,13 +13,13 @@ We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for
The following will serve as a minimal nginx configuration. Be sure to modify your server name and installation path appropriately.
```
```no-highlight
# apt-get install -y nginx
```
Once nginx is installed, save the following configuration to `/etc/nginx/sites-available/netbox`. Be sure to replace `netbox.example.com` with the domain name or IP address of your installation. (This should match the value configured for `ALLOWED_HOSTS` in `configuration.py`.)
```
```nginx
server {
listen 80;
@@ -43,7 +43,7 @@ server {
Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created.
```
```no-highlight
# cd /etc/nginx/sites-enabled/
# rm default
# ln -s /etc/nginx/sites-available/netbox
@@ -51,7 +51,7 @@ Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sit
Restart the nginx service to use the new configuration.
```
```no-highlight
# service nginx restart
```
@@ -59,13 +59,13 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
## Option B: Apache
```
```no-highlight
# apt-get install -y apache2
```
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
```
```apache
<VirtualHost *:80>
ProxyPreserveHost On
@@ -90,7 +90,7 @@ Once Apache is installed, proceed with the following configuration (Be sure to m
Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf`, enable the `proxy` and `proxy_http` modules, and reload Apache:
```
```no-highlight
# a2enmod proxy
# a2enmod proxy_http
# a2ensite netbox
@@ -103,7 +103,7 @@ To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https
Save the following configuration file in the root netbox installation path (in this example, `/opt/netbox/`) as `gunicorn_config.py`. Be sure to verify the location of the gunicorn executable (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
command = '/usr/bin/gunicorn'
pythonpath = '/opt/netbox/netbox'
bind = '127.0.0.1:8001'
@@ -115,7 +115,7 @@ user = 'www-data'
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed.
```
```no-highlight
[program:netbox]
command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi
directory = /opt/netbox/netbox/
@@ -124,7 +124,7 @@ user = www-data
Then, restart the supervisor service to detect and run the gunicorn service:
```
```no-highlight
# service supervisor restart
```

View File

@@ -54,7 +54,7 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
portal_url = forms.URLField(required=False, label='Portal')
noc_contact = forms.CharField(required=False, widget=SmallTextarea, label='NOC contact')
admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
comments = CommentField()
comments = CommentField(widget=SmallTextarea)
class Meta:
nullable_fields = ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
@@ -183,7 +183,7 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
comments = CommentField()
comments = CommentField(widget=SmallTextarea)
class Meta:
nullable_fields = ['tenant', 'port_speed', 'commit_rate', 'comments']

View File

@@ -42,7 +42,7 @@ class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
model = Provider
form_class = forms.ProviderForm
template_name = 'circuits/provider_edit.html'
cancel_url = 'circuits:provider_list'
obj_list_url = 'circuits:provider_list'
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -88,8 +88,8 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuittype'
model = CircuitType
form_class = forms.CircuitTypeForm
success_url = 'circuits:circuittype_list'
cancel_url = 'circuits:circuittype_list'
obj_list_url = 'circuits:circuittype_list'
use_obj_view = False
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -126,7 +126,7 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.CircuitForm
fields_initial = ['site']
template_name = 'circuits/circuit_edit.html'
cancel_url = 'circuits:circuit_list'
obj_list_url = 'circuits:circuit_list'
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):

View File

@@ -183,10 +183,14 @@ class DeviceAdmin(admin.ModelAdmin):
DeviceBayAdmin,
ModuleAdmin,
]
list_display = ['display_name', 'device_type', 'device_role', 'primary_ip', 'rack', 'position', 'asset_tag',
list_display = ['display_name', 'device_type_full_name', 'device_role', 'primary_ip', 'rack', 'position', 'asset_tag',
'serial']
list_filter = ['device_role']
def get_queryset(self, request):
qs = super(DeviceAdmin, self).get_queryset(request)
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip4', 'primary_ip6', 'rack')
def device_type_full_name(self, obj):
return obj.device_type.full_name
device_type_full_name.short_description = 'Device type'

View File

@@ -1,4 +1,5 @@
import django_filters
from netaddr.core import AddrFormatError
from django.db.models import Q
@@ -146,6 +147,10 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
action='search',
label='Search',
)
mac_address = django_filters.MethodFilter(
action='_mac_address',
label='MAC address',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='rack__site',
queryset=Site.objects.all(),
@@ -254,6 +259,15 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(comments__icontains=value)
).distinct()
def _mac_address(self, queryset, value):
value = value.strip()
if not value:
return queryset
try:
return queryset.filter(interfaces__mac_address=value).distinct()
except AddrFormatError:
return queryset.none()
class ConsolePortFilter(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter(

View File

@@ -5,7 +5,7 @@
"fields": {
"name": "Console Server",
"slug": "console-server",
"color": "teal"
"color": "009688"
}
},
{
@@ -14,7 +14,7 @@
"fields": {
"name": "Core Switch",
"slug": "core-switch",
"color": "blue"
"color": "2196f3"
}
},
{
@@ -23,7 +23,7 @@
"fields": {
"name": "Distribution Switch",
"slug": "distribution-switch",
"color": "blue"
"color": "2196f3"
}
},
{
@@ -32,7 +32,7 @@
"fields": {
"name": "Access Switch",
"slug": "access-switch",
"color": "blue"
"color": "2196f3"
}
},
{
@@ -41,7 +41,7 @@
"fields": {
"name": "Management Switch",
"slug": "management-switch",
"color": "orange"
"color": "ff9800"
}
},
{
@@ -50,7 +50,7 @@
"fields": {
"name": "Firewall",
"slug": "firewall",
"color": "red"
"color": "f44336"
}
},
{
@@ -59,7 +59,7 @@
"fields": {
"name": "Router",
"slug": "router",
"color": "purple"
"color": "9c27b0"
}
},
{
@@ -68,7 +68,7 @@
"fields": {
"name": "Server",
"slug": "server",
"color": "medium_gray"
"color": "9e9e9e"
}
},
{
@@ -77,7 +77,7 @@
"fields": {
"name": "PDU",
"slug": "pdu",
"color": "dark_gray"
"color": "607d8b"
}
},
{

View File

@@ -221,7 +221,7 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
u_height = forms.IntegerField(required=False, label='Height (U)')
comments = CommentField()
comments = CommentField(widget=SmallTextarea)
class Meta:
nullable_fields = ['group', 'tenant', 'role', 'comments']
@@ -612,6 +612,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
platform = FilterChoiceField(queryset=Platform.objects.annotate(filter_count=Count('devices')),
to_field_name='slug', null_option=(0, 'None'))
status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES))
mac_address = forms.CharField(required=False, label='MAC address')
#

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-12-06 16:35
from __future__ import unicode_literals
from django.db import migrations
import utilities.fields
COLOR_CONVERSION = {
'teal': '009688',
'green': '4caf50',
'blue': '2196f3',
'purple': '9c27b0',
'yellow': 'ffeb3b',
'orange': 'ff9800',
'red': 'f44336',
'light_gray': 'c0c0c0',
'medium_gray': '9e9e9e',
'dark_gray': '607d8b',
}
def color_names_to_rgb(apps, schema_editor):
RackRole = apps.get_model('dcim', 'RackRole')
DeviceRole = apps.get_model('dcim', 'DeviceRole')
for color_name, color_rgb in COLOR_CONVERSION.items():
RackRole.objects.filter(color=color_name).update(color=color_rgb)
DeviceRole.objects.filter(color=color_name).update(color=color_rgb)
def color_rgb_to_name(apps, schema_editor):
RackRole = apps.get_model('dcim', 'RackRole')
DeviceRole = apps.get_model('dcim', 'DeviceRole')
for color_name, color_rgb in COLOR_CONVERSION.items():
RackRole.objects.filter(color=color_rgb).update(color=color_name)
DeviceRole.objects.filter(color=color_rgb).update(color=color_name)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0021_add_ff_flexstack'),
]
operations = [
migrations.RunPython(color_names_to_rgb, color_rgb_to_name),
migrations.AlterField(
model_name='devicerole',
name='color',
field=utilities.fields.ColorField(max_length=6),
),
migrations.AlterField(
model_name='rackrole',
name='color',
field=utilities.fields.ColorField(max_length=6),
),
]

View File

@@ -3,7 +3,7 @@ from collections import OrderedDict
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import MultipleObjectsReturned, ValidationError
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@@ -12,7 +12,7 @@ from django.db.models import Count, Q, ObjectDoesNotExist
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
from extras.rpc import RPC_CLIENTS
from tenancy.models import Tenant
from utilities.fields import NullableCharField
from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel
@@ -54,29 +54,6 @@ SUBDEVICE_ROLE_CHOICES = (
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
COLOR_TEAL = 'teal'
COLOR_GREEN = 'green'
COLOR_BLUE = 'blue'
COLOR_PURPLE = 'purple'
COLOR_YELLOW = 'yellow'
COLOR_ORANGE = 'orange'
COLOR_RED = 'red'
COLOR_GRAY1 = 'light_gray'
COLOR_GRAY2 = 'medium_gray'
COLOR_GRAY3 = 'dark_gray'
ROLE_COLOR_CHOICES = [
[COLOR_TEAL, 'Teal'],
[COLOR_GREEN, 'Green'],
[COLOR_BLUE, 'Blue'],
[COLOR_PURPLE, 'Purple'],
[COLOR_YELLOW, 'Yellow'],
[COLOR_ORANGE, 'Orange'],
[COLOR_RED, 'Red'],
[COLOR_GRAY1, 'Light Gray'],
[COLOR_GRAY2, 'Medium Gray'],
[COLOR_GRAY3, 'Dark Gray'],
]
# Virtual
IFACE_FF_VIRTUAL = 0
# Ethernet
@@ -345,7 +322,7 @@ class RackRole(models.Model):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)
color = ColorField()
class Meta:
ordering = ['name']
@@ -584,7 +561,7 @@ class DeviceType(models.Model):
]
def __unicode__(self):
return u'{} {}'.format(self.manufacturer, self.model)
return self.model
def __init__(self, *args, **kwargs):
super(DeviceType, self).__init__(*args, **kwargs)
@@ -640,6 +617,10 @@ class DeviceType(models.Model):
'u_height': "Child device types must be 0U."
})
@property
def full_name(self):
return u'{} {}'.format(self.manufacturer.name, self.model)
@property
def is_parent_device(self):
return bool(self.subdevice_role)
@@ -761,7 +742,7 @@ class DeviceRole(models.Model):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)
color = ColorField()
class Meta:
ordering = ['name']
@@ -1173,16 +1154,13 @@ class Interface(models.Model):
return None
def get_connected_interface(self):
try:
connection = InterfaceConnection.objects.select_related().get(Q(interface_a=self) | Q(interface_b=self))
if connection.interface_a == self:
return connection.interface_b
else:
return connection.interface_a
except InterfaceConnection.DoesNotExist:
return None
except InterfaceConnection.MultipleObjectsReturned:
raise MultipleObjectsReturned("Multiple connections found for {} interface {}!".format(self.device, self))
connection = InterfaceConnection.objects.select_related().filter(Q(interface_a=self) | Q(interface_b=self))\
.first()
if connection and connection.interface_a == self:
return connection.interface_b
elif connection:
return connection.interface_a
return None
class InterfaceConnection(models.Model):

View File

@@ -11,7 +11,7 @@ from .models import (
COLOR_LABEL = """
<label class="label {{ record.color }}">{{ record }}</label>
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
"""
DEVICE_LINK = """
@@ -34,7 +34,7 @@ RACKROLE_ACTIONS = """
RACK_ROLE = """
{% if record.role %}
<label class="label {{ record.role.color }}">{{ value }}</label>
<label class="label" style="background-color: #{{ record.role.color }}">{{ value }}</label>
{% else %}
&mdash;
{% endif %}
@@ -59,7 +59,7 @@ PLATFORM_ACTIONS = """
"""
DEVICE_ROLE = """
<label class="label {{ record.device_role.color }}">{{ value }}</label>
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
"""
STATUS_ICON = """
@@ -294,7 +294,8 @@ class PlatformTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = Platform
@@ -313,7 +314,8 @@ class DeviceTable(BaseTable):
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
device_type = tables.Column(verbose_name='Type')
device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
text=lambda record: record.device_type.full_name)
primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address',
template_code="{{ record.primary_ip.address.ip }}")

View File

@@ -8,7 +8,6 @@ from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Count
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
@@ -99,7 +98,7 @@ class SiteEditView(PermissionRequiredMixin, ObjectEditView):
model = Site
form_class = forms.SiteForm
template_name = 'dcim/site_edit.html'
cancel_url = 'dcim:site_list'
obj_list_url = 'dcim:site_list'
class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -141,8 +140,8 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackgroup'
model = RackGroup
form_class = forms.RackGroupForm
success_url = 'dcim:rackgroup_list'
cancel_url = 'dcim:rackgroup_list'
obj_list_url = 'dcim:rackgroup_list'
use_obj_view = False
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -166,8 +165,8 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackrole'
model = RackRole
form_class = forms.RackRoleForm
success_url = 'dcim:rackrole_list'
cancel_url = 'dcim:rackrole_list'
obj_list_url = 'dcim:rackrole_list'
use_obj_view = False
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -214,7 +213,7 @@ class RackEditView(PermissionRequiredMixin, ObjectEditView):
model = Rack
form_class = forms.RackForm
template_name = 'dcim/rack_edit.html'
cancel_url = 'dcim:rack_list'
obj_list_url = 'dcim:rack_list'
class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -260,8 +259,8 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_manufacturer'
model = Manufacturer
form_class = forms.ManufacturerForm
success_url = 'dcim:manufacturer_list'
cancel_url = 'dcim:manufacturer_list'
obj_list_url = 'dcim:manufacturer_list'
use_obj_view = False
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -332,7 +331,7 @@ class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_devicetype'
model = DeviceType
form_class = forms.DeviceTypeForm
cancel_url = 'dcim:devicetype_list'
obj_list_url = 'dcim:devicetype_list'
class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -497,8 +496,8 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_devicerole'
model = DeviceRole
form_class = forms.DeviceRoleForm
success_url = 'dcim:devicerole_list'
cancel_url = 'dcim:devicerole_list'
obj_list_url = 'dcim:devicerole_list'
use_obj_view = False
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -522,8 +521,8 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_platform'
model = Platform
form_class = forms.PlatformForm
success_url = 'dcim:platform_list'
cancel_url = 'dcim:platform_list'
obj_list_url = 'dcim:platform_list'
use_obj_view = False
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -617,7 +616,7 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.DeviceForm
fields_initial = ['site', 'rack', 'position', 'face', 'device_bay']
template_name = 'dcim/device_edit.html'
cancel_url = 'dcim:device_list'
obj_list_url = 'dcim:device_list'
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):

View File

@@ -1,5 +1,6 @@
from django import forms
from django.contrib import admin
from django.utils.safestring import mark_safe
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction
@@ -54,4 +55,7 @@ class TopologyMapAdmin(admin.ModelAdmin):
@admin.register(UserAction)
class UserActionAdmin(admin.ModelAdmin):
actions = None
list_display = ['user', 'action', 'content_type', 'object_id', 'message']
list_display = ['user', 'action', 'content_type', 'object_id', '_message']
def _message(self, obj):
return mark_safe(obj.message)

View File

@@ -49,7 +49,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
# Select
elif cf.type == CF_TYPE_SELECT:
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
if bulk_edit or filterable_only:
if not cf.required or bulk_edit or filterable_only:
choices = [(None, '---------')] + choices
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)

View File

@@ -130,7 +130,7 @@ class CustomField(models.Model):
if self.type == CF_TYPE_SELECT:
# Could be ModelChoiceField or TypedChoiceField
return str(value.id) if hasattr(value, 'id') else str(value)
return str(value)
return value
def deserialize_value(self, serialized_value):
"""
@@ -165,7 +165,7 @@ class CustomFieldValue(models.Model):
unique_together = ['field', 'obj_type', 'obj_id']
def __unicode__(self):
return '{} {}'.format(self.obj, self.field)
return u'{} {}'.format(self.obj, self.field)
@property
def value(self):

View File

@@ -28,7 +28,7 @@ class RIRAdmin(admin.ModelAdmin):
prepopulated_fields = {
'slug': ['name'],
}
list_display = ['name', 'slug']
list_display = ['name', 'slug', 'is_private']
@admin.register(Aggregate)

View File

@@ -58,13 +58,13 @@ class RIRSerializer(serializers.ModelSerializer):
class Meta:
model = RIR
fields = ['id', 'name', 'slug']
fields = ['id', 'name', 'slug', 'is_private']
class RIRNestedSerializer(RIRSerializer):
class Meta(RIRSerializer.Meta):
pass
fields = ['id', 'name', 'slug']
#

View File

@@ -46,6 +46,13 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
fields = ['name', 'rd']
class RIRFilter(django_filters.FilterSet):
class Meta:
model = RIR
fields = ['is_private']
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',

View File

@@ -43,7 +43,8 @@
"pk": 1,
"fields": {
"name": "ARIN",
"slug": "arin"
"slug": "arin",
"is_private": false
}
},
{
@@ -51,7 +52,8 @@
"pk": 2,
"fields": {
"name": "RIPE",
"slug": "ripe"
"slug": "ripe",
"is_private": false
}
},
{
@@ -59,7 +61,8 @@
"pk": 3,
"fields": {
"name": "APNIC",
"slug": "apnic"
"slug": "apnic",
"is_private": false
}
},
{
@@ -67,7 +70,8 @@
"pk": 4,
"fields": {
"name": "LACNIC",
"slug": "lacnic"
"slug": "lacnic",
"is_private": false
}
},
{
@@ -75,7 +79,8 @@
"pk": 5,
"fields": {
"name": "AFRINIC",
"slug": "afrinic"
"slug": "afrinic",
"is_private": false
}
},
{
@@ -83,7 +88,8 @@
"pk": 6,
"fields": {
"name": "RFC 1918",
"slug": "rfc-1918"
"slug": "rfc-1918",
"is_private": true
}
},
{

View File

@@ -75,7 +75,15 @@ class RIRForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = RIR
fields = ['name', 'slug']
fields = ['name', 'slug', 'is_private']
class RIRFilterForm(forms.Form, BootstrapMixin):
is_private = forms.NullBooleanField(required=False, label='Private', widget=forms.Select(choices=[
('', '---------'),
('True', 'Yes'),
('False', 'No'),
]))
#

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-12-06 18:27
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0010_ipaddress_help_texts'),
]
operations = [
migrations.AddField(
model_name='rir',
name='is_private',
field=models.BooleanField(default=False, help_text=b'IP space managed by this RIR is considered private', verbose_name=b'Private'),
),
]

View File

@@ -103,6 +103,8 @@ class RIR(models.Model):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
is_private = models.BooleanField(default=False, verbose_name='Private',
help_text='IP space managed by this RIR is considered private')
class Meta:
ordering = ['name']

View File

@@ -126,6 +126,7 @@ class VRFTable(BaseTable):
class RIRTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
is_private = tables.BooleanColumn(verbose_name='Private')
aggregate_count = tables.Column(verbose_name='Aggregates')
stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
footer=lambda table: sum(r.stats['total'] for r in table.data))
@@ -142,7 +143,8 @@ class RIRTable(BaseTable):
class Meta(BaseTable.Meta):
model = RIR
fields = ('pk', 'name', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved', 'stats_deprecated', 'stats_available', 'utilization', 'actions')
fields = ('pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
'stats_deprecated', 'stats_available', 'utilization', 'actions')
#

View File

@@ -1,4 +1,3 @@
from collections import OrderedDict
from django_tables2 import RequestConfig
import netaddr
@@ -117,7 +116,7 @@ class VRFEditView(PermissionRequiredMixin, ObjectEditView):
model = VRF
form_class = forms.VRFForm
template_name = 'ipam/vrf_edit.html'
cancel_url = 'ipam:vrf_list'
obj_list_url = 'ipam:vrf_list'
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -154,6 +153,8 @@ class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class RIRListView(ObjectListView):
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
filter = filters.RIRFilter
filter_form = forms.RIRFilterForm
table = tables.RIRTable
edit_permissions = ['ipam.change_rir', 'ipam.delete_rir']
template_name = 'ipam/rir_list.html'
@@ -239,8 +240,8 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_rir'
model = RIR
form_class = forms.RIRForm
success_url = 'ipam:rir_list'
cancel_url = 'ipam:rir_list'
obj_list_url = 'ipam:rir_list'
use_obj_view = False
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -305,7 +306,7 @@ class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
model = Aggregate
form_class = forms.AggregateForm
template_name = 'ipam/aggregate_edit.html'
cancel_url = 'ipam:aggregate_list'
obj_list_url = 'ipam:aggregate_list'
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -351,8 +352,8 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_role'
model = Role
form_class = forms.RoleForm
success_url = 'ipam:role_list'
cancel_url = 'ipam:role_list'
obj_list_url = 'ipam:role_list'
use_obj_view = False
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -436,7 +437,7 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.PrefixForm
template_name = 'ipam/prefix_edit.html'
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
cancel_url = 'ipam:prefix_list'
obj_list_url = 'ipam:prefix_list'
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -600,7 +601,7 @@ class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.IPAddressForm
fields_initial = ['address', 'vrf']
template_name = 'ipam/ipaddress_edit.html'
cancel_url = 'ipam:ipaddress_list'
obj_list_url = 'ipam:ipaddress_list'
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -663,8 +664,8 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlangroup'
model = VLANGroup
form_class = forms.VLANGroupForm
success_url = 'ipam:vlangroup_list'
cancel_url = 'ipam:vlangroup_list'
obj_list_url = 'ipam:vlangroup_list'
use_obj_view = False
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -703,7 +704,7 @@ class VLANEditView(PermissionRequiredMixin, ObjectEditView):
model = VLAN
form_class = forms.VLANForm
template_name = 'ipam/vlan_edit.html'
cancel_url = 'ipam:vlan_list'
obj_list_url = 'ipam:vlan_list'
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):

View File

@@ -12,7 +12,7 @@ except ImportError:
"the documentation.")
VERSION = '1.7.1'
VERSION = '1.7.3'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@@ -185,10 +185,12 @@ SECRETS_MIN_PUBKEY_SIZE = 2048
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',)
}
if LOGIN_REQUIRED:
REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] = ('rest_framework.permissions.IsAuthenticated',)
# Swagger settings (API docs)
SWAGGER_SETTINGS = {
'base_path': '{}/api/docs'.format(ALLOWED_HOSTS[0]),
'base_path': '{}/{}api/docs'.format(ALLOWED_HOSTS[0], BASE_PATH),
}

View File

@@ -98,7 +98,7 @@ nav ul.pagination {
div.rack_header {
margin-left: 36px;
text-align: center;
width: 200px;
width: 230px;
}
ul.rack_legend {
float: left;
@@ -126,29 +126,16 @@ ul.rack {
list-style-type: none;
padding: 0;
position: absolute;
width: 200px;
width: 230px;
}
ul.rack li {
border-top: 1px solid #e0e0e0;
display: block;
font-size: 13px;
height: 20px;
overflow: hidden;
text-align: center;
}
ul.rack_empty li {
background-color: #f7f7f7;
border-bottom: 1px solid #dddddd;
height: 20px;
}
ul.rack li.empty:last-child {
border-bottom: 0;
}
ul.rack_far_face {
z-index: 100;
}
ul.rack_near_face {
z-index: 200;
}
ul.rack li.h2u { height: 40px; }
ul.rack li.h2u a, ul.rack li.h2u span { padding: 10px 0; }
ul.rack li.h3u { height: 60px; }
@@ -247,22 +234,9 @@ ul.rack li.h49u { height: 980px; }
ul.rack li.h49u a, ul.rack li.h49u span { padding: 480px 0; }
ul.rack li.h50u { height: 1000px; }
ul.rack li.h50u a, ul.rack li.h50u span { padding: 490px 0; }
ul.rack li.occupied a {
color: #ffffff;
display: block;
font-weight: bold;
}
ul.rack li.occupied a:hover {
text-decoration: none;
}
ul.rack li.occupied span {
display: block;
}
ul.rack_near_face li.empty {
border-bottom: 1px solid #e0e0e0;
}
ul.rack_near_face li.occupied {
color: #474747;
ul.rack_far_face {
background-color: #f7f7f7;
z-index: 100;
}
ul.rack_far_face li.occupied {
background: repeating-linear-gradient(
@@ -272,7 +246,6 @@ ul.rack_far_face li.occupied {
#f0f0f0 7px,
#f0f0f0 14px
);
color: #303030;
}
ul.rack_far_face li.blocked {
background: repeating-linear-gradient(
@@ -282,54 +255,46 @@ ul.rack_far_face li.blocked {
#ffc7c7 7px,
#ffc7c7 14px
);
border-bottom: 1px solid #e0e0e0;
color: #303030;
}
ul.rack_near_face li.empty a {
ul.rack_near_face {
z-index: 200;
}
ul.rack_near_face li.occupied {
border-top: 1px solid #474747;
color: #474747;
}
ul.rack_near_face li.occupied:hover {
background-image: url('../img/tint_20.png');
}
ul.rack_near_face li:first-child {
border-top: 0;
}
ul.rack_near_face li.available a {
color: #0000ff;
display: none;
text-decoration: none;
}
ul.rack_near_face li.empty:hover {
ul.rack_near_face li.available:hover {
background-color: #ffffff;
}
ul.rack_near_face li.empty:hover a {
ul.rack_near_face li.available:hover a {
display: block;
}
/* Colors (from http://flatuicolors.com) */
.teal { background-color: #1abc9c; }
.green { background-color: #2ecc71; }
.blue { background-color: #3498db; }
.purple { background-color: #9b59b6; }
.yellow { background-color: #f1c40f; }
.orange { background-color: #e67e22; }
.red { background-color: #e74c3c; }
.light_gray { background-color: #dce2e3; }
.medium_gray { background-color: #95a5a6; }
.dark_gray { background-color: #34495e; }
/* Rack elevation coloring */
ul.rack .teal { border-bottom: 1px solid #16a085; }
ul.rack .teal:hover { background-color: #16a085; }
ul.rack .green { border-bottom: 1px solid #27ae60; }
ul.rack .green:hover { background-color: #27ae60; }
ul.rack .blue { border-bottom: 1px solid #2980b9; }
ul.rack .blue:hover { background-color: #2980b9; }
ul.rack .purple { border-bottom: 1px solid #8e44ad; }
ul.rack .purple:hover { background-color: #8e44ad; }
ul.rack .yellow { border-bottom: 1px solid #f39c12; }
ul.rack .yellow:hover { background-color: #f39c12; }
ul.rack .orange { border-bottom: 1px solid #d35400; }
ul.rack .orange:hover { background-color: #d35400; }
ul.rack .red { border-bottom: 1px solid #c0392b; }
ul.rack .red:hover { background-color: #c0392b; }
ul.rack .light_gray { border-bottom: 1px solid #bdc3c7; }
ul.rack .light_gray:hover { background-color: #bdc3c7; }
ul.rack .medium_gray { border-bottom: 1px solid #7f8c8d; }
ul.rack .medium_gray:hover { background-color: #7f8c8d; }
ul.rack .dark_gray { border-bottom: 1px solid #2c3e50; }
ul.rack .dark_gray:hover { background-color: #2c3e50; }
ul.rack li.occupied a {
color: #ffffff;
display: block;
font-weight: bold;
}
ul.rack li.occupied a:hover {
text-decoration: none;
}
ul.rack li.occupied span {
cursor: default;
display: block;
}
li.occupied + li.available {
border-top: 1px solid #474747;
}
/* Misc */
.banner-bottom {

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

View File

@@ -30,8 +30,8 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'secrets.change_secretrole'
model = SecretRole
form_class = forms.SecretRoleForm
success_url = 'secrets:secretrole_list'
cancel_url = 'secrets:secretrole_list'
obj_list_url = 'secrets:secretrole_list'
use_obj_view = False
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):

View File

@@ -56,7 +56,7 @@
<tr>
<td>Device Type</td>
<td>
<span><a href="{% url 'dcim:devicetype' pk=device.device_type.pk %}">{{ device.device_type }}</a> ({{ device.device_type.u_height }}U)</span>
<span><a href="{% url 'dcim:devicetype' pk=device.device_type.pk %}">{{ device.device_type.full_name }}</a> ({{ device.device_type.u_height }}U)</span>
</td>
</tr>
<tr>
@@ -293,7 +293,7 @@
<td>
<a href="{% url 'dcim:rack' pk=rd.rack.pk %}">Rack {{ rd.rack }}</a>
</td>
<td>{{ rd.device_type }}</td>
<td>{{ rd.device_type.full_name }}</td>
</tr>
{% endfor %}
</table>

View File

@@ -24,7 +24,7 @@
{% for device in selected_devices %}
<tr>
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
<td>{{ device.device_type }}</td>
<td>{{ device.device_type.full_name }}</td>
<td>{{ device.device_role }}</td>
</tr>
{% endfor %}

View File

@@ -14,7 +14,7 @@
{% for device in selected_objects %}
<tr>
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
<td>{{ device.device_type }}</td>
<td>{{ device.device_type.full_name }}</td>
<td>{{ device.device_role }}</td>
<td>{{ device.tenant }}</td>
<td>{{ device.serial }}</td>

View File

@@ -13,7 +13,7 @@
<table class="table table-hover panel-body">
<tr>
<td>Model</td>
<td>{{ device.device_type }}</td>
<td>{{ device.device_type.full_name }}</td>
</tr>
<tr>
<td>Serial Number</td>

View File

@@ -2,7 +2,7 @@
{% load helpers %}
{% load render_table from django_tables2 %}
{% block title %}{{ devicetype }}{% endblock %}
{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
{% block content %}
<div class="row">
@@ -32,7 +32,7 @@
</div>
{% endif %}
<h1>{{ devicetype }}</h1>
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">

View File

@@ -12,7 +12,7 @@
<a href="{% url 'dcim:device' pk=devicebay.installed_device.pk %}">{{ devicebay.installed_device }}</a>
</td>
<td>
<span>{{ devicebay.installed_device.device_type }}</span>
<span>{{ devicebay.installed_device.device_type.full_name }}</span>
</td>
{% else %}
<td colspan="2">

View File

@@ -6,13 +6,6 @@
<div class="rack_frame">
<!-- Render all slots empty -->
<ul class="rack rack_empty">
{% for u in rack.units %}
<li></li>
{% endfor %}
</ul>
<!-- Render rear view of devices on far face -->
<ul class="rack rack_far_face">
{% for u in secondary_face %}
@@ -28,10 +21,10 @@
<ul class="rack rack_near_face">
{% for u in primary_face %}
{% if u.device %}
<li class="occupied h{{ u.device.device_type.u_height }}u{% ifequal u.device.face face_id %} {{ u.device.device_role.color }}{% endifequal %}">
<li class="occupied h{{ u.device.device_type.u_height }}u"{% ifequal u.device.face face_id %} style="background-color: #{{ u.device.device_role.color }}"{% endifequal %}>
{% ifequal u.device.face face_id %}
<a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type }} ({{ u.device.device_type.u_height }}U)">
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.full_name }} ({{ u.device.device_type.u_height }}U)">
{{ u.device.name|default:u.device.device_role }}
{% if u.device.devicebay_count %}
({{ u.device.get_children.count }}/{{ u.device.devicebay_count }})
@@ -42,7 +35,7 @@
{% endifequal %}
</li>
{% else %}
<li class="empty">
<li class="available">
{% if perms.dcim.add_device %}
<a href="{% url 'dcim:device_add' %}?site={{ rack.site.pk }}&rack={{ rack.pk }}&face={{ face_id }}&position={{ u.id }}" class="add_device" >add device</a>
{% endif %}

View File

@@ -1,7 +1,7 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% block title %}Assign an IP Address{% endblock %}
{% block title %}Assign a New IP Address{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
@@ -40,6 +40,7 @@
</div>
</div>
{% render_field form.interface %}
{% render_field form.set_as_primary %}
</div>
</div>
<div class="panel panel-default">

View File

@@ -153,8 +153,14 @@
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device.name }}</a>
</td>
<td>{{ device.device_role }}</td>
<td>{{ device.device_type }}</td>
<td>{% if device.parent_bay %}<a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay }}</a>{% endif %}</td>
<td>{{ device.device_type.full_name }}</td>
<td>
{% if device.parent_bay %}
<a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay }}</a>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>

View File

@@ -3,7 +3,7 @@
<ol class="breadcrumb">
<li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
{% if prefix.vrf %}
<li><a href="{% url 'ipam:prefix_list' %}?vrf={{ prefix.vrf.pk }}">{{ prefix.vrf }}</a></li>
<li><a href="{% url 'ipam:vrf' pk=prefix.vrf.pk %}">{{ prefix.vrf }}</a></li>
{% endif %}
<li>{{ prefix }}</li>
</ol>

View File

@@ -9,7 +9,7 @@
<ol class="breadcrumb">
<li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
{% if ipaddress.vrf %}
<li><a href="{% url 'ipam:ipaddress_list' %}?vrf={{ ipaddress.vrf.pk }}">{{ ipaddress.vrf }}</a></li>
<li><a href="{% url 'ipam:vrf' pk=ipaddress.vrf.pk %}">{{ ipaddress.vrf }}</a></li>
{% endif %}
<li>{{ ipaddress }}</li>
</ol>

View File

@@ -2,7 +2,7 @@
{% load static from staticfiles %}
{% load form_helpers %}
{% block title %}Assign IP Address{% endblock %}
{% block title %}Assign an IP Address{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
@@ -19,9 +19,25 @@
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Assign IP Address {{ ipaddress }} ({% if ipaddress.vrf %}VRF {{ ipaddress.vrf }}{% else %}Global Table{% endif %})</strong>
<strong>Assign an IP Address</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>

View File

@@ -12,7 +12,7 @@
IPv4 Stats
</a>
{% else %}
<a href="{% url 'ipam:rir_list' %}?family=6" class="btn btn-default">
<a href="{% url 'ipam:rir_list' %}?family=6{% if request.GET %}&{{ request.GET.urlencode }}{% endif %}" class="btn btn-default">
<span class="fa fa-table" aria-hidden="true"></span>
IPv6 Stats
</a>
@@ -26,11 +26,14 @@
</div>
<h1>RIRs</h1>
<div class="row">
<div class="col-md-12">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}
{% if request.GET.family == '6' %}
<div class="alert alert-info pull-right"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div>
{% endif %}
</div>
<div class="col-md-3">
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% if request.GET.family == '6' %}
<div class="pull-right text-muted"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div>
{% endif %}
{% endblock %}

View File

@@ -8,7 +8,9 @@
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></li>
<li><a href="{% url 'tenancy:tenant_list' %}?group={{ tenant.group.slug }}">{{ tenant.group }}</a></li>
{% if tenant.group %}
<li><a href="{% url 'tenancy:tenant_list' %}?group={{ tenant.group.slug }}">{{ tenant.group }}</a></li>
{% endif %}
<li>{{ tenant }}</li>
</ol>
</div>
@@ -50,7 +52,11 @@
<tr>
<td>Group</td>
<td>
<a href="{{ tenant.group.get_absolute_url }}">{{ tenant.group }}</a>
{% if tenant.group %}
<a href="{{ tenant.group.get_absolute_url }}">{{ tenant.group }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>

View File

@@ -40,7 +40,7 @@
</tr>
<tr>
<td>Group</td>
<td>Tenant group</td>
<td>Tenant group (optional)</td>
<td>Customers</td>
</tr>
<tr>

View File

@@ -48,6 +48,6 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
return ','.join([
self.name,
self.slug,
self.group.name,
self.group.name if self.group else '',
self.description,
])

View File

@@ -28,8 +28,8 @@ class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'tenancy.change_tenantgroup'
model = TenantGroup
form_class = forms.TenantGroupForm
success_url = 'tenancy:tenantgroup_list'
cancel_url = 'tenancy:tenantgroup_list'
obj_list_url = 'tenancy:tenantgroup_list'
use_obj_view = False
class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@@ -83,7 +83,7 @@ class TenantEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.TenantForm
fields_initial = ['group']
template_name = 'tenancy/tenant_edit.html'
cancel_url = 'tenancy:tenant_list'
obj_list_url = 'tenancy:tenant_list'
class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView):

View File

@@ -1,5 +1,11 @@
from django.core.validators import RegexValidator
from django.db import models
from .forms import ColorSelect
validate_color = RegexValidator('^[0-9a-f]{6}$', 'Enter a valid hexadecimal RGB color code.', 'invalid')
class NullableCharField(models.CharField):
description = "Stores empty values as NULL rather than ''"
@@ -11,3 +17,16 @@ class NullableCharField(models.CharField):
def get_prep_value(self, value):
return value or None
class ColorField(models.CharField):
default_validators = [validate_color]
description = "A hexadecimal RGB color code"
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 6
super(ColorField, self).__init__(*args, **kwargs)
def formfield(self, **kwargs):
kwargs['widget'] = ColorSelect
return super(ColorField, self).formfield(**kwargs)

View File

@@ -11,6 +11,32 @@ from django.utils.html import format_html
from django.utils.safestring import mark_safe
COLOR_CHOICES = (
('aa1409', 'Dark red'),
('f44336', 'Red'),
('e91e63', 'Pink'),
('ff66ff', 'Fuschia'),
('9c27b0', 'Purple'),
('673ab7', 'Dark purple'),
('3f51b5', 'Indigo'),
('2196f3', 'Blue'),
('03a9f4', 'Light blue'),
('00bcd4', 'Cyan'),
('009688', 'Teal'),
('2f6a31', 'Dark green'),
('4caf50', 'Green'),
('8bc34a', 'Light green'),
('cddc39', 'Lime'),
('ffeb3b', 'Yellow'),
('ffc107', 'Amber'),
('ff9800', 'Orange'),
('ff5722', 'Dark orange'),
('795548', 'Brown'),
('c0c0c0', 'Light grey'),
('9e9e9e', 'Grey'),
('607d8b', 'Dark grey'),
('111111', 'Black'),
)
NUMERIC_EXPANSION_PATTERN = '\[(\d+-\d+)\]'
IP4_EXPANSION_PATTERN = '\[([0-9]{1,3}-[0-9]{1,3})\]'
IP6_EXPANSION_PATTERN = '\[([0-9a-f]{1,4}-[0-9a-f]{1,4})\]'
@@ -71,6 +97,27 @@ class SmallTextarea(forms.Textarea):
pass
class ColorSelect(forms.Select):
def __init__(self, *args, **kwargs):
kwargs['choices'] = COLOR_CHOICES
super(ColorSelect, self).__init__(*args, **kwargs)
def render_option(self, selected_choices, option_value, option_label):
if option_value is None:
option_value = ''
option_value = force_text(option_value)
if option_value in selected_choices:
selected_html = mark_safe(' selected')
if not self.allow_multiple_selected:
# Only allow for a single selection.
selected_choices.remove(option_value)
else:
selected_html = ''
return format_html('<option value="{}"{} style="background-color: #{}">{}</option>',
option_value, selected_html, option_value, force_text(option_label))
class SelectWithDisabled(forms.Select):
"""
Modified the stock Select widget to accept choices using a dict() for a label. The dict for each option must include
@@ -234,6 +281,7 @@ class CommentField(forms.CharField):
A textarea with support for GitHub-Flavored Markdown. Exists mostly just to add a standard help_text.
"""
widget = forms.Textarea
default_label = 'Comments'
# TODO: Port GFM syntax cheat sheet to internal documentation
default_helptext = '<i class="fa fa-info-circle"></i> '\
'<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">'\
@@ -241,8 +289,9 @@ class CommentField(forms.CharField):
def __init__(self, *args, **kwargs):
required = kwargs.pop('required', False)
label = kwargs.pop('label', self.default_label)
help_text = kwargs.pop('help_text', self.default_helptext)
super(CommentField, self).__init__(required=required, help_text=help_text, *args, **kwargs)
super(CommentField, self).__init__(required=required, label=label, help_text=help_text, *args, **kwargs)
class FlexibleModelChoiceField(forms.ModelChoiceField):

View File

@@ -2,6 +2,7 @@ from django.http import HttpResponseRedirect
from django.conf import settings
BASE_PATH = getattr(settings, 'BASE_PATH', False)
LOGIN_REQUIRED = getattr(settings, 'LOGIN_REQUIRED', False)
@@ -11,5 +12,8 @@ class LoginRequiredMiddleware:
"""
def process_request(self, request):
if LOGIN_REQUIRED and not request.user.is_authenticated():
if request.path_info != settings.LOGIN_URL:
# Redirect unauthenticated requests to the login page. API requests are exempt from redirection as the API
# performs its own authentication.
api_path = '/{}api/'.format(BASE_PATH)
if not request.path_info.startswith(api_path) and request.path_info != settings.LOGIN_URL:
return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, request.path_info))

View File

@@ -119,8 +119,8 @@ class ObjectEditView(View):
form_class = None
fields_initial = []
template_name = 'utilities/obj_edit.html'
success_url = None
cancel_url = None
obj_list_url = None
use_obj_view = True
def get_object(self, kwargs):
# Look up object by slug if one has been provided. Otherwise, use PK.
@@ -129,12 +129,13 @@ class ObjectEditView(View):
else:
return get_object_or_404(self.model, pk=kwargs['pk'])
def get_cancel_url(self, obj):
if hasattr(obj, 'get_absolute_url'):
return obj.get_absolute_url()
if hasattr(obj, 'get_parent_url'):
return obj.get_parent_url()
return reverse(self.cancel_url)
def get_redirect_url(self, obj):
if obj and self.use_obj_view:
if hasattr(obj, 'get_absolute_url'):
return obj.get_absolute_url()
if hasattr(obj, 'get_parent_url'):
return obj.get_parent_url()
return reverse(self.obj_list_url)
def get(self, request, *args, **kwargs):
@@ -149,7 +150,7 @@ class ObjectEditView(View):
'obj': obj,
'obj_type': self.model._meta.verbose_name,
'form': form,
'cancel_url': self.get_cancel_url(obj),
'cancel_url': self.get_redirect_url(obj),
})
def post(self, request, *args, **kwargs):
@@ -179,18 +180,13 @@ class ObjectEditView(View):
if '_addanother' in request.POST:
return redirect(request.path)
elif self.success_url:
return redirect(self.success_url)
elif hasattr(obj, 'get_absolute_url'):
return redirect(obj.get_absolute_url())
elif hasattr(obj, 'get_parent_url'):
return redirect(obj.get_parent_url())
return redirect(self.get_redirect_url(obj))
return render(request, self.template_name, {
'obj': obj,
'obj_type': self.model._meta.verbose_name,
'form': form,
'cancel_url': self.get_cancel_url(obj),
'cancel_url': self.get_redirect_url(obj),
})

View File

@@ -1,3 +1,4 @@
cffi>=1.8
cryptography==1.4
Django==1.10
django-debug-toolbar==1.4