Browse Source

Merge branch 'develop' into virtualization

Jeremy Stretch 7 years ago
parent
commit
0634386b2e

+ 30 - 17
.github/ISSUE_TEMPLATE.md

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

+ 8 - 0
docs/configuration/optional-settings.md

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

+ 1 - 0
netbox/circuits/forms.py

@@ -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)')
 
 
 
 
 #
 #

+ 0 - 28
netbox/circuits/models.py

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

+ 2 - 0
netbox/dcim/constants.py

@@ -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
@@ -108,6 +109,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)'],
         ]
         ]
     ],
     ],
     [
     [

+ 1 - 0
netbox/dcim/filters.py

@@ -274,6 +274,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)',
     )
     )
 
 

File diff suppressed because it is too large
+ 25 - 0
netbox/dcim/migrations/0042_interface_ff_10ge_cx4.py


+ 70 - 0
netbox/dcim/migrations/0043_device_component_name_lengths.py

@@ -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),
+        ),
+    ]

+ 11 - 11
netbox/dcim/models.py

@@ -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']
@@ -1019,7 +1019,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)
@@ -1069,7 +1069,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()
 
 
@@ -1090,7 +1090,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)
@@ -1134,7 +1134,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()
 
 
@@ -1177,7 +1177,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')

+ 4 - 0
netbox/ipam/views.py

@@ -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,
@@ -630,6 +631,9 @@ class IPAddressView(View):
         ).prefetch_related(
         ).prefetch_related(
             'interface__device'
             'interface__device'
         )
         )
+        # 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

+ 4 - 0
netbox/netbox/configuration.example.py

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

+ 1 - 1
netbox/netbox/forms.py

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

+ 6 - 6
netbox/netbox/settings.py

@@ -13,7 +13,9 @@ except ImportError:
     )
     )
 
 
 
 
-VERSION = '2.1.4-dev'
+VERSION = '2.1.5-dev'
+
+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)
-PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
+MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
 NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
 NAPALM_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 = {
@@ -202,7 +203,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.)

+ 6 - 8
netbox/templates/_base.html

@@ -233,20 +233,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>

+ 1 - 1
netbox/templates/circuits/circuit.html

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

+ 19 - 1
netbox/templates/circuits/circuit_edit.html

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

+ 26 - 2
netbox/templates/circuits/circuittermination_edit.html

@@ -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 %}
-                        {% render_field form.upstream_speed %}
+                        <div class="form-group">
+                            <label class="col-md-3 control-label required" for="id_port_speed">{{ form.port_speed.label }}</label>
+                            <div class="col-md-9">
+                                <div class="input-group">
+                                    {{ form.port_speed }}
+                                    {% include 'circuits/inc/speed_widget.html' with target_field='port_speed' %}
+                                </div>
+                                <span class="help-block">{{ form.port_speed.help_text }}</span>
+                            </div>
+                        </div>
+                        <div class="form-group">
+                            <label class="col-md-3 control-label" for="id_upstream_speed">{{ form.upstream_speed.label }}</label>
+                            <div class="col-md-9">
+                                <div class="input-group">
+                                    {{ form.upstream_speed }}
+                                    {% include 'circuits/inc/speed_widget.html' with target_field='upstream_speed' %}
+                                </div>
+                                <span class="help-block">{{ form.upstream_speed.help_text }}</span>
+                            </div>
+                        </div>
                         {% render_field form.xconnect_id %}
                         {% render_field form.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 %}

+ 5 - 3
netbox/templates/circuits/inc/circuit_termination.html

@@ -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-up" title="Upstream"></i> {{ termination.upstream_speed_human }}
+                        <i class="fa fa-arrow-down" title="Downstream"></i> {{ termination.port_speed|humanize_speed }} &nbsp;
+                        <i class="fa fa-arrow-up" title="Upstream"></i> {{ termination.upstream_speed|humanize_speed }}
                     {% else %}
                     {% else %}
-                        {{ termination.port_speed_human }}
+                        {{ termination.port_speed|humanize_speed }}
                     {% endif %}
                     {% endif %}
                 </td>
                 </td>
             </tr>
             </tr>

+ 17 - 0
netbox/templates/circuits/inc/speed_widget.html

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

+ 1 - 1
netbox/templates/inc/table.html

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

+ 5 - 1
netbox/templates/ipam/ipaddress.html

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

+ 0 - 70
netbox/templates/ipam/ipaddress_assign.html

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

+ 0 - 8
netbox/templates/ipam/ipaddress_unassign.html

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

+ 1 - 1
netbox/templates/ipam/role_list.html

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

+ 9 - 2
netbox/utilities/api.py

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

+ 6 - 11
netbox/utilities/forms.py

@@ -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'),
@@ -444,17 +445,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)
     """
     """
-
-    class AnyURLScheme(object):
-        # A fake URL list which "contains" all scheme names abiding by the syntax defined in RFC 3986 section 3.1
-        def __contains__(self, item):
-            if not item or not re.match('^[a-z][0-9a-z+\-.]*$', item.lower()):
-                return False
-            return True
-
-    default_validators = [URLValidator(schemes=AnyURLScheme())]
+    default_validators = [EnhancedURLValidator()]
 
 
 
 
 #
 #

+ 1 - 1
netbox/utilities/tables.py

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

+ 21 - 0
netbox/utilities/templatetags/helpers.py

@@ -79,6 +79,27 @@ def bettertitle(value):
 
 
 
 
 @register.filter()
 @register.filter()
+def humanize_speed(speed):
+    """
+    Humanize speeds given in Kbps. Examples:
+
+        1544 => "1.544 Mbps"
+        100000 => "100 Mbps"
+        10000000 => "10 Gbps"
+    """
+    if speed >= 1000000000 and speed % 1000000000 == 0:
+        return '{} Tbps'.format(int(speed / 1000000000))
+    elif speed >= 1000000 and speed % 1000000 == 0:
+        return '{} Gbps'.format(int(speed / 1000000))
+    elif speed >= 1000 and speed % 1000 == 0:
+        return '{} Mbps'.format(int(speed / 1000))
+    elif speed >= 1000:
+        return '{} Mbps'.format(float(speed) / 1000)
+    else:
+        return '{} Kbps'.format(speed)
+
+
+@register.filter()
 def example_choices(field, arg=3):
 def example_choices(field, arg=3):
     """
     """
     Returns a number (default: 3) of example choices for a ChoiceFiled (useful for CSV import forms).
     Returns a number (default: 3) of example choices for a ChoiceFiled (useful for CSV import forms).

+ 30 - 0
netbox/utilities/validators.py

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