diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 02a08716b..b2700596e 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -12,31 +12,37 @@ Download and extract the latest version: # wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz # tar -xzf vX.Y.Z.tar.gz -C /opt # cd /opt/ -# ln -sf netbox-X.Y.Z/ netbox +# ln -sfn netbox-X.Y.Z/ netbox ``` Copy the 'configuration.py' you created when first installing to the new version: ```no-highlight -# cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py +# cp netbox-X.Y.Z/netbox/netbox/configuration.py netbox/netbox/netbox/configuration.py ``` Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.) ```no-highlight -# cp -pr /opt/netbox-X.Y.Z/netbox/media/ /opt/netbox/netbox/ +# cp -pr netbox-X.Y.Z/netbox/media/ netbox/netbox/ +``` + +Also make sure to copy over any reports that you've made. Note that if you made them in a separate directory (`/opt/netbox-reports` for example), then you will not need to copy them - the config file that you copied earlier will point to the correct location. + +```no-highlight +# cp -r /opt/netbox-X.Y.X/netbox/reports /opt/netbox/netbox/reports/ ``` If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well: ```no-highlight -# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py +# cp netbox-X.Y.Z/gunicorn_config.py netbox/gunicorn_config.py ``` Copy the LDAP configuration if using LDAP: ```no-highlight -# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py +# cp netbox-X.Y.Z/netbox/netbox/ldap_config.py netbox/netbox/netbox/ldap_config.py ``` ## Option B: Clone the Git Repository (latest master release) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 49d9500a0..6460d5d2c 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -169,13 +169,37 @@ class SiteCSVForm(forms.ModelForm): class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): - pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput) - status = forms.ChoiceField(choices=add_blank_choice(SITE_STATUS_CHOICES), required=False, initial='') - region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False) - tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) - asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN') - description = forms.CharField(max_length=100, required=False) - time_zone = TimeZoneFormField(required=False) + pk = forms.ModelMultipleChoiceField( + queryset=Site.objects.all(), + widget=forms.MultipleHiddenInput + ) + status = forms.ChoiceField( + choices=add_blank_choice(SITE_STATUS_CHOICES), + required=False, + initial='' + ) + region = TreeNodeChoiceField( + queryset=Region.objects.all(), + required=False + ) + tenant = forms.ModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + asn = forms.IntegerField( + min_value=1, + max_value=4294967295, + required=False, + label='ASN' + ) + description = forms.CharField( + max_length=100, + required=False + ) + time_zone = TimeZoneFormField( + choices=add_blank_choice(TimeZoneFormField().choices), + required=False + ) class Meta: nullable_fields = ['region', 'tenant', 'asn', 'description', 'time_zone'] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 9fe1a9ef7..c29caa189 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1586,7 +1586,7 @@ class ConsoleServerPort(models.Model): raise ValidationError("Console server ports must be assigned to devices.") device_type = self.device.device_type if not device_type.is_console_server: - raise ValidationError("The {} {} device type not support assignment of console server ports.".format( + raise ValidationError("The {} {} device type does not support assignment of console server ports.".format( device_type.manufacturer, device_type )) @@ -1688,7 +1688,7 @@ class PowerOutlet(models.Model): raise ValidationError("Power outlets must be assigned to devices.") device_type = self.device.device_type if not device_type.is_pdu: - raise ValidationError("The {} {} device type not support assignment of power outlets.".format( + raise ValidationError("The {} {} device type does not support assignment of power outlets.".format( device_type.manufacturer, device_type )) @@ -1794,7 +1794,7 @@ class Interface(models.Model): if self.device is not None: device_type = self.device.device_type if not device_type.is_network_device: - raise ValidationError("The {} {} device type not support assignment of network interfaces.".format( + raise ValidationError("The {} {} device type does not support assignment of network interfaces.".format( device_type.manufacturer, device_type )) @@ -1938,6 +1938,18 @@ class InterfaceConnection(models.Model): raise ValidationError({ 'interface_b': "Cannot connect an interface to itself." }) + if self.interface_a.form_factor in NONCONNECTABLE_IFACE_TYPES: + raise ValidationError({ + 'interface_a': '{} is not a connectable interface type.'.format( + self.interface_a.get_form_factor_display() + ) + }) + if self.interface_b.form_factor in NONCONNECTABLE_IFACE_TYPES: + raise ValidationError({ + 'interface_b': '{} is not a connectable interface type.'.format( + self.interface_b.get_form_factor_display() + ) + }) except ObjectDoesNotExist: pass diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 1e8888e97..c29a8a857 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -11,8 +11,13 @@ def assign_virtualchassis_master(instance, created, **kwargs): """ When a VirtualChassis is created, automatically assign its master device to the VC. """ + # Default to 1 but don't overwrite an existing position (see #2087) + if instance.master.vc_position is not None: + vc_position = instance.master.vc_position + else: + vc_position = 1 if created: - Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=1) + Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=vc_position) @receiver(pre_delete, sender=VirtualChassis) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 924fe67fb..6e7aa070c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -157,6 +157,7 @@ class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_region' cls = Region queryset = Region.objects.annotate(site_count=Count('sites')) + filter = filters.RegionFilter table = tables.RegionTable default_return_url = 'dcim:region_list' @@ -491,6 +492,7 @@ class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView): class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackreservation' cls = RackReservation + filter = filters.RackReservationFilter table = tables.RackReservationTable default_return_url = 'dcim:rackreservation_list' diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index e0842d10b..8c7dbb690 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.core.exceptions import ValidationError from django.db import models -from netaddr import IPNetwork +from netaddr import AddrFormatError, IPNetwork from .formfields import IPFormField from . import lookups @@ -26,7 +26,9 @@ class BaseIPField(models.Field): return value try: return IPNetwork(value) - except ValueError as e: + except AddrFormatError as e: + raise ValidationError("Invalid IP address format: {}".format(value)) + except (TypeError, ValueError) as e: raise ValidationError(e) def get_prep_value(self, value): diff --git a/netbox/templates/dcim/device_lldp_neighbors.html b/netbox/templates/dcim/device_lldp_neighbors.html index 4fe914f64..0e423ad56 100644 --- a/netbox/templates/dcim/device_lldp_neighbors.html +++ b/netbox/templates/dcim/device_lldp_neighbors.html @@ -53,7 +53,7 @@ $(document).ready(function() { success: function(json) { $.each(json['get_lldp_neighbors'], function(iface, neighbors) { var neighbor = neighbors[0]; - var row = $('#' + iface.split(".")[0].replace(/(\/)/g, "\\$1")); + var row = $('#' + iface.split(".")[0].replace(/([\/:])/g, "\\$1")); // Glean configured hostnames/interfaces from the DOM var configured_device = row.children('td.configured_device').attr('data'); diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index aa0a9cbd5..33e30b126 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -105,7 +105,7 @@ - + {% else %} diff --git a/netbox/templates/dcim/rackrole_list.html b/netbox/templates/dcim/rackrole_list.html index e18bf7941..51cb19781 100644 --- a/netbox/templates/dcim/rackrole_list.html +++ b/netbox/templates/dcim/rackrole_list.html @@ -1,22 +1,17 @@ {% extends '_base.html' %} -{% load helpers %} +{% load buttons %} {% block content %}