Merge pull request #1461 from digitalocean/develop

Release v2.1.4
This commit is contained in:
Jeremy Stretch 2017-08-30 14:43:01 -04:00 committed by GitHub
commit f6d1163ddd
31 changed files with 322 additions and 178 deletions

View File

@ -1,28 +1,41 @@
<!-- <!--
Please note: GitHub issues are to be used only for feature requests Before opening a new issue, please search through the existing issues to
and bug reports. For installation assistance or general discussion, see if your topic has already been addressed. Note that you may need to
please join us on the mailing list: 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 https://groups.google.com/forum/#!forum/netbox-discuss
Please indicate "bug report" or "feature request" below. Be sure to Please note that issues which do not fall under any of the below categories
search the existing set of issues (both open and closed) to see if will be closed.
a similar issue has already been raised. --->
--> ### Issue type
### 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 Please describe the environment in which you are running NetBox. (Be sure
you are running. (This is not necessary for feature requests.) to verify that you are running the latest stable release of NetBox before
submitting a bug report.)
--> -->
**Python version:** ### Environment
**NetBox version:** * Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.1.3 -->
<!-- <!--
If filing a bug, please record the exact steps taken to reproduce BUG REPORTS must include:
the bug and any errors messages that are generated. * 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 FEATURE REQUESTS must include:
model or workflow you would like to see implemented, and provide a * A detailed description of the proposed functionality
use case. * A use case for the new feature
* A rough description of any necessary changes to the database schema
* Any relevant third-party libraries which would be needed
--> -->
### Description

View File

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

View File

@ -51,7 +51,7 @@ Restart the nginx service to use the new configuration.
# service nginx restart # 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 ## Option B: Apache
@ -96,7 +96,7 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c
# service apache2 restart # 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 # gunicorn Installation

View File

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

View File

@ -13,22 +13,6 @@ from utilities.models import CreatedUpdatedModel
from .constants import * 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 @python_2_unicode_compatible
class Provider(CreatedUpdatedModel, CustomFieldModel): class Provider(CreatedUpdatedModel, CustomFieldModel):
""" """
@ -139,10 +123,6 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
def termination_z(self): def termination_z(self):
return self._get_termination('Z') 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 @python_2_unicode_compatible
class CircuitTermination(models.Model): 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) return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side)
except CircuitTermination.DoesNotExist: except CircuitTermination.DoesNotExist:
return None return None
def port_speed_human(self):
return humanize_speed(self.port_speed)
port_speed_human.admin_order_field = 'port_speed'
def upstream_speed_human(self):
return '' if not self.upstream_speed else humanize_speed(self.upstream_speed)
upstream_speed_human.admin_order_field = 'upstream_speed'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -93,6 +93,10 @@ MAINTENANCE_MODE = False
# all objects by specifying "?limit=0". # all objects by specifying "?limit=0".
MAX_PAGE_SIZE = 1000 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. # Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
NAPALM_USERNAME = '' NAPALM_USERNAME = ''
NAPALM_PASSWORD = '' NAPALM_PASSWORD = ''

View File

@ -35,7 +35,7 @@ OBJ_TYPE_CHOICES = (
class SearchForm(BootstrapMixin, forms.Form): class SearchForm(BootstrapMixin, forms.Form):
q = forms.CharField( 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( obj_type = forms.ChoiceField(
choices=OBJ_TYPE_CHOICES, required=False, label='Type' choices=OBJ_TYPE_CHOICES, required=False, label='Type'

View File

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

View File

@ -205,20 +205,18 @@
<li class="dropdown{% if request.path|contains:'/circuits/' %} active{% endif %}"> <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> <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"> <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> <li><a href="{% url 'circuits:circuit_list' %}"><strong>Circuits</strong></a></li>
{% if perms.circuits.add_circuit %} {% 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_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> <li class="subnav"><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download"></i> Import Circuits</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <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> <li><a href="{% url 'circuits:circuittype_list' %}"><strong>Circuit Types</strong></a></li>
{% if perms.circuits.add_circuittype %} {% 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> <li class="subnav"><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus"></i> Add a Circuit Type</a></li>

View File

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

View File

@ -9,7 +9,16 @@
{% render_field form.cid %} {% render_field form.cid %}
{% render_field form.type %} {% render_field form.type %}
{% render_field form.install_date %} {% 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 %} {% render_field form.description %}
</div> </div>
</div> </div>
@ -35,3 +44,12 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block javascript %}
<script type="text/javascript">
$("a.set_speed").click(function(e) {
e.preventDefault();
$("#id_" + $(this).attr("target")).val($(this).attr("data"));
});
</script>
{% endblock %}

View File

@ -49,8 +49,26 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Termination Details</strong></div> <div class="panel-heading"><strong>Termination Details</strong></div>
<div class="panel-body"> <div class="panel-body">
{% render_field form.port_speed %} <div class="form-group">
{% render_field form.upstream_speed %} <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.xconnect_id %}
{% render_field form.pp_info %} {% render_field form.pp_info %}
</div> </div>
@ -72,4 +90,10 @@
{% block javascript %} {% block javascript %}
<script src="{% static 'js/livesearch.js' %}?v{{ settings.VERSION }}"></script> <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 %} {% endblock %}

View File

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

View File

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

View File

@ -80,13 +80,19 @@ $(document).ready(function() {
$('#model').html(json['get_facts']['model']); $('#model').html(json['get_facts']['model']);
$('#serial_number').html(json['get_facts']['serial_number']); $('#serial_number').html(json['get_facts']['serial_number']);
$('#os_version').html(json['get_facts']['os_version']); $('#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) { $.each(json['get_environment']['cpu'], function(name, obj) {
var row="<tr><td>" + name + "</td><td>" + obj['%usage'] + "%</td></tr>"; var row="<tr><td>" + name + "</td><td>" + obj['%usage'] + "%</td></tr>";
$("#cpu").after(row) $("#cpu").after(row)
}); });
$('#memory').after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_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'] + "MB</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) { $.each(json['get_environment']['temperature'], function(name, obj) {
var style = "success"; var style = "success";
if (obj['is_alert']) { if (obj['is_alert']) {

View File

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

View File

@ -83,7 +83,11 @@
<tr> <tr>
<td>Role</td> <td>Role</td>
<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> </td>
</tr> </tr>
<tr> <tr>

View File

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

View File

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

View File

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

View File

@ -85,7 +85,13 @@ class ValidatedModelSerializer(ModelSerializer):
""" """
Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation. Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation.
""" """
def validate(self, attrs): 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: if self.instance is None:
instance = self.Meta.model(**attrs) instance = self.Meta.model(**attrs)
else: else:
@ -93,7 +99,8 @@ class ValidatedModelSerializer(ModelSerializer):
for k, v in attrs.items(): for k, v in attrs.items():
setattr(instance, k, v) setattr(instance, k, v)
instance.clean() instance.clean()
return attrs
return data
class ChoiceFieldSerializer(Field): class ChoiceFieldSerializer(Field):

View File

@ -7,9 +7,10 @@ from mptt.forms import TreeNodeMultipleChoiceField
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.validators import URLValidator
from django.urls import reverse_lazy from django.urls import reverse_lazy
from .validators import EnhancedURLValidator
COLOR_CHOICES = ( COLOR_CHOICES = (
('aa1409', 'Dark red'), ('aa1409', 'Dark red'),
@ -431,17 +432,11 @@ class FilterTreeNodeMultipleChoiceField(FilterChoiceFieldMixin, TreeNodeMultiple
class LaxURLField(forms.URLField): 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)
""" """
default_validators = [EnhancedURLValidator()]
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())]
# #

View File

@ -14,7 +14,7 @@ class BaseTable(tables.Table):
# Set default empty_text if none was provided # Set default empty_text if none was provided
if self.empty_text is None: 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: class Meta:
attrs = { attrs = {

View File

@ -62,6 +62,27 @@ def bettertitle(value):
return ' '.join([w[0].upper() + w[1:] for w in value.split()]) 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() @register.filter()
def example_choices(field, arg=3): def example_choices(field, arg=3):
""" """

View File

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