From f1d5e28f13d3c8f7fe694860004b7b1eb169a717 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Fri, 10 Jan 2020 14:26:39 +0000 Subject: [PATCH 001/174] CSV import/export custom fields --- netbox/circuits/forms.py | 6 ++--- netbox/dcim/forms.py | 28 ++++++++++++------------ netbox/ipam/forms.py | 16 +++++++------- netbox/secrets/forms.py | 4 ++-- netbox/tenancy/forms.py | 4 ++-- netbox/utilities/templatetags/helpers.py | 3 +++ netbox/utilities/views.py | 17 ++++++++++++-- netbox/virtualization/forms.py | 8 +++---- 8 files changed, 51 insertions(+), 35 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 4a5c06a6e..c5decc9e7 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -46,7 +46,7 @@ class ProviderForm(BootstrapMixin, CustomFieldForm): } -class ProviderCSVForm(forms.ModelForm): +class ProviderCSVForm(CustomFieldForm): slug = SlugField() class Meta: @@ -144,7 +144,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm): ] -class CircuitTypeCSVForm(forms.ModelForm): +class CircuitTypeCSVForm(CustomFieldForm): slug = SlugField() class Meta: @@ -187,7 +187,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class CircuitCSVForm(forms.ModelForm): +class CircuitCSVForm(CustomFieldForm): provider = forms.ModelChoiceField( queryset=Provider.objects.all(), to_field_name='name', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6086491d0..5f10752bc 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -151,7 +151,7 @@ class RegionForm(BootstrapMixin, forms.ModelForm): } -class RegionCSVForm(forms.ModelForm): +class RegionCSVForm(CustomFieldForm): parent = forms.ModelChoiceField( queryset=Region.objects.all(), required=False, @@ -231,7 +231,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class SiteCSVForm(forms.ModelForm): +class SiteCSVForm(CustomFieldForm): status = CSVChoiceField( choices=SITE_STATUS_CHOICES, required=False, @@ -355,7 +355,7 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm): } -class RackGroupCSVForm(forms.ModelForm): +class RackGroupCSVForm(CustomFieldForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -411,7 +411,7 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm): ] -class RackRoleCSVForm(forms.ModelForm): +class RackRoleCSVForm(CustomFieldForm): slug = SlugField() class Meta: @@ -472,7 +472,7 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class RackCSVForm(forms.ModelForm): +class RackCSVForm(CustomFieldForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -852,7 +852,7 @@ class ManufacturerForm(BootstrapMixin, forms.ModelForm): ] -class ManufacturerCSVForm(forms.ModelForm): +class ManufacturerCSVForm(CustomFieldForm): class Meta: model = Manufacturer @@ -890,7 +890,7 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm): } -class DeviceTypeCSVForm(forms.ModelForm): +class DeviceTypeCSVForm(CustomFieldForm): manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), required=True, @@ -1308,7 +1308,7 @@ class DeviceRoleForm(BootstrapMixin, forms.ModelForm): ] -class DeviceRoleCSVForm(forms.ModelForm): +class DeviceRoleCSVForm(CustomFieldForm): slug = SlugField() class Meta: @@ -1342,7 +1342,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm): } -class PlatformCSVForm(forms.ModelForm): +class PlatformCSVForm(CustomFieldForm): slug = SlugField() manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), @@ -1564,7 +1564,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.initial['rack'] = self.instance.parent_bay.device.rack_id -class BaseDeviceCSVForm(forms.ModelForm): +class BaseDeviceCSVForm(CustomFieldForm): device_role = forms.ModelChoiceField( queryset=DeviceRole.objects.all(), to_field_name='name', @@ -2919,7 +2919,7 @@ class CableForm(BootstrapMixin, forms.ModelForm): ] -class CableCSVForm(forms.ModelForm): +class CableCSVForm(CustomFieldForm): # Termination A side_a_device = FlexibleModelChoiceField( @@ -3294,7 +3294,7 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm): } -class InventoryItemCSVForm(forms.ModelForm): +class InventoryItemCSVForm(CustomFieldForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -3623,7 +3623,7 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm): } -class PowerPanelCSVForm(forms.ModelForm): +class PowerPanelCSVForm(CustomFieldForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -3747,7 +3747,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldForm): self.initial['site'] = self.instance.power_panel.site -class PowerFeedCSVForm(forms.ModelForm): +class PowerFeedCSVForm(CustomFieldForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 413e72eaf..46346cf9c 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -48,7 +48,7 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class VRFCSVForm(forms.ModelForm): +class VRFCSVForm(CustomFieldForm): tenant = forms.ModelChoiceField( queryset=Tenant.objects.all(), required=False, @@ -118,7 +118,7 @@ class RIRForm(BootstrapMixin, forms.ModelForm): ] -class RIRCSVForm(forms.ModelForm): +class RIRCSVForm(CustomFieldForm): slug = SlugField() class Meta: @@ -165,7 +165,7 @@ class AggregateForm(BootstrapMixin, CustomFieldForm): } -class AggregateCSVForm(forms.ModelForm): +class AggregateCSVForm(CustomFieldForm): rir = forms.ModelChoiceField( queryset=RIR.objects.all(), to_field_name='name', @@ -247,7 +247,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm): ] -class RoleCSVForm(forms.ModelForm): +class RoleCSVForm(CustomFieldForm): slug = SlugField() class Meta: @@ -340,7 +340,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.fields['vrf'].empty_label = 'Global' -class PrefixCSVForm(forms.ModelForm): +class PrefixCSVForm(CustomFieldForm): vrf = FlexibleModelChoiceField( queryset=VRF.objects.all(), to_field_name='rd', @@ -759,7 +759,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.fields['vrf'].empty_label = 'Global' -class IPAddressCSVForm(forms.ModelForm): +class IPAddressCSVForm(CustomFieldForm): vrf = FlexibleModelChoiceField( queryset=VRF.objects.all(), to_field_name='rd', @@ -1025,7 +1025,7 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm): } -class VLANGroupCSVForm(forms.ModelForm): +class VLANGroupCSVForm(CustomFieldForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, @@ -1122,7 +1122,7 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class VLANCSVForm(forms.ModelForm): +class VLANCSVForm(CustomFieldForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index ed0f455c1..47fb27bbb 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -50,7 +50,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm): } -class SecretRoleCSVForm(forms.ModelForm): +class SecretRoleCSVForm(CustomFieldForm): slug = SlugField() class Meta: @@ -113,7 +113,7 @@ class SecretForm(BootstrapMixin, CustomFieldForm): }) -class SecretCSVForm(forms.ModelForm): +class SecretCSVForm(CustomFieldForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index f8aaa45e5..77f8305f3 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -23,7 +23,7 @@ class TenantGroupForm(BootstrapMixin, forms.ModelForm): ] -class TenantGroupCSVForm(forms.ModelForm): +class TenantGroupCSVForm(CustomFieldForm): slug = SlugField() class Meta: @@ -57,7 +57,7 @@ class TenantForm(BootstrapMixin, CustomFieldForm): } -class TenantCSVForm(forms.ModelForm): +class TenantCSVForm(CustomFieldForm): slug = SlugField() group = forms.ModelChoiceField( queryset=TenantGroup.objects.all(), diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 7b1e059a6..a13a5f2b0 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -149,6 +149,9 @@ def example_choices(field, arg=3): break if not value or not label: continue + # Handling for custom fields + if hasattr(label, 'value'): + label = label.value examples.append(label) return ', '.join(examples) or 'None' diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 1aa358fba..96be35130 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -78,15 +78,28 @@ class ObjectListView(View): Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method. """ csv_data = [] + custom_fields = [] # Start with the column headers headers = ','.join(self.queryset.model.csv_headers) + + # Add custom field headers + content_type = ContentType.objects.get_for_model(self.queryset.model) + + for custom_field in CustomField.objects.filter(obj_type=content_type): + headers += ',cf_{}'.format(custom_field.name) + custom_fields.append(custom_field.name) + csv_data.append(headers) # Iterate through the queryset appending each object for obj in self.queryset: - data = csv_format(obj.to_csv()) - csv_data.append(data) + data = obj.to_csv() + + for custom_field in custom_fields: + data += (obj.cf.get(custom_field, ''),) + + csv_data.append(csv_format(data)) return csv_data diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 427e676f6..90ba2e123 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -37,7 +37,7 @@ class ClusterTypeForm(BootstrapMixin, forms.ModelForm): ] -class ClusterTypeCSVForm(forms.ModelForm): +class ClusterTypeCSVForm(CustomFieldForm): slug = SlugField() class Meta: @@ -62,7 +62,7 @@ class ClusterGroupForm(BootstrapMixin, forms.ModelForm): ] -class ClusterGroupCSVForm(forms.ModelForm): +class ClusterGroupCSVForm(CustomFieldForm): slug = SlugField() class Meta: @@ -101,7 +101,7 @@ class ClusterForm(BootstrapMixin, CustomFieldForm): } -class ClusterCSVForm(forms.ModelForm): +class ClusterCSVForm(CustomFieldForm): type = forms.ModelChoiceField( queryset=ClusterType.objects.all(), to_field_name='name', @@ -416,7 +416,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.fields['primary_ip6'].widget.attrs['readonly'] = True -class VirtualMachineCSVForm(forms.ModelForm): +class VirtualMachineCSVForm(CustomFieldForm): status = CSVChoiceField( choices=VM_STATUS_CHOICES, required=False, From 37322fc1006e0f267c592408c2f2445068617cfb Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Fri, 10 Jan 2020 14:58:15 +0000 Subject: [PATCH 002/174] Fixed import choice name --- netbox/extras/forms.py | 6 +++--- netbox/utilities/forms.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 4f7f57fff..efb33905c 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -10,8 +10,8 @@ from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, - CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, LaxURLField, JSONField, - SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, + CSVCustomFieldChoiceField, CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, + LaxURLField, JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, ) from .constants import * from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag @@ -71,7 +71,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F default_choice = cf.choices.get(value=initial).pk except ObjectDoesNotExist: pass - field = forms.TypedChoiceField( + field = CSVCustomFieldChoiceField( choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2() ) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index eeee719ae..a98f29a8e 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -469,6 +469,23 @@ class CSVChoiceField(forms.ChoiceField): return self.choice_values[value] +class CSVCustomFieldChoiceField(forms.TypedChoiceField): + """ + Invert the provided set of choices to take the human-friendly label as input, and return the database value. + """ + + def __init__(self, choices, *args, **kwargs): + super().__init__(choices=choices, *args, **kwargs) + self.choice_values = {str(label): value for value, label in unpack_grouped_choices(choices)} + + def clean(self, value): + if not value: + return None + if value in self.choice_values: + return self.choice_values[value] + return super().clean(value) + + class ExpandableNameField(forms.CharField): """ A field which allows for numeric range expansion From de1355e6bc6c42f10dd135c89478322a924073a4 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Fri, 10 Jan 2020 15:00:57 +0000 Subject: [PATCH 003/174] Changelog #568 --- docs/release-notes/version-2.6.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 52b682746..6f4019460 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -2,6 +2,7 @@ ## Enhancements +* [#568](https://github.com/netbox-community/netbox/issues/568) - Allow custom fields to be imported and exported using CSV * [#1982](https://github.com/netbox-community/netbox/issues/1982) - Improved NAPALM method documentation in Swagger * [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link * [#2113](https://github.com/netbox-community/netbox/issues/2113) - Allow NAPALM driver settings to be changed with request headers From 834fd408bdde93d1600ae3c73f466b732465bcd5 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Sat, 11 Jan 2020 15:18:27 +0000 Subject: [PATCH 004/174] Fixes #2921: Replace tags filter with Select2 widget --- docs/release-notes/version-2.6.md | 1 + netbox/templates/circuits/circuit_list.html | 1 - netbox/templates/circuits/provider_list.html | 1 - netbox/templates/dcim/device_list.html | 1 - netbox/templates/dcim/devicetype_list.html | 1 - netbox/templates/dcim/powerfeed_list.html | 1 - netbox/templates/dcim/rack_list.html | 1 - netbox/templates/dcim/site_list.html | 1 - .../templates/dcim/virtualchassis_list.html | 1 - netbox/templates/inc/tags_panel.html | 13 ----------- netbox/templates/ipam/aggregate_list.html | 1 - netbox/templates/ipam/ipaddress_list.html | 1 - netbox/templates/ipam/prefix_list.html | 1 - netbox/templates/ipam/service_list.html | 1 - netbox/templates/ipam/vlan_list.html | 1 - netbox/templates/ipam/vrf_list.html | 1 - netbox/templates/secrets/secret_list.html | 1 - netbox/templates/tenancy/tenant_list.html | 1 - .../virtualization/cluster_list.html | 1 - .../virtualization/virtualmachine_list.html | 1 - netbox/utilities/views.py | 22 ++++++++++++------- 21 files changed, 15 insertions(+), 39 deletions(-) delete mode 100644 netbox/templates/inc/tags_panel.html diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 88cd9c120..c313d7e1b 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -6,6 +6,7 @@ * [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link * [#2113](https://github.com/netbox-community/netbox/issues/2113) - Allow NAPALM driver settings to be changed with request headers * [#2589](https://github.com/netbox-community/netbox/issues/2589) - Toggle for showing available prefixes/ip addresses +* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget * [#3009](https://github.com/netbox-community/netbox/issues/3009) - Search by description when assigning IP address * [#3090](https://github.com/netbox-community/netbox/issues/3090) - Add filter field for device interfaces * [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations diff --git a/netbox/templates/circuits/circuit_list.html b/netbox/templates/circuits/circuit_list.html index d686bdf7a..169aab072 100644 --- a/netbox/templates/circuits/circuit_list.html +++ b/netbox/templates/circuits/circuit_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/circuits/provider_list.html b/netbox/templates/circuits/provider_list.html index e4ee7fb2b..4126f75ec 100644 --- a/netbox/templates/circuits/provider_list.html +++ b/netbox/templates/circuits/provider_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html index 623d69aa2..8b991689f 100644 --- a/netbox/templates/dcim/device_list.html +++ b/netbox/templates/dcim/device_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/devicetype_list.html b/netbox/templates/dcim/devicetype_list.html index 3b8988ed8..75f587f5d 100644 --- a/netbox/templates/dcim/devicetype_list.html +++ b/netbox/templates/dcim/devicetype_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/powerfeed_list.html b/netbox/templates/dcim/powerfeed_list.html index cfe2c989c..e384cb2c2 100644 --- a/netbox/templates/dcim/powerfeed_list.html +++ b/netbox/templates/dcim/powerfeed_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/rack_list.html b/netbox/templates/dcim/rack_list.html index 72da3048e..2724e4427 100644 --- a/netbox/templates/dcim/rack_list.html +++ b/netbox/templates/dcim/rack_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/site_list.html b/netbox/templates/dcim/site_list.html index 64948a6f9..ef9e0e411 100644 --- a/netbox/templates/dcim/site_list.html +++ b/netbox/templates/dcim/site_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/virtualchassis_list.html b/netbox/templates/dcim/virtualchassis_list.html index 8c26f3c3e..55cfc1691 100644 --- a/netbox/templates/dcim/virtualchassis_list.html +++ b/netbox/templates/dcim/virtualchassis_list.html @@ -13,7 +13,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/inc/tags_panel.html b/netbox/templates/inc/tags_panel.html deleted file mode 100644 index a7923fbed..000000000 --- a/netbox/templates/inc/tags_panel.html +++ /dev/null @@ -1,13 +0,0 @@ -{% load helpers %} - -
-
- - Tags -
-
- {% for tag in tags %} - {{ tag }} {{ tag.count }} - {% endfor %} -
-
diff --git a/netbox/templates/ipam/aggregate_list.html b/netbox/templates/ipam/aggregate_list.html index aad747b2d..27363a56d 100644 --- a/netbox/templates/ipam/aggregate_list.html +++ b/netbox/templates/ipam/aggregate_list.html @@ -17,7 +17,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
Statistics diff --git a/netbox/templates/ipam/ipaddress_list.html b/netbox/templates/ipam/ipaddress_list.html index 12f227301..b7920a434 100644 --- a/netbox/templates/ipam/ipaddress_list.html +++ b/netbox/templates/ipam/ipaddress_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/prefix_list.html b/netbox/templates/ipam/prefix_list.html index b80af8e1d..f0754d37b 100644 --- a/netbox/templates/ipam/prefix_list.html +++ b/netbox/templates/ipam/prefix_list.html @@ -21,7 +21,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/service_list.html b/netbox/templates/ipam/service_list.html index a39bec22e..4aac520d9 100644 --- a/netbox/templates/ipam/service_list.html +++ b/netbox/templates/ipam/service_list.html @@ -12,7 +12,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/vlan_list.html b/netbox/templates/ipam/vlan_list.html index b4d313a8c..24d538f88 100644 --- a/netbox/templates/ipam/vlan_list.html +++ b/netbox/templates/ipam/vlan_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/vrf_list.html b/netbox/templates/ipam/vrf_list.html index 566e2f3e6..975c73a37 100644 --- a/netbox/templates/ipam/vrf_list.html +++ b/netbox/templates/ipam/vrf_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/secrets/secret_list.html b/netbox/templates/secrets/secret_list.html index b6d792765..ee631b439 100644 --- a/netbox/templates/secrets/secret_list.html +++ b/netbox/templates/secrets/secret_list.html @@ -15,7 +15,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/tenancy/tenant_list.html b/netbox/templates/tenancy/tenant_list.html index 91463c52c..a77636a5b 100644 --- a/netbox/templates/tenancy/tenant_list.html +++ b/netbox/templates/tenancy/tenant_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/virtualization/cluster_list.html b/netbox/templates/virtualization/cluster_list.html index 3fef90c03..6f5f058ad 100644 --- a/netbox/templates/virtualization/cluster_list.html +++ b/netbox/templates/virtualization/cluster_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine_list.html b/netbox/templates/virtualization/virtualmachine_list.html index b10341547..821f956a2 100644 --- a/netbox/templates/virtualization/virtualmachine_list.html +++ b/netbox/templates/virtualization/virtualmachine_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 1aa358fba..525fd92a9 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError from django.db import transaction, IntegrityError from django.db.models import Count, ProtectedError from django.db.models.query import QuerySet -from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea +from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleChoiceField, MultipleHiddenInput, Textarea from django.http import HttpResponse, HttpResponseServerError from django.shortcuts import get_object_or_404, redirect, render from django.template import loader @@ -24,7 +24,7 @@ from django_tables2 import RequestConfig from extras.models import CustomField, CustomFieldValue, ExportTemplate from extras.querysets import CustomFieldQueryset -from utilities.forms import BootstrapMixin, CSVDataField +from utilities.forms import BootstrapMixin, CSVDataField, StaticSelect2Multiple from utilities.utils import csv_format from .error_handlers import handle_protectederror from .forms import ConfirmationForm @@ -94,6 +94,7 @@ class ObjectListView(View): model = self.queryset.model content_type = ContentType.objects.get_for_model(model) + filter_form = self.filter_form(request.GET, label_suffix='') if self.filter_form else None if self.filter: self.queryset = self.filter(request.GET, self.queryset).qs @@ -142,11 +143,17 @@ class ObjectListView(View): if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']): table.columns.show('pk') - # Construct queryset for tags list - if hasattr(model, 'tags'): + # Add the tags filter field to the from if the model has tags + if hasattr(model, 'tags') and filter_form: tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name') - else: - tags = None + choices = [(str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags] + + filter_form.fields['tag'] = MultipleChoiceField( + label='Tags', + choices=choices, + required=False, + widget=StaticSelect2Multiple(), + ) # Apply the request context paginate = { @@ -159,8 +166,7 @@ class ObjectListView(View): 'content_type': content_type, 'table': table, 'permissions': permissions, - 'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None, - 'tags': tags, + 'filter_form': filter_form, } context.update(self.extra_context()) From a8d9fe799b819c2866c3868078a92ae51340b210 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Mon, 13 Jan 2020 19:06:05 +0000 Subject: [PATCH 005/174] Removed tags filter field from view --- netbox/utilities/views.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 525fd92a9..ffe6f78a9 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -6,9 +6,9 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import transaction, IntegrityError -from django.db.models import Count, ProtectedError +from django.db.models import ProtectedError from django.db.models.query import QuerySet -from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleChoiceField, MultipleHiddenInput, Textarea +from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea from django.http import HttpResponse, HttpResponseServerError from django.shortcuts import get_object_or_404, redirect, render from django.template import loader @@ -24,7 +24,7 @@ from django_tables2 import RequestConfig from extras.models import CustomField, CustomFieldValue, ExportTemplate from extras.querysets import CustomFieldQueryset -from utilities.forms import BootstrapMixin, CSVDataField, StaticSelect2Multiple +from utilities.forms import BootstrapMixin, CSVDataField from utilities.utils import csv_format from .error_handlers import handle_protectederror from .forms import ConfirmationForm @@ -94,7 +94,6 @@ class ObjectListView(View): model = self.queryset.model content_type = ContentType.objects.get_for_model(model) - filter_form = self.filter_form(request.GET, label_suffix='') if self.filter_form else None if self.filter: self.queryset = self.filter(request.GET, self.queryset).qs @@ -143,18 +142,6 @@ class ObjectListView(View): if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']): table.columns.show('pk') - # Add the tags filter field to the from if the model has tags - if hasattr(model, 'tags') and filter_form: - tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name') - choices = [(str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags] - - filter_form.fields['tag'] = MultipleChoiceField( - label='Tags', - choices=choices, - required=False, - widget=StaticSelect2Multiple(), - ) - # Apply the request context paginate = { 'paginator_class': EnhancedPaginator, @@ -166,7 +153,7 @@ class ObjectListView(View): 'content_type': content_type, 'table': table, 'permissions': permissions, - 'filter_form': filter_form, + 'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None, } context.update(self.extra_context()) From 2f28dec891ed83066dc7c7bd707af1d3ea770713 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Mon, 13 Jan 2020 20:16:13 +0000 Subject: [PATCH 006/174] Tag filter field for filter forms --- netbox/circuits/forms.py | 12 ++++++++++-- netbox/dcim/forms.py | 31 ++++++++++++++++++++++++++++++- netbox/ipam/forms.py | 26 +++++++++++++++++++++++++- netbox/secrets/forms.py | 6 +++++- netbox/tenancy/forms.py | 6 +++++- netbox/utilities/forms.py | 17 +++++++++++++++++ netbox/virtualization/forms.py | 10 +++++++++- 7 files changed, 101 insertions(+), 7 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 4a5c06a6e..4438dbc1c 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -6,8 +6,8 @@ from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEdit from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( - APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, - DatePicker, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple + APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker, + FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField ) from .constants import * from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -129,6 +129,10 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): label='ASN' ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # Circuit types @@ -333,6 +337,10 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm label='Commit rate (Kbps)' ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # Circuit terminations diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index f0b91c2f5..de7678b52 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -23,7 +23,8 @@ from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField, - SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES + SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, + BOOLEAN_WITH_BLANK_CHOICES ) from virtualization.models import Cluster, ClusterGroup from .constants import * @@ -335,6 +336,10 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # Rack groups @@ -713,6 +718,10 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # Rack elevations @@ -1005,6 +1014,10 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # Device component templates @@ -1947,6 +1960,10 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # Bulk device component creation @@ -3405,6 +3422,10 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # Virtual chassis @@ -3591,6 +3612,10 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # Power panels @@ -3967,3 +3992,7 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): max_utilization = forms.IntegerField( required=False ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index c3387a5aa..e64582b03 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -10,7 +10,7 @@ from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, - SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES + SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES ) from virtualization.models import VirtualMachine from .constants import * @@ -103,6 +103,10 @@ class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): label='Search' ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # RIRs @@ -232,6 +236,10 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm): ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # Roles @@ -578,6 +586,10 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) label='Expand prefix hierarchy' ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # IP addresses @@ -1006,6 +1018,10 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # VLAN groups @@ -1293,6 +1309,10 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # Services @@ -1353,6 +1373,10 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index ed0f455c1..8b8467f04 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -7,7 +7,7 @@ from dcim.models import Device from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldForm from utilities.forms import ( APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField, - StaticSelect2Multiple + StaticSelect2Multiple, TagFilterField ) from .models import Secret, SecretRole, UserKey @@ -185,6 +185,10 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm): ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # UserKeys diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index f8aaa45e5..f398b965a 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -4,7 +4,7 @@ from taggit.forms import TagField from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from utilities.forms import ( APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, - FilterChoiceField, SlugField, + FilterChoiceField, SlugField, TagFilterField ) from .models import Tenant, TenantGroup @@ -114,6 +114,10 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # Form extensions diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 39422c265..d1d19a6cb 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -6,6 +6,7 @@ from io import StringIO from django import forms from django.conf import settings from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput +from django.db.models import Count from mptt.forms import TreeNodeMultipleChoiceField from .constants import * @@ -596,6 +597,22 @@ class SlugField(forms.SlugField): self.widget.attrs['slug-source'] = slug_source +class TagFilterField(forms.MultipleChoiceField): + """ + A filter field for the tags of a model. Only the tags used by a model are displayed. + + :param model: The model of the filter + """ + widget = StaticSelect2Multiple + + def __init__(self, model, *args, **kwargs): + if hasattr(model, 'tags'): + tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name') + choices = [(str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags] + + super().__init__(label='Tags', choices=choices, required=False, *args, **kwargs) + + class FilterChoiceIterator(forms.models.ModelChoiceIterator): def __iter__(self): diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 427e676f6..36b84c7b1 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -13,7 +13,7 @@ from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, - SmallTextarea, StaticSelect2, StaticSelect2Multiple + SmallTextarea, StaticSelect2, StaticSelect2Multiple, TagFilterField ) from .constants import * from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -217,6 +217,10 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm): ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): region = forms.ModelChoiceField( @@ -623,6 +627,10 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil label='MAC address' ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tag'] = TagFilterField(self.model) + # # VM interfaces From 865e3e7c9f20dab7a994c662c7a210def5ad2c9e Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Mon, 13 Jan 2020 20:17:47 +0000 Subject: [PATCH 007/174] Updated changelog --- docs/release-notes/version-2.6.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index c313d7e1b..e3aa122e5 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,4 +1,12 @@ -# v2.6.12 (FUTURE) +# v2.6.13 (FUTURE) + +## Enhancements + +* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget + +--- + +# v2.6.12 (2020-01-13) ## Enhancements @@ -6,7 +14,6 @@ * [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link * [#2113](https://github.com/netbox-community/netbox/issues/2113) - Allow NAPALM driver settings to be changed with request headers * [#2589](https://github.com/netbox-community/netbox/issues/2589) - Toggle for showing available prefixes/ip addresses -* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget * [#3009](https://github.com/netbox-community/netbox/issues/3009) - Search by description when assigning IP address * [#3090](https://github.com/netbox-community/netbox/issues/3090) - Add filter field for device interfaces * [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations From e10333bf2bc90465cee4e00cef6ad8c9ce032949 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Tue, 14 Jan 2020 08:22:27 +0000 Subject: [PATCH 008/174] Fetch choices during form initialization --- netbox/circuits/forms.py | 10 ++-------- netbox/dcim/forms.py | 35 +++++++--------------------------- netbox/ipam/forms.py | 30 ++++++----------------------- netbox/secrets/forms.py | 5 +---- netbox/tenancy/forms.py | 5 +---- netbox/utilities/forms.py | 11 ++++++++--- netbox/virtualization/forms.py | 10 ++-------- 7 files changed, 27 insertions(+), 79 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 4438dbc1c..decb954d8 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -128,10 +128,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='ASN' ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -336,10 +333,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm min_value=0, label='Commit rate (Kbps)' ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 82dd99c3d..b9041e953 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -335,10 +335,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): value_field="slug", ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -717,10 +714,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): null_option=True, ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -1013,10 +1007,7 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -1959,10 +1950,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt choices=BOOLEAN_WITH_BLANK_CHOICES ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -3435,10 +3423,7 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -3625,10 +3610,7 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -4006,7 +3988,4 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): max_utilization = forms.IntegerField( required=False ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index e64582b03..46788e6be 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -102,10 +102,7 @@ class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): required=False, label='Search' ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -235,10 +232,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm): value_field="slug", ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -585,10 +579,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) required=False, label='Expand prefix hierarchy' ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -1017,10 +1008,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo choices=BOOLEAN_WITH_BLANK_CHOICES ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -1308,10 +1296,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): null_option=True, ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # @@ -1372,10 +1357,7 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm): port = forms.IntegerField( required=False, ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 8b8467f04..73ce55899 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -184,10 +184,7 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm): value_field="slug", ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index f398b965a..0acf377a7 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -113,10 +113,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index d1d19a6cb..2744b249f 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -606,11 +606,16 @@ class TagFilterField(forms.MultipleChoiceField): widget = StaticSelect2Multiple def __init__(self, model, *args, **kwargs): + # Only instanitate the field if the model supports tags (i.e. hide if not) if hasattr(model, 'tags'): - tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name') - choices = [(str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags] + self.model = model - super().__init__(label='Tags', choices=choices, required=False, *args, **kwargs) + # Choices are fetched during form initialization + super().__init__(label='Tags', choices=self._choices, required=False, *args, **kwargs) + + def _choices(self): + tags = self.model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name') + return [(str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags] class FilterChoiceIterator(forms.models.ModelChoiceIterator): diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 36b84c7b1..e26f21480 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -216,10 +216,7 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): @@ -626,10 +623,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil required=False, label='MAC address' ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['tag'] = TagFilterField(self.model) + tag = TagFilterField(model) # From 8f91e9b079b52121b5bf7617560faa8c7cdca773 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 16 Jan 2020 15:34:11 +0000 Subject: [PATCH 009/174] Added #2921 changelog --- docs/release-notes/version-2.6.md | 8 -------- docs/release-notes/version-2.7.md | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 62b78149f..9fd258b0f 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,11 +1,3 @@ -# v2.6.13 (FUTURE) - -## Enhancements - -* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget - ---- - # v2.6.12 (2020-01-13) ## Enhancements diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index ac9d81e2c..79dfe4967 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -226,6 +226,7 @@ PATCH) to maintain backward compatibility. This behavior will be discontinued be * [#1865](https://github.com/netbox-community/netbox/issues/1865) - Add console port and console server port types * [#2669](https://github.com/netbox-community/netbox/issues/2669) - Relax uniqueness constraint on device and VM names * [#2902](https://github.com/netbox-community/netbox/issues/2902) - Replace `supervisord` with `systemd` +* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget * [#3455](https://github.com/netbox-community/netbox/issues/3455) - Add tenant assignment to cluster * [#3520](https://github.com/netbox-community/netbox/issues/3520) - Add Jinja2 template support for Graphs * [#3525](https://github.com/netbox-community/netbox/issues/3525) - Enable IP address filtering with multiple address terms From 26ebed0182ad479f940e3dd7498aa0e2c3b4c3b6 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 16 Jan 2020 15:42:31 +0000 Subject: [PATCH 010/174] Removed legacy work regarding inc/tags_panel.html --- netbox/templates/dcim/device_component_list.html | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/templates/dcim/device_component_list.html b/netbox/templates/dcim/device_component_list.html index 3936a1c19..28322973e 100644 --- a/netbox/templates/dcim/device_component_list.html +++ b/netbox/templates/dcim/device_component_list.html @@ -14,7 +14,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} From a2d5aca1d9a9a3ff717c0eacda279464a6a9d7ea Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 16 Jan 2020 16:05:45 +0000 Subject: [PATCH 011/174] Moved changelog to v2.7 --- docs/release-notes/version-2.6.md | 8 -------- docs/release-notes/version-2.7.md | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 51a397b3c..9fd258b0f 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,11 +1,3 @@ -# v2.6.13 (FUTURE) - -## Enhancements - -* [#568](https://github.com/netbox-community/netbox/issues/568) - Allow custom fields to be imported and exported using CSV - ---- - # v2.6.12 (2020-01-13) ## Enhancements diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index ac9d81e2c..800389228 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -221,6 +221,7 @@ PATCH) to maintain backward compatibility. This behavior will be discontinued be ## Enhancements * [#33](https://github.com/netbox-community/netbox/issues/33) - Add ability to clone objects (pre-populate form fields) +* [#568](https://github.com/netbox-community/netbox/issues/568) - Allow custom fields to be imported and exported using CSV * [#648](https://github.com/netbox-community/netbox/issues/648) - Pre-populate forms when selecting "create and add another" * [#792](https://github.com/netbox-community/netbox/issues/792) - Add power port and power outlet types * [#1865](https://github.com/netbox-community/netbox/issues/1865) - Add console port and console server port types From 9f68f8d1a615b210a8c33c76a68802a6c6bec1ea Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 16 Jan 2020 16:07:24 +0000 Subject: [PATCH 012/174] Update component CSV forms --- netbox/dcim/forms.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index a768039cf..9c3332ff3 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2175,7 +2175,7 @@ class ConsolePortCreateForm(ComponentForm): ) -class ConsolePortCSVForm(forms.ModelForm): +class ConsolePortCSVForm(CustomFieldForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -2267,7 +2267,7 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm): ) -class ConsoleServerPortCSVForm(forms.ModelForm): +class ConsoleServerPortCSVForm(CustomFieldForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -2334,7 +2334,7 @@ class PowerPortCreateForm(ComponentForm): ) -class PowerPortCSVForm(forms.ModelForm): +class PowerPortCSVForm(CustomFieldForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -2419,7 +2419,7 @@ class PowerOutletCreateForm(ComponentForm): self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent) -class PowerOutletCSVForm(forms.ModelForm): +class PowerOutletCSVForm(CustomFieldForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -2668,7 +2668,7 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): self.fields['lag'].queryset = Interface.objects.none() -class InterfaceCSVForm(forms.ModelForm): +class InterfaceCSVForm(CustomFieldForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), required=False, @@ -2925,7 +2925,7 @@ class FrontPortCreateForm(ComponentForm): } -class FrontPortCSVForm(forms.ModelForm): +class FrontPortCSVForm(CustomFieldForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -3051,7 +3051,7 @@ class RearPortCreateForm(ComponentForm): ) -class RearPortCSVForm(forms.ModelForm): +class RearPortCSVForm(CustomFieldForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -3655,7 +3655,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form): ).exclude(pk=device_bay.device.pk) -class DeviceBayCSVForm(forms.ModelForm): +class DeviceBayCSVForm(CustomFieldForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', From e05cecb48182fe60faf52324bf79b929fb2082ba Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 16 Jan 2020 21:51:01 +0000 Subject: [PATCH 013/174] Moved into v2.7.1 --- docs/release-notes/version-2.7.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 79dfe4967..938baaf74 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -1,3 +1,11 @@ +# v2.7.1 (FUTURE) + +## Enhancements + +* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget + +--- + # v2.7.0 (FUTURE) **Note:** NetBox v2.7 is the last major release that will support Python 3.5. Beginning with NetBox v2.8, Python 3.6 or @@ -226,7 +234,6 @@ PATCH) to maintain backward compatibility. This behavior will be discontinued be * [#1865](https://github.com/netbox-community/netbox/issues/1865) - Add console port and console server port types * [#2669](https://github.com/netbox-community/netbox/issues/2669) - Relax uniqueness constraint on device and VM names * [#2902](https://github.com/netbox-community/netbox/issues/2902) - Replace `supervisord` with `systemd` -* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget * [#3455](https://github.com/netbox-community/netbox/issues/3455) - Add tenant assignment to cluster * [#3520](https://github.com/netbox-community/netbox/issues/3520) - Add Jinja2 template support for Graphs * [#3525](https://github.com/netbox-community/netbox/issues/3525) - Enable IP address filtering with multiple address terms From 1b26afdfbb5edc39c4a6a09e6862c855e89cb461 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 23 Jan 2020 14:26:04 +0000 Subject: [PATCH 014/174] Fixes #3935: Swagger DEFAULT_INFO --- netbox/netbox/settings.py | 1 + netbox/netbox/urls.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e5925184d..f747c3b1d 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -503,6 +503,7 @@ SWAGGER_SETTINGS = { 'utilities.custom_inspectors.IdInFilterInspector', 'drf_yasg.inspectors.CoreAPICompatInspector', ], + 'DEFAULT_INFO': 'netbox.urls.openapi_info', 'DEFAULT_MODEL_DEPTH': 1, 'DEFAULT_PAGINATOR_INSPECTORS': [ 'utilities.custom_inspectors.NullablePaginatorInspector', diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 6b6dfe22d..66ab982eb 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -9,14 +9,16 @@ from netbox.views import APIRootView, HomeView, SearchView from users.views import LoginView, LogoutView from .admin import admin_site +openapi_info = openapi.Info( + title="NetBox API", + default_version='v2', + description="API to access NetBox", + terms_of_service="https://github.com/netbox-community/netbox", + license=openapi.License(name="Apache v2 License"), +) + schema_view = get_schema_view( - openapi.Info( - title="NetBox API", - default_version='v2', - description="API to access NetBox", - terms_of_service="https://github.com/netbox-community/netbox", - license=openapi.License(name="Apache v2 License"), - ), + openapi_info, validators=['flex', 'ssv'], public=True, ) From 9128435113e35bc6941a71fd9c4ee93d8e973f87 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 23 Jan 2020 17:03:14 +0000 Subject: [PATCH 015/174] Removed CustomFieldForm class from models without custom fields --- netbox/circuits/forms.py | 2 +- netbox/dcim/forms.py | 34 +++++++++++++++++----------------- netbox/ipam/forms.py | 6 +++--- netbox/secrets/forms.py | 2 +- netbox/tenancy/forms.py | 2 +- netbox/virtualization/forms.py | 4 ++-- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 5416a055f..d14403815 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -144,7 +144,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm): ] -class CircuitTypeCSVForm(CustomFieldForm): +class CircuitTypeCSVForm(forms.ModelForm): slug = SlugField() class Meta: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 75a0992ec..a5e8a782f 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -184,7 +184,7 @@ class RegionForm(BootstrapMixin, forms.ModelForm): } -class RegionCSVForm(CustomFieldForm): +class RegionCSVForm(forms.ModelForm): parent = forms.ModelChoiceField( queryset=Region.objects.all(), required=False, @@ -388,7 +388,7 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm): } -class RackGroupCSVForm(CustomFieldForm): +class RackGroupCSVForm(forms.ModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -444,7 +444,7 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm): ] -class RackRoleCSVForm(CustomFieldForm): +class RackRoleCSVForm(forms.ModelForm): slug = SlugField() class Meta: @@ -882,7 +882,7 @@ class ManufacturerForm(BootstrapMixin, forms.ModelForm): ] -class ManufacturerCSVForm(CustomFieldForm): +class ManufacturerCSVForm(forms.ModelForm): class Meta: model = Manufacturer @@ -1458,7 +1458,7 @@ class DeviceRoleForm(BootstrapMixin, forms.ModelForm): ] -class DeviceRoleCSVForm(CustomFieldForm): +class DeviceRoleCSVForm(forms.ModelForm): slug = SlugField() class Meta: @@ -1492,7 +1492,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm): } -class PlatformCSVForm(CustomFieldForm): +class PlatformCSVForm(forms.ModelForm): slug = SlugField() manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), @@ -2179,7 +2179,7 @@ class ConsolePortCreateForm(ComponentForm): ) -class ConsolePortCSVForm(CustomFieldForm): +class ConsolePortCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -2271,7 +2271,7 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm): ) -class ConsoleServerPortCSVForm(CustomFieldForm): +class ConsoleServerPortCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -2338,7 +2338,7 @@ class PowerPortCreateForm(ComponentForm): ) -class PowerPortCSVForm(CustomFieldForm): +class PowerPortCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -2423,7 +2423,7 @@ class PowerOutletCreateForm(ComponentForm): self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent) -class PowerOutletCSVForm(CustomFieldForm): +class PowerOutletCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -2672,7 +2672,7 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): self.fields['lag'].queryset = Interface.objects.none() -class InterfaceCSVForm(CustomFieldForm): +class InterfaceCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), required=False, @@ -2929,7 +2929,7 @@ class FrontPortCreateForm(ComponentForm): } -class FrontPortCSVForm(CustomFieldForm): +class FrontPortCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -3055,7 +3055,7 @@ class RearPortCreateForm(ComponentForm): ) -class RearPortCSVForm(CustomFieldForm): +class RearPortCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -3365,7 +3365,7 @@ class CableForm(BootstrapMixin, forms.ModelForm): ] -class CableCSVForm(CustomFieldForm): +class CableCSVForm(forms.ModelForm): # Termination A side_a_device = FlexibleModelChoiceField( @@ -3659,7 +3659,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form): ).exclude(pk=device_bay.device.pk) -class DeviceBayCSVForm(CustomFieldForm): +class DeviceBayCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -3801,7 +3801,7 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm): } -class InventoryItemCSVForm(CustomFieldForm): +class InventoryItemCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -4130,7 +4130,7 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm): } -class PowerPanelCSVForm(CustomFieldForm): +class PowerPanelCSVForm(forms.ModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index d2efc2bcc..3bafad51b 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -118,7 +118,7 @@ class RIRForm(BootstrapMixin, forms.ModelForm): ] -class RIRCSVForm(CustomFieldForm): +class RIRCSVForm(forms.ModelForm): slug = SlugField() class Meta: @@ -247,7 +247,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm): ] -class RoleCSVForm(CustomFieldForm): +class RoleCSVForm(forms.ModelForm): slug = SlugField() class Meta: @@ -1026,7 +1026,7 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm): } -class VLANGroupCSVForm(CustomFieldForm): +class VLANGroupCSVForm(forms.ModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 0b2cb3cb4..bddb1c109 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -52,7 +52,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm): } -class SecretRoleCSVForm(CustomFieldForm): +class SecretRoleCSVForm(forms.ModelForm): slug = SlugField() class Meta: diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 77f8305f3..22deae434 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -23,7 +23,7 @@ class TenantGroupForm(BootstrapMixin, forms.ModelForm): ] -class TenantGroupCSVForm(CustomFieldForm): +class TenantGroupCSVForm(forms.ModelForm): slug = SlugField() class Meta: diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 457369851..27c22fa71 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -33,7 +33,7 @@ class ClusterTypeForm(BootstrapMixin, forms.ModelForm): ] -class ClusterTypeCSVForm(CustomFieldForm): +class ClusterTypeCSVForm(forms.ModelForm): slug = SlugField() class Meta: @@ -58,7 +58,7 @@ class ClusterGroupForm(BootstrapMixin, forms.ModelForm): ] -class ClusterGroupCSVForm(CustomFieldForm): +class ClusterGroupCSVForm(forms.ModelForm): slug = SlugField() class Meta: From 0ab19d723dc75975c1c5840d9d91abfbbaece950 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 23 Jan 2020 17:18:58 +0000 Subject: [PATCH 016/174] Moved the header join logic after the custom fields are added --- netbox/utilities/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 67af4371f..4801d61a3 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -91,16 +91,16 @@ class ObjectListView(View): custom_fields = [] # Start with the column headers - headers = ','.join(self.queryset.model.csv_headers) + headers = self.queryset.model.csv_headers.copy() # Add custom field headers content_type = ContentType.objects.get_for_model(self.queryset.model) for custom_field in CustomField.objects.filter(obj_type=content_type): - headers += ',cf_{}'.format(custom_field.name) + headers.append(custom_field.name) custom_fields.append(custom_field.name) - csv_data.append(headers) + csv_data.append(','.join(headers)) # Iterate through the queryset appending each object for obj in self.queryset: From 0a5eecd0e3114043f4939a4e473c65eaf762b88a Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 23 Jan 2020 17:37:51 +0000 Subject: [PATCH 017/174] Explicitly use the value of the choice, instead of relying on __str__ --- netbox/extras/forms.py | 2 +- netbox/utilities/templatetags/helpers.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 17ae6c907..6aa0e3552 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -61,7 +61,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F # Select elif cf.type == CustomFieldTypeChoices.TYPE_SELECT: - choices = [(cfc.pk, cfc) for cfc in cf.choices.all()] + choices = [(cfc.pk, cfc.value) for cfc in cf.choices.all()] if not cf.required or bulk_edit or filterable_only: choices = [(None, '---------')] + choices # Check for a default choice diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 19a26b3c9..c4b3bb6ea 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -149,9 +149,6 @@ def example_choices(field, arg=3): break if not value or not label: continue - # Handling for custom fields - if hasattr(label, 'value'): - label = label.value examples.append(label) return ', '.join(examples) or 'None' From 8f86244b4fe0c38134022a90f80151f6e446fcd9 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 23 Jan 2020 18:54:37 +0000 Subject: [PATCH 018/174] Cleaned the CustomField choice field --- netbox/extras/forms.py | 4 ++-- netbox/utilities/forms.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 6aa0e3552..314455b83 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -10,7 +10,7 @@ from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, - CSVCustomFieldChoiceField, CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, + CustomFieldChoiceField, CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, ) from .choices import * @@ -71,7 +71,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F default_choice = cf.choices.get(value=initial).pk except ObjectDoesNotExist: pass - field = CSVCustomFieldChoiceField( + field = CustomFieldChoiceField( choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2() ) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 453561303..1dd2c06a7 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -442,18 +442,18 @@ class CSVChoiceField(forms.ChoiceField): return self.choice_values[value] -class CSVCustomFieldChoiceField(forms.TypedChoiceField): +class CustomFieldChoiceField(forms.TypedChoiceField): """ - Invert the provided set of choices to take the human-friendly label as input, and return the database value. + Accept human-friendly label as input, and return the database value. If the label is not matched, the normal, + value-based input is assumed. """ def __init__(self, choices, *args, **kwargs): super().__init__(choices=choices, *args, **kwargs) - self.choice_values = {str(label): value for value, label in unpack_grouped_choices(choices)} + self.choice_values = {label: value for value, label in unpack_grouped_choices(choices)} def clean(self, value): - if not value: - return None + # Check if the value is actually a label if value in self.choice_values: return self.choice_values[value] return super().clean(value) From bed08a7b07eccbb6797cfe7ee29dbb38a1a1976e Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 23 Jan 2020 20:26:21 +0000 Subject: [PATCH 019/174] Use model's `get_custom_fields` --- netbox/utilities/views.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 4801d61a3..d900a8545 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -93,12 +93,11 @@ class ObjectListView(View): # Start with the column headers headers = self.queryset.model.csv_headers.copy() - # Add custom field headers - content_type = ContentType.objects.get_for_model(self.queryset.model) - - for custom_field in CustomField.objects.filter(obj_type=content_type): - headers.append(custom_field.name) - custom_fields.append(custom_field.name) + # Add custom field headers, if any + if hasattr(self.queryset.model, 'get_custom_fields'): + for custom_field in self.queryset.model().get_custom_fields(): + headers.append(custom_field.name) + custom_fields.append(custom_field.name) csv_data.append(','.join(headers)) From eb4c2e5d7f94aff1e190acab04975e1b85e135a8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 24 Jan 2020 16:00:20 -0500 Subject: [PATCH 020/174] Remove obsolete fixtures files --- netbox/dcim/fixtures/dcim.json | 5790 ----------------- netbox/extras/fixtures/extras.json | 35 - netbox/ipam/fixtures/ipam.json | 329 - .../fixtures/virtualization.json | 170 - 4 files changed, 6324 deletions(-) delete mode 100644 netbox/dcim/fixtures/dcim.json delete mode 100644 netbox/extras/fixtures/extras.json delete mode 100644 netbox/ipam/fixtures/ipam.json delete mode 100644 netbox/virtualization/fixtures/virtualization.json diff --git a/netbox/dcim/fixtures/dcim.json b/netbox/dcim/fixtures/dcim.json deleted file mode 100644 index 2b379b9ff..000000000 --- a/netbox/dcim/fixtures/dcim.json +++ /dev/null @@ -1,5790 +0,0 @@ -[ -{ - "model": "dcim.site", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "name": "TEST1", - "slug": "test1", - "facility": "Test Facility", - "asn": 65535, - "physical_address": "555 Test Ave.\r\nTest, NY 55555", - "shipping_address": "", - "comments": "" - } -}, -{ - "model": "dcim.rack", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "name": "A1R1", - "facility_id": "T23A01", - "site": 1, - "group": null, - "u_height": 42, - "comments": "" - } -}, -{ - "model": "dcim.rack", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "name": "A1R2", - "facility_id": "T24A01", - "site": 1, - "group": null, - "u_height": 42, - "comments": "" - } -}, -{ - "model": "dcim.manufacturer", - "pk": 1, - "fields": { - "name": "Juniper", - "slug": "juniper" - } -}, -{ - "model": "dcim.manufacturer", - "pk": 2, - "fields": { - "name": "Opengear", - "slug": "opengear" - } -}, -{ - "model": "dcim.manufacturer", - "pk": 3, - "fields": { - "name": "ServerTech", - "slug": "servertech" - } -}, -{ - "model": "dcim.manufacturer", - "pk": 4, - "fields": { - "name": "Dell", - "slug": "dell" - } -}, -{ - "model": "dcim.devicetype", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "MX960", - "slug": "mx960", - "u_height": 16, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "EX9214", - "slug": "ex9214", - "u_height": 16, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 3, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "QFX5100-24Q", - "slug": "qfx5100-24q", - "u_height": 1, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 4, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 1, - "model": "QFX5100-48S", - "slug": "qfx5100-48s", - "u_height": 1, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 5, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 2, - "model": "CM4148", - "slug": "cm4148", - "u_height": 1, - "is_full_depth": true - } -}, -{ - "model": "dcim.devicetype", - "pk": 6, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 3, - "model": "CWG-24VYM415C9", - "slug": "cwg-24vym415c9", - "u_height": 0, - "is_full_depth": false - } -}, -{ - "model": "dcim.devicetype", - "pk": 7, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "manufacturer": 4, - "model": "PowerEdge R640", - "slug": "poweredge-r640", - "u_height": 1, - "is_full_depth": false - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 1, - "fields": { - "device_type": 1, - "name": "Console (RE0)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 2, - "fields": { - "device_type": 1, - "name": "Console (RE1)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 3, - "fields": { - "device_type": 2, - "name": "Console (RE0)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 4, - "fields": { - "device_type": 2, - "name": "Console (RE1)" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 5, - "fields": { - "device_type": 3, - "name": "Console" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 6, - "fields": { - "device_type": 5, - "name": "Console" - } -}, -{ - "model": "dcim.consoleporttemplate", - "pk": 7, - "fields": { - "device_type": 6, - "name": "Serial" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 1, - "fields": { - "device_type": 3, - "name": "Console" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 3, - "fields": { - "device_type": 4, - "name": "Console" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 4, - "fields": { - "device_type": 5, - "name": "Port 1" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 5, - "fields": { - "device_type": 5, - "name": "Port 2" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 6, - "fields": { - "device_type": 5, - "name": "Port 3" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 7, - "fields": { - "device_type": 5, - "name": "Port 4" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 8, - "fields": { - "device_type": 5, - "name": "Port 5" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 9, - "fields": { - "device_type": 5, - "name": "Port 6" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 10, - "fields": { - "device_type": 5, - "name": "Port 7" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 11, - "fields": { - "device_type": 5, - "name": "Port 8" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 12, - "fields": { - "device_type": 5, - "name": "Port 9" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 13, - "fields": { - "device_type": 5, - "name": "Port 10" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 14, - "fields": { - "device_type": 5, - "name": "Port 11" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 15, - "fields": { - "device_type": 5, - "name": "Port 12" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 16, - "fields": { - "device_type": 5, - "name": "Port 13" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 17, - "fields": { - "device_type": 5, - "name": "Port 14" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 18, - "fields": { - "device_type": 5, - "name": "Port 15" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 19, - "fields": { - "device_type": 5, - "name": "Port 16" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 20, - "fields": { - "device_type": 5, - "name": "Port 17" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 21, - "fields": { - "device_type": 5, - "name": "Port 18" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 22, - "fields": { - "device_type": 5, - "name": "Port 19" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 23, - "fields": { - "device_type": 5, - "name": "Port 20" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 24, - "fields": { - "device_type": 5, - "name": "Port 21" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 25, - "fields": { - "device_type": 5, - "name": "Port 22" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 26, - "fields": { - "device_type": 5, - "name": "Port 23" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 27, - "fields": { - "device_type": 5, - "name": "Port 24" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 28, - "fields": { - "device_type": 5, - "name": "Port 25" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 29, - "fields": { - "device_type": 5, - "name": "Port 26" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 30, - "fields": { - "device_type": 5, - "name": "Port 27" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 31, - "fields": { - "device_type": 5, - "name": "Port 28" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 32, - "fields": { - "device_type": 5, - "name": "Port 29" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 33, - "fields": { - "device_type": 5, - "name": "Port 30" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 34, - "fields": { - "device_type": 5, - "name": "Port 31" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 35, - "fields": { - "device_type": 5, - "name": "Port 32" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 36, - "fields": { - "device_type": 5, - "name": "Port 33" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 37, - "fields": { - "device_type": 5, - "name": "Port 34" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 38, - "fields": { - "device_type": 5, - "name": "Port 35" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 39, - "fields": { - "device_type": 5, - "name": "Port 36" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 40, - "fields": { - "device_type": 5, - "name": "Port 37" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 41, - "fields": { - "device_type": 5, - "name": "Port 38" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 42, - "fields": { - "device_type": 5, - "name": "Port 39" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 43, - "fields": { - "device_type": 5, - "name": "Port 40" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 44, - "fields": { - "device_type": 5, - "name": "Port 41" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 45, - "fields": { - "device_type": 5, - "name": "Port 42" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 46, - "fields": { - "device_type": 5, - "name": "Port 43" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 47, - "fields": { - "device_type": 5, - "name": "Port 44" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 48, - "fields": { - "device_type": 5, - "name": "Port 45" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 49, - "fields": { - "device_type": 5, - "name": "Port 46" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 50, - "fields": { - "device_type": 5, - "name": "Port 47" - } -}, -{ - "model": "dcim.consoleserverporttemplate", - "pk": 51, - "fields": { - "device_type": 5, - "name": "Port 48" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 1, - "fields": { - "device_type": 1, - "name": "PEM0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 2, - "fields": { - "device_type": 1, - "name": "PEM1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 3, - "fields": { - "device_type": 1, - "name": "PEM2" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 4, - "fields": { - "device_type": 1, - "name": "PEM3" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 5, - "fields": { - "device_type": 2, - "name": "PEM0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 6, - "fields": { - "device_type": 2, - "name": "PEM1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 7, - "fields": { - "device_type": 2, - "name": "PEM2" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 8, - "fields": { - "device_type": 2, - "name": "PEM3" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 9, - "fields": { - "device_type": 4, - "name": "PSU0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 11, - "fields": { - "device_type": 3, - "name": "PSU0" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 12, - "fields": { - "device_type": 3, - "name": "PSU1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 13, - "fields": { - "device_type": 4, - "name": "PSU1" - } -}, -{ - "model": "dcim.powerporttemplate", - "pk": 14, - "fields": { - "device_type": 5, - "name": "PSU" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 4, - "fields": { - "device_type": 6, - "name": "AA1" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 5, - "fields": { - "device_type": 6, - "name": "AA2" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 6, - "fields": { - "device_type": 6, - "name": "AA3" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 7, - "fields": { - "device_type": 6, - "name": "AA4" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 8, - "fields": { - "device_type": 6, - "name": "AA5" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 9, - "fields": { - "device_type": 6, - "name": "AA6" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 10, - "fields": { - "device_type": 6, - "name": "AA7" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 11, - "fields": { - "device_type": 6, - "name": "AA8" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 12, - "fields": { - "device_type": 6, - "name": "AB1" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 13, - "fields": { - "device_type": 6, - "name": "AB2" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 14, - "fields": { - "device_type": 6, - "name": "AB3" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 15, - "fields": { - "device_type": 6, - "name": "AB4" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 16, - "fields": { - "device_type": 6, - "name": "AB5" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 17, - "fields": { - "device_type": 6, - "name": "AB6" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 18, - "fields": { - "device_type": 6, - "name": "AB7" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 19, - "fields": { - "device_type": 6, - "name": "AB8" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 20, - "fields": { - "device_type": 6, - "name": "AC1" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 21, - "fields": { - "device_type": 6, - "name": "AC2" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 22, - "fields": { - "device_type": 6, - "name": "AC3" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 23, - "fields": { - "device_type": 6, - "name": "AC4" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 24, - "fields": { - "device_type": 6, - "name": "AC5" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 25, - "fields": { - "device_type": 6, - "name": "AC6" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 26, - "fields": { - "device_type": 6, - "name": "AC7" - } -}, -{ - "model": "dcim.poweroutlettemplate", - "pk": 27, - "fields": { - "device_type": 6, - "name": "AC8" - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 1, - "fields": { - "device_type": 1, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 2, - "fields": { - "device_type": 1, - "name": "fxp0 (RE1)", - "type": 800, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 3, - "fields": { - "device_type": 1, - "name": "lo0", - "type": 0, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 4, - "fields": { - "device_type": 2, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 5, - "fields": { - "device_type": 2, - "name": "fxp0 (RE1)", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 6, - "fields": { - "device_type": 2, - "name": "lo0", - "type": 0, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 7, - "fields": { - "device_type": 3, - "name": "em0", - "type": 800, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 8, - "fields": { - "device_type": 3, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 9, - "fields": { - "device_type": 3, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 10, - "fields": { - "device_type": 3, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 11, - "fields": { - "device_type": 3, - "name": "et-0/0/3", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 12, - "fields": { - "device_type": 3, - "name": "et-0/0/4", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 13, - "fields": { - "device_type": 3, - "name": "et-0/0/5", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 14, - "fields": { - "device_type": 3, - "name": "et-0/0/6", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 15, - "fields": { - "device_type": 3, - "name": "et-0/0/7", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 16, - "fields": { - "device_type": 3, - "name": "et-0/0/8", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 17, - "fields": { - "device_type": 3, - "name": "et-0/0/9", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 18, - "fields": { - "device_type": 3, - "name": "et-0/0/10", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 19, - "fields": { - "device_type": 3, - "name": "et-0/0/11", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 20, - "fields": { - "device_type": 3, - "name": "et-0/0/12", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 21, - "fields": { - "device_type": 3, - "name": "et-0/0/13", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 22, - "fields": { - "device_type": 3, - "name": "et-0/0/14", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 23, - "fields": { - "device_type": 3, - "name": "et-0/0/15", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 24, - "fields": { - "device_type": 3, - "name": "et-0/0/16", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 25, - "fields": { - "device_type": 3, - "name": "et-0/0/17", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 26, - "fields": { - "device_type": 3, - "name": "et-0/0/18", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 27, - "fields": { - "device_type": 3, - "name": "et-0/0/19", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 28, - "fields": { - "device_type": 3, - "name": "et-0/0/20", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 29, - "fields": { - "device_type": 3, - "name": "et-0/0/21", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 30, - "fields": { - "device_type": 3, - "name": "et-0/0/22", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 31, - "fields": { - "device_type": 3, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 32, - "fields": { - "device_type": 3, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 33, - "fields": { - "device_type": 3, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 34, - "fields": { - "device_type": 3, - "name": "et-0/1/3", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 35, - "fields": { - "device_type": 3, - "name": "et-0/2/0", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 36, - "fields": { - "device_type": 3, - "name": "et-0/2/1", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 37, - "fields": { - "device_type": 3, - "name": "et-0/2/2", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 38, - "fields": { - "device_type": 3, - "name": "et-0/2/3", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 138, - "fields": { - "device_type": 4, - "name": "em0", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 139, - "fields": { - "device_type": 4, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 140, - "fields": { - "device_type": 4, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 141, - "fields": { - "device_type": 4, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 142, - "fields": { - "device_type": 4, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 143, - "fields": { - "device_type": 4, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 144, - "fields": { - "device_type": 4, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 145, - "fields": { - "device_type": 4, - "name": "xe-0/0/6", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 146, - "fields": { - "device_type": 4, - "name": "xe-0/0/7", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 147, - "fields": { - "device_type": 4, - "name": "xe-0/0/8", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 148, - "fields": { - "device_type": 4, - "name": "xe-0/0/9", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 149, - "fields": { - "device_type": 4, - "name": "xe-0/0/10", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 150, - "fields": { - "device_type": 4, - "name": "xe-0/0/11", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 151, - "fields": { - "device_type": 4, - "name": "xe-0/0/12", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 152, - "fields": { - "device_type": 4, - "name": "xe-0/0/13", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 153, - "fields": { - "device_type": 4, - "name": "xe-0/0/14", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 154, - "fields": { - "device_type": 4, - "name": "xe-0/0/15", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 155, - "fields": { - "device_type": 4, - "name": "xe-0/0/16", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 156, - "fields": { - "device_type": 4, - "name": "xe-0/0/17", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 157, - "fields": { - "device_type": 4, - "name": "xe-0/0/18", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 158, - "fields": { - "device_type": 4, - "name": "xe-0/0/19", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 159, - "fields": { - "device_type": 4, - "name": "xe-0/0/20", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 160, - "fields": { - "device_type": 4, - "name": "xe-0/0/21", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 161, - "fields": { - "device_type": 4, - "name": "xe-0/0/22", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 162, - "fields": { - "device_type": 4, - "name": "xe-0/0/23", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 163, - "fields": { - "device_type": 4, - "name": "xe-0/0/24", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 164, - "fields": { - "device_type": 4, - "name": "xe-0/0/25", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 165, - "fields": { - "device_type": 4, - "name": "xe-0/0/26", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 166, - "fields": { - "device_type": 4, - "name": "xe-0/0/27", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 167, - "fields": { - "device_type": 4, - "name": "xe-0/0/28", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 168, - "fields": { - "device_type": 4, - "name": "xe-0/0/29", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 169, - "fields": { - "device_type": 4, - "name": "xe-0/0/30", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 170, - "fields": { - "device_type": 4, - "name": "xe-0/0/31", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 171, - "fields": { - "device_type": 4, - "name": "xe-0/0/32", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 172, - "fields": { - "device_type": 4, - "name": "xe-0/0/33", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 173, - "fields": { - "device_type": 4, - "name": "xe-0/0/34", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 174, - "fields": { - "device_type": 4, - "name": "xe-0/0/35", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 175, - "fields": { - "device_type": 4, - "name": "xe-0/0/36", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 176, - "fields": { - "device_type": 4, - "name": "xe-0/0/37", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 177, - "fields": { - "device_type": 4, - "name": "xe-0/0/38", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 178, - "fields": { - "device_type": 4, - "name": "xe-0/0/39", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 179, - "fields": { - "device_type": 4, - "name": "xe-0/0/40", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 180, - "fields": { - "device_type": 4, - "name": "xe-0/0/41", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 181, - "fields": { - "device_type": 4, - "name": "xe-0/0/42", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 182, - "fields": { - "device_type": 4, - "name": "xe-0/0/43", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 183, - "fields": { - "device_type": 4, - "name": "xe-0/0/44", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 184, - "fields": { - "device_type": 4, - "name": "xe-0/0/45", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 185, - "fields": { - "device_type": 4, - "name": "xe-0/0/46", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 186, - "fields": { - "device_type": 4, - "name": "xe-0/0/47", - "type": 1200, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 187, - "fields": { - "device_type": 4, - "name": "et-0/0/48", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 188, - "fields": { - "device_type": 4, - "name": "et-0/0/49", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 189, - "fields": { - "device_type": 4, - "name": "et-0/0/50", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 190, - "fields": { - "device_type": 4, - "name": "et-0/0/51", - "type": 1400, - "mgmt_only": false - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 191, - "fields": { - "device_type": 5, - "name": "eth0", - "type": 1000, - "mgmt_only": true - } -}, -{ - "model": "dcim.interfacetemplate", - "pk": 192, - "fields": { - "device_type": 6, - "name": "Net", - "type": 800, - "mgmt_only": true - } -}, -{ - "model": "dcim.devicerole", - "pk": 1, - "fields": { - "name": "Router", - "slug": "router", - "color": "purple" - } -}, -{ - "model": "dcim.devicerole", - "pk": 2, - "fields": { - "name": "Spine Switch", - "slug": "spine-switch", - "color": "green" - } -}, -{ - "model": "dcim.devicerole", - "pk": 3, - "fields": { - "name": "Core Switch", - "slug": "core-switch", - "color": "red" - } -}, -{ - "model": "dcim.devicerole", - "pk": 4, - "fields": { - "name": "Leaf Switch", - "slug": "leaf-switch", - "color": "teal" - } -}, -{ - "model": "dcim.devicerole", - "pk": 5, - "fields": { - "name": "OOB Switch", - "slug": "oob-switch", - "color": "purple" - } -}, -{ - "model": "dcim.devicerole", - "pk": 6, - "fields": { - "name": "PDU", - "slug": "pdu", - "color": "yellow" - } -}, -{ - "model": "dcim.devicerole", - "pk": 7, - "fields": { - "name": "Server", - "slug": "server", - "color": "grey" - } -}, -{ - "model": "dcim.platform", - "pk": 1, - "fields": { - "name": "Juniper Junos", - "slug": "juniper-junos" - } -}, -{ - "model": "dcim.platform", - "pk": 2, - "fields": { - "name": "Opengear", - "slug": "opengear" - } -}, -{ - "model": "dcim.device", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 1, - "device_role": 1, - "platform": 1, - "name": "test1-edge1", - "serial": "5555555555", - "site": 1, - "rack": 1, - "position": 1, - "face": "front", - "status": true, - "primary_ip4": 1, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 2, - "device_role": 3, - "platform": 1, - "name": "test1-core1", - "serial": "", - "site": 1, - "rack": 1, - "position": 17, - "face": "rear", - "status": true, - "primary_ip4": 5, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 3, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 3, - "device_role": 2, - "platform": 1, - "name": "test1-spine1", - "serial": "", - "site": 1, - "rack": 1, - "position": 33, - "face": "rear", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 4, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 4, - "device_role": 4, - "platform": 1, - "name": "test1-leaf1", - "serial": "", - "site": 1, - "rack": 1, - "position": 34, - "face": "rear", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 5, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 4, - "device_role": 4, - "platform": 1, - "name": "test1-leaf2", - "serial": "9823478293748", - "site": 1, - "rack": 2, - "position": 34, - "face": "rear", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 6, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 3, - "device_role": 2, - "platform": 1, - "name": "test1-spine2", - "serial": "45649818158", - "site": 1, - "rack": 2, - "position": 33, - "face": "rear", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 7, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 1, - "device_role": 1, - "platform": 1, - "name": "test1-edge2", - "serial": "7567356345", - "site": 1, - "rack": 2, - "position": 1, - "face": "rear", - "status": true, - "primary_ip4": 3, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 8, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 2, - "device_role": 3, - "platform": 1, - "name": "test1-core2", - "serial": "67856734534", - "site": 1, - "rack": 2, - "position": 17, - "face": "rear", - "status": true, - "primary_ip4": 19, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 9, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 5, - "device_role": 5, - "platform": 2, - "name": "test1-oob1", - "serial": "98273942938", - "site": 1, - "rack": 1, - "position": 42, - "face": "rear", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 11, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 6, - "device_role": 6, - "platform": null, - "name": "test1-pdu1", - "serial": "", - "site": 1, - "rack": 1, - "position": null, - "face": "", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 12, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 6, - "device_role": 6, - "platform": null, - "name": "test1-pdu2", - "serial": "", - "site": 1, - "rack": 2, - "position": null, - "face": "", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "comments": "" - } -}, -{ - "model": "dcim.device", - "pk": 13, - "fields": { - "local_context_data": null, - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "device_type": 7, - "device_role": 6, - "tenant": null, - "platform": null, - "name": "test1-server1", - "serial": "", - "asset_tag": null, - "site": 1, - "rack": 2, - "position": null, - "face": "", - "status": true, - "primary_ip4": null, - "primary_ip6": null, - "cluster": 4, - "virtual_chassis": null, - "vc_position": null, - "vc_priority": null, - "comments": "" - } -}, -{ - "model": "dcim.consoleport", - "pk": 1, - "fields": { - "device": 1, - "name": "Console (RE0)", - "connected_endpoint": 27, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 2, - "fields": { - "device": 1, - "name": "Console (RE1)", - "connected_endpoint": 38, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 3, - "fields": { - "device": 2, - "name": "Console (RE0)", - "connected_endpoint": 5, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 4, - "fields": { - "device": 2, - "name": "Console (RE1)", - "connected_endpoint": 16, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 5, - "fields": { - "device": 3, - "name": "Console", - "connected_endpoint": 49, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 6, - "fields": { - "device": 4, - "name": "Console", - "connected_endpoint": 48, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 7, - "fields": { - "device": 5, - "name": "Console", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 8, - "fields": { - "device": 6, - "name": "Console", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 9, - "fields": { - "device": 7, - "name": "Console (RE0)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 10, - "fields": { - "device": 7, - "name": "Console (RE1)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 11, - "fields": { - "device": 8, - "name": "Console (RE0)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 12, - "fields": { - "device": 8, - "name": "Console (RE1)", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 13, - "fields": { - "device": 9, - "name": "Console", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 15, - "fields": { - "device": 11, - "name": "Serial", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleport", - "pk": 16, - "fields": { - "device": 12, - "name": "Serial", - "connected_endpoint": null, - "connection_status": true - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 5, - "fields": { - "device": 9, - "name": "Port 1" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 6, - "fields": { - "device": 9, - "name": "Port 10" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 7, - "fields": { - "device": 9, - "name": "Port 11" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 8, - "fields": { - "device": 9, - "name": "Port 12" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 9, - "fields": { - "device": 9, - "name": "Port 13" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 10, - "fields": { - "device": 9, - "name": "Port 14" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 11, - "fields": { - "device": 9, - "name": "Port 15" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 12, - "fields": { - "device": 9, - "name": "Port 16" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 13, - "fields": { - "device": 9, - "name": "Port 17" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 14, - "fields": { - "device": 9, - "name": "Port 18" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 15, - "fields": { - "device": 9, - "name": "Port 19" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 16, - "fields": { - "device": 9, - "name": "Port 2" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 17, - "fields": { - "device": 9, - "name": "Port 20" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 18, - "fields": { - "device": 9, - "name": "Port 21" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 19, - "fields": { - "device": 9, - "name": "Port 22" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 20, - "fields": { - "device": 9, - "name": "Port 23" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 21, - "fields": { - "device": 9, - "name": "Port 24" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 22, - "fields": { - "device": 9, - "name": "Port 25" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 23, - "fields": { - "device": 9, - "name": "Port 26" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 24, - "fields": { - "device": 9, - "name": "Port 27" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 25, - "fields": { - "device": 9, - "name": "Port 28" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 26, - "fields": { - "device": 9, - "name": "Port 29" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 27, - "fields": { - "device": 9, - "name": "Port 3" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 28, - "fields": { - "device": 9, - "name": "Port 30" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 29, - "fields": { - "device": 9, - "name": "Port 31" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 30, - "fields": { - "device": 9, - "name": "Port 32" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 31, - "fields": { - "device": 9, - "name": "Port 33" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 32, - "fields": { - "device": 9, - "name": "Port 34" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 33, - "fields": { - "device": 9, - "name": "Port 35" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 34, - "fields": { - "device": 9, - "name": "Port 36" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 35, - "fields": { - "device": 9, - "name": "Port 37" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 36, - "fields": { - "device": 9, - "name": "Port 38" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 37, - "fields": { - "device": 9, - "name": "Port 39" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 38, - "fields": { - "device": 9, - "name": "Port 4" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 39, - "fields": { - "device": 9, - "name": "Port 40" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 40, - "fields": { - "device": 9, - "name": "Port 41" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 41, - "fields": { - "device": 9, - "name": "Port 42" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 42, - "fields": { - "device": 9, - "name": "Port 43" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 43, - "fields": { - "device": 9, - "name": "Port 44" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 44, - "fields": { - "device": 9, - "name": "Port 45" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 45, - "fields": { - "device": 9, - "name": "Port 46" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 46, - "fields": { - "device": 9, - "name": "Port 47" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 47, - "fields": { - "device": 9, - "name": "Port 48" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 48, - "fields": { - "device": 9, - "name": "Port 5" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 49, - "fields": { - "device": 9, - "name": "Port 6" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 50, - "fields": { - "device": 9, - "name": "Port 7" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 51, - "fields": { - "device": 9, - "name": "Port 8" - } -}, -{ - "model": "dcim.consoleserverport", - "pk": 52, - "fields": { - "device": 9, - "name": "Port 9" - } -}, -{ - "model": "dcim.powerport", - "pk": 1, - "fields": { - "device": 1, - "name": "PEM0", - "_connected_poweroutlet": 25, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 2, - "fields": { - "device": 1, - "name": "PEM1", - "_connected_poweroutlet": 49, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 3, - "fields": { - "device": 1, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 4, - "fields": { - "device": 1, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 5, - "fields": { - "device": 2, - "name": "PEM0", - "_connected_poweroutlet": 26, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 6, - "fields": { - "device": 2, - "name": "PEM1", - "_connected_poweroutlet": 50, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 7, - "fields": { - "device": 2, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 8, - "fields": { - "device": 2, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 9, - "fields": { - "device": 4, - "name": "PSU0", - "_connected_poweroutlet": 28, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 10, - "fields": { - "device": 4, - "name": "PSU1", - "_connected_poweroutlet": 52, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 11, - "fields": { - "device": 5, - "name": "PSU0", - "_connected_poweroutlet": 56, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 12, - "fields": { - "device": 5, - "name": "PSU1", - "_connected_poweroutlet": 32, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 13, - "fields": { - "device": 3, - "name": "PSU0", - "_connected_poweroutlet": 27, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 14, - "fields": { - "device": 3, - "name": "PSU1", - "_connected_poweroutlet": 51, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 15, - "fields": { - "device": 7, - "name": "PEM0", - "_connected_poweroutlet": 53, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 16, - "fields": { - "device": 7, - "name": "PEM1", - "_connected_poweroutlet": 29, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 17, - "fields": { - "device": 7, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 18, - "fields": { - "device": 7, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 19, - "fields": { - "device": 8, - "name": "PEM0", - "_connected_poweroutlet": 54, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 20, - "fields": { - "device": 8, - "name": "PEM1", - "_connected_poweroutlet": 30, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 21, - "fields": { - "device": 8, - "name": "PEM2", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 22, - "fields": { - "device": 8, - "name": "PEM3", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 23, - "fields": { - "device": 6, - "name": "PSU0", - "_connected_poweroutlet": 55, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 24, - "fields": { - "device": 6, - "name": "PSU1", - "_connected_poweroutlet": 31, - "connection_status": true - } -}, -{ - "model": "dcim.powerport", - "pk": 25, - "fields": { - "device": 9, - "name": "PSU", - "_connected_poweroutlet": null, - "connection_status": true - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 25, - "fields": { - "device": 11, - "name": "AA1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 26, - "fields": { - "device": 11, - "name": "AA2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 27, - "fields": { - "device": 11, - "name": "AA3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 28, - "fields": { - "device": 11, - "name": "AA4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 29, - "fields": { - "device": 11, - "name": "AA5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 30, - "fields": { - "device": 11, - "name": "AA6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 31, - "fields": { - "device": 11, - "name": "AA7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 32, - "fields": { - "device": 11, - "name": "AA8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 33, - "fields": { - "device": 11, - "name": "AB1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 34, - "fields": { - "device": 11, - "name": "AB2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 35, - "fields": { - "device": 11, - "name": "AB3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 36, - "fields": { - "device": 11, - "name": "AB4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 37, - "fields": { - "device": 11, - "name": "AB5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 38, - "fields": { - "device": 11, - "name": "AB6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 39, - "fields": { - "device": 11, - "name": "AB7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 40, - "fields": { - "device": 11, - "name": "AB8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 41, - "fields": { - "device": 11, - "name": "AC1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 42, - "fields": { - "device": 11, - "name": "AC2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 43, - "fields": { - "device": 11, - "name": "AC3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 44, - "fields": { - "device": 11, - "name": "AC4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 45, - "fields": { - "device": 11, - "name": "AC5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 46, - "fields": { - "device": 11, - "name": "AC6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 47, - "fields": { - "device": 11, - "name": "AC7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 48, - "fields": { - "device": 11, - "name": "AC8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 49, - "fields": { - "device": 12, - "name": "AA1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 50, - "fields": { - "device": 12, - "name": "AA2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 51, - "fields": { - "device": 12, - "name": "AA3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 52, - "fields": { - "device": 12, - "name": "AA4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 53, - "fields": { - "device": 12, - "name": "AA5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 54, - "fields": { - "device": 12, - "name": "AA6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 55, - "fields": { - "device": 12, - "name": "AA7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 56, - "fields": { - "device": 12, - "name": "AA8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 57, - "fields": { - "device": 12, - "name": "AB1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 58, - "fields": { - "device": 12, - "name": "AB2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 59, - "fields": { - "device": 12, - "name": "AB3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 60, - "fields": { - "device": 12, - "name": "AB4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 61, - "fields": { - "device": 12, - "name": "AB5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 62, - "fields": { - "device": 12, - "name": "AB6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 63, - "fields": { - "device": 12, - "name": "AB7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 64, - "fields": { - "device": 12, - "name": "AB8" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 65, - "fields": { - "device": 12, - "name": "AC1" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 66, - "fields": { - "device": 12, - "name": "AC2" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 67, - "fields": { - "device": 12, - "name": "AC3" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 68, - "fields": { - "device": 12, - "name": "AC4" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 69, - "fields": { - "device": 12, - "name": "AC5" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 70, - "fields": { - "device": 12, - "name": "AC6" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 71, - "fields": { - "device": 12, - "name": "AC7" - } -}, -{ - "model": "dcim.poweroutlet", - "pk": 72, - "fields": { - "device": 12, - "name": "AC8" - } -}, -{ - "model": "dcim.interface", - "pk": 1, - "fields": { - "device": 1, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 2, - "fields": { - "device": 1, - "name": "fxp0 (RE1)", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 3, - "fields": { - "device": 1, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 4, - "fields": { - "device": 1, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "TEST" - } -}, -{ - "model": "dcim.interface", - "pk": 5, - "fields": { - "device": 1, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 6, - "fields": { - "device": 1, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 7, - "fields": { - "device": 1, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 8, - "fields": { - "device": 1, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 9, - "fields": { - "device": 1, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 10, - "fields": { - "device": 2, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 11, - "fields": { - "device": 2, - "name": "fxp0 (RE1)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 12, - "fields": { - "device": 2, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 13, - "fields": { - "device": 3, - "name": "em0", - "mac_address": "00-00-00-AA-BB-CC", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 14, - "fields": { - "device": 3, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 15, - "fields": { - "device": 3, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 16, - "fields": { - "device": 3, - "name": "et-0/0/10", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 17, - "fields": { - "device": 3, - "name": "et-0/0/11", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 18, - "fields": { - "device": 3, - "name": "et-0/0/12", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 19, - "fields": { - "device": 3, - "name": "et-0/0/13", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 20, - "fields": { - "device": 3, - "name": "et-0/0/14", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 21, - "fields": { - "device": 3, - "name": "et-0/0/15", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 22, - "fields": { - "device": 3, - "name": "et-0/0/16", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 23, - "fields": { - "device": 3, - "name": "et-0/0/17", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 24, - "fields": { - "device": 3, - "name": "et-0/0/18", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 25, - "fields": { - "device": 3, - "name": "et-0/0/19", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 26, - "fields": { - "device": 3, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 27, - "fields": { - "device": 3, - "name": "et-0/0/20", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 28, - "fields": { - "device": 3, - "name": "et-0/0/21", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 29, - "fields": { - "device": 3, - "name": "et-0/0/22", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 30, - "fields": { - "device": 3, - "name": "et-0/0/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 31, - "fields": { - "device": 3, - "name": "et-0/0/4", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 32, - "fields": { - "device": 3, - "name": "et-0/0/5", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 33, - "fields": { - "device": 3, - "name": "et-0/0/6", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 34, - "fields": { - "device": 3, - "name": "et-0/0/7", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 35, - "fields": { - "device": 3, - "name": "et-0/0/8", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 36, - "fields": { - "device": 3, - "name": "et-0/0/9", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 37, - "fields": { - "device": 3, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 38, - "fields": { - "device": 3, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 39, - "fields": { - "device": 3, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 40, - "fields": { - "device": 3, - "name": "et-0/1/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 41, - "fields": { - "device": 3, - "name": "et-0/2/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 42, - "fields": { - "device": 3, - "name": "et-0/2/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 43, - "fields": { - "device": 3, - "name": "et-0/2/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 44, - "fields": { - "device": 3, - "name": "et-0/2/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 45, - "fields": { - "device": 4, - "name": "em0", - "type": 1000, - "mac_address": "ff-ee-dd-33-22-11", - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 46, - "fields": { - "device": 4, - "name": "et-0/0/48", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 47, - "fields": { - "device": 4, - "name": "et-0/0/49", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 48, - "fields": { - "device": 4, - "name": "et-0/0/50", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 49, - "fields": { - "device": 4, - "name": "et-0/0/51", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 50, - "fields": { - "device": 4, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 51, - "fields": { - "device": 4, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 52, - "fields": { - "device": 4, - "name": "xe-0/0/10", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 53, - "fields": { - "device": 4, - "name": "xe-0/0/11", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 54, - "fields": { - "device": 4, - "name": "xe-0/0/12", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 55, - "fields": { - "device": 4, - "name": "xe-0/0/13", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 56, - "fields": { - "device": 4, - "name": "xe-0/0/14", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 57, - "fields": { - "device": 4, - "name": "xe-0/0/15", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 58, - "fields": { - "device": 4, - "name": "xe-0/0/16", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 59, - "fields": { - "device": 4, - "name": "xe-0/0/17", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 60, - "fields": { - "device": 4, - "name": "xe-0/0/18", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 61, - "fields": { - "device": 4, - "name": "xe-0/0/19", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 62, - "fields": { - "device": 4, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 63, - "fields": { - "device": 4, - "name": "xe-0/0/20", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 64, - "fields": { - "device": 4, - "name": "xe-0/0/21", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 65, - "fields": { - "device": 4, - "name": "xe-0/0/22", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 66, - "fields": { - "device": 4, - "name": "xe-0/0/23", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 67, - "fields": { - "device": 4, - "name": "xe-0/0/24", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 68, - "fields": { - "device": 4, - "name": "xe-0/0/25", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 69, - "fields": { - "device": 4, - "name": "xe-0/0/26", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 70, - "fields": { - "device": 4, - "name": "xe-0/0/27", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 71, - "fields": { - "device": 4, - "name": "xe-0/0/28", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 72, - "fields": { - "device": 4, - "name": "xe-0/0/29", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 73, - "fields": { - "device": 4, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 74, - "fields": { - "device": 4, - "name": "xe-0/0/30", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 75, - "fields": { - "device": 4, - "name": "xe-0/0/31", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 76, - "fields": { - "device": 4, - "name": "xe-0/0/32", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 77, - "fields": { - "device": 4, - "name": "xe-0/0/33", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 78, - "fields": { - "device": 4, - "name": "xe-0/0/34", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 79, - "fields": { - "device": 4, - "name": "xe-0/0/35", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 80, - "fields": { - "device": 4, - "name": "xe-0/0/36", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 81, - "fields": { - "device": 4, - "name": "xe-0/0/37", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 82, - "fields": { - "device": 4, - "name": "xe-0/0/38", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 83, - "fields": { - "device": 4, - "name": "xe-0/0/39", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 84, - "fields": { - "device": 4, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 85, - "fields": { - "device": 4, - "name": "xe-0/0/40", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 86, - "fields": { - "device": 4, - "name": "xe-0/0/41", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 87, - "fields": { - "device": 4, - "name": "xe-0/0/42", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 88, - "fields": { - "device": 4, - "name": "xe-0/0/43", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 89, - "fields": { - "device": 4, - "name": "xe-0/0/44", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 90, - "fields": { - "device": 4, - "name": "xe-0/0/45", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 91, - "fields": { - "device": 4, - "name": "xe-0/0/46", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 92, - "fields": { - "device": 4, - "name": "xe-0/0/47", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 93, - "fields": { - "device": 4, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 94, - "fields": { - "device": 4, - "name": "xe-0/0/6", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 95, - "fields": { - "device": 4, - "name": "xe-0/0/7", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 96, - "fields": { - "device": 4, - "name": "xe-0/0/8", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 97, - "fields": { - "device": 4, - "name": "xe-0/0/9", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 98, - "fields": { - "device": 5, - "name": "em0", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 99, - "fields": { - "device": 5, - "name": "et-0/0/48", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 100, - "fields": { - "device": 5, - "name": "et-0/0/49", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 101, - "fields": { - "device": 5, - "name": "et-0/0/50", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 102, - "fields": { - "device": 5, - "name": "et-0/0/51", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 103, - "fields": { - "device": 5, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 104, - "fields": { - "device": 5, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 105, - "fields": { - "device": 5, - "name": "xe-0/0/10", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 106, - "fields": { - "device": 5, - "name": "xe-0/0/11", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 107, - "fields": { - "device": 5, - "name": "xe-0/0/12", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 108, - "fields": { - "device": 5, - "name": "xe-0/0/13", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 109, - "fields": { - "device": 5, - "name": "xe-0/0/14", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 110, - "fields": { - "device": 5, - "name": "xe-0/0/15", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 111, - "fields": { - "device": 5, - "name": "xe-0/0/16", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 112, - "fields": { - "device": 5, - "name": "xe-0/0/17", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 113, - "fields": { - "device": 5, - "name": "xe-0/0/18", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 114, - "fields": { - "device": 5, - "name": "xe-0/0/19", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 115, - "fields": { - "device": 5, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 116, - "fields": { - "device": 5, - "name": "xe-0/0/20", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 117, - "fields": { - "device": 5, - "name": "xe-0/0/21", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 118, - "fields": { - "device": 5, - "name": "xe-0/0/22", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 119, - "fields": { - "device": 5, - "name": "xe-0/0/23", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 120, - "fields": { - "device": 5, - "name": "xe-0/0/24", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 121, - "fields": { - "device": 5, - "name": "xe-0/0/25", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 122, - "fields": { - "device": 5, - "name": "xe-0/0/26", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 123, - "fields": { - "device": 5, - "name": "xe-0/0/27", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 124, - "fields": { - "device": 5, - "name": "xe-0/0/28", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 125, - "fields": { - "device": 5, - "name": "xe-0/0/29", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 126, - "fields": { - "device": 5, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 127, - "fields": { - "device": 5, - "name": "xe-0/0/30", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 128, - "fields": { - "device": 5, - "name": "xe-0/0/31", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 129, - "fields": { - "device": 5, - "name": "xe-0/0/32", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 130, - "fields": { - "device": 5, - "name": "xe-0/0/33", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 131, - "fields": { - "device": 5, - "name": "xe-0/0/34", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 132, - "fields": { - "device": 5, - "name": "xe-0/0/35", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 133, - "fields": { - "device": 5, - "name": "xe-0/0/36", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 134, - "fields": { - "device": 5, - "name": "xe-0/0/37", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 135, - "fields": { - "device": 5, - "name": "xe-0/0/38", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 136, - "fields": { - "device": 5, - "name": "xe-0/0/39", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 137, - "fields": { - "device": 5, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 138, - "fields": { - "device": 5, - "name": "xe-0/0/40", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 139, - "fields": { - "device": 5, - "name": "xe-0/0/41", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 140, - "fields": { - "device": 5, - "name": "xe-0/0/42", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 141, - "fields": { - "device": 5, - "name": "xe-0/0/43", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 142, - "fields": { - "device": 5, - "name": "xe-0/0/44", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 143, - "fields": { - "device": 5, - "name": "xe-0/0/45", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 144, - "fields": { - "device": 5, - "name": "xe-0/0/46", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 145, - "fields": { - "device": 5, - "name": "xe-0/0/47", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 146, - "fields": { - "device": 5, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 147, - "fields": { - "device": 5, - "name": "xe-0/0/6", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 148, - "fields": { - "device": 5, - "name": "xe-0/0/7", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 149, - "fields": { - "device": 5, - "name": "xe-0/0/8", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 150, - "fields": { - "device": 5, - "name": "xe-0/0/9", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 151, - "fields": { - "device": 6, - "name": "em0", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 152, - "fields": { - "device": 6, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 153, - "fields": { - "device": 6, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 154, - "fields": { - "device": 6, - "name": "et-0/0/10", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 155, - "fields": { - "device": 6, - "name": "et-0/0/11", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 156, - "fields": { - "device": 6, - "name": "et-0/0/12", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 157, - "fields": { - "device": 6, - "name": "et-0/0/13", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 158, - "fields": { - "device": 6, - "name": "et-0/0/14", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 159, - "fields": { - "device": 6, - "name": "et-0/0/15", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 160, - "fields": { - "device": 6, - "name": "et-0/0/16", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 161, - "fields": { - "device": 6, - "name": "et-0/0/17", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 162, - "fields": { - "device": 6, - "name": "et-0/0/18", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 163, - "fields": { - "device": 6, - "name": "et-0/0/19", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 164, - "fields": { - "device": 6, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 165, - "fields": { - "device": 6, - "name": "et-0/0/20", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 166, - "fields": { - "device": 6, - "name": "et-0/0/21", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 167, - "fields": { - "device": 6, - "name": "et-0/0/22", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 168, - "fields": { - "device": 6, - "name": "et-0/0/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 169, - "fields": { - "device": 6, - "name": "et-0/0/4", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 170, - "fields": { - "device": 6, - "name": "et-0/0/5", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 171, - "fields": { - "device": 6, - "name": "et-0/0/6", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 172, - "fields": { - "device": 6, - "name": "et-0/0/7", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 173, - "fields": { - "device": 6, - "name": "et-0/0/8", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 174, - "fields": { - "device": 6, - "name": "et-0/0/9", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 175, - "fields": { - "device": 6, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 176, - "fields": { - "device": 6, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 177, - "fields": { - "device": 6, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 178, - "fields": { - "device": 6, - "name": "et-0/1/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 179, - "fields": { - "device": 6, - "name": "et-0/2/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 180, - "fields": { - "device": 6, - "name": "et-0/2/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 181, - "fields": { - "device": 6, - "name": "et-0/2/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 182, - "fields": { - "device": 6, - "name": "et-0/2/3", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 183, - "fields": { - "device": 7, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 184, - "fields": { - "device": 7, - "name": "fxp0 (RE1)", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 185, - "fields": { - "device": 7, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 186, - "fields": { - "device": 8, - "name": "fxp0 (RE0)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 187, - "fields": { - "device": 8, - "name": "fxp0 (RE1)", - "type": 1000, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 188, - "fields": { - "device": 8, - "name": "lo0", - "type": 0, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 189, - "fields": { - "device": 2, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 190, - "fields": { - "device": 2, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 191, - "fields": { - "device": 2, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 192, - "fields": { - "device": 2, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 193, - "fields": { - "device": 2, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 194, - "fields": { - "device": 2, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 195, - "fields": { - "device": 8, - "name": "et-0/0/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 196, - "fields": { - "device": 8, - "name": "et-0/0/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 197, - "fields": { - "device": 8, - "name": "et-0/0/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 198, - "fields": { - "device": 8, - "name": "et-0/1/0", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 199, - "fields": { - "device": 8, - "name": "et-0/1/1", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 200, - "fields": { - "device": 8, - "name": "et-0/1/2", - "type": 1400, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 201, - "fields": { - "device": 2, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 202, - "fields": { - "device": 2, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 203, - "fields": { - "device": 2, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 204, - "fields": { - "device": 2, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 205, - "fields": { - "device": 2, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 206, - "fields": { - "device": 2, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 207, - "fields": { - "device": 8, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 208, - "fields": { - "device": 8, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 209, - "fields": { - "device": 8, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 210, - "fields": { - "device": 8, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 211, - "fields": { - "device": 8, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 212, - "fields": { - "device": 8, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 213, - "fields": { - "device": 7, - "name": "xe-0/0/0", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 214, - "fields": { - "device": 7, - "name": "xe-0/0/1", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 215, - "fields": { - "device": 7, - "name": "xe-0/0/2", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 216, - "fields": { - "device": 7, - "name": "xe-0/0/3", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 217, - "fields": { - "device": 7, - "name": "xe-0/0/4", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 218, - "fields": { - "device": 7, - "name": "xe-0/0/5", - "type": 1200, - "mgmt_only": false, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 219, - "fields": { - "device": 9, - "name": "eth0", - "type": 1000, - "mac_address": "44-55-66-77-88-99", - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 221, - "fields": { - "device": 11, - "name": "Net", - "type": 800, - "mgmt_only": true, - "description": "" - } -}, -{ - "model": "dcim.interface", - "pk": 222, - "fields": { - "device": 12, - "name": "Net", - "type": 800, - "mgmt_only": true, - "description": "" - } -} -] diff --git a/netbox/extras/fixtures/extras.json b/netbox/extras/fixtures/extras.json deleted file mode 100644 index 83b947cb2..000000000 --- a/netbox/extras/fixtures/extras.json +++ /dev/null @@ -1,35 +0,0 @@ -[ -{ - "model": "extras.graph", - "pk": 1, - "fields": { - "type": 300, - "weight": 1000, - "name": "Site Test Graph", - "source": "http://localhost/na.png", - "link": "" - } -}, -{ - "model": "extras.graph", - "pk": 2, - "fields": { - "type": 200, - "weight": 1000, - "name": "Provider Test Graph", - "source": "http://localhost/provider_graph.png", - "link": "" - } -}, -{ - "model": "extras.graph", - "pk": 3, - "fields": { - "type": 100, - "weight": 1000, - "name": "Interface Test Graph", - "source": "http://localhost/interface_graph.png", - "link": "" - } -} -] \ No newline at end of file diff --git a/netbox/ipam/fixtures/ipam.json b/netbox/ipam/fixtures/ipam.json deleted file mode 100644 index e722b3629..000000000 --- a/netbox/ipam/fixtures/ipam.json +++ /dev/null @@ -1,329 +0,0 @@ -[ -{ - "model": "ipam.rir", - "pk": 1, - "fields": { - "name": "RFC1918", - "slug": "rfc1918" - } -}, -{ - "model": "ipam.aggregate", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "prefix": "10.0.0.0/8", - "rir": 1, - "date_added": null, - "description": "" - } -}, -{ - "model": "ipam.role", - "pk": 1, - "fields": { - "name": "Lab Network", - "slug": "lab-network", - "weight": 1000 - } -}, -{ - "model": "ipam.prefix", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "prefix": "10.1.1.0/24", - "site": 1, - "vrf": null, - "vlan": null, - "status": "active", - "role": 1, - "description": "" - } -}, -{ - "model": "ipam.prefix", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "prefix": "10.0.255.0/24", - "site": 1, - "vrf": null, - "vlan": null, - "status": "active", - "role": 1, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.0.255.1/32", - "vrf": null, - "interface_id": 3, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 2, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "169.254.254.1/31", - "vrf": null, - "interface_id": 4, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 3, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.0.255.2/32", - "vrf": null, - "interface_id": 185, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 4, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "169.254.1.1/31", - "vrf": null, - "interface_id": 213, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 5, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.0.254.1/24", - "vrf": null, - "interface_id": 12, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 8, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.21.1/31", - "vrf": null, - "interface_id": 218, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 9, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.21.2/31", - "vrf": null, - "interface_id": 9, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 10, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.22.1/31", - "vrf": null, - "interface_id": 8, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 11, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.20.1/31", - "vrf": null, - "interface_id": 7, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 12, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.16.20.1/31", - "vrf": null, - "interface_id": 216, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 13, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.22.2/31", - "vrf": null, - "interface_id": 206, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 14, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.16.22.1/31", - "vrf": null, - "interface_id": 217, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 15, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.16.22.2/31", - "vrf": null, - "interface_id": 205, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 16, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.16.20.2/31", - "vrf": null, - "interface_id": 211, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 17, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.15.22.2/31", - "vrf": null, - "interface_id": 212, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 19, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "10.0.254.2/32", - "vrf": null, - "interface_id": 188, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 20, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "169.254.1.1/31", - "vrf": null, - "interface_id": 200, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.ipaddress", - "pk": 21, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "family": 4, - "address": "169.254.1.2/31", - "vrf": null, - "interface_id": 194, - "nat_inside": null, - "description": "" - } -}, -{ - "model": "ipam.vlan", - "pk": 1, - "fields": { - "created": "2016-06-23", - "last_updated": "2016-06-23T03:19:56.521Z", - "site": 1, - "vid": 999, - "name": "TEST", - "status": "active", - "role": 1 - } -} -] \ No newline at end of file diff --git a/netbox/virtualization/fixtures/virtualization.json b/netbox/virtualization/fixtures/virtualization.json deleted file mode 100644 index 3c9537802..000000000 --- a/netbox/virtualization/fixtures/virtualization.json +++ /dev/null @@ -1,170 +0,0 @@ -[ -{ - "model": "virtualization.clustertype", - "pk": 1, - "fields": { - "created": "2016-08-01", - "last_updated": "2016-08-01T15:22:42.289Z", - "name": "Public Cloud", - "slug": "public-cloud" - } -}, -{ - "model": "virtualization.clustertype", - "pk": 2, - "fields": { - "created": "2016-08-01", - "last_updated": "2016-08-01T15:22:42.289Z", - "name": "vSphere", - "slug": "vsphere" - } -}, -{ - "model": "virtualization.clustertype", - "pk": 3, - "fields": { - "created": "2016-08-01", - "last_updated": "2016-08-01T15:22:42.289Z", - "name": "Hyper-V", - "slug": "hyper-v" - } -}, -{ - "model": "virtualization.clustertype", - "pk": 4, - "fields": { - "created": "2016-08-01", - "last_updated": "2016-08-01T15:22:42.289Z", - "name": "libvirt", - "slug": "libvirt" - } -}, -{ - "model": "virtualization.clustertype", - "pk": 5, - "fields": { - "created": "2016-08-01", - "last_updated": "2016-08-01T15:22:42.289Z", - "name": "LXD", - "slug": "lxd" - } -}, -{ - "model": "virtualization.clustertype", - "pk": 6, - "fields": { - "created": "2016-08-01", - "last_updated": "2016-08-01T15:22:42.289Z", - "name": "Docker", - "slug": "docker" - } -}, -{ - "model": "virtualization.clustergroup", - "pk": 1, - "fields": { - "created": "2016-08-01", - "last_updated": "2016-08-01T15:22:42.289Z", - "name": "VM Host", - "slug": "vm-host" - } -}, -{ - "model": "virtualization.cluster", - "pk": 1, - "fields": { - "created": "2016-08-01", - "last_updated": "2016-08-01T15:22:42.289Z", - "name": "Digital Ocean", - "type": 1, - "group": 1, - "tenant": null, - "site": null, - "comments": "" - } -}, -{ - "model": "virtualization.cluster", - "pk": 2, - "fields": { - "created": "2016-08-01", - "last_updated": "2016-08-01T15:22:42.289Z", - "name": "Amazon EC2", - "type": 1, - "group": 1, - "tenant": null, - "site": null, - "comments": "" - } -}, -{ - "model": "virtualization.cluster", - "pk": 3, - "fields": { - "created": "2016-08-01", - "last_updated": "2016-08-01T15:22:42.289Z", - "name": "Microsoft Azure", - "type": 1, - "group": 1, - "tenant": null, - "site": null, - "comments": "" - } -}, -{ - "model": "virtualization.cluster", - "pk": 4, - "fields": { - "created": "2016-08-01", - "last_updated": "2016-08-01T15:22:42.289Z", - "name": "vSphere Cluster", - "type": 2, - "group": 1, - "tenant": null, - "site": null, - "comments": "" - } -}, -{ - "model": "virtualization.virtualmachine", - "pk": 1, - "fields": { - "local_context_data": null, - "created": "2019-12-19", - "last_updated": "2019-12-19T05:24:19.146Z", - "cluster": 2, - "tenant": null, - "platform": null, - "name": "vm1", - "status": "active", - "role": null, - "primary_ip4": null, - "primary_ip6": null, - "vcpus": null, - "memory": null, - "disk": null, - "comments": "" - } -}, -{ - "model": "virtualization.virtualmachine", - "pk": 2, - "fields": { - "local_context_data": null, - "created": "2019-12-19", - "last_updated": "2019-12-19T05:24:41.478Z", - "cluster": 1, - "tenant": null, - "platform": null, - "name": "vm2", - "status": "active", - "role": null, - "primary_ip4": null, - "primary_ip6": null, - "vcpus": null, - "memory": null, - "disk": null, - "comments": "" - } -} -] From 47962ea732c802b82bdd2e82b14bb7e151792a79 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 24 Jan 2020 16:30:43 -0500 Subject: [PATCH 021/174] Adapt form tests to work without fixture data --- netbox/dcim/tests/test_forms.py | 126 +++++++++++++++++++------------- 1 file changed, 76 insertions(+), 50 deletions(-) diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 5bbe36716..29e741560 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -2,6 +2,7 @@ from django.test import TestCase from dcim.forms import * from dcim.models import * +from virtualization.models import Cluster, ClusterGroup, ClusterType def get_id(model, slug): @@ -10,83 +11,108 @@ def get_id(model, slug): class DeviceTestCase(TestCase): - fixtures = ['dcim', 'ipam', 'virtualization'] + @classmethod + def setUpTestData(cls): + + site = Site.objects.create(name='Site 1', slug='site-1') + rack = Rack.objects.create(name='Rack 1', site=site) + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create( + manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1 + ) + device_role = DeviceRole.objects.create( + name='Device Role 1', slug='device-role-1', color='ff0000' + ) + Platform.objects.create(name='Platform 1', slug='platform-1') + Device.objects.create( + name='Device 1', device_type=device_type, device_role=device_role, site=site, rack=rack, position=1 + ) + cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') + cluster_group = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1') + Cluster.objects.create(name='Cluster 1', type=cluster_type, group=cluster_group) def test_racked_device(self): - test = DeviceForm(data={ - 'name': 'test', - 'device_role': get_id(DeviceRole, 'leaf-switch'), + form = DeviceForm(data={ + 'name': 'New Device', + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'juniper'), - 'device_type': get_id(DeviceType, 'qfx5100-48s'), - 'site': get_id(Site, 'test1'), - 'rack': '1', + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': Rack.objects.first().pk, 'face': DeviceFaceChoices.FACE_FRONT, - 'position': 41, - 'platform': get_id(Platform, 'juniper-junos'), + 'position': 2, + 'platform': Platform.objects.first().pk, 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertTrue(test.is_valid(), test.fields['position'].choices) - self.assertTrue(test.save()) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) def test_racked_device_occupied(self): - test = DeviceForm(data={ + form = DeviceForm(data={ 'name': 'test', - 'device_role': get_id(DeviceRole, 'leaf-switch'), + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'juniper'), - 'device_type': get_id(DeviceType, 'qfx5100-48s'), - 'site': get_id(Site, 'test1'), - 'rack': '1', + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': Rack.objects.first().pk, 'face': DeviceFaceChoices.FACE_FRONT, 'position': 1, - 'platform': get_id(Platform, 'juniper-junos'), + 'platform': Platform.objects.first().pk, 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertFalse(test.is_valid()) + self.assertFalse(form.is_valid()) + self.assertIn('position', form.errors) def test_non_racked_device(self): - test = DeviceForm(data={ - 'name': 'test', - 'device_role': get_id(DeviceRole, 'pdu'), + form = DeviceForm(data={ + 'name': 'New Device', + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'servertech'), - 'device_type': get_id(DeviceType, 'cwg-24vym415c9'), - 'site': get_id(Site, 'test1'), - 'rack': '1', - 'face': '', + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': None, + 'face': None, 'position': None, - 'platform': None, + 'platform': Platform.objects.first().pk, 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertTrue(test.is_valid()) - self.assertTrue(test.save()) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) - def test_non_racked_device_with_face(self): - test = DeviceForm(data={ - 'name': 'test', - 'device_role': get_id(DeviceRole, 'pdu'), + def test_non_racked_device_with_face_position(self): + form = DeviceForm(data={ + 'name': 'New Device', + 'device_role': DeviceRole.objects.first().pk, 'tenant': None, - 'manufacturer': get_id(Manufacturer, 'servertech'), - 'device_type': get_id(DeviceType, 'cwg-24vym415c9'), - 'site': get_id(Site, 'test1'), - 'rack': '1', + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': None, 'face': DeviceFaceChoices.FACE_REAR, - 'position': None, + 'position': 10, 'platform': None, 'status': DeviceStatusChoices.STATUS_ACTIVE, }) - self.assertTrue(test.is_valid()) - self.assertTrue(test.save()) + self.assertFalse(form.is_valid()) + self.assertIn('face', form.errors) + self.assertIn('position', form.errors) - def test_cloned_cluster_device_initial_data(self): + def test_initial_data_population(self): + device_type = DeviceType.objects.first() + cluster = Cluster.objects.first() test = DeviceForm(initial={ - 'device_type': get_id(DeviceType, 'poweredge-r640'), - 'device_role': get_id(DeviceRole, 'server'), + 'device_type': device_type.pk, + 'device_role': DeviceRole.objects.first().pk, 'status': DeviceStatusChoices.STATUS_ACTIVE, - 'site': get_id(Site, 'test1'), - "cluster": Cluster.objects.get(id=4).id, + 'site': Site.objects.first().pk, + 'cluster': cluster.pk, }) - self.assertEqual(test.initial['manufacturer'], get_id(Manufacturer, 'dell')) - self.assertIn('cluster_group', test.initial) - self.assertEqual(test.initial['cluster_group'], get_id(ClusterGroup, 'vm-host')) + + # Check that the initial value for the manufacturer is set automatically when assigning the device type + self.assertEqual(test.initial['manufacturer'], device_type.manufacturer.pk) + + # Check that the initial value for the cluster group is set automatically when assigning the cluster + self.assertEqual(test.initial['cluster_group'], cluster.group.pk) From c22024b618b32928a98ee18398f10c7f9bfb27ca Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Fri, 24 Jan 2020 22:15:09 +0000 Subject: [PATCH 022/174] Added CSV import test --- netbox/extras/tests/test_customfields.py | 64 +++++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 192958840..7871e031e 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -1,14 +1,14 @@ from datetime import date from django.contrib.contenttypes.models import ContentType -from django.test import TestCase +from django.test import Client, TestCase from django.urls import reverse from rest_framework import status from dcim.models import Site from extras.choices import * from extras.models import CustomField, CustomFieldValue, CustomFieldChoice -from utilities.testing import APITestCase +from utilities.testing import APITestCase, create_test_user from virtualization.models import VirtualMachine @@ -364,3 +364,63 @@ class CustomFieldChoiceAPITest(APITestCase): self.assertEqual(self.cf_choice_1.pk, response.data[self.cf_1.name][self.cf_choice_1.value]) self.assertEqual(self.cf_choice_2.pk, response.data[self.cf_1.name][self.cf_choice_2.value]) self.assertEqual(self.cf_choice_3.pk, response.data[self.cf_2.name][self.cf_choice_3.value]) + + +class CustomFieldCSV(TestCase): + def setUp(self): + super().setUp() + + user = create_test_user( + permissions=[ + 'dcim.view_site', + 'dcim.add_site', + ] + ) + self.client = Client() + self.client.force_login(user) + + obj_type = ContentType.objects.get_for_model(Site) + + self.cf_text = CustomField.objects.create(name="text", type=CustomFieldTypeChoices.TYPE_TEXT) + self.cf_text.obj_type.set([obj_type]) + self.cf_text.save() + + self.cf_choice = CustomField.objects.create(name="choice", type=CustomFieldTypeChoices.TYPE_SELECT) + self.cf_choice.obj_type.set([obj_type]) + self.cf_choice.save() + + self.cf_choice_1 = CustomFieldChoice.objects.create(field=self.cf_choice, value="cf_field_1") + self.cf_choice_2 = CustomFieldChoice.objects.create(field=self.cf_choice, value="cf_field_2") + self.cf_choice_3 = CustomFieldChoice.objects.create(field=self.cf_choice, value="cf_field_3") + + def test_import(self): + """ + Import a site with custom fields + """ + csv_data = ( + "name,slug,cf_text,cf_choice", + "Site 1,site-1,something,cf_field_1", + ) + + response = self.client.post(reverse('dcim:site_import'), {'csv': '\n'.join(csv_data)}) + self.assertEqual(response.status_code, 200) + + site1_custom_fields = Site.objects.get(name='Site 1').get_custom_fields() + self.assertEqual(len(site1_custom_fields), 2) + self.assertEqual(site1_custom_fields[self.cf_text], 'something') + self.assertEqual(site1_custom_fields[self.cf_choice], self.cf_choice_1) + + + def test_import_invalid_choice(self): + """ + Import a site with an invalid choice + """ + csv_data = ( + "name,slug,cf_choice", + "Site 2,site-2,cf_field_4", + ) + + response = self.client.post(reverse('dcim:site_import'), {'csv': '\n'.join(csv_data)}) + self.assertEqual(response.status_code, 200) + + self.assertFalse(len(Site.objects.filter(name="Site 2")), 0) From 8ec0ad96bddbc98c724ce2072a96f0e5844cf30a Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Fri, 24 Jan 2020 22:20:41 +0000 Subject: [PATCH 023/174] Formatting --- netbox/extras/tests/test_customfields.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 7871e031e..005f049b5 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -410,7 +410,6 @@ class CustomFieldCSV(TestCase): self.assertEqual(site1_custom_fields[self.cf_text], 'something') self.assertEqual(site1_custom_fields[self.cf_choice], self.cf_choice_1) - def test_import_invalid_choice(self): """ Import a site with an invalid choice From 724d3b8894dde8e72402fb4e757899b5c05b90be Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Sat, 25 Jan 2020 15:56:24 +0000 Subject: [PATCH 024/174] Fixes #3313: YAML-format the config context in the GUI --- netbox/project-static/js/configcontext.js | 11 +++++++++++ netbox/templates/extras/configcontext.html | 8 +++++++- netbox/templates/extras/inc/configcontext_data.html | 8 ++++++++ netbox/templates/extras/inc/configcontext_format.html | 6 ++++++ netbox/templates/extras/object_configcontext.html | 8 +++++++- netbox/utilities/templatetags/helpers.py | 9 +++++++++ 6 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 netbox/project-static/js/configcontext.js create mode 100644 netbox/templates/extras/inc/configcontext_data.html create mode 100644 netbox/templates/extras/inc/configcontext_format.html diff --git a/netbox/project-static/js/configcontext.js b/netbox/project-static/js/configcontext.js new file mode 100644 index 000000000..1d731e696 --- /dev/null +++ b/netbox/project-static/js/configcontext.js @@ -0,0 +1,11 @@ +$('.rendered-context-format').on('click', function() { + if (!$(this).hasClass('active')) { + // Update selection in the button group + $('span.rendered-context-format').removeClass('active'); + $('span.rendered-context-format[data-format=' + $(this).data('format') + ']').addClass('active'); + + // Hide all rendered contexts and only show the selected one + $('div.rendered-context-data').hide(); + $('div.rendered-context-data[data-format=' + $(this).data('format') + ']').show(); + } +}); diff --git a/netbox/templates/extras/configcontext.html b/netbox/templates/extras/configcontext.html index 7cec3f403..067bf1e96 100644 --- a/netbox/templates/extras/configcontext.html +++ b/netbox/templates/extras/configcontext.html @@ -1,5 +1,6 @@ {% extends '_base.html' %} {% load helpers %} +{% load static %} {% block header %}
@@ -183,11 +184,16 @@
Data + {% include 'extras/inc/configcontext_format.html' %}
-
{{ configcontext.data|render_json }}
+ {% include 'extras/inc/configcontext_data.html' with data=configcontext.data %}
{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/netbox/templates/extras/inc/configcontext_data.html b/netbox/templates/extras/inc/configcontext_data.html new file mode 100644 index 000000000..d987b2acb --- /dev/null +++ b/netbox/templates/extras/inc/configcontext_data.html @@ -0,0 +1,8 @@ +{% load helpers %} + +
+
{{ data|render_yaml }}
+
+ diff --git a/netbox/templates/extras/inc/configcontext_format.html b/netbox/templates/extras/inc/configcontext_format.html new file mode 100644 index 000000000..eb34adfdf --- /dev/null +++ b/netbox/templates/extras/inc/configcontext_format.html @@ -0,0 +1,6 @@ +
+
+ YAML + JSON +
+
diff --git a/netbox/templates/extras/object_configcontext.html b/netbox/templates/extras/object_configcontext.html index d23455c19..f8191936e 100644 --- a/netbox/templates/extras/object_configcontext.html +++ b/netbox/templates/extras/object_configcontext.html @@ -1,5 +1,6 @@ {% extends base_template %} {% load helpers %} +{% load static %} {% block title %}{{ block.super }} - Config Context{% endblock %} @@ -9,9 +10,10 @@
Rendered Context + {% include 'extras/inc/configcontext_format.html' %}
-
{{ rendered_context|render_json }}
+ {% include 'extras/inc/configcontext_data.html' with data=rendered_context %}
@@ -58,3 +60,7 @@ {% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index c4b3bb6ea..4278b3b95 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -1,6 +1,7 @@ import datetime import json import re +import yaml from django import template from django.utils.html import strip_tags @@ -76,6 +77,14 @@ def render_json(value): return json.dumps(value, indent=4, sort_keys=True) +@register.filter() +def render_yaml(value): + """ + Render a dictionary as formatted YAML. + """ + return yaml.dump(dict(value)) + + @register.filter() def model_name(obj): """ From 265d5c87e7f007f04fda6bf6c69bf52c0d2def12 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Sat, 25 Jan 2020 16:12:37 +0000 Subject: [PATCH 025/174] Format for local and source contexts --- netbox/templates/extras/object_configcontext.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/extras/object_configcontext.html b/netbox/templates/extras/object_configcontext.html index f8191936e..784b5805f 100644 --- a/netbox/templates/extras/object_configcontext.html +++ b/netbox/templates/extras/object_configcontext.html @@ -24,7 +24,7 @@
{% if obj.local_context_data %} -
{{ obj.local_context_data|render_json }}
+ {% include 'extras/inc/configcontext_data.html' with data=obj.local_context_data %} {% else %} None {% endif %} @@ -49,7 +49,7 @@ {% if context.description %}
{{ context.description }} {% endif %} -
{{ context.data|render_json }}
+ {% include 'extras/inc/configcontext_data.html' with data=context.data %}
{% empty %}
From 7cfdc5188c13e361ffa20ccdcdde6158ad6d4f07 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Sat, 25 Jan 2020 17:55:01 +0000 Subject: [PATCH 026/174] Corrected ConfigContext data --- netbox/extras/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 792390121..bb5d484a0 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -49,7 +49,7 @@ class ConfigContextTestCase(TestCase): for i in range(1, 4): configcontext = ConfigContext( name='Config Context {}'.format(i), - data='{{"foo": {}}}'.format(i) + data={"foo": i} ) configcontext.save() configcontext.sites.add(site) From 4abd3866abe7bcc2d21efd6679b47ceb13793174 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Sun, 26 Jan 2020 10:53:58 +0000 Subject: [PATCH 027/174] Fixes #3886: Config context cluster (group) --- netbox/extras/filters.py | 17 +++++++++++ netbox/extras/forms.py | 25 ++++++++++++++-- .../0037_configcontexts_clusters.py | 24 +++++++++++++++ netbox/extras/models.py | 10 +++++++ netbox/extras/querysets.py | 6 ++++ netbox/extras/tests/test_filters.py | 30 +++++++++++++++++++ netbox/templates/extras/configcontext.html | 28 +++++++++++++++++ .../templates/extras/configcontext_edit.html | 2 ++ 8 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 netbox/extras/migrations/0037_configcontexts_clusters.py diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 8a0d32b33..dcd4f3ede 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -4,6 +4,7 @@ from django.db.models import Q from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup +from virtualization.models import Cluster, ClusterGroup from .choices import * from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag @@ -170,6 +171,22 @@ class ConfigContextFilterSet(django_filters.FilterSet): to_field_name='slug', label='Platform (slug)', ) + cluster_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='cluster_groups', + queryset=ClusterGroup.objects.all(), + label='Cluster group', + ) + cluster_group = django_filters.ModelMultipleChoiceFilter( + field_name='cluster_groups__slug', + queryset=ClusterGroup.objects.all(), + to_field_name='slug', + label='Cluster group (slug)', + ) + cluster_id = django_filters.ModelMultipleChoiceFilter( + field_name='clusters', + queryset=Cluster.objects.all(), + label='Cluster', + ) tenant_group_id = django_filters.ModelMultipleChoiceFilter( field_name='tenant_groups', queryset=TenantGroup.objects.all(), diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index edde6c6c5..5c33c7c98 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -254,8 +254,8 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConfigContext fields = [ - 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups', - 'tenants', 'tags', 'data', + 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups', + 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', ] widgets = { 'regions': APISelectMultiple( @@ -270,6 +270,12 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm): 'platforms': APISelectMultiple( api_url="/api/dcim/platforms/" ), + 'cluster_groups': APISelectMultiple( + api_url="/api/virtualization/cluster-groups/" + ), + 'clusters': APISelectMultiple( + api_url="/api/virtualization/clusters/" + ), 'tenant_groups': APISelectMultiple( api_url="/api/tenancy/tenant-groups/" ), @@ -340,6 +346,21 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form): value_field="slug", ) ) + cluster_group = FilterChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/virtualization/cluster-groups/", + value_field="slug", + ) + ) + cluster_id = FilterChoiceField( + queryset=Tenant.objects.all(), + label='Cluster', + widget=APISelectMultiple( + api_url="/api/virtualization/clusters/", + ) + ) tenant_group = FilterChoiceField( queryset=TenantGroup.objects.all(), to_field_name='slug', diff --git a/netbox/extras/migrations/0037_configcontexts_clusters.py b/netbox/extras/migrations/0037_configcontexts_clusters.py new file mode 100644 index 000000000..201aed94a --- /dev/null +++ b/netbox/extras/migrations/0037_configcontexts_clusters.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.8 on 2020-01-17 18:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('virtualization', '0013_deterministic_ordering'), + ('extras', '0036_contenttype_filters_to_q_objects'), + ] + + operations = [ + migrations.AddField( + model_name='configcontext', + name='cluster_groups', + field=models.ManyToManyField(blank=True, related_name='_configcontext_cluster_groups_+', to='virtualization.ClusterGroup'), + ), + migrations.AddField( + model_name='configcontext', + name='clusters', + field=models.ManyToManyField(blank=True, related_name='_configcontext_clusters_+', to='virtualization.Cluster'), + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index a03494bb2..f247fe1c2 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -694,6 +694,16 @@ class ConfigContext(models.Model): related_name='+', blank=True ) + cluster_groups = models.ManyToManyField( + to='virtualization.ClusterGroup', + related_name='+', + blank=True + ) + clusters = models.ManyToManyField( + to='virtualization.Cluster', + related_name='+', + blank=True + ) tenant_groups = models.ManyToManyField( to='tenancy.TenantGroup', related_name='+', diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 22ab489bd..812c66714 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -29,6 +29,10 @@ class ConfigContextQuerySet(QuerySet): # `device_role` for Device; `role` for VirtualMachine role = getattr(obj, 'device_role', None) or obj.role + # Virtualization cluster for VirtualMachine + cluster = getattr(obj, 'cluster', None) + cluster_group = getattr(cluster, 'group', None) + # Get the group of the assigned tenant, if any tenant_group = obj.tenant.group if obj.tenant else None @@ -44,6 +48,8 @@ class ConfigContextQuerySet(QuerySet): Q(sites=obj.site) | Q(sites=None), Q(roles=role) | Q(roles=None), Q(platforms=obj.platform) | Q(platforms=None), + Q(cluster_groups=cluster_group) | Q(cluster_groups=None), + Q(clusters=cluster) | Q(clusters=None), Q(tenant_groups=tenant_group) | Q(tenant_groups=None), Q(tenants=obj.tenant) | Q(tenants=None), Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None), diff --git a/netbox/extras/tests/test_filters.py b/netbox/extras/tests/test_filters.py index 130f94298..5ef96faa2 100644 --- a/netbox/extras/tests/test_filters.py +++ b/netbox/extras/tests/test_filters.py @@ -7,6 +7,7 @@ from extras.constants import GRAPH_MODELS from extras.filters import * from extras.models import ConfigContext, ExportTemplate, Graph from tenancy.models import Tenant, TenantGroup +from virtualization.models import Cluster, ClusterGroup, ClusterType class GraphTestCase(TestCase): @@ -107,6 +108,21 @@ class ConfigContextTestCase(TestCase): ) Platform.objects.bulk_create(platforms) + cluster_groups = ( + ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'), + ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'), + ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'), + ) + ClusterGroup.objects.bulk_create(cluster_groups) + + cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') + clusters = ( + Cluster(name='Cluster 1', type=cluster_type), + Cluster(name='Cluster 2', type=cluster_type), + Cluster(name='Cluster 3', type=cluster_type), + ) + Cluster.objects.bulk_create(clusters) + tenant_groups = ( TenantGroup(name='Tenant Group 1', slug='tenant-group-1'), TenantGroup(name='Tenant Group 2', slug='tenant-group-2'), @@ -132,6 +148,8 @@ class ConfigContextTestCase(TestCase): c.sites.set([sites[i]]) c.roles.set([device_roles[i]]) c.platforms.set([platforms[i]]) + c.cluster_groups.set([cluster_groups[i]]) + c.clusters.set([clusters[i]]) c.tenant_groups.set([tenant_groups[i]]) c.tenants.set([tenants[i]]) @@ -173,6 +191,18 @@ class ConfigContextTestCase(TestCase): params = {'platform': [platforms[0].slug, platforms[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_cluster_group(self): + cluster_groups = ClusterGroup.objects.all()[:2] + params = {'cluster_group_id': [cluster_groups[0].pk, cluster_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'cluster_group': [cluster_groups[0].slug, cluster_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_cluster(self): + clusters = Cluster.objects.all()[:2] + params = {'cluster_id': [clusters[0].pk, clusters[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_tenant_group(self): tenant_groups = TenantGroup.objects.all()[:2] params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} diff --git a/netbox/templates/extras/configcontext.html b/netbox/templates/extras/configcontext.html index 7cec3f403..f9ea26c2b 100644 --- a/netbox/templates/extras/configcontext.html +++ b/netbox/templates/extras/configcontext.html @@ -134,6 +134,34 @@ {% endif %} + + Cluster Groups + + {% if configcontext.cluster_groups.all %} +
    + {% for cluster_group in configcontext.cluster_groups.all %} +
  • {{ cluster_group }}
  • + {% endfor %} +
+ {% else %} + None + {% endif %} + + + + Clusters + + {% if configcontext.clusters.all %} +
    + {% for cluster in configcontext.clusters.all %} +
  • {{ cluster }}
  • + {% endfor %} +
+ {% else %} + None + {% endif %} + + Tenant Groups diff --git a/netbox/templates/extras/configcontext_edit.html b/netbox/templates/extras/configcontext_edit.html index d31aa5c57..9e922108c 100644 --- a/netbox/templates/extras/configcontext_edit.html +++ b/netbox/templates/extras/configcontext_edit.html @@ -18,6 +18,8 @@ {% render_field form.sites %} {% render_field form.roles %} {% render_field form.platforms %} + {% render_field form.cluster_groups %} + {% render_field form.clusters %} {% render_field form.tenant_groups %} {% render_field form.tenants %} {% render_field form.tags %} From 4e4a05d3b9fc1f5ac033934ccfdd525da06f5d19 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Sun, 26 Jan 2020 12:52:18 +0000 Subject: [PATCH 028/174] Fixes #4016: Removed duplicate tenant field for cluster edit form --- netbox/templates/virtualization/cluster_edit.html | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/templates/virtualization/cluster_edit.html b/netbox/templates/virtualization/cluster_edit.html index bf81ffd42..c4d39d12e 100644 --- a/netbox/templates/virtualization/cluster_edit.html +++ b/netbox/templates/virtualization/cluster_edit.html @@ -8,7 +8,6 @@ {% render_field form.name %} {% render_field form.type %} {% render_field form.group %} - {% render_field form.tenant %} {% render_field form.site %}
From 011280b0bfced7d21d275392da174d5b0b784aec Mon Sep 17 00:00:00 2001 From: hellerve Date: Mon, 27 Jan 2020 13:13:07 +0100 Subject: [PATCH 029/174] dcim: add borders on the rear of devices as well --- netbox/dcim/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 37ee0a266..350330757 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -405,7 +405,7 @@ class RackElevationHelperMixin: @staticmethod def _draw_device_rear(drawing, device, start, end, text): - rect = drawing.rect(start, end, class_="blocked") + rect = drawing.rect(start, end, class_="slot blocked") rect.set_desc('{} — {} ({}U) {} {}'.format( device.device_role, device.device_type.display_name, device.device_type.u_height, device.asset_tag or '', device.serial or '' From 1901f63b4ce0562ad562406f41a6352d04edfa4e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jan 2020 09:45:18 -0500 Subject: [PATCH 030/174] Update changelog --- docs/release-notes/version-2.7.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 5c489a96c..92fb7203b 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -14,6 +14,8 @@ * [#3989](https://github.com/netbox-community/netbox/issues/3989) - Correct HTTP content type assignment for webhooks * [#3999](https://github.com/netbox-community/netbox/issues/3999) - Do not filter child results by null if non-required parent fields are blank * [#4008](https://github.com/netbox-community/netbox/issues/4008) - Toggle rack elevation face using front/rear strings +* [#4017](https://github.com/netbox-community/netbox/issues/4017) - Remove redundant tenant field from cluster form +* [#4019](https://github.com/netbox-community/netbox/issues/4019) - Restore border around background devices in rack elevations --- From 46d0e88da3e145e34f49f0d78d1b8f141570bdb8 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Mon, 27 Jan 2020 15:49:15 +0000 Subject: [PATCH 031/174] Fixes #4010: Fixes IP addresses table when filtering interfaces --- netbox/project-static/js/interface_toggles.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/netbox/project-static/js/interface_toggles.js b/netbox/project-static/js/interface_toggles.js index a46d3185c..87b59b081 100644 --- a/netbox/project-static/js/interface_toggles.js +++ b/netbox/project-static/js/interface_toggles.js @@ -2,9 +2,9 @@ $('button.toggle-ips').click(function() { var selected = $(this).attr('selected'); if (selected) { - $('#interfaces_table tr.ipaddresses').hide(); + $('#interfaces_table tr.interface:visible + tr.ipaddresses').hide(); } else { - $('#interfaces_table tr.ipaddresses').show(); + $('#interfaces_table tr.interface:visible + tr.ipaddresses').show(); } $(this).attr('selected', !selected); $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked'); @@ -14,10 +14,11 @@ $('button.toggle-ips').click(function() { // Inteface filtering $('input.interface-filter').on('input', function() { var filter = new RegExp(this.value); + var interface; - for (interface of $(this).closest('div.panel').find('tbody > tr')) { + for (interface of $('#interfaces_table > tbody > tr')) { // Slice off 'interface_' at the start of the ID - if (filter && filter.test(interface.id.slice(10))) { + if (filter.test(interface.id.slice(10))) { // Match the toggle in case the filter now matches the interface $(interface).find('input:checkbox[name=pk]').prop('checked', $('input.toggle').prop('checked')); $(interface).show(); @@ -27,4 +28,9 @@ $('input.interface-filter').on('input', function() { $(interface).hide(); } } + + // Show the ip addresses table row for the visible (matched) interfaces, if checked + if ($('button.toggle-ips').attr('selected')) { + $('#interfaces_table > tbody > tr:visible').next('tr.ipaddresses').show(); + } }); From 00b50f9c65629c406f39e75c9695584766126a9f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jan 2020 12:34:52 -0500 Subject: [PATCH 032/174] Remove obsolete constants --- netbox/utilities/middleware.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/netbox/utilities/middleware.py b/netbox/utilities/middleware.py index a44273ab0..564771821 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -7,9 +7,6 @@ from django.urls import reverse from .views import server_error -BASE_PATH = getattr(settings, 'BASE_PATH', False) -LOGIN_REQUIRED = getattr(settings, 'LOGIN_REQUIRED', False) - class LoginRequiredMiddleware(object): """ @@ -19,7 +16,7 @@ class LoginRequiredMiddleware(object): self.get_response = get_response def __call__(self, request): - if LOGIN_REQUIRED and not request.user.is_authenticated: + if settings.LOGIN_REQUIRED and not request.user.is_authenticated: # Redirect unauthenticated requests to the login page. API requests are exempt from redirection as the API # performs its own authentication. Also metrics can be read without login. api_path = reverse('api-root') From 73b35e72d8dbd5a4179c149cec13ec74107d70ba Mon Sep 17 00:00:00 2001 From: Samuel Mutel Date: Mon, 27 Jan 2020 21:09:49 +0100 Subject: [PATCH 033/174] Update nginx documentation --- docs/installation/3-http-daemon.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/3-http-daemon.md b/docs/installation/3-http-daemon.md index 4ca566aa3..5e19f54a2 100644 --- a/docs/installation/3-http-daemon.md +++ b/docs/installation/3-http-daemon.md @@ -29,7 +29,7 @@ server { location / { proxy_pass http://127.0.0.1:8001; - proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } From d0f127e575ad7a7840a459d4a670545fde3afbe0 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Mon, 27 Jan 2020 21:53:10 +0000 Subject: [PATCH 034/174] Fixes #3338: Added termination A and Z to the circuit --- netbox/circuits/api/serializers.py | 18 +++++++++++++++--- netbox/circuits/api/views.py | 4 +++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index b22135b3f..6bac48a59 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -3,11 +3,11 @@ from taggit_serializer.serializers import TaggitSerializer, TagListSerializerFie from circuits.choices import CircuitStatusChoices from circuits.models import Provider, Circuit, CircuitTermination, CircuitType -from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer +from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer from dcim.api.serializers import ConnectedEndpointSerializer from extras.api.customfields import CustomFieldModelSerializer from tenancy.api.nested_serializers import NestedTenantSerializer -from utilities.api import ChoiceField, ValidatedModelSerializer +from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer from .nested_serializers import * @@ -39,18 +39,30 @@ class CircuitTypeSerializer(ValidatedModelSerializer): fields = ['id', 'name', 'slug', 'description', 'circuit_count'] +class CircuitCircuitTerminationSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') + site = NestedSiteSerializer() + connected_endpoint = NestedInterfaceSerializer() + + class Meta: + model = CircuitTermination + fields = ['id', 'url', 'site', 'connected_endpoint', 'port_speed', 'upstream_speed', 'xconnect_id'] + + class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer): provider = NestedProviderSerializer() status = ChoiceField(choices=CircuitStatusChoices, required=False) type = NestedCircuitTypeSerializer() tenant = NestedTenantSerializer(required=False, allow_null=True) + termination_a = CircuitCircuitTerminationSerializer(read_only=True) + termination_z = CircuitCircuitTerminationSerializer(read_only=True) tags = TagListSerializerField(required=False) class Meta: model = Circuit fields = [ 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 98b7c9184..c98eb0c64 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -62,7 +62,9 @@ class CircuitTypeViewSet(ModelViewSet): # class CircuitViewSet(CustomFieldModelViewSet): - queryset = Circuit.objects.prefetch_related('type', 'tenant', 'provider').prefetch_related('tags') + queryset = Circuit.objects.prefetch_related( + 'type', 'tenant', 'provider', 'terminations__site', 'terminations__connected_endpoint' + ).prefetch_related('tags') serializer_class = serializers.CircuitSerializer filterset_class = filters.CircuitFilterSet From 5c5b9c95aaf0fc6b003fab7be6746cae408976c6 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Mon, 27 Jan 2020 22:07:42 +0000 Subject: [PATCH 035/174] Interface selector restricted to only interface --- netbox/project-static/js/interface_toggles.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/netbox/project-static/js/interface_toggles.js b/netbox/project-static/js/interface_toggles.js index 87b59b081..df8ac064b 100644 --- a/netbox/project-static/js/interface_toggles.js +++ b/netbox/project-static/js/interface_toggles.js @@ -16,21 +16,20 @@ $('input.interface-filter').on('input', function() { var filter = new RegExp(this.value); var interface; - for (interface of $('#interfaces_table > tbody > tr')) { + for (interface of $('#interfaces_table > tbody > tr.interface')) { // Slice off 'interface_' at the start of the ID if (filter.test(interface.id.slice(10))) { // Match the toggle in case the filter now matches the interface $(interface).find('input:checkbox[name=pk]').prop('checked', $('input.toggle').prop('checked')); $(interface).show(); + if ($('button.toggle-ips').attr('selected')) { + $(interface).next('tr.ipaddresses').show(); + } } else { // Uncheck to prevent actions from including it when it doesn't match $(interface).find('input:checkbox[name=pk]').prop('checked', false); $(interface).hide(); + $(interface).next('tr.ipaddresses').hide(); } } - - // Show the ip addresses table row for the visible (matched) interfaces, if checked - if ($('button.toggle-ips').attr('selected')) { - $('#interfaces_table > tbody > tr:visible').next('tr.ipaddresses').show(); - } }); From 93fa00b673db4e4d6118a8076ea0ba61bc2a4283 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jan 2020 17:21:57 -0500 Subject: [PATCH 036/174] #3338: Prefetch termination devices to avoid extra database queries --- netbox/circuits/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index c98eb0c64..75f7e0e3e 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -63,7 +63,7 @@ class CircuitTypeViewSet(ModelViewSet): class CircuitViewSet(CustomFieldModelViewSet): queryset = Circuit.objects.prefetch_related( - 'type', 'tenant', 'provider', 'terminations__site', 'terminations__connected_endpoint' + 'type', 'tenant', 'provider', 'terminations__site', 'terminations__connected_endpoint__device' ).prefetch_related('tags') serializer_class = serializers.CircuitSerializer filterset_class = filters.CircuitFilterSet From 9c4f1d5795e5f1092048a037fdfcc82914f348b0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Jan 2020 17:24:00 -0500 Subject: [PATCH 037/174] Changelog for #3338 --- docs/release-notes/version-2.7.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 92fb7203b..b3adafab0 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -3,6 +3,7 @@ ## Enhancements * [#3310](https://github.com/netbox-community/netbox/issues/3310) - Pre-select site/rack for B side when creating a new cable +* [#3338](https://github.com/netbox-community/netbox/issues/3338) - Include circuit terminations in API representation of circuits * [#3509](https://github.com/netbox-community/netbox/issues/3509) - Add IP address variables for custom scripts * [#4005](https://github.com/netbox-community/netbox/issues/4005) - Include timezone context in webhook timestamps From 3bce8e97160ef082d4c58cea93a23ab2ed230c14 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Mon, 27 Jan 2020 22:44:38 +0000 Subject: [PATCH 038/174] Fixes #4025: Cable status class --- netbox/templates/dcim/cable_trace.html | 2 +- netbox/templates/dcim/inc/consoleport.html | 2 +- netbox/templates/dcim/inc/consoleserverport.html | 2 +- netbox/templates/dcim/inc/frontport.html | 2 +- netbox/templates/dcim/inc/interface.html | 2 +- netbox/templates/dcim/inc/poweroutlet.html | 2 +- netbox/templates/dcim/inc/powerport.html | 2 +- netbox/templates/dcim/inc/rearport.html | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index 4dd145058..8c7b69f26 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -32,7 +32,7 @@ {% if cable.label %}{{ cable.label }}{% else %}Cable #{{ cable.pk }}{% endif %} -

{{ cable.get_status_display }}

+

{{ cable.get_status_display }}

{{ cable.get_type_display|default:"" }}

{% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %} {% if cable.color %} diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index f9fc40fee..9089f19b4 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -1,4 +1,4 @@ - + {# Name #} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index f5b19ed75..0d649f812 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -1,6 +1,6 @@ {% load helpers %} - + {# Checkbox #} {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %} diff --git a/netbox/templates/dcim/inc/frontport.html b/netbox/templates/dcim/inc/frontport.html index 1b7f85e2c..12915f64d 100644 --- a/netbox/templates/dcim/inc/frontport.html +++ b/netbox/templates/dcim/inc/frontport.html @@ -1,5 +1,5 @@ {% load helpers %} - + {# Checkbox #} {% if perms.dcim.change_frontport or perms.dcim.delete_frontport %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 6ec46824b..095930c92 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -1,5 +1,5 @@ {% load helpers %} - + {# Checkbox #} {% if perms.dcim.change_interface or perms.dcim.delete_interface %} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 5691608b4..1c0630310 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -1,6 +1,6 @@ {% load helpers %} - + {# Checkbox #} {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index 679082654..045b25dfd 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -1,4 +1,4 @@ - + {# Name #} diff --git a/netbox/templates/dcim/inc/rearport.html b/netbox/templates/dcim/inc/rearport.html index 27609e726..73ccd6b70 100644 --- a/netbox/templates/dcim/inc/rearport.html +++ b/netbox/templates/dcim/inc/rearport.html @@ -1,5 +1,5 @@ {% load helpers %} - + {# Checkbox #} {% if perms.dcim.change_rearport or perms.dcim.delete_rearport %} From 8306976b3ee8734c2f3a2008a59f95ceda597ffb Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Mon, 27 Jan 2020 22:49:36 +0000 Subject: [PATCH 039/174] Removed erroneous double-space --- netbox/templates/dcim/inc/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 095930c92..2fe970fd7 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -1,5 +1,5 @@ {% load helpers %} - + {# Checkbox #} {% if perms.dcim.change_interface or perms.dcim.delete_interface %} From 720bd872921f398fbff35d42dd5598bdaa1733ce Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Mon, 27 Jan 2020 22:56:25 +0000 Subject: [PATCH 040/174] Fixed interface mark connected/planned buttons --- netbox/templates/dcim/inc/cable_toggle_buttons.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/dcim/inc/cable_toggle_buttons.html b/netbox/templates/dcim/inc/cable_toggle_buttons.html index 3e0209e01..507aab3be 100644 --- a/netbox/templates/dcim/inc/cable_toggle_buttons.html +++ b/netbox/templates/dcim/inc/cable_toggle_buttons.html @@ -1,5 +1,5 @@ {% if perms.dcim.change_cable %} - {% if cable.status %} + {% if cable.status == 'connected' %} From 8d547e99066782393e6725ea512ba71423607821 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jan 2020 09:47:33 -0500 Subject: [PATCH 041/174] Fixes #4028: Correct URL patterns to match Unicode characters in tag slugs --- docs/release-notes/version-2.7.md | 1 + netbox/extras/urls.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index b3adafab0..255abbc85 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -17,6 +17,7 @@ * [#4008](https://github.com/netbox-community/netbox/issues/4008) - Toggle rack elevation face using front/rear strings * [#4017](https://github.com/netbox-community/netbox/issues/4017) - Remove redundant tenant field from cluster form * [#4019](https://github.com/netbox-community/netbox/issues/4019) - Restore border around background devices in rack elevations +* [#4028](https://github.com/netbox-community/netbox/issues/4028) - Correct URL patterns to match Unicode characters in tag slugs --- diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index edc3ffcad..653fe7c7f 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -11,10 +11,10 @@ urlpatterns = [ path(r'tags/', views.TagListView.as_view(), name='tag_list'), path(r'tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'), path(r'tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'), - path(r'tags//', views.TagView.as_view(), name='tag'), - path(r'tags//edit/', views.TagEditView.as_view(), name='tag_edit'), - path(r'tags//delete/', views.TagDeleteView.as_view(), name='tag_delete'), - path(r'tags//changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}), + path(r'tags//', views.TagView.as_view(), name='tag'), + path(r'tags//edit/', views.TagEditView.as_view(), name='tag_edit'), + path(r'tags//delete/', views.TagDeleteView.as_view(), name='tag_delete'), + path(r'tags//changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}), # Config contexts path(r'config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'), From 3c5346f60ac93d50239a0058a267e2e1622bc693 Mon Sep 17 00:00:00 2001 From: kobayashi Date: Tue, 28 Jan 2020 10:22:28 -0500 Subject: [PATCH 042/174] Fixes #3978: VRF filtering for NAT IP search --- docs/release-notes/version-2.7.md | 1 + netbox/ipam/forms.py | 11 +++++++++++ netbox/templates/ipam/ipaddress_edit.html | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index b3adafab0..85a9e87e3 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -5,6 +5,7 @@ * [#3310](https://github.com/netbox-community/netbox/issues/3310) - Pre-select site/rack for B side when creating a new cable * [#3338](https://github.com/netbox-community/netbox/issues/3338) - Include circuit terminations in API representation of circuits * [#3509](https://github.com/netbox-community/netbox/issues/3509) - Add IP address variables for custom scripts +* [#3978](https://github.com/netbox-community/netbox/issues/3978) - Add VRF filtering to search NAT IP * [#4005](https://github.com/netbox-community/netbox/issues/4005) - Include timezone context in webhook timestamps ## Bug Fixes diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 35cf12dfb..183fcb717 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -635,6 +635,17 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) } ) ) + nat_vrf = forms.ModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF', + widget=APISelect( + api_url="/api/ipam/vrfs/", + filter_for={ + 'nat_inside': 'vrf_id' + } + ) + ) nat_inside = ChainedModelChoiceField( queryset=IPAddress.objects.all(), chains=( diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html index c24c94c87..e3f694fe3 100644 --- a/netbox/templates/ipam/ipaddress_edit.html +++ b/netbox/templates/ipam/ipaddress_edit.html @@ -61,7 +61,7 @@ {% render_field form.nat_device %} {% render_field form.nat_inside %} From 8849f4b0a5db2f53bb8831fe492505bd09bc1388 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Tue, 28 Jan 2020 17:30:26 +0000 Subject: [PATCH 043/174] Added cluster groups and clusters to serializers --- netbox/extras/api/serializers.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 0e27a8ee5..58433df25 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -20,6 +20,8 @@ from utilities.api import ( ChoiceField, ContentTypeField, get_serializer_for_model, SerializerNotFound, SerializedPKRelatedField, ValidatedModelSerializer, ) +from virtualization.api.nested_serializers import NestedClusterGroupSerializer, NestedClusterSerializer +from virtualization.models import Cluster, ClusterGroup from .nested_serializers import * @@ -161,6 +163,18 @@ class ConfigContextSerializer(ValidatedModelSerializer): required=False, many=True ) + cluster_groups = SerializedPKRelatedField( + queryset=ClusterGroup.objects.all(), + serializer=NestedClusterGroupSerializer, + required=False, + many=True + ) + clusters = SerializedPKRelatedField( + queryset=Cluster.objects.all(), + serializer=NestedClusterSerializer, + required=False, + many=True + ) tenant_groups = SerializedPKRelatedField( queryset=TenantGroup.objects.all(), serializer=NestedTenantGroupSerializer, @@ -184,7 +198,7 @@ class ConfigContextSerializer(ValidatedModelSerializer): model = ConfigContext fields = [ 'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', - 'tenant_groups', 'tenants', 'tags', 'data', + 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', ] From 9b9e5684460efc68a851407befc7a58015747c32 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jan 2020 12:49:00 -0500 Subject: [PATCH 044/174] Fixes #4027: Repair schema migration for #3569 to convert IP addresses with DHCP status --- docs/release-notes/version-2.7.md | 1 + .../migrations/0029_3569_ipaddress_fields.py | 2 +- .../0034_fix_ipaddress_status_dhcp.py | 21 +++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 255abbc85..a7b6ec0e7 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -17,6 +17,7 @@ * [#4008](https://github.com/netbox-community/netbox/issues/4008) - Toggle rack elevation face using front/rear strings * [#4017](https://github.com/netbox-community/netbox/issues/4017) - Remove redundant tenant field from cluster form * [#4019](https://github.com/netbox-community/netbox/issues/4019) - Restore border around background devices in rack elevations +* [#4027](https://github.com/netbox-community/netbox/issues/4027) - Repair schema migration for #3569 to convert IP addresses with DHCP status * [#4028](https://github.com/netbox-community/netbox/issues/4028) - Correct URL patterns to match Unicode characters in tag slugs --- diff --git a/netbox/ipam/migrations/0029_3569_ipaddress_fields.py b/netbox/ipam/migrations/0029_3569_ipaddress_fields.py index 528efb4fb..195b630db 100644 --- a/netbox/ipam/migrations/0029_3569_ipaddress_fields.py +++ b/netbox/ipam/migrations/0029_3569_ipaddress_fields.py @@ -2,10 +2,10 @@ from django.db import migrations, models IPADDRESS_STATUS_CHOICES = ( - (0, 'container'), (1, 'active'), (2, 'reserved'), (3, 'deprecated'), + (5, 'dhcp'), ) IPADDRESS_ROLE_CHOICES = ( diff --git a/netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py b/netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py new file mode 100644 index 000000000..9e496153e --- /dev/null +++ b/netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py @@ -0,0 +1,21 @@ +from django.db import migrations + + +def ipaddress_status_dhcp_to_slug(apps, schema_editor): + IPAddress = apps.get_model('ipam', 'IPAddress') + IPAddress.objects.filter(status='5').update(status='dhcp') + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0033_deterministic_ordering'), + ] + + operations = [ + # Fixes a missed integer substitution from #3569; see bug #4027. The original migration has also been fixed, + # so this can be omitted when squashing in the future. + migrations.RunPython( + code=ipaddress_status_dhcp_to_slug + ), + ] From 77292050d4d0ffa1be7320bbe82f04f13e0d786b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jan 2020 13:38:03 -0500 Subject: [PATCH 045/174] Changelog for #4025 --- docs/release-notes/version-2.7.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index a7b6ec0e7..e6becd4c9 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -17,6 +17,7 @@ * [#4008](https://github.com/netbox-community/netbox/issues/4008) - Toggle rack elevation face using front/rear strings * [#4017](https://github.com/netbox-community/netbox/issues/4017) - Remove redundant tenant field from cluster form * [#4019](https://github.com/netbox-community/netbox/issues/4019) - Restore border around background devices in rack elevations +* [#4025](https://github.com/netbox-community/netbox/issues/4025) - Correct display of cable status (various places) * [#4027](https://github.com/netbox-community/netbox/issues/4027) - Repair schema migration for #3569 to convert IP addresses with DHCP status * [#4028](https://github.com/netbox-community/netbox/issues/4028) - Correct URL patterns to match Unicode characters in tag slugs From ede576a2ae250b404ea65404cf11d8da69855aef Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jan 2020 13:55:44 -0500 Subject: [PATCH 046/174] Changelog for #4022 --- docs/release-notes/version-2.7.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index ef04aa19c..25aad4283 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -18,6 +18,7 @@ * [#4008](https://github.com/netbox-community/netbox/issues/4008) - Toggle rack elevation face using front/rear strings * [#4017](https://github.com/netbox-community/netbox/issues/4017) - Remove redundant tenant field from cluster form * [#4019](https://github.com/netbox-community/netbox/issues/4019) - Restore border around background devices in rack elevations +* [#4022](https://github.com/netbox-community/netbox/issues/4022) - Fix display of assigned IPs when filtering device interfaces * [#4025](https://github.com/netbox-community/netbox/issues/4025) - Correct display of cable status (various places) * [#4027](https://github.com/netbox-community/netbox/issues/4027) - Repair schema migration for #3569 to convert IP addresses with DHCP status * [#4028](https://github.com/netbox-community/netbox/issues/4028) - Correct URL patterns to match Unicode characters in tag slugs From 0a11fc1221075569b47557398e9e0e52effb7ef8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jan 2020 14:19:29 -0500 Subject: [PATCH 047/174] Fixes #4030: Fix exception when setting interfaces to tagged mode in bulk --- docs/release-notes/version-2.7.md | 1 + netbox/dcim/forms.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 25aad4283..ae4be060a 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -22,6 +22,7 @@ * [#4025](https://github.com/netbox-community/netbox/issues/4025) - Correct display of cable status (various places) * [#4027](https://github.com/netbox-community/netbox/issues/4027) - Repair schema migration for #3569 to convert IP addresses with DHCP status * [#4028](https://github.com/netbox-community/netbox/issues/4028) - Correct URL patterns to match Unicode characters in tag slugs +* [#4030](https://github.com/netbox-community/netbox/issues/4030) - Fix exception when setting interfaces to tagged mode in bulk --- diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index f0667c143..e713d2c7c 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2748,7 +2748,7 @@ class InterfaceCSVForm(forms.ModelForm): return self.cleaned_data['enabled'] -class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput() @@ -2829,6 +2829,18 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo else: self.fields['lag'].choices = [] + def clean(self): + + # Untagged interfaces cannot be assigned tagged VLANs + if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']: + raise forms.ValidationError({ + 'mode': "An access interface cannot have tagged VLANs assigned." + }) + + # Remove all tagged VLAN assignments from "tagged all" interfaces + elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL: + self.cleaned_data['tagged_vlans'] = [] + class InterfaceBulkRenameForm(BulkRenameForm): pk = forms.ModelMultipleChoiceField( From 8de9f521519c21c529c79b053cf261d9bac53d84 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jan 2020 16:09:10 -0500 Subject: [PATCH 048/174] Fixes #4033: Restore missing comments field label of various bulk edit forms --- docs/release-notes/version-2.7.md | 1 + netbox/circuits/forms.py | 3 ++- netbox/dcim/forms.py | 8 +++++--- netbox/virtualization/forms.py | 6 ++++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index ae4be060a..ff8391585 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -23,6 +23,7 @@ * [#4027](https://github.com/netbox-community/netbox/issues/4027) - Repair schema migration for #3569 to convert IP addresses with DHCP status * [#4028](https://github.com/netbox-community/netbox/issues/4028) - Correct URL patterns to match Unicode characters in tag slugs * [#4030](https://github.com/netbox-community/netbox/issues/4030) - Fix exception when setting interfaces to tagged mode in bulk +* [#4033](https://github.com/netbox-community/netbox/issues/4033) - Restore missing comments field label of various bulk edit forms --- diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index d5d78e7bd..165e32eb3 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -89,7 +89,8 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdi label='Admin contact' ) comments = CommentField( - widget=SmallTextarea() + widget=SmallTextarea, + label='Comments' ) class Meta: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index e713d2c7c..ca5d25389 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -676,7 +676,8 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor widget=StaticSelect2() ) comments = CommentField( - widget=SmallTextarea + widget=SmallTextarea, + label='Comments' ) class Meta: @@ -4418,8 +4419,9 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd max_utilization = forms.IntegerField( required=False ) - comments = forms.CharField( - required=False + comments = CommentField( + widget=SmallTextarea, + label='Comments' ) class Meta: diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index ae516fcb3..cbabc20c4 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -171,7 +171,8 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit ) ) comments = CommentField( - widget=SmallTextarea() + widget=SmallTextarea, + label='Comments' ) class Meta: @@ -535,7 +536,8 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB label='Disk (GB)' ) comments = CommentField( - widget=SmallTextarea() + widget=SmallTextarea, + label='Comments' ) class Meta: From be716a33458d4f35b7d537a90709adfe13e37e61 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jan 2020 16:33:55 -0500 Subject: [PATCH 049/174] Release v2.7.3 --- docs/release-notes/version-2.7.md | 2 +- netbox/netbox/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index ff8391585..a6a6b65cf 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -1,4 +1,4 @@ -# v2.7.3 (FUTURE) +# v2.7.3 (2020-01-28) ## Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index f747c3b1d..3a780f55a 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured # Environment setup # -VERSION = '2.7.3-dev' +VERSION = '2.7.3' # Hostname HOSTNAME = platform.node() From 4629cda9ad8bbd59075cfcf7323e5fd03efcdb30 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jan 2020 16:42:33 -0500 Subject: [PATCH 050/174] 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 3a780f55a..8cdbb60a3 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured # Environment setup # -VERSION = '2.7.3' +VERSION = '2.7.4-dev' # Hostname HOSTNAME = platform.node() From 6fefa3c7dd10d931d801ebd7e735813734a7ac7e Mon Sep 17 00:00:00 2001 From: Kevin Newland Date: Tue, 28 Jan 2020 18:34:26 -0600 Subject: [PATCH 051/174] update ldap documentation use new ldap cache configuration in documentation https://github.com/netbox-community/netbox/blob/develop/netbox/netbox/settings.py#L360 --- docs/installation/4-ldap.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/4-ldap.md b/docs/installation/4-ldap.md index a41400808..953d3cb28 100644 --- a/docs/installation/4-ldap.md +++ b/docs/installation/4-ldap.md @@ -110,8 +110,8 @@ AUTH_LDAP_USER_FLAGS_BY_GROUP = { AUTH_LDAP_FIND_GROUP_PERMS = True # Cache groups for one hour to reduce LDAP traffic -AUTH_LDAP_CACHE_GROUPS = True -AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600 +AUTH_LDAP_CACHE_TIMEOUT = 3600 + ``` * `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in. From e0d538ad310265a97b5f89c980d522cdd174ef2a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 09:40:17 -0500 Subject: [PATCH 052/174] Fixes #4043: Fix toggling of required fields in custom scripts --- docs/release-notes/version-2.7.md | 8 ++++++++ netbox/extras/scripts.py | 9 +++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index a6a6b65cf..ab1e2de0d 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -1,3 +1,11 @@ +# v2.7.4 (FUTURE) + +## Bug Fixes + +* [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts + +--- + # v2.7.3 (2020-01-28) ## Enhancements diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index bd7e864e1..6567fe707 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -53,14 +53,15 @@ class ScriptVariable: # Initialize field attributes if not hasattr(self, 'field_attrs'): self.field_attrs = {} - if description: - self.field_attrs['help_text'] = description if label: self.field_attrs['label'] = label + if description: + self.field_attrs['help_text'] = description if default: self.field_attrs['initial'] = default - if required: - self.field_attrs['required'] = True + self.field_attrs['required'] = required + + # Initialize the list of optional validators if none have already been defined if 'validators' not in self.field_attrs: self.field_attrs['validators'] = [] From bc7cf63958b9f625c2e05b5aaae2944d957df1bc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 10:49:02 -0500 Subject: [PATCH 053/174] Rename and refactor CustomFieldForm --- netbox/circuits/forms.py | 10 +++++----- netbox/dcim/forms.py | 20 ++++++++++---------- netbox/extras/forms.py | 29 ++++++++++++++++------------- netbox/ipam/forms.py | 26 +++++++++++++------------- netbox/secrets/forms.py | 6 +++--- netbox/tenancy/forms.py | 6 +++--- netbox/virtualization/forms.py | 10 +++++----- 7 files changed, 55 insertions(+), 52 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 6a528e5d6..b97f531b5 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -2,7 +2,7 @@ from django import forms from taggit.forms import TagField from dcim.models import Region, Site -from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( @@ -17,7 +17,7 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider # Providers # -class ProviderForm(BootstrapMixin, CustomFieldForm): +class ProviderForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() comments = CommentField() tags = TagField( @@ -46,7 +46,7 @@ class ProviderForm(BootstrapMixin, CustomFieldForm): } -class ProviderCSVForm(CustomFieldForm): +class ProviderCSVForm(CustomFieldModelForm): slug = SlugField() class Meta: @@ -160,7 +160,7 @@ class CircuitTypeCSVForm(forms.ModelForm): # Circuits # -class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): comments = CommentField() tags = TagField( required=False @@ -188,7 +188,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class CircuitCSVForm(CustomFieldForm): +class CircuitCSVForm(CustomFieldModelForm): provider = forms.ModelChoiceField( queryset=Provider.objects.all(), to_field_name='name', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 18779217a..216af8b63 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -13,7 +13,7 @@ from timezone_field import TimeZoneFormField from circuits.models import Circuit, Provider from extras.forms import ( - AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm + AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm ) from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN from ipam.models import IPAddress, VLAN @@ -215,7 +215,7 @@ class RegionFilterForm(BootstrapMixin, forms.Form): # Sites # -class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): region = TreeNodeChoiceField( queryset=Region.objects.all(), required=False, @@ -263,7 +263,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class SiteCSVForm(CustomFieldForm): +class SiteCSVForm(CustomFieldModelForm): status = CSVChoiceField( choices=SiteStatusChoices, required=False, @@ -459,7 +459,7 @@ class RackRoleCSVForm(forms.ModelForm): # Racks # -class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): group = ChainedModelChoiceField( queryset=RackGroup.objects.all(), chains=( @@ -504,7 +504,7 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class RackCSVForm(CustomFieldForm): +class RackCSVForm(CustomFieldModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -897,7 +897,7 @@ class ManufacturerCSVForm(forms.ModelForm): # Device types # -class DeviceTypeForm(BootstrapMixin, CustomFieldForm): +class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField( slug_source='model' ) @@ -1516,7 +1516,7 @@ class PlatformCSVForm(forms.ModelForm): # Devices # -class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), widget=APISelect( @@ -1724,7 +1724,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.initial['rack'] = self.instance.parent_bay.device.rack_id -class BaseDeviceCSVForm(CustomFieldForm): +class BaseDeviceCSVForm(CustomFieldModelForm): device_role = forms.ModelChoiceField( queryset=DeviceRole.objects.all(), to_field_name='name', @@ -4241,7 +4241,7 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): # Power feeds # -class PowerFeedForm(BootstrapMixin, CustomFieldForm): +class PowerFeedForm(BootstrapMixin, CustomFieldModelForm): site = ChainedModelChoiceField( queryset=Site.objects.all(), required=False, @@ -4286,7 +4286,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldForm): self.initial['site'] = self.instance.power_panel.site -class PowerFeedCSVForm(CustomFieldForm): +class PowerFeedCSVForm(CustomFieldModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 314455b83..bb1015638 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -10,8 +10,8 @@ from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, - CustomFieldChoiceField, CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, - LaxURLField, JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, + CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, LaxURLField, JSONField, SlugField, + StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, ) from .choices import * from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag @@ -71,7 +71,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F default_choice = cf.choices.get(value=initial).pk except ObjectDoesNotExist: pass - field = CustomFieldChoiceField( + field = forms.TypedChoiceField( choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2() ) @@ -93,21 +93,15 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F return field_dict -class CustomFieldForm(forms.ModelForm): +class CustomFieldModelForm(forms.ModelForm): def __init__(self, *args, **kwargs): - self.custom_fields = [] - self.obj_type = ContentType.objects.get_for_model(self._meta.model) - super().__init__(*args, **kwargs) - # Add all applicable CustomFields to the form - custom_fields = [] - for name, field in get_custom_fields_for_model(self.obj_type).items(): - self.fields[name] = field - custom_fields.append(name) - self.custom_fields = custom_fields + # Append form fields for CustomFields + self.obj_type = ContentType.objects.get_for_model(self._meta.model) + self._append_customfield_fields() # If editing an existing object, initialize values for all custom fields if self.instance.pk: @@ -118,6 +112,15 @@ class CustomFieldForm(forms.ModelForm): for cfv in existing_values: self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.serialized_value + def _append_customfield_fields(self): + """ + Append form fields for all applicable CustomFields. + """ + self.custom_fields = [] + for name, field in get_custom_fields_for_model(self.obj_type).items(): + self.fields[name] = field + self.custom_fields.append(name) + def _save_custom_fields(self): for field_name in self.custom_fields: diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 3dda6aba5..594e95f58 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -4,7 +4,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from taggit.forms import TagField from dcim.models import Device, Interface, Rack, Region, Site -from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( @@ -31,7 +31,7 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([ # VRFs # -class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): tags = TagField( required=False ) @@ -49,7 +49,7 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class VRFCSVForm(CustomFieldForm): +class VRFCSVForm(CustomFieldModelForm): tenant = forms.ModelChoiceField( queryset=Tenant.objects.all(), required=False, @@ -144,7 +144,7 @@ class RIRFilterForm(BootstrapMixin, forms.Form): # Aggregates # -class AggregateForm(BootstrapMixin, CustomFieldForm): +class AggregateForm(BootstrapMixin, CustomFieldModelForm): tags = TagField( required=False ) @@ -166,7 +166,7 @@ class AggregateForm(BootstrapMixin, CustomFieldForm): } -class AggregateCSVForm(CustomFieldForm): +class AggregateCSVForm(CustomFieldModelForm): rir = forms.ModelChoiceField( queryset=RIR.objects.all(), to_field_name='name', @@ -263,7 +263,7 @@ class RoleCSVForm(forms.ModelForm): # Prefixes # -class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, @@ -341,7 +341,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.fields['vrf'].empty_label = 'Global' -class PrefixCSVForm(CustomFieldForm): +class PrefixCSVForm(CustomFieldModelForm): vrf = FlexibleModelChoiceField( queryset=VRF.objects.all(), to_field_name='rd', @@ -584,7 +584,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) # IP addresses # -class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm): +class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm): interface = forms.ModelChoiceField( queryset=Interface.objects.all(), required=False @@ -751,7 +751,7 @@ class IPAddressBulkCreateForm(BootstrapMixin, forms.Form): ) -class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class Meta: model = IPAddress @@ -771,7 +771,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.fields['vrf'].empty_label = 'Global' -class IPAddressCSVForm(CustomFieldForm): +class IPAddressCSVForm(CustomFieldModelForm): vrf = FlexibleModelChoiceField( queryset=VRF.objects.all(), to_field_name='rd', @@ -1087,7 +1087,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form): # VLANs # -class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, @@ -1135,7 +1135,7 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class VLANCSVForm(CustomFieldForm): +class VLANCSVForm(CustomFieldModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, @@ -1310,7 +1310,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): # Services # -class ServiceForm(BootstrapMixin, CustomFieldForm): +class ServiceForm(BootstrapMixin, CustomFieldModelForm): port = forms.IntegerField( min_value=SERVICE_PORT_MIN, max_value=SERVICE_PORT_MAX diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index ace977b3a..e8300a4cc 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -4,7 +4,7 @@ from django import forms from taggit.forms import TagField from dcim.models import Device -from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldForm +from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm from utilities.forms import ( APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField, StaticSelect2Multiple @@ -68,7 +68,7 @@ class SecretRoleCSVForm(forms.ModelForm): # Secrets # -class SecretForm(BootstrapMixin, CustomFieldForm): +class SecretForm(BootstrapMixin, CustomFieldModelForm): plaintext = forms.CharField( max_length=SECRET_PLAINTEXT_MAX_LENGTH, required=False, @@ -116,7 +116,7 @@ class SecretForm(BootstrapMixin, CustomFieldForm): }) -class SecretCSVForm(CustomFieldForm): +class SecretCSVForm(CustomFieldModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 22deae434..c713f37f5 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -1,7 +1,7 @@ from django import forms from taggit.forms import TagField -from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm from utilities.forms import ( APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField, SlugField, @@ -38,7 +38,7 @@ class TenantGroupCSVForm(forms.ModelForm): # Tenants # -class TenantForm(BootstrapMixin, CustomFieldForm): +class TenantForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() comments = CommentField() tags = TagField( @@ -57,7 +57,7 @@ class TenantForm(BootstrapMixin, CustomFieldForm): } -class TenantCSVForm(CustomFieldForm): +class TenantCSVForm(CustomFieldModelForm): slug = SlugField() group = forms.ModelChoiceField( queryset=TenantGroup.objects.all(), diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index a6bda262f..fb6a633b8 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -6,7 +6,7 @@ from dcim.choices import InterfaceModeChoices from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN from dcim.forms import INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site -from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm +from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelForm, CustomFieldFilterForm from ipam.models import IPAddress, VLANGroup, VLAN from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant @@ -74,7 +74,7 @@ class ClusterGroupCSVForm(forms.ModelForm): # Clusters # -class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): comments = CommentField() tags = TagField( required=False @@ -98,7 +98,7 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldForm): } -class ClusterCSVForm(CustomFieldForm): +class ClusterCSVForm(CustomFieldModelForm): type = forms.ModelChoiceField( queryset=ClusterType.objects.all(), to_field_name='name', @@ -327,7 +327,7 @@ class ClusterRemoveDevicesForm(ConfirmationForm): # Virtual Machines # -class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): +class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): cluster_group = forms.ModelChoiceField( queryset=ClusterGroup.objects.all(), required=False, @@ -430,7 +430,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.fields['primary_ip6'].widget.attrs['readonly'] = True -class VirtualMachineCSVForm(CustomFieldForm): +class VirtualMachineCSVForm(CustomFieldModelForm): status = CSVChoiceField( choices=VirtualMachineStatusChoices, required=False, From f12199dcb589faef4ee192f3c586a99c21a08420 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 11:00:03 -0500 Subject: [PATCH 054/174] Rename and simplify CustomFieldChoiceField --- netbox/utilities/forms.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 1dd2c06a7..c175df3cd 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -442,21 +442,18 @@ class CSVChoiceField(forms.ChoiceField): return self.choice_values[value] -class CustomFieldChoiceField(forms.TypedChoiceField): +class CSVCustomFieldChoiceField(forms.TypedChoiceField): """ - Accept human-friendly label as input, and return the database value. If the label is not matched, the normal, - value-based input is assumed. + Invert the choice tuples: CSV import takes the human-friendly label as input rather than the database value """ + def __init__(self, *args, **kwargs): - def __init__(self, choices, *args, **kwargs): - super().__init__(choices=choices, *args, **kwargs) - self.choice_values = {label: value for value, label in unpack_grouped_choices(choices)} + if 'choices' in kwargs: + kwargs['choices'] = { + label: value for value, label in kwargs['choices'] + } - def clean(self, value): - # Check if the value is actually a label - if value in self.choice_values: - return self.choice_values[value] - return super().clean(value) + super().__init__(*args, **kwargs) class ExpandableNameField(forms.CharField): From 9929a05bfe2be621e8d9e53cae6eba322662f28b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 11:00:46 -0500 Subject: [PATCH 055/174] Update release notes --- docs/release-notes/version-2.7.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index ada79c9fa..163a9c55c 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -1,5 +1,9 @@ # v2.7.4 (FUTURE) +## Enhancements + +* [#568](https://github.com/netbox-community/netbox/issues/568) - Allow custom fields to be imported and exported using CSV + ## Bug Fixes * [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts @@ -10,7 +14,6 @@ ## Enhancements -* [#568](https://github.com/netbox-community/netbox/issues/568) - Allow custom fields to be imported and exported using CSV * [#3310](https://github.com/netbox-community/netbox/issues/3310) - Pre-select site/rack for B side when creating a new cable * [#3338](https://github.com/netbox-community/netbox/issues/3338) - Include circuit terminations in API representation of circuits * [#3509](https://github.com/netbox-community/netbox/issues/3509) - Add IP address variables for custom scripts From 585ea71d1a5deed877ea42ad63faa4e940013e36 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 11:25:51 -0500 Subject: [PATCH 056/174] Move form field generation logic to CustomField class --- netbox/extras/forms.py | 63 ++------------------------------------- netbox/extras/models.py | 66 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 60 deletions(-) diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index bb1015638..674c3ff4c 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -3,15 +3,14 @@ from collections import OrderedDict from django import forms from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist from taggit.forms import TagField from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, - CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, LaxURLField, JSONField, SlugField, - StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, + CommentField, ContentTypeSelect, DateTimePicker, FilterChoiceField, JSONField, SlugField, StaticSelect2, + BOOLEAN_WITH_BLANK_CHOICES, ) from .choices import * from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag @@ -32,63 +31,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F for cf in custom_fields: field_name = 'cf_{}'.format(str(cf.name)) - initial = cf.default if not bulk_edit else None - - # Integer - if cf.type == CustomFieldTypeChoices.TYPE_INTEGER: - field = forms.IntegerField(required=cf.required, initial=initial) - - # Boolean - elif cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN: - choices = ( - (None, '---------'), - (1, 'True'), - (0, 'False'), - ) - if initial is not None and initial.lower() in ['true', 'yes', '1']: - initial = 1 - elif initial is not None and initial.lower() in ['false', 'no', '0']: - initial = 0 - else: - initial = None - field = forms.NullBooleanField( - required=cf.required, initial=initial, widget=StaticSelect2(choices=choices) - ) - - # Date - elif cf.type == CustomFieldTypeChoices.TYPE_DATE: - field = forms.DateField(required=cf.required, initial=initial, widget=DatePicker()) - - # Select - elif cf.type == CustomFieldTypeChoices.TYPE_SELECT: - choices = [(cfc.pk, cfc.value) for cfc in cf.choices.all()] - if not cf.required or bulk_edit or filterable_only: - choices = [(None, '---------')] + choices - # Check for a default choice - default_choice = None - if initial: - try: - default_choice = cf.choices.get(value=initial).pk - except ObjectDoesNotExist: - pass - field = forms.TypedChoiceField( - choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2() - ) - - # URL - elif cf.type == CustomFieldTypeChoices.TYPE_URL: - field = LaxURLField(required=cf.required, initial=initial) - - # Text - else: - field = forms.CharField(max_length=255, required=cf.required, initial=initial) - - field.model = cf - field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize() - if cf.description: - field.help_text = cf.description - - field_dict[field_name] = field + field_dict[field_name] = cf.to_form_field(set_initial=not bulk_edit) return field_dict diff --git a/netbox/extras/models.py b/netbox/extras/models.py index a03494bb2..be8feda7b 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -1,6 +1,7 @@ from collections import OrderedDict from datetime import date +from django import forms from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -14,6 +15,7 @@ from django.utils.text import slugify from taggit.models import TagBase, GenericTaggedItemBase from utilities.fields import ColorField +from utilities.forms import DatePicker, LaxURLField, StaticSelect2, add_blank_choice from utilities.utils import deepmerge, render_jinja2 from .choices import * from .constants import * @@ -280,6 +282,70 @@ class CustomField(models.Model): return self.choices.get(pk=int(serialized_value)) return serialized_value + def to_form_field(self, set_initial=True): + """ + Return a form field suitable for setting a CustomField's value for an object. + + set_initial: Set initial date for the field. This should be false when generating a field for bulk editing. + """ + initial = self.default if set_initial else None + + # Integer + if self.type == CustomFieldTypeChoices.TYPE_INTEGER: + field = forms.IntegerField(required=self.required, initial=initial) + + # Boolean + elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN: + choices = ( + (None, '---------'), + (1, 'True'), + (0, 'False'), + ) + if initial is not None and initial.lower() in ['true', 'yes', '1']: + initial = 1 + elif initial is not None and initial.lower() in ['false', 'no', '0']: + initial = 0 + else: + initial = None + field = forms.NullBooleanField( + required=self.required, initial=initial, widget=StaticSelect2(choices=choices) + ) + + # Date + elif self.type == CustomFieldTypeChoices.TYPE_DATE: + field = forms.DateField(required=self.required, initial=initial, widget=DatePicker()) + + # Select + elif self.type == CustomFieldTypeChoices.TYPE_SELECT: + choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()] + # TODO: Accommodate bulk edit/filtering + # if not self.required or bulk_edit or filterable_only: + if not self.required: + choices = add_blank_choice(choices) + # Set the initial value to the PK of the default choice, if any + if set_initial: + default_choice = self.choices.filter(value=self.default).first() + if default_choice: + initial = default_choice.pk + field = forms.TypedChoiceField( + choices=choices, coerce=int, required=self.required, initial=initial, widget=StaticSelect2() + ) + + # URL + elif self.type == CustomFieldTypeChoices.TYPE_URL: + field = LaxURLField(required=self.required, initial=initial) + + # Text + else: + field = forms.CharField(max_length=255, required=self.required, initial=initial) + + field.model = self + field.label = self.label if self.label else self.name.replace('_', ' ').capitalize() + if self.description: + field.help_text = self.description + + return field + class CustomFieldValue(models.Model): field = models.ForeignKey( From c3f86456d669d989661f4e3451ad7eb4a862aa0a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 12:12:47 -0500 Subject: [PATCH 057/174] Remove get_custom_fields_for_model() --- netbox/extras/forms.py | 45 ++++++++++++++--------------------------- netbox/extras/models.py | 20 +++++++++--------- 2 files changed, 26 insertions(+), 39 deletions(-) diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 674c3ff4c..f5d331ac4 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -20,22 +20,6 @@ from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachmen # Custom fields # -def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False): - """ - Retrieve all CustomFields applicable to the given ContentType - """ - field_dict = OrderedDict() - custom_fields = CustomField.objects.filter(obj_type=content_type) - if filterable_only: - custom_fields = custom_fields.exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) - - for cf in custom_fields: - field_name = 'cf_{}'.format(str(cf.name)) - field_dict[field_name] = cf.to_form_field(set_initial=not bulk_edit) - - return field_dict - - class CustomFieldModelForm(forms.ModelForm): def __init__(self, *args, **kwargs): @@ -60,9 +44,10 @@ class CustomFieldModelForm(forms.ModelForm): Append form fields for all applicable CustomFields. """ self.custom_fields = [] - for name, field in get_custom_fields_for_model(self.obj_type).items(): - self.fields[name] = field - self.custom_fields.append(name) + custom_fields = CustomField.objects.filter(obj_type=self.obj_type) + for cf in custom_fields: + self.fields[cf.name] = cf.to_form_field() + self.custom_fields.append(cf.name) def _save_custom_fields(self): @@ -106,15 +91,14 @@ class CustomFieldBulkEditForm(BulkEditForm): self.obj_type = ContentType.objects.get_for_model(self.model) # Add all applicable CustomFields to the form - custom_fields = get_custom_fields_for_model(self.obj_type, bulk_edit=True).items() - for name, field in custom_fields: + custom_fields = CustomField.objects.filter(obj_type=self.obj_type) + for cf in custom_fields: # Annotate non-required custom fields as nullable - if not field.required: - self.nullable_fields.append(name) - field.required = False - self.fields[name] = field + if not cf.required: + self.nullable_fields.append(cf.name) + self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False) # Annotate this as a custom field - self.custom_fields.append(name) + self.custom_fields.append(cf.name) class CustomFieldFilterForm(forms.Form): @@ -126,10 +110,11 @@ class CustomFieldFilterForm(forms.Form): super().__init__(*args, **kwargs) # Add all applicable CustomFields to the form - custom_fields = get_custom_fields_for_model(self.obj_type, filterable_only=True).items() - for name, field in custom_fields: - field.required = False - self.fields[name] = field + custom_fields = CustomField.objects.filter(obj_type=self.obj_type).exclude( + filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED + ) + for cf in custom_fields: + self.fields[cf.name] = cf.to_form_field(set_initial=True, enforce_required=False) # diff --git a/netbox/extras/models.py b/netbox/extras/models.py index be8feda7b..db42fc845 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -282,17 +282,19 @@ class CustomField(models.Model): return self.choices.get(pk=int(serialized_value)) return serialized_value - def to_form_field(self, set_initial=True): + def to_form_field(self, set_initial=True, enforce_required=True): """ Return a form field suitable for setting a CustomField's value for an object. - set_initial: Set initial date for the field. This should be false when generating a field for bulk editing. + set_initial: Set initial date for the field. This should be False when generating a field for bulk editing. + enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing. """ initial = self.default if set_initial else None + required = self.required if enforce_required else False # Integer if self.type == CustomFieldTypeChoices.TYPE_INTEGER: - field = forms.IntegerField(required=self.required, initial=initial) + field = forms.IntegerField(required=required, initial=initial) # Boolean elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN: @@ -308,19 +310,19 @@ class CustomField(models.Model): else: initial = None field = forms.NullBooleanField( - required=self.required, initial=initial, widget=StaticSelect2(choices=choices) + required=required, initial=initial, widget=StaticSelect2(choices=choices) ) # Date elif self.type == CustomFieldTypeChoices.TYPE_DATE: - field = forms.DateField(required=self.required, initial=initial, widget=DatePicker()) + field = forms.DateField(required=required, initial=initial, widget=DatePicker()) # Select elif self.type == CustomFieldTypeChoices.TYPE_SELECT: choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()] # TODO: Accommodate bulk edit/filtering # if not self.required or bulk_edit or filterable_only: - if not self.required: + if not required: choices = add_blank_choice(choices) # Set the initial value to the PK of the default choice, if any if set_initial: @@ -328,16 +330,16 @@ class CustomField(models.Model): if default_choice: initial = default_choice.pk field = forms.TypedChoiceField( - choices=choices, coerce=int, required=self.required, initial=initial, widget=StaticSelect2() + choices=choices, coerce=int, required=required, initial=initial, widget=StaticSelect2() ) # URL elif self.type == CustomFieldTypeChoices.TYPE_URL: - field = LaxURLField(required=self.required, initial=initial) + field = LaxURLField(required=required, initial=initial) # Text else: - field = forms.CharField(max_length=255, required=self.required, initial=initial) + field = forms.CharField(max_length=255, required=required, initial=initial) field.model = self field.label = self.label if self.label else self.name.replace('_', ' ').capitalize() From 35f2291edc43265864b6cf6aece3467d4ce4e120 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 13:31:36 -0500 Subject: [PATCH 058/174] Fix assignment of initial CustomField values when editing an object --- netbox/extras/forms.py | 43 +++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index f5d331ac4..0123fead9 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from django import forms from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType @@ -24,30 +22,37 @@ class CustomFieldModelForm(forms.ModelForm): def __init__(self, *args, **kwargs): + self.obj_type = ContentType.objects.get_for_model(self._meta.model) + self.custom_fields = [] + self.custom_field_values = {} + super().__init__(*args, **kwargs) - # Append form fields for CustomFields - self.obj_type = ContentType.objects.get_for_model(self._meta.model) self._append_customfield_fields() - # If editing an existing object, initialize values for all custom fields - if self.instance.pk: - existing_values = CustomFieldValue.objects.filter( - obj_type=self.obj_type, - obj_id=self.instance.pk - ).prefetch_related('field') - for cfv in existing_values: - self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.serialized_value - def _append_customfield_fields(self): """ - Append form fields for all applicable CustomFields. + Append form fields for all CustomFields assigned to this model. """ - self.custom_fields = [] - custom_fields = CustomField.objects.filter(obj_type=self.obj_type) - for cf in custom_fields: - self.fields[cf.name] = cf.to_form_field() - self.custom_fields.append(cf.name) + # Retrieve initial CustomField values for the instance + if self.instance.pk: + for cfv in CustomFieldValue.objects.filter( + obj_type=self.obj_type, + obj_id=self.instance.pk + ).prefetch_related('field'): + self.custom_field_values[cfv.field.name] = cfv.serialized_value + + # Append form fields; assign initial values if modifying and existing object + for cf in CustomField.objects.filter(obj_type=self.obj_type): + field_name = 'cf_{}'.format(cf.name) + if self.instance.pk: + self.fields[field_name] = cf.to_form_field(set_initial=False) + self.fields[field_name].initial = self.custom_field_values.get(cf.name) + else: + self.fields[field_name] = cf.to_form_field() + + # Annotate the field in the list of CustomField form fields + self.custom_fields.append(field_name) def _save_custom_fields(self): From e6b018909db8a2eb8ca650c439953547adee1b80 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 13:53:26 -0500 Subject: [PATCH 059/174] Introduced CustomFieldModelCSVForm --- netbox/circuits/forms.py | 8 +++++--- netbox/dcim/forms.py | 11 ++++++----- netbox/extras/forms.py | 13 +++++++++++++ netbox/ipam/forms.py | 14 ++++++++------ netbox/secrets/forms.py | 6 ++++-- netbox/tenancy/forms.py | 4 +++- netbox/virtualization/forms.py | 8 +++++--- 7 files changed, 44 insertions(+), 20 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index b97f531b5..643359b74 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -2,7 +2,9 @@ from django import forms from taggit.forms import TagField from dcim.models import Region, Site -from extras.forms import AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import ( + AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm, +) from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( @@ -46,7 +48,7 @@ class ProviderForm(BootstrapMixin, CustomFieldModelForm): } -class ProviderCSVForm(CustomFieldModelForm): +class ProviderCSVForm(CustomFieldModelCSVForm): slug = SlugField() class Meta: @@ -188,7 +190,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } -class CircuitCSVForm(CustomFieldModelForm): +class CircuitCSVForm(CustomFieldModelCSVForm): provider = forms.ModelChoiceField( queryset=Provider.objects.all(), to_field_name='name', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 216af8b63..f57f942e0 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -13,7 +13,8 @@ from timezone_field import TimeZoneFormField from circuits.models import Circuit, Provider from extras.forms import ( - AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm + AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, CustomFieldModelForm, + LocalConfigContextFilterForm, ) from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN from ipam.models import IPAddress, VLAN @@ -263,7 +264,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } -class SiteCSVForm(CustomFieldModelForm): +class SiteCSVForm(CustomFieldModelCSVForm): status = CSVChoiceField( choices=SiteStatusChoices, required=False, @@ -504,7 +505,7 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } -class RackCSVForm(CustomFieldModelForm): +class RackCSVForm(CustomFieldModelCSVForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -1724,7 +1725,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): self.initial['rack'] = self.instance.parent_bay.device.rack_id -class BaseDeviceCSVForm(CustomFieldModelForm): +class BaseDeviceCSVForm(CustomFieldModelCSVForm): device_role = forms.ModelChoiceField( queryset=DeviceRole.objects.all(), to_field_name='name', @@ -4286,7 +4287,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm): self.initial['site'] = self.instance.power_panel.site -class PowerFeedCSVForm(CustomFieldModelForm): +class PowerFeedCSVForm(CustomFieldModelCSVForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 0123fead9..2c6b6cb65 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -87,6 +87,19 @@ class CustomFieldModelForm(forms.ModelForm): return obj +class CustomFieldModelCSVForm(CustomFieldModelForm): + + def _append_customfield_fields(self): + + # Append form fields + for cf in CustomField.objects.filter(obj_type=self.obj_type): + field_name = 'cf_{}'.format(cf.name) + self.fields[field_name] = cf.to_form_field() + + # Annotate the field in the list of CustomField form fields + self.custom_fields.append(field_name) + + class CustomFieldBulkEditForm(BulkEditForm): def __init__(self, *args, **kwargs): diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 594e95f58..237ee2238 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -4,7 +4,9 @@ from django.core.validators import MaxValueValidator, MinValueValidator from taggit.forms import TagField from dcim.models import Device, Interface, Rack, Region, Site -from extras.forms import AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import ( + AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm, +) from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( @@ -49,7 +51,7 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } -class VRFCSVForm(CustomFieldModelForm): +class VRFCSVForm(CustomFieldModelCSVForm): tenant = forms.ModelChoiceField( queryset=Tenant.objects.all(), required=False, @@ -166,7 +168,7 @@ class AggregateForm(BootstrapMixin, CustomFieldModelForm): } -class AggregateCSVForm(CustomFieldModelForm): +class AggregateCSVForm(CustomFieldModelCSVForm): rir = forms.ModelChoiceField( queryset=RIR.objects.all(), to_field_name='name', @@ -341,7 +343,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): self.fields['vrf'].empty_label = 'Global' -class PrefixCSVForm(CustomFieldModelForm): +class PrefixCSVForm(CustomFieldModelCSVForm): vrf = FlexibleModelChoiceField( queryset=VRF.objects.all(), to_field_name='rd', @@ -771,7 +773,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): self.fields['vrf'].empty_label = 'Global' -class IPAddressCSVForm(CustomFieldModelForm): +class IPAddressCSVForm(CustomFieldModelCSVForm): vrf = FlexibleModelChoiceField( queryset=VRF.objects.all(), to_field_name='rd', @@ -1135,7 +1137,7 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } -class VLANCSVForm(CustomFieldModelForm): +class VLANCSVForm(CustomFieldModelCSVForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index e8300a4cc..46b2c12f4 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -4,7 +4,9 @@ from django import forms from taggit.forms import TagField from dcim.models import Device -from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm +from extras.forms import ( + AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm, +) from utilities.forms import ( APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField, StaticSelect2Multiple @@ -116,7 +118,7 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm): }) -class SecretCSVForm(CustomFieldModelForm): +class SecretCSVForm(CustomFieldModelCSVForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), to_field_name='name', diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index c713f37f5..22a4809de 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -1,7 +1,9 @@ from django import forms from taggit.forms import TagField -from extras.forms import AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import ( + AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, +) from utilities.forms import ( APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField, SlugField, diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index fb6a633b8..066e162d3 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -6,7 +6,9 @@ from dcim.choices import InterfaceModeChoices from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN from dcim.forms import INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site -from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelForm, CustomFieldFilterForm +from extras.forms import ( + AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm, +) from ipam.models import IPAddress, VLANGroup, VLAN from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant @@ -98,7 +100,7 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } -class ClusterCSVForm(CustomFieldModelForm): +class ClusterCSVForm(CustomFieldModelCSVForm): type = forms.ModelChoiceField( queryset=ClusterType.objects.all(), to_field_name='name', @@ -430,7 +432,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): self.fields['primary_ip6'].widget.attrs['readonly'] = True -class VirtualMachineCSVForm(CustomFieldModelForm): +class VirtualMachineCSVForm(CustomFieldModelCSVForm): status = CSVChoiceField( choices=VirtualMachineStatusChoices, required=False, From 193435b554c59df0dcc188f769ee7f452d466960 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 14:29:47 -0500 Subject: [PATCH 060/174] Enable CSV import for custom fields --- netbox/extras/forms.py | 2 +- netbox/extras/models.py | 15 +++++++++------ netbox/utilities/forms.py | 14 -------------- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 2c6b6cb65..751b2ddb0 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -94,7 +94,7 @@ class CustomFieldModelCSVForm(CustomFieldModelForm): # Append form fields for cf in CustomField.objects.filter(obj_type=self.obj_type): field_name = 'cf_{}'.format(cf.name) - self.fields[field_name] = cf.to_form_field() + self.fields[field_name] = cf.to_form_field(for_csv_import=True) # Annotate the field in the list of CustomField form fields self.custom_fields.append(field_name) diff --git a/netbox/extras/models.py b/netbox/extras/models.py index db42fc845..70f7661c8 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -15,7 +15,7 @@ from django.utils.text import slugify from taggit.models import TagBase, GenericTaggedItemBase from utilities.fields import ColorField -from utilities.forms import DatePicker, LaxURLField, StaticSelect2, add_blank_choice +from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice from utilities.utils import deepmerge, render_jinja2 from .choices import * from .constants import * @@ -282,12 +282,13 @@ class CustomField(models.Model): return self.choices.get(pk=int(serialized_value)) return serialized_value - def to_form_field(self, set_initial=True, enforce_required=True): + def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False): """ Return a form field suitable for setting a CustomField's value for an object. set_initial: Set initial date for the field. This should be False when generating a field for bulk editing. enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing. + for_csv_import: Return a form field suitable for bulk import of objects in CSV format. """ initial = self.default if set_initial else None required = self.required if enforce_required else False @@ -320,17 +321,19 @@ class CustomField(models.Model): # Select elif self.type == CustomFieldTypeChoices.TYPE_SELECT: choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()] - # TODO: Accommodate bulk edit/filtering - # if not self.required or bulk_edit or filterable_only: + if not required: choices = add_blank_choice(choices) + # Set the initial value to the PK of the default choice, if any if set_initial: default_choice = self.choices.filter(value=self.default).first() if default_choice: initial = default_choice.pk - field = forms.TypedChoiceField( - choices=choices, coerce=int, required=required, initial=initial, widget=StaticSelect2() + + field_class = CSVChoiceField if for_csv_import else forms.ChoiceField + field = field_class( + choices=choices, required=required, initial=initial, widget=StaticSelect2() ) # URL diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index c175df3cd..a14ec9305 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -442,20 +442,6 @@ class CSVChoiceField(forms.ChoiceField): return self.choice_values[value] -class CSVCustomFieldChoiceField(forms.TypedChoiceField): - """ - Invert the choice tuples: CSV import takes the human-friendly label as input rather than the database value - """ - def __init__(self, *args, **kwargs): - - if 'choices' in kwargs: - kwargs['choices'] = { - label: value for value, label in kwargs['choices'] - } - - super().__init__(*args, **kwargs) - - class ExpandableNameField(forms.CharField): """ A field which allows for numeric range expansion From c75315fda63a6b9809344bccea99b08ecfb3a365 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 15:34:55 -0500 Subject: [PATCH 061/174] Extend CSV import test --- netbox/extras/tests/test_customfields.py | 89 +++++++++++++++--------- 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 005f049b5..703b0b0d0 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -366,9 +366,9 @@ class CustomFieldChoiceAPITest(APITestCase): self.assertEqual(self.cf_choice_3.pk, response.data[self.cf_2.name][self.cf_choice_3.value]) -class CustomFieldCSV(TestCase): +class CustomFieldImportTest(TestCase): + def setUp(self): - super().setUp() user = create_test_user( permissions=[ @@ -379,47 +379,68 @@ class CustomFieldCSV(TestCase): self.client = Client() self.client.force_login(user) - obj_type = ContentType.objects.get_for_model(Site) + @classmethod + def setUpTestData(cls): - self.cf_text = CustomField.objects.create(name="text", type=CustomFieldTypeChoices.TYPE_TEXT) - self.cf_text.obj_type.set([obj_type]) - self.cf_text.save() + custom_fields = ( + CustomField(name='text', type=CustomFieldTypeChoices.TYPE_TEXT), + CustomField(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER), + CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN), + CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE), + CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL), + CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT), + ) + for cf in custom_fields: + cf.save() + cf.obj_type.set([ContentType.objects.get_for_model(Site)]) - self.cf_choice = CustomField.objects.create(name="choice", type=CustomFieldTypeChoices.TYPE_SELECT) - self.cf_choice.obj_type.set([obj_type]) - self.cf_choice.save() - - self.cf_choice_1 = CustomFieldChoice.objects.create(field=self.cf_choice, value="cf_field_1") - self.cf_choice_2 = CustomFieldChoice.objects.create(field=self.cf_choice, value="cf_field_2") - self.cf_choice_3 = CustomFieldChoice.objects.create(field=self.cf_choice, value="cf_field_3") + CustomFieldChoice.objects.bulk_create(( + CustomFieldChoice(field=custom_fields[5], value='Choice A'), + CustomFieldChoice(field=custom_fields[5], value='Choice B'), + CustomFieldChoice(field=custom_fields[5], value='Choice C'), + )) def test_import(self): """ - Import a site with custom fields + Import a Site in CSV format, including a value for each CustomField. """ - csv_data = ( - "name,slug,cf_text,cf_choice", - "Site 1,site-1,something,cf_field_1", + data = ( + ('name', 'slug', 'cf_text', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_select'), + ('Site 1', 'site-1', 'ABC', '123', 'True', '2020-01-01', 'http://example.com/1', 'Choice A'), + ('Site 2', 'site-2', 'DEF', '456', 'False', '2020-01-02', 'http://example.com/2', 'Choice B'), + ('Site 3', 'site-3', '', '', '', '', '', ''), ) + csv_data = '\n'.join(','.join(row) for row in data) - response = self.client.post(reverse('dcim:site_import'), {'csv': '\n'.join(csv_data)}) + response = self.client.post(reverse('dcim:site_import'), {'csv': csv_data}) self.assertEqual(response.status_code, 200) - site1_custom_fields = Site.objects.get(name='Site 1').get_custom_fields() - self.assertEqual(len(site1_custom_fields), 2) - self.assertEqual(site1_custom_fields[self.cf_text], 'something') - self.assertEqual(site1_custom_fields[self.cf_choice], self.cf_choice_1) + # Validate data for site 1 + custom_field_values = { + cf.name: value for cf, value in Site.objects.get(name='Site 1').get_custom_fields().items() + } + self.assertEqual(len(custom_field_values), 6) + self.assertEqual(custom_field_values['text'], 'ABC') + self.assertEqual(custom_field_values['integer'], 123) + self.assertEqual(custom_field_values['boolean'], True) + self.assertEqual(custom_field_values['date'], date(2020, 1, 1)) + self.assertEqual(custom_field_values['url'], 'http://example.com/1') + self.assertEqual(custom_field_values['select'].value, 'Choice A') - def test_import_invalid_choice(self): - """ - Import a site with an invalid choice - """ - csv_data = ( - "name,slug,cf_choice", - "Site 2,site-2,cf_field_4", - ) + # Validate data for site 2 + custom_field_values = { + cf.name: value for cf, value in Site.objects.get(name='Site 2').get_custom_fields().items() + } + self.assertEqual(len(custom_field_values), 6) + self.assertEqual(custom_field_values['text'], 'DEF') + self.assertEqual(custom_field_values['integer'], 456) + self.assertEqual(custom_field_values['boolean'], False) + self.assertEqual(custom_field_values['date'], date(2020, 1, 2)) + self.assertEqual(custom_field_values['url'], 'http://example.com/2') + self.assertEqual(custom_field_values['select'].value, 'Choice B') - response = self.client.post(reverse('dcim:site_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) - - self.assertFalse(len(Site.objects.filter(name="Site 2")), 0) + # No CustomFieldValues should be created for site 3 + obj_type = ContentType.objects.get_for_model(Site) + site3 = Site.objects.get(name='Site 3') + self.assertFalse(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site3.pk).exists()) + self.assertEqual(CustomFieldValue.objects.count(), 12) # Sanity check From eafeaab014ce31e1703d0d634986dea4d00e8318 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 16:07:32 -0500 Subject: [PATCH 062/174] Add tests for invalid import data --- netbox/extras/tests/test_customfields.py | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 703b0b0d0..a6e2bfcec 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -5,6 +5,7 @@ from django.test import Client, TestCase from django.urls import reverse from rest_framework import status +from dcim.forms import SiteCSVForm from dcim.models import Site from extras.choices import * from extras.models import CustomField, CustomFieldValue, CustomFieldChoice @@ -444,3 +445,33 @@ class CustomFieldImportTest(TestCase): site3 = Site.objects.get(name='Site 3') self.assertFalse(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site3.pk).exists()) self.assertEqual(CustomFieldValue.objects.count(), 12) # Sanity check + + def test_import_missing_required(self): + """ + Attempt to import an object missing a required custom field. + """ + # Set one of our CustomFields to required + CustomField.objects.filter(name='text').update(required=True) + + form_data = { + 'name': 'Site 1', + 'slug': 'site-1', + } + + form = SiteCSVForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertIn('cf_text', form.errors) + + def test_import_invalid_choice(self): + """ + Attempt to import an object with an invalid choice selection. + """ + form_data = { + 'name': 'Site 1', + 'slug': 'site-1', + 'cf_select': 'Choice X' + } + + form = SiteCSVForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertIn('cf_select', form.errors) From 03087e9d01c678600beb70a7eca8fca5506cd8b9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jan 2020 16:22:06 -0500 Subject: [PATCH 063/174] Fixes #4049: Restore missing tags field in IPAM service serializer --- docs/release-notes/version-2.7.md | 1 + netbox/ipam/api/serializers.py | 5 +++-- netbox/ipam/tests/test_api.py | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index ab1e2de0d..68487ebb8 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -3,6 +3,7 @@ ## Bug Fixes * [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts +* [#4049](https://github.com/netbox-community/netbox/issues/4049) - Restore missing `tags` field in IPAM service serializer --- diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 44f67d538..e52c172e5 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -237,7 +237,7 @@ class AvailableIPSerializer(serializers.Serializer): # Services # -class ServiceSerializer(CustomFieldModelSerializer): +class ServiceSerializer(TaggitSerializer, CustomFieldModelSerializer): device = NestedDeviceSerializer(required=False, allow_null=True) virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True) protocol = ChoiceField(choices=ServiceProtocolChoices) @@ -247,10 +247,11 @@ class ServiceSerializer(CustomFieldModelSerializer): required=False, many=True ) + tags = TagListSerializerField(required=False) class Meta: model = Service fields = [ - 'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', + 'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 983787b0c..99a7eaca4 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -1064,6 +1064,7 @@ class ServiceTest(APITestCase): 'name': 'Test Service 4', 'protocol': ServiceProtocolChoices.PROTOCOL_TCP, 'port': 4, + 'tags': ['Foo', 'Bar'], } url = reverse('ipam-api:service-list') @@ -1076,6 +1077,8 @@ class ServiceTest(APITestCase): self.assertEqual(service4.name, data['name']) self.assertEqual(service4.protocol, data['protocol']) self.assertEqual(service4.port, data['port']) + tags = [tag.name for tag in service4.tags.all()] + self.assertEqual(sorted(tags), sorted(data['tags'])) def test_create_service_bulk(self): From 4ba25799368e9edfc14ad14f2ba892600a049466 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 10:12:53 -0500 Subject: [PATCH 064/174] Closes #4051: Disable the makemigrations management command --- docs/release-notes/version-2.7.md | 4 ++++ netbox/netbox/settings.py | 1 + .../management/commands/makemigrations.py | 23 ++++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 68487ebb8..5ee902558 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -1,5 +1,9 @@ # v2.7.4 (FUTURE) +## Enhancements + +* [#4051](https://github.com/netbox-community/netbox/issues/4051) - Disable the `makemigrations` management command + ## Bug Fixes * [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 8cdbb60a3..483c21121 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -74,6 +74,7 @@ CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', []) DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') DEBUG = getattr(configuration, 'DEBUG', False) +DEVELOPER = getattr(configuration, 'DEVELOPER', False) EMAIL = getattr(configuration, 'EMAIL', {}) ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False) EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) diff --git a/netbox/utilities/management/commands/makemigrations.py b/netbox/utilities/management/commands/makemigrations.py index fbcf82eaf..69f699796 100644 --- a/netbox/utilities/management/commands/makemigrations.py +++ b/netbox/utilities/management/commands/makemigrations.py @@ -1,7 +1,28 @@ # noinspection PyUnresolvedReferences -from django.core.management.commands.makemigrations import Command +from django.conf import settings +from django.core.management.base import CommandError +from django.core.management.commands.makemigrations import Command as _Command from django.db import models from . import custom_deconstruct models.Field.deconstruct = custom_deconstruct + + +class Command(_Command): + + def handle(self, *args, **kwargs): + """ + This built-in management command enables the creation of new database schema migration files, which should + never be required by and ordinary user. We prevent this command from executing unless the configuration + indicates that the user is a developer (i.e. configuration.DEVELOPER == True). + """ + if not settings.DEVELOPER: + raise CommandError( + "This command is available for development purposes only. It will\n" + "NOT resolve any issues with missing or unapplied migrations. For assistance,\n" + "please post to the NetBox mailing list:\n" + " https://groups.google.com/forum/#!forum/netbox-discuss" + ) + + super().handle(*args, **kwargs) From 923c2728b38407335dec892386aa41ddc980204d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 12:08:40 -0500 Subject: [PATCH 065/174] Fixes #4056: Repair schema migration for Rack.outer_unit (from #3569) --- docs/release-notes/version-2.7.md | 1 + .../dcim/migrations/0079_3569_rack_fields.py | 2 +- .../migrations/0092_fix_rack_outer_unit.py | 27 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 netbox/dcim/migrations/0092_fix_rack_outer_unit.py diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 68487ebb8..9fc1c1c91 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -4,6 +4,7 @@ * [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts * [#4049](https://github.com/netbox-community/netbox/issues/4049) - Restore missing `tags` field in IPAM service serializer +* [#4056](https://github.com/netbox-community/netbox/issues/4056) - Repair schema migration for Rack.outer_unit (from #3569) --- diff --git a/netbox/dcim/migrations/0079_3569_rack_fields.py b/netbox/dcim/migrations/0079_3569_rack_fields.py index 4e76a270f..da544bb7a 100644 --- a/netbox/dcim/migrations/0079_3569_rack_fields.py +++ b/netbox/dcim/migrations/0079_3569_rack_fields.py @@ -37,7 +37,7 @@ def rack_status_to_slug(apps, schema_editor): def rack_outer_unit_to_slug(apps, schema_editor): Rack = apps.get_model('dcim', 'Rack') for id, slug in RACK_DIMENSION_CHOICES: - Rack.objects.filter(status=str(id)).update(status=slug) + Rack.objects.filter(outer_unit=str(id)).update(outer_unit=slug) class Migration(migrations.Migration): diff --git a/netbox/dcim/migrations/0092_fix_rack_outer_unit.py b/netbox/dcim/migrations/0092_fix_rack_outer_unit.py new file mode 100644 index 000000000..2a8cbf4e5 --- /dev/null +++ b/netbox/dcim/migrations/0092_fix_rack_outer_unit.py @@ -0,0 +1,27 @@ +from django.db import migrations + +RACK_DIMENSION_CHOICES = ( + (1000, 'mm'), + (2000, 'in'), +) + + +def rack_outer_unit_to_slug(apps, schema_editor): + Rack = apps.get_model('dcim', 'Rack') + for id, slug in RACK_DIMENSION_CHOICES: + Rack.objects.filter(outer_unit=str(id)).update(outer_unit=slug) + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0091_interface_type_other'), + ] + + operations = [ + # Fixes a missed field migration from #3569; see bug #4056. The original migration has also been fixed, + # so this can be omitted when squashing in the future. + migrations.RunPython( + code=rack_outer_unit_to_slug + ), + ] From 2375d66f75c8ad7f9acae49a57c7d5e0b9690656 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 30 Jan 2020 17:45:03 +0000 Subject: [PATCH 066/174] Added TagFilterField to device components' filter forms --- netbox/dcim/forms.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 794163a9e..fb9e033a7 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2158,6 +2158,7 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): class ConsolePortFilterForm(DeviceComponentFilterForm): model = ConsolePort + tag = TagFilterField(model) class ConsolePortForm(BootstrapMixin, forms.ModelForm): @@ -2215,6 +2216,7 @@ class ConsolePortCSVForm(forms.ModelForm): class ConsoleServerPortFilterForm(DeviceComponentFilterForm): model = ConsoleServerPort + tag = TagFilterField(model) class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): @@ -2307,6 +2309,7 @@ class ConsoleServerPortCSVForm(forms.ModelForm): class PowerPortFilterForm(DeviceComponentFilterForm): model = PowerPort + tag = TagFilterField(model) class PowerPortForm(BootstrapMixin, forms.ModelForm): @@ -2374,6 +2377,7 @@ class PowerPortCSVForm(forms.ModelForm): class PowerOutletFilterForm(DeviceComponentFilterForm): model = PowerOutlet + tag = TagFilterField(model) class PowerOutletForm(BootstrapMixin, forms.ModelForm): @@ -2542,6 +2546,7 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm): class InterfaceFilterForm(DeviceComponentFilterForm): model = Interface + tag = TagFilterField(model) class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): @@ -2855,6 +2860,7 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): class FrontPortFilterForm(DeviceComponentFilterForm): model = FrontPort + tag = TagFilterField(model) class FrontPortForm(BootstrapMixin, forms.ModelForm): @@ -3032,6 +3038,7 @@ class FrontPortBulkDisconnectForm(ConfirmationForm): class RearPortFilterForm(DeviceComponentFilterForm): model = RearPort + tag = TagFilterField(model) class RearPortForm(BootstrapMixin, forms.ModelForm): @@ -3636,6 +3643,7 @@ class CableFilterForm(BootstrapMixin, forms.Form): class DeviceBayFilterForm(DeviceComponentFilterForm): model = DeviceBay + tag = TagFilterField(model) class DeviceBayForm(BootstrapMixin, forms.ModelForm): From 5879671971000816970a12a65159afeae153627d Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 30 Jan 2020 17:49:42 +0000 Subject: [PATCH 067/174] Avoid overriding private attribute in super --- netbox/utilities/forms.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index a1cd4024f..a6eca7382 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -571,16 +571,12 @@ class TagFilterField(forms.MultipleChoiceField): widget = StaticSelect2Multiple def __init__(self, model, *args, **kwargs): - # Only instanitate the field if the model supports tags (i.e. hide if not) - if hasattr(model, 'tags'): - self.model = model + def get_choices(): + tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name') + return [(str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags] - # Choices are fetched during form initialization - super().__init__(label='Tags', choices=self._choices, required=False, *args, **kwargs) - - def _choices(self): - tags = self.model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name') - return [(str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags] + # Choices are fetched each time the form is initialized + super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs) class FilterChoiceIterator(forms.models.ModelChoiceIterator): From 7897ebb2edf74e9d1044a4eb4227227b7639c82b Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 30 Jan 2020 17:52:30 +0000 Subject: [PATCH 068/174] Corrected changelog --- docs/release-notes/version-2.7.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 80cad3254..7611ffce1 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -6,13 +6,16 @@ * [#4049](https://github.com/netbox-community/netbox/issues/4049) - Restore missing `tags` field in IPAM service serializer * [#4056](https://github.com/netbox-community/netbox/issues/4056) - Repair schema migration for Rack.outer_unit (from #3569) +## Enhancements + +* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget + --- # v2.7.3 (2020-01-28) ## Enhancements -* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget * [#3310](https://github.com/netbox-community/netbox/issues/3310) - Pre-select site/rack for B side when creating a new cable * [#3338](https://github.com/netbox-community/netbox/issues/3338) - Include circuit terminations in API representation of circuits * [#3509](https://github.com/netbox-community/netbox/issues/3509) - Add IP address variables for custom scripts From d9b8bc0422144780846c7ce2b977f123dc861ef4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 13:39:50 -0500 Subject: [PATCH 069/174] Fix VM interfaces table header alignment --- netbox/templates/virtualization/virtualmachine.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 8cf1fd490..982ef0e5e 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -265,7 +265,9 @@ Name LAG Description + MTU Mode + Cable Connection From 4b02d294ceafc6d5fa7fa955992b88d1e3b42a07 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 13:55:39 -0500 Subject: [PATCH 070/174] Fixes #4052: Fix error when bulk importing interfaces to virtual machines --- docs/release-notes/version-2.7.md | 1 + netbox/dcim/forms.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 9fc1c1c91..1f6917730 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -4,6 +4,7 @@ * [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts * [#4049](https://github.com/netbox-community/netbox/issues/4049) - Restore missing `tags` field in IPAM service serializer +* [#4052](https://github.com/netbox-community/netbox/issues/4052) - Fix error when bulk importing interfaces to virtual machines * [#4056](https://github.com/netbox-community/netbox/issues/4056) - Repair schema migration for Rack.outer_unit (from #3569) --- diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index ca5d25389..70fe7afaf 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2726,7 +2726,7 @@ class InterfaceCSVForm(forms.ModelForm): super().__init__(*args, **kwargs) # Limit LAG choices to interfaces belonging to this device (or VC master) - if self.is_bound: + if self.is_bound and 'device' in self.data: try: device = self.fields['device'].to_python(self.data['device']) except forms.ValidationError: From d0d2af4cab9426a2e67767b708b2de0beedf4de1 Mon Sep 17 00:00:00 2001 From: agrrajag <57500846+agrrajag@users.noreply.github.com> Date: Thu, 30 Jan 2020 13:00:37 -0600 Subject: [PATCH 071/174] Update 3-http-daemon.md (#4055) There was no documentation to move back into the netbox folder after installing/configuring nginx. You would move into nginx on line 42 then try and figure out why you couldn't copy gunicorn on line 113. --- docs/installation/3-http-daemon.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/installation/3-http-daemon.md b/docs/installation/3-http-daemon.md index 5e19f54a2..cc1065fef 100644 --- a/docs/installation/3-http-daemon.md +++ b/docs/installation/3-http-daemon.md @@ -107,9 +107,10 @@ Install gunicorn: # pip3 install gunicorn ``` -Copy `contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade. +Copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade. ```no-highlight +# cd /opt/netbox # cp contrib/gunicorn.py /opt/netbox/gunicorn.py ``` From 1a25f5a7f255ad332a6e4bfa43cb97294565c215 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 15:12:10 -0500 Subject: [PATCH 072/174] Fixes #4030: Fix exception when bulk editing interfaces (revised) --- docs/release-notes/version-2.7.md | 1 + netbox/dcim/models/device_components.py | 2 +- netbox/utilities/views.py | 28 ++++++++++++++++++------- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 196c041c5..ba33e062c 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -6,6 +6,7 @@ ## Bug Fixes +* [#4030](https://github.com/netbox-community/netbox/issues/4030) - Fix exception when bulk editing interfaces (revised) * [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts * [#4049](https://github.com/netbox-community/netbox/issues/4049) - Restore missing `tags` field in IPAM service serializer * [#4052](https://github.com/netbox-community/netbox/issues/4052) - Fix error when bulk importing interfaces to virtual machines diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 68bab8037..e37569f79 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -676,7 +676,7 @@ class Interface(CableTermination, ComponentModel): self.untagged_vlan = None # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.) - if self.pk and self.mode is not InterfaceModeChoices.MODE_TAGGED: + if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED: self.tagged_vlans.clear() return super().save(*args, **kwargs) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index d900a8545..88e5005bc 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -6,7 +6,7 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import transaction, IntegrityError -from django.db.models import Count, ProtectedError +from django.db.models import Count, ManyToManyField, ProtectedError from django.db.models.query import QuerySet from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea from django.http import HttpResponse, HttpResponseServerError @@ -650,7 +650,9 @@ class BulkEditView(GetReturnURLMixin, View): if form.is_valid(): custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else [] - standard_fields = [field for field in form.fields if field not in custom_fields and field != 'pk'] + standard_fields = [ + field for field in form.fields if field not in custom_fields + ['pk', 'add_tags', 'remove_tags'] + ] nullified_fields = request.POST.getlist('_nullify') try: @@ -662,14 +664,24 @@ class BulkEditView(GetReturnURLMixin, View): # Update standard fields. If a field is listed in _nullify, delete its value. for name in standard_fields: - if name in form.nullable_fields and name in nullified_fields and isinstance(form.cleaned_data[name], QuerySet): - getattr(obj, name).set([]) - elif name in form.nullable_fields and name in nullified_fields: - setattr(obj, name, '' if isinstance(form.fields[name], CharField) else None) - elif isinstance(form.cleaned_data[name], QuerySet) and form.cleaned_data[name]: + + model_field = model._meta.get_field(name) + + # Handle nullification + if name in form.nullable_fields and name in nullified_fields: + if isinstance(model_field, ManyToManyField): + getattr(obj, name).set([]) + else: + setattr(obj, name, None if model_field.null else '') + + # ManyToManyFields + elif isinstance(model_field, ManyToManyField): getattr(obj, name).set(form.cleaned_data[name]) - elif form.cleaned_data[name] not in (None, '') and not isinstance(form.cleaned_data[name], QuerySet): + + # Normal fields + elif form.cleaned_data[name] not in (None, ''): setattr(obj, name, form.cleaned_data[name]) + obj.full_clean() obj.save() From 43b2c36066e9ab232cf3ca66dfe09f945498aef7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 16:03:52 -0500 Subject: [PATCH 073/174] Introduced a custom TestCase --- netbox/utilities/testing.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/netbox/utilities/testing.py b/netbox/utilities/testing.py index 791eb64cb..39b43ab83 100644 --- a/netbox/utilities/testing.py +++ b/netbox/utilities/testing.py @@ -2,11 +2,44 @@ import logging from contextlib import contextmanager from django.contrib.auth.models import Permission, User +from django.test import Client, TestCase as _TestCase from rest_framework.test import APITestCase as _APITestCase from users.models import Token +class TestCase(_TestCase): + user_permissions = () + + def setUp(self): + + # Create the test user and assign permissions + self.user = User.objects.create_user(username='testuser') + self.add_permissions(*self.user_permissions) + + # Initialize the test client + self.client = Client() + self.client.force_login(self.user) + + def add_permissions(self, *names): + """ + Assign a set of permissions to the test user. Accepts permission names in the form ._. + """ + for name in names: + app, codename = name.split('.') + perm = Permission.objects.get(content_type__app_label=app, codename=codename) + self.user.user_permissions.add(perm) + + def remove_permissions(self, *names): + """ + Remove a set of permissions from the test user, if assigned. + """ + for name in names: + app, codename = name.split('.') + perm = Permission.objects.get(content_type__app_label=app, codename=codename) + self.user.user_permissions.remove(perm) + + class APITestCase(_APITestCase): def setUp(self): From 61ac7c44ba2cfb55b484dc4d0e36bf775c2e59f6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 16:37:40 -0500 Subject: [PATCH 074/174] Migrate view tests to use new TestCase class --- netbox/circuits/tests/test_views.py | 48 ++-- netbox/dcim/tests/test_views.py | 326 +++++++++------------- netbox/ipam/tests/test_views.py | 132 ++++----- netbox/secrets/tests/test_views.py | 52 ++-- netbox/tenancy/tests/test_views.py | 33 +-- netbox/virtualization/tests/test_views.py | 63 ++--- 6 files changed, 271 insertions(+), 383 deletions(-) diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index 576437ef1..d10d2df85 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -1,23 +1,18 @@ import urllib.parse -from django.test import Client, TestCase from django.urls import reverse from circuits.models import Circuit, CircuitType, Provider -from utilities.testing import create_test_user +from utilities.testing import TestCase class ProviderTestCase(TestCase): + user_permissions = ( + 'circuits.view_provider', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'circuits.view_provider', - 'circuits.add_provider', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): Provider.objects.bulk_create([ Provider(name='Provider 1', slug='provider-1', asn=65001), @@ -42,6 +37,7 @@ class ProviderTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_provider_import(self): + self.add_permissions('circuits.add_provider') csv_data = ( "name,slug", @@ -57,16 +53,12 @@ class ProviderTestCase(TestCase): class CircuitTypeTestCase(TestCase): + user_permissions = ( + 'circuits.view_circuittype', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'circuits.view_circuittype', - 'circuits.add_circuittype', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): CircuitType.objects.bulk_create([ CircuitType(name='Circuit Type 1', slug='circuit-type-1'), @@ -82,6 +74,7 @@ class CircuitTypeTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_circuittype_import(self): + self.add_permissions('circuits.add_circuittype') csv_data = ( "name,slug", @@ -97,16 +90,12 @@ class CircuitTypeTestCase(TestCase): class CircuitTestCase(TestCase): + user_permissions = ( + 'circuits.view_circuit', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'circuits.view_circuit', - 'circuits.add_circuit', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): provider = Provider(name='Provider 1', slug='provider-1', asn=65001) provider.save() @@ -138,6 +127,7 @@ class CircuitTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_circuit_import(self): + self.add_permissions('circuits.add_circuit') csv_data = ( "cid,provider,type", diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 856862a3e..45887f171 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1,26 +1,22 @@ import urllib.parse import yaml -from django.test import Client, TestCase +from django.contrib.auth.models import User from django.urls import reverse from dcim.choices import * from dcim.constants import * from dcim.models import * -from utilities.testing import create_test_user +from utilities.testing import TestCase class RegionTestCase(TestCase): + user_permissions = ( + 'dcim.view_region', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_region', - 'dcim.add_region', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): # Create three Regions for i in range(1, 4): @@ -34,6 +30,7 @@ class RegionTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_region_import(self): + self.add_permissions('dcim.add_region') csv_data = ( "name,slug", @@ -49,16 +46,12 @@ class RegionTestCase(TestCase): class SiteTestCase(TestCase): + user_permissions = ( + 'dcim.view_site', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_site', - 'dcim.add_site', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): region = Region(name='Region 1', slug='region-1') region.save() @@ -86,6 +79,7 @@ class SiteTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_site_import(self): + self.add_permissions('dcim.add_site') csv_data = ( "name,slug", @@ -101,16 +95,12 @@ class SiteTestCase(TestCase): class RackGroupTestCase(TestCase): + user_permissions = ( + 'dcim.view_rackgroup', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_rackgroup', - 'dcim.add_rackgroup', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -129,6 +119,7 @@ class RackGroupTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_rackgroup_import(self): + self.add_permissions('dcim.add_rackgroup') csv_data = ( "site,name,slug", @@ -144,16 +135,12 @@ class RackGroupTestCase(TestCase): class RackRoleTestCase(TestCase): + user_permissions = ( + 'dcim.view_rackrole', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_rackrole', - 'dcim.add_rackrole', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): RackRole.objects.bulk_create([ RackRole(name='Rack Role 1', slug='rack-role-1'), @@ -169,6 +156,7 @@ class RackRoleTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_rackrole_import(self): + self.add_permissions('dcim.add_rackrole') csv_data = ( "name,slug,color", @@ -184,11 +172,14 @@ class RackRoleTestCase(TestCase): class RackReservationTestCase(TestCase): + user_permissions = ( + 'dcim.view_rackreservation', + ) - def setUp(self): - user = create_test_user(permissions=['dcim.view_rackreservation']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): + + user = User.objects.create_user(username='testuser2') site = Site(name='Site 1', slug='site-1') site.save() @@ -211,16 +202,12 @@ class RackReservationTestCase(TestCase): class RackTestCase(TestCase): + user_permissions = ( + 'dcim.view_rack', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_rack', - 'dcim.add_rack', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -248,6 +235,7 @@ class RackTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_rack_import(self): + self.add_permissions('dcim.add_rack') csv_data = ( "site,name,width,u_height", @@ -263,16 +251,12 @@ class RackTestCase(TestCase): class ManufacturerTypeTestCase(TestCase): + user_permissions = ( + 'dcim.view_manufacturer', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_manufacturer', - 'dcim.add_manufacturer', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): Manufacturer.objects.bulk_create([ Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), @@ -288,6 +272,7 @@ class ManufacturerTypeTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_manufacturer_import(self): + self.add_permissions('dcim.add_manufacturer') csv_data = ( "name,slug", @@ -303,11 +288,12 @@ class ManufacturerTypeTestCase(TestCase): class DeviceTypeTestCase(TestCase): + user_permissions = ( + 'dcim.view_devicetype', + ) - def setUp(self): - user = create_test_user(permissions=['dcim.view_devicetype']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') manufacturer.save() @@ -420,9 +406,8 @@ device-bays: # Create the manufacturer Manufacturer(name='Generic', slug='generic').save() - # Authenticate as user with necessary permissions - user = create_test_user(username='testuser2', permissions=[ - 'dcim.view_devicetype', + # Add all required permissions to the test user + self.add_permissions( 'dcim.add_devicetype', 'dcim.add_consoleporttemplate', 'dcim.add_consoleserverporttemplate', @@ -432,8 +417,7 @@ device-bays: 'dcim.add_frontporttemplate', 'dcim.add_rearporttemplate', 'dcim.add_devicebaytemplate', - ]) - self.client.force_login(user) + ) form_data = { 'data': IMPORT_DATA, @@ -489,16 +473,12 @@ device-bays: class DeviceRoleTestCase(TestCase): + user_permissions = ( + 'dcim.view_devicerole', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_devicerole', - 'dcim.add_devicerole', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): DeviceRole.objects.bulk_create([ DeviceRole(name='Device Role 1', slug='device-role-1'), @@ -514,6 +494,7 @@ class DeviceRoleTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_devicerole_import(self): + self.add_permissions('dcim.add_devicerole') csv_data = ( "name,slug,color", @@ -529,16 +510,12 @@ class DeviceRoleTestCase(TestCase): class PlatformTestCase(TestCase): + user_permissions = ( + 'dcim.view_platform', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_platform', - 'dcim.add_platform', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): Platform.objects.bulk_create([ Platform(name='Platform 1', slug='platform-1'), @@ -554,6 +531,7 @@ class PlatformTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_platform_import(self): + self.add_permissions('dcim.add_platform') csv_data = ( "name,slug", @@ -569,16 +547,12 @@ class PlatformTestCase(TestCase): class DeviceTestCase(TestCase): + user_permissions = ( + 'dcim.view_device', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_device', - 'dcim.add_device', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -616,6 +590,7 @@ class DeviceTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_device_import(self): + self.add_permissions('dcim.add_device') csv_data = ( "device_role,manufacturer,model_name,status,site,name", @@ -631,16 +606,12 @@ class DeviceTestCase(TestCase): class ConsolePortTestCase(TestCase): + user_permissions = ( + 'dcim.view_consoleport', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_consoleport', - 'dcim.add_consoleport', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -671,6 +642,7 @@ class ConsolePortTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_consoleport_import(self): + self.add_permissions('dcim.add_consoleport') csv_data = ( "device,name", @@ -686,16 +658,12 @@ class ConsolePortTestCase(TestCase): class ConsoleServerPortTestCase(TestCase): + user_permissions = ( + 'dcim.view_consoleserverport', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_consoleserverport', - 'dcim.add_consoleserverport', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -726,6 +694,7 @@ class ConsoleServerPortTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_consoleserverport_import(self): + self.add_permissions('dcim.add_consoleserverport') csv_data = ( "device,name", @@ -741,16 +710,12 @@ class ConsoleServerPortTestCase(TestCase): class PowerPortTestCase(TestCase): + user_permissions = ( + 'dcim.view_powerport', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_powerport', - 'dcim.add_powerport', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -781,6 +746,7 @@ class PowerPortTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_powerport_import(self): + self.add_permissions('dcim.add_powerport') csv_data = ( "device,name", @@ -796,16 +762,12 @@ class PowerPortTestCase(TestCase): class PowerOutletTestCase(TestCase): + user_permissions = ( + 'dcim.view_poweroutlet', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_poweroutlet', - 'dcim.add_poweroutlet', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -836,6 +798,7 @@ class PowerOutletTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_poweroutlet_import(self): + self.add_permissions('dcim.add_poweroutlet') csv_data = ( "device,name", @@ -851,16 +814,12 @@ class PowerOutletTestCase(TestCase): class InterfaceTestCase(TestCase): + user_permissions = ( + 'dcim.view_interface', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_interface', - 'dcim.add_interface', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -891,6 +850,7 @@ class InterfaceTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_interface_import(self): + self.add_permissions('dcim.add_interface') csv_data = ( "device,name,type", @@ -906,16 +866,12 @@ class InterfaceTestCase(TestCase): class FrontPortTestCase(TestCase): + user_permissions = ( + 'dcim.view_frontport', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_frontport', - 'dcim.add_frontport', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -958,6 +914,7 @@ class FrontPortTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_frontport_import(self): + self.add_permissions('dcim.add_frontport') csv_data = ( "device,name,type,rear_port,rear_port_position", @@ -973,16 +930,12 @@ class FrontPortTestCase(TestCase): class RearPortTestCase(TestCase): + user_permissions = ( + 'dcim.view_rearport', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_rearport', - 'dcim.add_rearport', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -1013,6 +966,7 @@ class RearPortTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_rearport_import(self): + self.add_permissions('dcim.add_rearport') csv_data = ( "device,name,type,positions", @@ -1028,16 +982,12 @@ class RearPortTestCase(TestCase): class DeviceBayTestCase(TestCase): + user_permissions = ( + 'dcim.view_devicebay', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_devicebay', - 'dcim.add_devicebay', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -1072,6 +1022,7 @@ class DeviceBayTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_devicebay_import(self): + self.add_permissions('dcim.add_devicebay') csv_data = ( "device,name", @@ -1087,16 +1038,12 @@ class DeviceBayTestCase(TestCase): class InventoryItemTestCase(TestCase): + user_permissions = ( + 'dcim.view_inventoryitem', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_inventoryitem', - 'dcim.add_inventoryitem', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -1130,6 +1077,7 @@ class InventoryItemTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_inventoryitem_import(self): + self.add_permissions('dcim.add_inventoryitem') csv_data = ( "device,name", @@ -1145,16 +1093,12 @@ class InventoryItemTestCase(TestCase): class CableTestCase(TestCase): + user_permissions = ( + 'dcim.view_cable', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'dcim.view_cable', - 'dcim.add_cable', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -1219,6 +1163,7 @@ class CableTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_cable_import(self): + self.add_permissions('dcim.add_cable') csv_data = ( "side_a_device,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name", @@ -1234,11 +1179,12 @@ class CableTestCase(TestCase): class VirtualChassisTestCase(TestCase): + user_permissions = ( + 'dcim.view_virtualchassis', + ) - def setUp(self): - user = create_test_user(permissions=['dcim.view_virtualchassis']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site.objects.create(name='Site 1', slug='site-1') manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1') diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 6f08f2d47..66742e1a9 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -1,26 +1,21 @@ from netaddr import IPNetwork import urllib.parse -from django.test import Client, TestCase from django.urls import reverse from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from ipam.choices import ServiceProtocolChoices from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF -from utilities.testing import create_test_user +from utilities.testing import TestCase class VRFTestCase(TestCase): + user_permissions = ( + 'ipam.view_vrf', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_vrf', - 'ipam.add_vrf', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): VRF.objects.bulk_create([ VRF(name='VRF 1', rd='65000:1'), @@ -45,6 +40,7 @@ class VRFTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_vrf_import(self): + self.add_permissions('ipam.add_vrf') csv_data = ( "name", @@ -60,16 +56,12 @@ class VRFTestCase(TestCase): class RIRTestCase(TestCase): + user_permissions = ( + 'ipam.view_rir', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_rir', - 'ipam.add_rir', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): RIR.objects.bulk_create([ RIR(name='RIR 1', slug='rir-1'), @@ -85,6 +77,7 @@ class RIRTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_rir_import(self): + self.add_permissions('ipam.add_rir') csv_data = ( "name,slug", @@ -100,16 +93,12 @@ class RIRTestCase(TestCase): class AggregateTestCase(TestCase): + user_permissions = ( + 'ipam.view_aggregate', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_aggregate', - 'ipam.add_aggregate', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): rir = RIR(name='RIR 1', slug='rir-1') rir.save() @@ -137,6 +126,7 @@ class AggregateTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_aggregate_import(self): + self.add_permissions('ipam.add_aggregate') csv_data = ( "prefix,rir", @@ -152,16 +142,12 @@ class AggregateTestCase(TestCase): class RoleTestCase(TestCase): + user_permissions = ( + 'ipam.view_role', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_role', - 'ipam.add_role', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): Role.objects.bulk_create([ Role(name='Role 1', slug='role-1'), @@ -177,6 +163,7 @@ class RoleTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_role_import(self): + self.add_permissions('ipam.add_role') csv_data = ( "name,slug,weight", @@ -192,16 +179,12 @@ class RoleTestCase(TestCase): class PrefixTestCase(TestCase): + user_permissions = ( + 'ipam.view_prefix', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_prefix', - 'ipam.add_prefix', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -229,6 +212,7 @@ class PrefixTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_prefix_import(self): + self.add_permissions('ipam.add_prefix') csv_data = ( "prefix,status", @@ -244,16 +228,12 @@ class PrefixTestCase(TestCase): class IPAddressTestCase(TestCase): + user_permissions = ( + 'ipam.view_ipaddress', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_ipaddress', - 'ipam.add_ipaddress', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): vrf = VRF(name='VRF 1', rd='65000:1') vrf.save() @@ -281,6 +261,7 @@ class IPAddressTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_ipaddress_import(self): + self.add_permissions('ipam.add_ipaddress') csv_data = ( "address,status", @@ -296,16 +277,12 @@ class IPAddressTestCase(TestCase): class VLANGroupTestCase(TestCase): + user_permissions = ( + 'ipam.view_vlangroup', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_vlangroup', - 'ipam.add_vlangroup', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -327,6 +304,7 @@ class VLANGroupTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_vlangroup_import(self): + self.add_permissions('ipam.add_vlangroup') csv_data = ( "name,slug", @@ -342,16 +320,12 @@ class VLANGroupTestCase(TestCase): class VLANTestCase(TestCase): + user_permissions = ( + 'ipam.view_vlan', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'ipam.view_vlan', - 'ipam.add_vlan', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1') vlangroup.save() @@ -379,6 +353,7 @@ class VLANTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_vlan_import(self): + self.add_permissions('ipam.add_vlan') csv_data = ( "vid,name,status", @@ -394,11 +369,12 @@ class VLANTestCase(TestCase): class ServiceTestCase(TestCase): + user_permissions = ( + 'ipam.view_service', + ) - def setUp(self): - user = create_test_user(permissions=['ipam.view_service']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() diff --git a/netbox/secrets/tests/test_views.py b/netbox/secrets/tests/test_views.py index 43ae10dc6..14074630b 100644 --- a/netbox/secrets/tests/test_views.py +++ b/netbox/secrets/tests/test_views.py @@ -1,26 +1,21 @@ import base64 import urllib.parse -from django.test import Client, TestCase from django.urls import reverse from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from secrets.models import Secret, SecretRole, SessionKey, UserKey -from utilities.testing import create_test_user +from utilities.testing import TestCase from .constants import PRIVATE_KEY, PUBLIC_KEY class SecretRoleTestCase(TestCase): + user_permissions = ( + 'secrets.view_secretrole', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'secrets.view_secretrole', - 'secrets.add_secretrole', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): SecretRole.objects.bulk_create([ SecretRole(name='Secret Role 1', slug='secret-role-1'), @@ -36,6 +31,7 @@ class SecretRoleTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_secretrole_import(self): + self.add_permissions('secrets.add_secretrole') csv_data = ( "name,slug", @@ -51,24 +47,12 @@ class SecretRoleTestCase(TestCase): class SecretTestCase(TestCase): + user_permissions = ( + 'secrets.view_secret', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'secrets.view_secret', - 'secrets.add_secret', - ] - ) - - # Set up a master key - userkey = UserKey(user=user, public_key=PUBLIC_KEY) - userkey.save() - master_key = userkey.get_master_key(PRIVATE_KEY) - self.session_key = SessionKey(userkey=userkey) - self.session_key.save(master_key) - - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -94,6 +78,17 @@ class SecretTestCase(TestCase): Secret(device=device, role=secretrole, name='Secret 3', ciphertext=b'1234567890'), ]) + def setUp(self): + + super().setUp() + + # Set up a master key for the test user + userkey = UserKey(user=self.user, public_key=PUBLIC_KEY) + userkey.save() + master_key = userkey.get_master_key(PRIVATE_KEY) + self.session_key = SessionKey(userkey=userkey) + self.session_key.save(master_key) + def test_secret_list(self): url = reverse('secrets:secret_list') @@ -111,6 +106,7 @@ class SecretTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_secret_import(self): + self.add_permissions('secrets.add_secret') csv_data = ( "device,role,name,plaintext", diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index 10ee354d4..3cb04d6b2 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/netbox/tenancy/tests/test_views.py @@ -1,23 +1,18 @@ import urllib.parse -from django.test import Client, TestCase from django.urls import reverse from tenancy.models import Tenant, TenantGroup -from utilities.testing import create_test_user +from utilities.testing import TestCase class TenantGroupTestCase(TestCase): + user_permissions = ( + 'tenancy.view_tenantgroup', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'tenancy.view_tenantgroup', - 'tenancy.add_tenantgroup', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): TenantGroup.objects.bulk_create([ TenantGroup(name='Tenant Group 1', slug='tenant-group-1'), @@ -33,6 +28,7 @@ class TenantGroupTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_tenantgroup_import(self): + self.add_permissions('tenancy.add_tenantgroup') csv_data = ( "name,slug", @@ -48,16 +44,12 @@ class TenantGroupTestCase(TestCase): class TenantTestCase(TestCase): + user_permissions = ( + 'tenancy.view_tenant', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'tenancy.view_tenant', - 'tenancy.add_tenant', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): tenantgroup = TenantGroup(name='Tenant Group 1', slug='tenant-group-1') tenantgroup.save() @@ -85,6 +77,7 @@ class TenantTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_tenant_import(self): + self.add_permissions('tenancy.add_tenant') csv_data = ( "name,slug", diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 57af2ffc8..67df8fe1e 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -1,23 +1,18 @@ import urllib.parse -from django.test import Client, TestCase from django.urls import reverse -from utilities.testing import create_test_user +from utilities.testing import TestCase from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine class ClusterGroupTestCase(TestCase): + user_permissions = ( + 'virtualization.view_clustergroup', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'virtualization.view_clustergroup', - 'virtualization.add_clustergroup', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): ClusterGroup.objects.bulk_create([ ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'), @@ -33,6 +28,7 @@ class ClusterGroupTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_clustergroup_import(self): + self.add_permissions('virtualization.add_clustergroup') csv_data = ( "name,slug", @@ -48,16 +44,12 @@ class ClusterGroupTestCase(TestCase): class ClusterTypeTestCase(TestCase): + user_permissions = ( + 'virtualization.view_clustertype', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'virtualization.view_clustertype', - 'virtualization.add_clustertype', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): ClusterType.objects.bulk_create([ ClusterType(name='Cluster Type 1', slug='cluster-type-1'), @@ -73,6 +65,7 @@ class ClusterTypeTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_clustertype_import(self): + self.add_permissions('virtualization.add_clustertype') csv_data = ( "name,slug", @@ -88,16 +81,12 @@ class ClusterTypeTestCase(TestCase): class ClusterTestCase(TestCase): + user_permissions = ( + 'virtualization.view_cluster', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'virtualization.view_cluster', - 'virtualization.add_cluster', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): clustergroup = ClusterGroup(name='Cluster Group 1', slug='cluster-group-1') clustergroup.save() @@ -129,6 +118,7 @@ class ClusterTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_cluster_import(self): + self.add_permissions('virtualization.add_cluster') csv_data = ( "name,type", @@ -144,16 +134,12 @@ class ClusterTestCase(TestCase): class VirtualMachineTestCase(TestCase): + user_permissions = ( + 'virtualization.view_virtualmachine', + ) - def setUp(self): - user = create_test_user( - permissions=[ - 'virtualization.view_virtualmachine', - 'virtualization.add_virtualmachine', - ] - ) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1') clustertype.save() @@ -184,6 +170,7 @@ class VirtualMachineTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_virtualmachine_import(self): + self.add_permissions('virtualization.add_virtualmachine') csv_data = ( "name,cluster", From c8c9f78829c24e798d9e29b7431fea3c38155db6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 16:47:44 -0500 Subject: [PATCH 075/174] Documented the new DEVELOPER configuration parameter --- docs/configuration/optional-settings.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index bc79e90ab..03dcb1264 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -90,6 +90,14 @@ This setting enables debugging. This should be done only during development or t --- +# DEVELOPER + +Default: False + +This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Set this to `True` **only** if you are actively developing the NetBox code base. + +--- + ## EMAIL In order to send email, NetBox needs an email server configured. The following items can be defined within the `EMAIL` setting: From 179abcc79de43893482a7e6adc6e6a8f8f7adc01 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 17:57:34 -0500 Subject: [PATCH 076/174] Refactor APITestCase to subclass TestCase --- netbox/utilities/testing.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/netbox/utilities/testing.py b/netbox/utilities/testing.py index 39b43ab83..f86a32ab0 100644 --- a/netbox/utilities/testing.py +++ b/netbox/utilities/testing.py @@ -3,7 +3,7 @@ from contextlib import contextmanager from django.contrib.auth.models import Permission, User from django.test import Client, TestCase as _TestCase -from rest_framework.test import APITestCase as _APITestCase +from rest_framework.test import APIClient from users.models import Token @@ -21,6 +21,10 @@ class TestCase(_TestCase): self.client = Client() self.client.force_login(self.user) + # + # Permissions management + # + def add_permissions(self, *names): """ Assign a set of permissions to the test user. Accepts permission names in the form ._. @@ -39,8 +43,22 @@ class TestCase(_TestCase): perm = Permission.objects.get(content_type__app_label=app, codename=codename) self.user.user_permissions.remove(perm) + # + # Convenience methods + # -class APITestCase(_APITestCase): + def assertHttpStatus(self, response, expected_status): + """ + TestCase method. Provide more detail in the event of an unexpected HTTP response. + """ + err_message = "Expected HTTP status {}; received {}: {}" + self.assertEqual(response.status_code, expected_status, err_message.format( + expected_status, response.status_code, getattr(response, 'data', 'No data') + )) + + +class APITestCase(TestCase): + client_class = APIClient def setUp(self): """ @@ -50,15 +68,6 @@ class APITestCase(_APITestCase): self.token = Token.objects.create(user=self.user) self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)} - def assertHttpStatus(self, response, expected_status): - """ - Provide more detail in the event of an unexpected HTTP response. - """ - err_message = "Expected HTTP status {}; received {}: {}" - self.assertEqual(response.status_code, expected_status, err_message.format( - expected_status, response.status_code, getattr(response, 'data', 'No data') - )) - def create_test_user(username='testuser', permissions=list()): """ From 67fafb2b9dc700a5d91dd6ea243bd82bba73638d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 18:08:25 -0500 Subject: [PATCH 077/174] Use assertHttpStatus for evaluating HTTP response codes --- netbox/circuits/tests/test_views.py | 20 ++--- netbox/dcim/tests/test_views.py | 96 +++++++++++------------ netbox/ipam/tests/test_views.py | 46 +++++------ netbox/netbox/tests/test_views.py | 6 +- netbox/secrets/tests/test_views.py | 10 +-- netbox/tenancy/tests/test_views.py | 10 +-- netbox/virtualization/tests/test_views.py | 20 ++--- 7 files changed, 102 insertions(+), 106 deletions(-) diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index d10d2df85..6ecc88e5c 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -13,7 +13,6 @@ class ProviderTestCase(TestCase): @classmethod def setUpTestData(cls): - Provider.objects.bulk_create([ Provider(name='Provider 1', slug='provider-1', asn=65001), Provider(name='Provider 2', slug='provider-2', asn=65002), @@ -21,24 +20,21 @@ class ProviderTestCase(TestCase): ]) def test_provider_list(self): - url = reverse('circuits:provider_list') params = { "q": "test", } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_provider(self): - provider = Provider.objects.first() response = self.client.get(provider.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_provider_import(self): self.add_permissions('circuits.add_provider') - csv_data = ( "name,slug", "Provider 4,provider-4", @@ -48,7 +44,7 @@ class ProviderTestCase(TestCase): response = self.client.post(reverse('circuits:provider_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Provider.objects.count(), 6) @@ -71,7 +67,7 @@ class CircuitTypeTestCase(TestCase): url = reverse('circuits:circuittype_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_circuittype_import(self): self.add_permissions('circuits.add_circuittype') @@ -85,7 +81,7 @@ class CircuitTypeTestCase(TestCase): response = self.client.post(reverse('circuits:circuittype_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(CircuitType.objects.count(), 6) @@ -118,13 +114,13 @@ class CircuitTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_circuit(self): circuit = Circuit.objects.first() response = self.client.get(circuit.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_circuit_import(self): self.add_permissions('circuits.add_circuit') @@ -138,5 +134,5 @@ class CircuitTestCase(TestCase): response = self.client.post(reverse('circuits:circuit_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Circuit.objects.count(), 6) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 45887f171..6a07e0153 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -27,7 +27,7 @@ class RegionTestCase(TestCase): url = reverse('dcim:region_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_region_import(self): self.add_permissions('dcim.add_region') @@ -41,7 +41,7 @@ class RegionTestCase(TestCase): response = self.client.post(reverse('dcim:region_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Region.objects.count(), 6) @@ -70,13 +70,13 @@ class SiteTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_site(self): site = Site.objects.first() response = self.client.get(site.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_site_import(self): self.add_permissions('dcim.add_site') @@ -90,7 +90,7 @@ class SiteTestCase(TestCase): response = self.client.post(reverse('dcim:site_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Site.objects.count(), 6) @@ -116,7 +116,7 @@ class RackGroupTestCase(TestCase): url = reverse('dcim:rackgroup_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_rackgroup_import(self): self.add_permissions('dcim.add_rackgroup') @@ -130,7 +130,7 @@ class RackGroupTestCase(TestCase): response = self.client.post(reverse('dcim:rackgroup_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(RackGroup.objects.count(), 6) @@ -153,7 +153,7 @@ class RackRoleTestCase(TestCase): url = reverse('dcim:rackrole_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_rackrole_import(self): self.add_permissions('dcim.add_rackrole') @@ -167,7 +167,7 @@ class RackRoleTestCase(TestCase): response = self.client.post(reverse('dcim:rackrole_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(RackRole.objects.count(), 6) @@ -198,7 +198,7 @@ class RackReservationTestCase(TestCase): url = reverse('dcim:rackreservation_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) class RackTestCase(TestCase): @@ -226,13 +226,13 @@ class RackTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_rack(self): rack = Rack.objects.first() response = self.client.get(rack.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_rack_import(self): self.add_permissions('dcim.add_rack') @@ -246,7 +246,7 @@ class RackTestCase(TestCase): response = self.client.post(reverse('dcim:rack_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Rack.objects.count(), 6) @@ -269,7 +269,7 @@ class ManufacturerTypeTestCase(TestCase): url = reverse('dcim:manufacturer_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_manufacturer_import(self): self.add_permissions('dcim.add_manufacturer') @@ -283,7 +283,7 @@ class ManufacturerTypeTestCase(TestCase): response = self.client.post(reverse('dcim:manufacturer_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Manufacturer.objects.count(), 6) @@ -312,14 +312,14 @@ class DeviceTypeTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_devicetype_export(self): url = reverse('dcim:devicetype_list') response = self.client.get('{}?export'.format(url)) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader)) self.assertEqual(len(data), 3) self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1') @@ -329,7 +329,7 @@ class DeviceTypeTestCase(TestCase): devicetype = DeviceType.objects.first() response = self.client.get(devicetype.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_devicetype_import(self): @@ -424,7 +424,7 @@ device-bays: 'format': 'yaml' } response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) dt = DeviceType.objects.get(model='TEST-1000') @@ -491,7 +491,7 @@ class DeviceRoleTestCase(TestCase): url = reverse('dcim:devicerole_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_devicerole_import(self): self.add_permissions('dcim.add_devicerole') @@ -505,7 +505,7 @@ class DeviceRoleTestCase(TestCase): response = self.client.post(reverse('dcim:devicerole_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(DeviceRole.objects.count(), 6) @@ -528,7 +528,7 @@ class PlatformTestCase(TestCase): url = reverse('dcim:platform_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_platform_import(self): self.add_permissions('dcim.add_platform') @@ -542,7 +542,7 @@ class PlatformTestCase(TestCase): response = self.client.post(reverse('dcim:platform_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Platform.objects.count(), 6) @@ -581,13 +581,13 @@ class DeviceTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_device(self): device = Device.objects.first() response = self.client.get(device.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_device_import(self): self.add_permissions('dcim.add_device') @@ -601,7 +601,7 @@ class DeviceTestCase(TestCase): response = self.client.post(reverse('dcim:device_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Device.objects.count(), 6) @@ -639,7 +639,7 @@ class ConsolePortTestCase(TestCase): url = reverse('dcim:consoleport_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_consoleport_import(self): self.add_permissions('dcim.add_consoleport') @@ -653,7 +653,7 @@ class ConsolePortTestCase(TestCase): response = self.client.post(reverse('dcim:consoleport_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(ConsolePort.objects.count(), 6) @@ -691,7 +691,7 @@ class ConsoleServerPortTestCase(TestCase): url = reverse('dcim:consoleserverport_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_consoleserverport_import(self): self.add_permissions('dcim.add_consoleserverport') @@ -705,7 +705,7 @@ class ConsoleServerPortTestCase(TestCase): response = self.client.post(reverse('dcim:consoleserverport_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(ConsoleServerPort.objects.count(), 6) @@ -743,7 +743,7 @@ class PowerPortTestCase(TestCase): url = reverse('dcim:powerport_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_powerport_import(self): self.add_permissions('dcim.add_powerport') @@ -757,7 +757,7 @@ class PowerPortTestCase(TestCase): response = self.client.post(reverse('dcim:powerport_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(PowerPort.objects.count(), 6) @@ -795,7 +795,7 @@ class PowerOutletTestCase(TestCase): url = reverse('dcim:poweroutlet_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_poweroutlet_import(self): self.add_permissions('dcim.add_poweroutlet') @@ -809,7 +809,7 @@ class PowerOutletTestCase(TestCase): response = self.client.post(reverse('dcim:poweroutlet_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(PowerOutlet.objects.count(), 6) @@ -847,7 +847,7 @@ class InterfaceTestCase(TestCase): url = reverse('dcim:interface_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_interface_import(self): self.add_permissions('dcim.add_interface') @@ -861,7 +861,7 @@ class InterfaceTestCase(TestCase): response = self.client.post(reverse('dcim:interface_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Interface.objects.count(), 6) @@ -911,7 +911,7 @@ class FrontPortTestCase(TestCase): url = reverse('dcim:frontport_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_frontport_import(self): self.add_permissions('dcim.add_frontport') @@ -925,7 +925,7 @@ class FrontPortTestCase(TestCase): response = self.client.post(reverse('dcim:frontport_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(FrontPort.objects.count(), 6) @@ -963,7 +963,7 @@ class RearPortTestCase(TestCase): url = reverse('dcim:rearport_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_rearport_import(self): self.add_permissions('dcim.add_rearport') @@ -977,7 +977,7 @@ class RearPortTestCase(TestCase): response = self.client.post(reverse('dcim:rearport_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(RearPort.objects.count(), 6) @@ -1019,7 +1019,7 @@ class DeviceBayTestCase(TestCase): url = reverse('dcim:devicebay_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_devicebay_import(self): self.add_permissions('dcim.add_devicebay') @@ -1033,7 +1033,7 @@ class DeviceBayTestCase(TestCase): response = self.client.post(reverse('dcim:devicebay_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(DeviceBay.objects.count(), 6) @@ -1074,7 +1074,7 @@ class InventoryItemTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_inventoryitem_import(self): self.add_permissions('dcim.add_inventoryitem') @@ -1088,7 +1088,7 @@ class InventoryItemTestCase(TestCase): response = self.client.post(reverse('dcim:inventoryitem_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(InventoryItem.objects.count(), 6) @@ -1154,13 +1154,13 @@ class CableTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_cable(self): cable = Cable.objects.first() response = self.client.get(cable.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_cable_import(self): self.add_permissions('dcim.add_cable') @@ -1174,7 +1174,7 @@ class CableTestCase(TestCase): response = self.client.post(reverse('dcim:cable_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Cable.objects.count(), 6) @@ -1228,4 +1228,4 @@ class VirtualChassisTestCase(TestCase): url = reverse('dcim:virtualchassis_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 66742e1a9..1ea5f2e2b 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -31,13 +31,13 @@ class VRFTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_vrf(self): vrf = VRF.objects.first() response = self.client.get(vrf.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_vrf_import(self): self.add_permissions('ipam.add_vrf') @@ -51,7 +51,7 @@ class VRFTestCase(TestCase): response = self.client.post(reverse('ipam:vrf_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(VRF.objects.count(), 6) @@ -74,7 +74,7 @@ class RIRTestCase(TestCase): url = reverse('ipam:rir_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_rir_import(self): self.add_permissions('ipam.add_rir') @@ -88,7 +88,7 @@ class RIRTestCase(TestCase): response = self.client.post(reverse('ipam:rir_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(RIR.objects.count(), 6) @@ -117,13 +117,13 @@ class AggregateTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_aggregate(self): aggregate = Aggregate.objects.first() response = self.client.get(aggregate.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_aggregate_import(self): self.add_permissions('ipam.add_aggregate') @@ -137,7 +137,7 @@ class AggregateTestCase(TestCase): response = self.client.post(reverse('ipam:aggregate_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Aggregate.objects.count(), 6) @@ -160,7 +160,7 @@ class RoleTestCase(TestCase): url = reverse('ipam:role_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_role_import(self): self.add_permissions('ipam.add_role') @@ -174,7 +174,7 @@ class RoleTestCase(TestCase): response = self.client.post(reverse('ipam:role_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Role.objects.count(), 6) @@ -203,13 +203,13 @@ class PrefixTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_prefix(self): prefix = Prefix.objects.first() response = self.client.get(prefix.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_prefix_import(self): self.add_permissions('ipam.add_prefix') @@ -223,7 +223,7 @@ class PrefixTestCase(TestCase): response = self.client.post(reverse('ipam:prefix_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Prefix.objects.count(), 6) @@ -252,13 +252,13 @@ class IPAddressTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_ipaddress(self): ipaddress = IPAddress.objects.first() response = self.client.get(ipaddress.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_ipaddress_import(self): self.add_permissions('ipam.add_ipaddress') @@ -272,7 +272,7 @@ class IPAddressTestCase(TestCase): response = self.client.post(reverse('ipam:ipaddress_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(IPAddress.objects.count(), 6) @@ -301,7 +301,7 @@ class VLANGroupTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_vlangroup_import(self): self.add_permissions('ipam.add_vlangroup') @@ -315,7 +315,7 @@ class VLANGroupTestCase(TestCase): response = self.client.post(reverse('ipam:vlangroup_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(VLANGroup.objects.count(), 6) @@ -344,13 +344,13 @@ class VLANTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_vlan(self): vlan = VLAN.objects.first() response = self.client.get(vlan.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_vlan_import(self): self.add_permissions('ipam.add_vlan') @@ -364,7 +364,7 @@ class VLANTestCase(TestCase): response = self.client.post(reverse('ipam:vlan_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(VLAN.objects.count(), 6) @@ -405,10 +405,10 @@ class ServiceTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_service(self): service = Service.objects.first() response = self.client.get(service.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) diff --git a/netbox/netbox/tests/test_views.py b/netbox/netbox/tests/test_views.py index db84dcd1a..1942471b0 100644 --- a/netbox/netbox/tests/test_views.py +++ b/netbox/netbox/tests/test_views.py @@ -1,6 +1,6 @@ import urllib.parse -from django.test import TestCase +from utilities.testing import TestCase from django.urls import reverse @@ -11,7 +11,7 @@ class HomeViewTestCase(TestCase): url = reverse('home') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_search(self): @@ -21,4 +21,4 @@ class HomeViewTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) diff --git a/netbox/secrets/tests/test_views.py b/netbox/secrets/tests/test_views.py index 14074630b..336a33320 100644 --- a/netbox/secrets/tests/test_views.py +++ b/netbox/secrets/tests/test_views.py @@ -28,7 +28,7 @@ class SecretRoleTestCase(TestCase): url = reverse('secrets:secretrole_list') response = self.client.get(url, follow=True) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_secretrole_import(self): self.add_permissions('secrets.add_secretrole') @@ -42,7 +42,7 @@ class SecretRoleTestCase(TestCase): response = self.client.post(reverse('secrets:secretrole_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(SecretRole.objects.count(), 6) @@ -97,13 +97,13 @@ class SecretTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_secret(self): secret = Secret.objects.first() response = self.client.get(secret.get_absolute_url(), follow=True) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_secret_import(self): self.add_permissions('secrets.add_secret') @@ -121,5 +121,5 @@ class SecretTestCase(TestCase): response = self.client.post(reverse('secrets:secret_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Secret.objects.count(), 6) diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index 3cb04d6b2..8646abe38 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/netbox/tenancy/tests/test_views.py @@ -25,7 +25,7 @@ class TenantGroupTestCase(TestCase): url = reverse('tenancy:tenantgroup_list') response = self.client.get(url, follow=True) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_tenantgroup_import(self): self.add_permissions('tenancy.add_tenantgroup') @@ -39,7 +39,7 @@ class TenantGroupTestCase(TestCase): response = self.client.post(reverse('tenancy:tenantgroup_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(TenantGroup.objects.count(), 6) @@ -68,13 +68,13 @@ class TenantTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_tenant(self): tenant = Tenant.objects.first() response = self.client.get(tenant.get_absolute_url(), follow=True) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_tenant_import(self): self.add_permissions('tenancy.add_tenant') @@ -88,5 +88,5 @@ class TenantTestCase(TestCase): response = self.client.post(reverse('tenancy:tenant_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Tenant.objects.count(), 6) diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 67df8fe1e..df346d11e 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -25,7 +25,7 @@ class ClusterGroupTestCase(TestCase): url = reverse('virtualization:clustergroup_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_clustergroup_import(self): self.add_permissions('virtualization.add_clustergroup') @@ -39,7 +39,7 @@ class ClusterGroupTestCase(TestCase): response = self.client.post(reverse('virtualization:clustergroup_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(ClusterGroup.objects.count(), 6) @@ -62,7 +62,7 @@ class ClusterTypeTestCase(TestCase): url = reverse('virtualization:clustertype_list') response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_clustertype_import(self): self.add_permissions('virtualization.add_clustertype') @@ -76,7 +76,7 @@ class ClusterTypeTestCase(TestCase): response = self.client.post(reverse('virtualization:clustertype_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(ClusterType.objects.count(), 6) @@ -109,13 +109,13 @@ class ClusterTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_cluster(self): cluster = Cluster.objects.first() response = self.client.get(cluster.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_cluster_import(self): self.add_permissions('virtualization.add_cluster') @@ -129,7 +129,7 @@ class ClusterTestCase(TestCase): response = self.client.post(reverse('virtualization:cluster_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(Cluster.objects.count(), 6) @@ -161,13 +161,13 @@ class VirtualMachineTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_virtualmachine(self): virtualmachine = VirtualMachine.objects.first() response = self.client.get(virtualmachine.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_virtualmachine_import(self): self.add_permissions('virtualization.add_virtualmachine') @@ -181,5 +181,5 @@ class VirtualMachineTestCase(TestCase): response = self.client.post(reverse('virtualization:virtualmachine_import'), {'csv': '\n'.join(csv_data)}) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) self.assertEqual(VirtualMachine.objects.count(), 6) From a44c4d14e4dd4ad3cf71e4d41cf2cd0096418ee6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 18:13:02 -0500 Subject: [PATCH 078/174] Convert view tests under extras to the new TestCase --- netbox/extras/tests/test_views.py | 41 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 792390121..fc77a81f5 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -2,21 +2,21 @@ import urllib.parse import uuid from django.contrib.auth.models import User -from django.test import Client, TestCase from django.urls import reverse from dcim.models import Site from extras.choices import ObjectChangeActionChoices from extras.models import ConfigContext, ObjectChange, Tag -from utilities.testing import create_test_user +from utilities.testing import TestCase class TagTestCase(TestCase): + user_permissions = ( + 'extras.view_tag', + ) - def setUp(self): - user = create_test_user(permissions=['extras.view_tag']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): Tag.objects.bulk_create([ Tag(name='Tag 1', slug='tag-1'), @@ -32,15 +32,16 @@ class TagTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) class ConfigContextTestCase(TestCase): + user_permissions = ( + 'extras.view_configcontext', + ) - def setUp(self): - user = create_test_user(permissions=['extras.view_configcontext']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() @@ -62,26 +63,28 @@ class ConfigContextTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_configcontext(self): configcontext = ConfigContext.objects.first() response = self.client.get(configcontext.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) class ObjectChangeTestCase(TestCase): + user_permissions = ( + 'extras.view_objectchange', + ) - def setUp(self): - user = create_test_user(permissions=['extras.view_objectchange']) - self.client = Client() - self.client.force_login(user) + @classmethod + def setUpTestData(cls): site = Site(name='Site 1', slug='site-1') site.save() # Create three ObjectChanges + user = User.objects.create_user(username='testuser2') for i in range(1, 4): oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE) oc.user = user @@ -96,10 +99,10 @@ class ObjectChangeTestCase(TestCase): } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) def test_objectchange(self): objectchange = ObjectChange.objects.first() response = self.client.get(objectchange.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertHttpStatus(response, 200) From 4522a285e0e1a23ff220b9828fe13710a3c5d3e0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 20:05:27 -0500 Subject: [PATCH 079/174] Fix headings --- docs/configuration/optional-settings.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 03dcb1264..8cadddeb5 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -90,7 +90,7 @@ This setting enables debugging. This should be done only during development or t --- -# DEVELOPER +## DEVELOPER Default: False @@ -135,7 +135,7 @@ EXEMPT_VIEW_PERMISSIONS = ['*'] --- -# ENFORCE_GLOBAL_UNIQUE +## ENFORCE_GLOBAL_UNIQUE Default: False From e01c984c01fcd45a9976067f5bd043d661038739 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 20:48:26 -0500 Subject: [PATCH 080/174] Introduced a custom model_to_dict() --- netbox/utilities/testing.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/netbox/utilities/testing.py b/netbox/utilities/testing.py index f86a32ab0..629810995 100644 --- a/netbox/utilities/testing.py +++ b/netbox/utilities/testing.py @@ -2,6 +2,7 @@ import logging from contextlib import contextmanager from django.contrib.auth.models import Permission, User +from django.forms.models import model_to_dict as _model_to_dict from django.test import Client, TestCase as _TestCase from rest_framework.test import APIClient @@ -69,6 +70,24 @@ class APITestCase(TestCase): self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)} +def model_to_dict(instance, fields=None, exclude=None): + """ + Customized wrapper for Django's built-in model_to_dict(). Does the following: + - Excludes the instance ID field + - Convert any assigned tags to a comma-separated string + """ + _exclude = ['id'] + if exclude is not None: + _exclude += exclude + + model_dict = _model_to_dict(instance, fields=fields, exclude=_exclude) + + if 'tags' in model_dict: + model_dict['tags'] = ','.join(sorted([tag.name for tag in model_dict['tags']])) + + return model_dict + + def create_test_user(username='testuser', permissions=list()): """ Create a User with the given permissions. From 98cce7eee4aa4fe2642ad45b3eea0e982de050da Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 30 Jan 2020 21:56:29 -0500 Subject: [PATCH 081/174] Added ViewTestCase (WIP) --- netbox/circuits/tests/test_views.py | 153 +++++++++++----------------- netbox/utilities/testing.py | 146 ++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 95 deletions(-) diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index 6ecc88e5c..bebc3b287 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -1,58 +1,59 @@ -import urllib.parse - -from django.urls import reverse +import datetime +from circuits.choices import * from circuits.models import Circuit, CircuitType, Provider -from utilities.testing import TestCase +from utilities.testing import ViewTestCase -class ProviderTestCase(TestCase): - user_permissions = ( - 'circuits.view_provider', +class ProviderTestCase(ViewTestCase): + model = Provider + form_data = { + 'name': 'Provider X', + 'slug': 'provider-x', + 'asn': 65123, + 'account': '1234', + 'portal_url': 'http://example.com/portal', + 'noc_contact': 'noc@example.com', + 'admin_contact': 'admin@example.com', + 'comments': 'Another provider', + 'tags': 'Alpha,Bravo,Charlie', + } + csv_data = ( + "name,slug", + "Provider 4,provider-4", + "Provider 5,provider-5", + "Provider 6,provider-6", ) @classmethod def setUpTestData(cls): + Provider.objects.bulk_create([ Provider(name='Provider 1', slug='provider-1', asn=65001), Provider(name='Provider 2', slug='provider-2', asn=65002), Provider(name='Provider 3', slug='provider-3', asn=65003), ]) - def test_provider_list(self): - url = reverse('circuits:provider_list') - params = { - "q": "test", - } - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertHttpStatus(response, 200) - - def test_provider(self): - provider = Provider.objects.first() - response = self.client.get(provider.get_absolute_url()) - self.assertHttpStatus(response, 200) - - def test_provider_import(self): - self.add_permissions('circuits.add_provider') - csv_data = ( - "name,slug", - "Provider 4,provider-4", - "Provider 5,provider-5", - "Provider 6,provider-6", - ) - - response = self.client.post(reverse('circuits:provider_import'), {'csv': '\n'.join(csv_data)}) - - self.assertHttpStatus(response, 200) - self.assertEqual(Provider.objects.count(), 6) - - -class CircuitTypeTestCase(TestCase): - user_permissions = ( - 'circuits.view_circuittype', +class CircuitTypeTestCase(ViewTestCase): + model = CircuitType + views = ('list', 'add', 'edit', 'import') + form_data = { + 'name': 'Circuit Type X', + 'slug': 'circuit-type-x', + 'description': 'A new circuit type', + } + csv_data = ( + "name,slug", + "Circuit Type 4,circuit-type-4", + "Circuit Type 5,circuit-type-5", + "Circuit Type 6,circuit-type-6", ) + # Disable inapplicable tests + test_get_object = None + test_delete_object = None + @classmethod def setUpTestData(cls): @@ -62,32 +63,26 @@ class CircuitTypeTestCase(TestCase): CircuitType(name='Circuit Type 3', slug='circuit-type-3'), ]) - def test_circuittype_list(self): - url = reverse('circuits:circuittype_list') - - response = self.client.get(url) - self.assertHttpStatus(response, 200) - - def test_circuittype_import(self): - self.add_permissions('circuits.add_circuittype') - - csv_data = ( - "name,slug", - "Circuit Type 4,circuit-type-4", - "Circuit Type 5,circuit-type-5", - "Circuit Type 6,circuit-type-6", - ) - - response = self.client.post(reverse('circuits:circuittype_import'), {'csv': '\n'.join(csv_data)}) - - self.assertHttpStatus(response, 200) - self.assertEqual(CircuitType.objects.count(), 6) - - -class CircuitTestCase(TestCase): - user_permissions = ( - 'circuits.view_circuit', +class CircuitTestCase(ViewTestCase): + model = Circuit + # TODO: Determine how to lazily resolve related objects + form_data = { + 'cid': 'Circuit X', + 'provider': Provider.objects.first(), + 'type': CircuitType.objects.first(), + 'status': CircuitStatusChoices.STATUS_ACTIVE, + 'tenant': None, + 'install_date': datetime.date(2020, 1, 1), + 'commit_rate': 1000, + 'description': 'A new circuit', + 'comments': 'Some comments', + } + csv_data = ( + "cid,provider,type", + "Circuit 4,Provider 1,Circuit Type 1", + "Circuit 5,Provider 1,Circuit Type 1", + "Circuit 6,Provider 1,Circuit Type 1", ) @classmethod @@ -104,35 +99,3 @@ class CircuitTestCase(TestCase): Circuit(cid='Circuit 2', provider=provider, type=circuittype), Circuit(cid='Circuit 3', provider=provider, type=circuittype), ]) - - def test_circuit_list(self): - - url = reverse('circuits:circuit_list') - params = { - "provider": Provider.objects.first().slug, - "type": CircuitType.objects.first().slug, - } - - response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) - self.assertHttpStatus(response, 200) - - def test_circuit(self): - - circuit = Circuit.objects.first() - response = self.client.get(circuit.get_absolute_url()) - self.assertHttpStatus(response, 200) - - def test_circuit_import(self): - self.add_permissions('circuits.add_circuit') - - csv_data = ( - "cid,provider,type", - "Circuit 4,Provider 1,Circuit Type 1", - "Circuit 5,Provider 1,Circuit Type 1", - "Circuit 6,Provider 1,Circuit Type 1", - ) - - response = self.client.post(reverse('circuits:circuit_import'), {'csv': '\n'.join(csv_data)}) - - self.assertHttpStatus(response, 200) - self.assertEqual(Circuit.objects.count(), 6) diff --git a/netbox/utilities/testing.py b/netbox/utilities/testing.py index 629810995..d0004c8dd 100644 --- a/netbox/utilities/testing.py +++ b/netbox/utilities/testing.py @@ -2,8 +2,10 @@ import logging from contextlib import contextmanager from django.contrib.auth.models import Permission, User +from django.core.exceptions import ObjectDoesNotExist from django.forms.models import model_to_dict as _model_to_dict from django.test import Client, TestCase as _TestCase +from django.urls import reverse from rest_framework.test import APIClient from users.models import Token @@ -70,6 +72,133 @@ class APITestCase(TestCase): self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)} +# TODO: Omit this from tests +class ViewTestCase(TestCase): + """ + Stock TestCase suitable for testing all standard View functions: + - List objects + - View single object + - Create new object + - Modify existing object + - Delete existing object + - Import multiple new objects + """ + model = None + form_data = {} + csv_data = {} + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + self.base_url_name = '{}:{}_{{}}'.format(self.model._meta.app_label, self.model._meta.model_name) + + def test_list_objects(self): + response = self.client.get(reverse(self.base_url_name.format('list'))) + self.assertHttpStatus(response, 200) + + def test_get_object(self): + instance = self.model.objects.first() + response = self.client.get(instance.get_absolute_url()) + self.assertHttpStatus(response, 200) + + def test_create_object(self): + initial_count = self.model.objects.count() + request = { + 'path': reverse(self.base_url_name.format('add')), + 'data': post_data(self.form_data), + 'follow': True, + } + print(request['data']) + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.post(**request), 403) + + # Assign the required permission and submit again + self.add_permissions('{}.add_{}'.format(self.model._meta.app_label, self.model._meta.model_name)) + response = self.client.post(**request) + self.assertHttpStatus(response, 200) + + self.assertEqual(initial_count, self.model.objects.count() + 1) + instance = self.model.objects.order_by('-pk').first() + self.assertDictEqual(model_to_dict(instance), self.form_data) + + def test_edit_object(self): + instance = self.model.objects.first() + + # Determine the proper kwargs to pass to the edit URL + if hasattr(instance, 'slug'): + kwargs = {'slug': instance.slug} + else: + kwargs = {'pk': instance.pk} + + request = { + 'path': reverse(self.base_url_name.format('edit'), kwargs=kwargs), + 'data': post_data(self.form_data), + 'follow': True, + } + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.post(**request), 403) + + # Assign the required permission and submit again + self.add_permissions('{}.change_{}'.format(self.model._meta.app_label, self.model._meta.model_name)) + response = self.client.post(**request) + self.assertHttpStatus(response, 200) + + instance = self.model.objects.get(pk=instance.pk) + self.assertDictEqual(model_to_dict(instance), self.form_data) + + def test_delete_object(self): + instance = self.model.objects.first() + + # Determine the proper kwargs to pass to the deletion URL + if hasattr(instance, 'slug'): + kwargs = {'slug': instance.slug} + else: + kwargs = {'pk': instance.pk} + + request = { + 'path': reverse(self.base_url_name.format('delete'), kwargs=kwargs), + 'data': {'confirm': True}, + 'follow': True, + } + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.post(**request), 403) + + # Assign the required permission and submit again + self.add_permissions('{}.delete_{}'.format(self.model._meta.app_label, self.model._meta.model_name)) + response = self.client.post(**request) + self.assertHttpStatus(response, 200) + + with self.assertRaises(ObjectDoesNotExist): + self.model.objects.get(pk=instance.pk) + + def test_import_objects(self): + request = { + 'path': reverse(self.base_url_name.format('import')), + 'data': { + 'csv': '\n'.join(self.csv_data) + } + } + initial_count = self.model.objects.count() + + # Attempt to make the request without required permissions + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.post(**request), 403) + + # Assign the required permission and submit again + self.add_permissions('{}.add_{}'.format(self.model._meta.app_label, self.model._meta.model_name)) + response = self.client.post(**request) + self.assertHttpStatus(response, 200) + + self.assertEqual(self.model.objects.count(), initial_count + len(self.csv_data) - 1) + + def model_to_dict(instance, fields=None, exclude=None): """ Customized wrapper for Django's built-in model_to_dict(). Does the following: @@ -88,6 +217,23 @@ def model_to_dict(instance, fields=None, exclude=None): return model_dict +def post_data(data): + """ + Take a dictionary of test data (suitable for comparison to an instance) and return a dict suitable for POSTing. + """ + ret = {} + + for key, value in data.items(): + if value is None: + ret[key] = '' + elif hasattr(value, 'pk'): + ret[key] = getattr(value, 'pk') + else: + ret[key] = str(value) + + return ret + + def create_test_user(username='testuser', permissions=list()): """ Create a User with the given permissions. From 0d18c296a9864b16b01acd63702e22d1b6c24388 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Fri, 31 Jan 2020 11:11:42 +0000 Subject: [PATCH 082/174] Set default config context format to JSON to maintain existing behavior --- netbox/templates/extras/inc/configcontext_data.html | 8 ++++---- netbox/templates/extras/inc/configcontext_format.html | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/netbox/templates/extras/inc/configcontext_data.html b/netbox/templates/extras/inc/configcontext_data.html index d987b2acb..d91960e2c 100644 --- a/netbox/templates/extras/inc/configcontext_data.html +++ b/netbox/templates/extras/inc/configcontext_data.html @@ -1,8 +1,8 @@ {% load helpers %} -
-
{{ data|render_yaml }}
-
-