From dd5f37391f35cd9b8b6016662347798c8818dc8c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 11 Jan 2019 10:17:06 -0500 Subject: [PATCH 1/6] Fixes #2777: Fix cable validation to handle duplicate connections on import --- CHANGELOG.md | 1 + netbox/dcim/models.py | 87 ++++++++++++++++++++++--------------------- 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55bee2542..6e98477d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ v2.5.3 (FUTURE) * [#2742](https://github.com/digitalocean/netbox/issues/2742) - Preserve cluster assignment when editing a device * [#2757](https://github.com/digitalocean/netbox/issues/2757) - Always treat first/last IPs within a /31 or /127 as usable * [#2762](https://github.com/digitalocean/netbox/issues/2762) - Add missing DCIM field values to API `_choices` endpoint +* [#2777](https://github.com/digitalocean/netbox/issues/2777) - Fix cable validation to handle duplicate connections on import --- diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 567f9046e..1d13d119b 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2558,52 +2558,55 @@ class Cable(ChangeLoggedModel): def clean(self): - # Check that termination types are compatible - type_a = self.termination_a_type.model - type_b = self.termination_b_type.model - if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a): - raise ValidationError("Incompatible termination types: {} and {}".format( - self.termination_a_type, self.termination_b_type - )) + if self.termination_a and self.termination_b: - # A termination point cannot be connected to itself - if self.termination_a == self.termination_b: - raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type)) + type_a = self.termination_a_type.model + type_b = self.termination_b_type.model - # A front port cannot be connected to its corresponding rear port - if ( - type_a in ['frontport', 'rearport'] and - type_b in ['frontport', 'rearport'] and - ( - getattr(self.termination_a, 'rear_port', None) == self.termination_b or - getattr(self.termination_b, 'rear_port', None) == self.termination_a - ) - ): - raise ValidationError("A front port cannot be connected to it corresponding rear port") + # Check that termination types are compatible + if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a): + raise ValidationError("Incompatible termination types: {} and {}".format( + self.termination_a_type, self.termination_b_type + )) - # Check for an existing Cable connected to either termination object - if self.termination_a.cable not in (None, self): - raise ValidationError("{} already has a cable attached (#{})".format( - self.termination_a, self.termination_a.cable_id - )) - if self.termination_b.cable not in (None, self): - raise ValidationError("{} already has a cable attached (#{})".format( - self.termination_b, self.termination_b.cable_id - )) + # A termination point cannot be connected to itself + if self.termination_a == self.termination_b: + raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type)) - # Virtual interfaces cannot be connected - endpoint_a, endpoint_b, _ = self.get_path_endpoints() - if ( - ( - isinstance(endpoint_a, Interface) and - endpoint_a.form_factor == IFACE_FF_VIRTUAL - ) or - ( - isinstance(endpoint_b, Interface) and - endpoint_b.form_factor == IFACE_FF_VIRTUAL - ) - ): - raise ValidationError("Cannot connect to a virtual interface") + # A front port cannot be connected to its corresponding rear port + if ( + type_a in ['frontport', 'rearport'] and + type_b in ['frontport', 'rearport'] and + ( + getattr(self.termination_a, 'rear_port', None) == self.termination_b or + getattr(self.termination_b, 'rear_port', None) == self.termination_a + ) + ): + raise ValidationError("A front port cannot be connected to it corresponding rear port") + + # Check for an existing Cable connected to either termination object + if self.termination_a.cable not in (None, self): + raise ValidationError("{} already has a cable attached (#{})".format( + self.termination_a, self.termination_a.cable_id + )) + if self.termination_b.cable not in (None, self): + raise ValidationError("{} already has a cable attached (#{})".format( + self.termination_b, self.termination_b.cable_id + )) + + # Virtual interfaces cannot be connected + endpoint_a, endpoint_b, _ = self.get_path_endpoints() + if ( + ( + isinstance(endpoint_a, Interface) and + endpoint_a.form_factor == IFACE_FF_VIRTUAL + ) or + ( + isinstance(endpoint_b, Interface) and + endpoint_b.form_factor == IFACE_FF_VIRTUAL + ) + ): + raise ValidationError("Cannot connect to a virtual interface") # Validate length and length_unit if self.length is not None and self.length_unit is None: From d356e288a2cf8f868b337eb7031821c8aa1925a5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 11 Jan 2019 10:30:35 -0500 Subject: [PATCH 2/6] Expanded the bug report template --- .github/ISSUE_TEMPLATE/bug_report.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4bf6fb9c6..b01cb6dc8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -17,15 +17,20 @@ about: Report a reproducible bug in the current release of NetBox --> ### Environment * Python version: -* NetBox version: +* NetBox version: ### Steps to Reproduce - +1. +2. +3. ### Expected Behavior From 28a2981a4fa4dbbc7405dc697a92fa8c95876f0b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 11 Jan 2019 11:27:59 -0500 Subject: [PATCH 3/6] Release v2.5.3 --- CHANGELOG.md | 2 +- netbox/netbox/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e98477d4..5b1dcf69e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -v2.5.3 (FUTURE) +v2.5.3 (2019-01-11) ## Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e50e9bd72..029f1a6c5 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ except ImportError: ) -VERSION = '2.5.3-dev' +VERSION = '2.5.3' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 9e492cbb4d27823364ef7ec941b3672fa9fb2071 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 11 Jan 2019 11:35:06 -0500 Subject: [PATCH 4/6] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 029f1a6c5..63e4a4207 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ except ImportError: ) -VERSION = '2.5.3' +VERSION = '2.5.4-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 4a290f3834e3947fad8e1f9c484a580a412f0916 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 15 Jan 2019 10:44:11 -0500 Subject: [PATCH 5/6] Fixes #2783: Fix AttributeError exception when attempting to delete region(s) --- CHANGELOG.md | 8 ++++++++ netbox/dcim/views.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b1dcf69e..2cd8d314a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +v2.5.4 (FUTURE) + +## Bug Fixes + +* [#2783](https://github.com/digitalocean/netbox/issues/2783) - Fix AttributeError exception when attempting to delete region(s) + +--- + v2.5.3 (2019-01-11) ## Enhancements diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 4c2dee5df..42b71c7af 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -162,7 +162,7 @@ class RegionBulkImportView(PermissionRequiredMixin, BulkImportView): class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_region' - queryset = Region.objects.annotate(site_count=Count('sites')) + queryset = Region.objects.all() filter = filters.RegionFilter table = tables.RegionTable default_return_url = 'dcim:region_list' From 1d7a7e2d1d0aade580c3bb90db309e06f0491e05 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 15 Jan 2019 11:26:41 -0500 Subject: [PATCH 6/6] Fixes #2779: Include "none" option when filter IP addresses by role --- CHANGELOG.md | 1 + netbox/ipam/forms.py | 1 + netbox/utilities/forms.py | 25 ++++++++++++++++++++----- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd8d314a..3b356cc46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ v2.5.4 (FUTURE) ## Bug Fixes +* [#2779](https://github.com/digitalocean/netbox/issues/2779) - Include "none" option when filter IP addresses by role * [#2783](https://github.com/digitalocean/netbox/issues/2783) - Fix AttributeError exception when attempting to delete region(s) --- diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index b6209f5df..26d64d9b3 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -913,6 +913,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): choices=IPADDRESS_ROLE_CHOICES, annotate=IPAddress.objects.all(), annotate_field='role', + include_null=True, required=False ) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index b531fa637..010db31e1 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -538,6 +538,8 @@ class AnnotatedMultipleChoiceField(forms.MultipleChoiceField): """ def annotate_choices(self): + + # Aggregate objects by choice field values queryset = self.annotate.values( self.annotate_field ).annotate( @@ -548,18 +550,31 @@ class AnnotatedMultipleChoiceField(forms.MultipleChoiceField): choice_counts = { c[self.annotate_field]: c['count'] for c in queryset } - annotated_choices = [ - (c[0], '{} ({})'.format(c[1], choice_counts.get(c[0], 0))) for c in self.static_choices - ] + + annotated_choices = [] + + # Optionally add a "none" choice + if self.include_null: + annotated_choices.append(( + settings.FILTERS_NULL_CHOICE_VALUE, + '-- {} --'.format(settings.FILTERS_NULL_CHOICE_LABEL) + )) + + # Append each choice and its annotated count + for c in self.static_choices: + annotated_choices.append( + (c[0], '{} ({})'.format(c[1], choice_counts.get(c[0], 0))) + ) return annotated_choices - def __init__(self, choices, annotate, annotate_field, *args, **kwargs): + def __init__(self, choices, annotate, annotate_field, include_null=False, **kwargs): self.annotate = annotate self.annotate_field = annotate_field + self.include_null = include_null self.static_choices = unpack_grouped_choices(choices) - super().__init__(choices=self.annotate_choices, *args, **kwargs) + super().__init__(choices=self.annotate_choices, **kwargs) class LaxURLField(forms.URLField):