diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index dbfcf4527..0027599f4 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,28 +1,41 @@ -### Issue type: + Please note that issues which do not fall under any of the below categories + will be closed. +---> +### Issue type +[ ] Feature request +[ ] Bug report +[ ] Documentation -**Python version:** -**NetBox version:** +### Environment +* Python version: +* NetBox version: +### Description diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 83b289b9a..22916d54c 100644 --- a/docs/configuration/optional-settings.md +++ b/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_PASSWORD diff --git a/docs/installation/web-server.md b/docs/installation/web-server.md index 0acedccc6..f9a304ff5 100644 --- a/docs/installation/web-server.md +++ b/docs/installation/web-server.md @@ -51,7 +51,7 @@ Restart the nginx service to use the new configuration. # service nginx restart ``` -To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04). +To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04). ## Option B: Apache @@ -96,7 +96,7 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c # service apache2 restart ``` -To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-14-04). +To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-16-04). # gunicorn Installation diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index d9954e55b..2a2cd6843 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -170,6 +170,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')), to_field_name='slug' ) + commit_rate = forms.IntegerField(required=False, min_value=0, label='Commit rate (Kbps)') # diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 1acd3f4a0..17b38a5d8 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -13,22 +13,6 @@ from utilities.models import CreatedUpdatedModel from .constants import * -def humanize_speed(speed): - """ - Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps') - """ - if speed >= 1000000000 and speed % 1000000000 == 0: - return '{} Tbps'.format(speed / 1000000000) - elif speed >= 1000000 and speed % 1000000 == 0: - return '{} Gbps'.format(speed / 1000000) - elif speed >= 1000 and speed % 1000 == 0: - return '{} Mbps'.format(speed / 1000) - elif speed >= 1000: - return '{} Mbps'.format(float(speed) / 1000) - else: - return '{} Kbps'.format(speed) - - @python_2_unicode_compatible class Provider(CreatedUpdatedModel, CustomFieldModel): """ @@ -139,10 +123,6 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): def termination_z(self): return self._get_termination('Z') - def commit_rate_human(self): - return '' if not self.commit_rate else humanize_speed(self.commit_rate) - commit_rate_human.admin_order_field = 'commit_rate' - @python_2_unicode_compatible class CircuitTermination(models.Model): @@ -173,11 +153,3 @@ class CircuitTermination(models.Model): return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side) except CircuitTermination.DoesNotExist: return None - - def port_speed_human(self): - return humanize_speed(self.port_speed) - port_speed_human.admin_order_field = 'port_speed' - - def upstream_speed_human(self): - return '' if not self.upstream_speed else humanize_speed(self.upstream_speed) - upstream_speed_human.admin_order_field = 'upstream_speed' diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index f2c047910..9c2993b60 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -58,6 +58,7 @@ IFACE_FF_1GE_FIXED = 1000 IFACE_FF_1GE_GBIC = 1050 IFACE_FF_1GE_SFP = 1100 IFACE_FF_10GE_FIXED = 1150 +IFACE_FF_10GE_CX4 = 1170 IFACE_FF_10GE_SFP_PLUS = 1200 IFACE_FF_10GE_XFP = 1300 IFACE_FF_10GE_XENPAK = 1310 @@ -106,6 +107,7 @@ IFACE_FF_CHOICES = [ [IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'], [IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'], [IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'], + [IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'], ] ], [ diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 54a7af4e2..a9f58d2a4 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -273,6 +273,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet): class DeviceTypeComponentFilterSet(django_filters.FilterSet): devicetype_id = django_filters.ModelMultipleChoiceFilter( queryset=DeviceType.objects.all(), + name='device_type_id', label='Device type (ID)', ) diff --git a/netbox/dcim/migrations/0042_interface_ff_10ge_cx4.py b/netbox/dcim/migrations/0042_interface_ff_10ge_cx4.py new file mode 100644 index 000000000..77bea6bc6 --- /dev/null +++ b/netbox/dcim/migrations/0042_interface_ff_10ge_cx4.py @@ -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), + ), + ] diff --git a/netbox/dcim/migrations/0043_device_component_name_lengths.py b/netbox/dcim/migrations/0043_device_component_name_lengths.py new file mode 100644 index 000000000..a52f50859 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 9f72fc83e..a44097f51 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -574,7 +574,7 @@ class ConsolePortTemplate(models.Model): A template for a ConsolePort to be created for a new Device. """ device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=30) + name = models.CharField(max_length=50) class Meta: ordering = ['device_type', 'name'] @@ -590,7 +590,7 @@ class ConsoleServerPortTemplate(models.Model): A template for a ConsoleServerPort to be created for a new Device. """ device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=30) + name = models.CharField(max_length=50) class Meta: ordering = ['device_type', 'name'] @@ -606,7 +606,7 @@ class PowerPortTemplate(models.Model): A template for a PowerPort to be created for a new Device. """ device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=30) + name = models.CharField(max_length=50) class Meta: ordering = ['device_type', 'name'] @@ -622,7 +622,7 @@ class PowerOutletTemplate(models.Model): A template for a PowerOutlet to be created for a new Device. """ device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=30) + name = models.CharField(max_length=50) class Meta: ordering = ['device_type', 'name'] @@ -685,7 +685,7 @@ class InterfaceTemplate(models.Model): A template for a physical data interface on a new Device. """ device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=30) + name = models.CharField(max_length=64) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) mgmt_only = models.BooleanField(default=False, verbose_name='Management only') @@ -705,7 +705,7 @@ class DeviceBayTemplate(models.Model): A template for a DeviceBay to be created for a new parent Device. """ device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=30) + name = models.CharField(max_length=50) class Meta: ordering = ['device_type', 'name'] @@ -1012,7 +1012,7 @@ class ConsolePort(models.Model): A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. """ device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE) - name = models.CharField(max_length=30) + name = models.CharField(max_length=50) cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL, verbose_name='Console server port', blank=True, null=True) connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED) @@ -1062,7 +1062,7 @@ class ConsoleServerPort(models.Model): A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. """ device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE) - name = models.CharField(max_length=30) + name = models.CharField(max_length=50) objects = ConsoleServerPortManager() @@ -1083,7 +1083,7 @@ class PowerPort(models.Model): A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. """ device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE) - name = models.CharField(max_length=30) + name = models.CharField(max_length=50) power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL, blank=True, null=True) connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED) @@ -1127,7 +1127,7 @@ class PowerOutlet(models.Model): A physical power outlet (output) within a Device which provides power to a PowerPort. """ device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE) - name = models.CharField(max_length=30) + name = models.CharField(max_length=50) objects = PowerOutletManager() @@ -1157,7 +1157,7 @@ class Interface(models.Model): blank=True, verbose_name='Parent LAG' ) - name = models.CharField(max_length=30) + name = models.CharField(max_length=64) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) enabled = models.BooleanField(default=True) mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address') diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 05f16aa35..d0225e567 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -16,6 +16,7 @@ from utilities.views import ( BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from . import filters, forms, tables +from .constants import IPADDRESS_ROLE_ANYCAST from .models import ( Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF, @@ -624,6 +625,9 @@ class IPAddressView(View): ).select_related( 'interface__device', 'nat_inside' ) + # Exclude anycast IPs if this IP is anycast + if ipaddress.role == IPADDRESS_ROLE_ANYCAST: + duplicate_ips = duplicate_ips.exclude(role=IPADDRESS_ROLE_ANYCAST) duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False) # Related IP table diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 78e870072..192653dc4 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -93,6 +93,10 @@ MAINTENANCE_MODE = False # all objects by specifying "?limit=0". MAX_PAGE_SIZE = 1000 +# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that +# the default value of this setting is derived from the installed location. +# MEDIA_ROOT = '/opt/netbox/netbox/media' + # Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM. NAPALM_USERNAME = '' NAPALM_PASSWORD = '' diff --git a/netbox/netbox/forms.py b/netbox/netbox/forms.py index 85343ec77..72a3ab8de 100644 --- a/netbox/netbox/forms.py +++ b/netbox/netbox/forms.py @@ -35,7 +35,7 @@ OBJ_TYPE_CHOICES = ( class SearchForm(BootstrapMixin, forms.Form): q = forms.CharField( - label='Query', widget=forms.TextInput(attrs={'style': 'width: 350px'}) + label='Search', widget=forms.TextInput(attrs={'style': 'width: 350px'}) ) obj_type = forms.ChoiceField( choices=OBJ_TYPE_CHOICES, required=False, label='Type' diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 1c3f3688b..d3162bfe2 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -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 ALLOWED_HOSTS = DATABASE = SECRET_KEY = None @@ -44,14 +46,15 @@ LOGGING = getattr(configuration, 'LOGGING', {}) LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False) MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False) MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000) -PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50) -PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) +MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/') NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '') NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '') NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30) NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {}) NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '') # Deprecated NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '') # Deprecated +PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50) +PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') @@ -104,8 +107,6 @@ if LDAP_CONFIGURED: "netbox/ldap_config.py to disable LDAP." ) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - # Database configuration.DATABASE.update({'ENGINE': 'django.db.backends.postgresql'}) DATABASES = { @@ -201,7 +202,6 @@ STATICFILES_DIRS = ( ) # Media -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/{}media/'.format(BASE_PATH) # Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.) diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index c56a95abe..66f23a4cb 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -205,20 +205,18 @@