From 71b8a8c511dcc178ff6bc1385efdd60659798537 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 21 Mar 2023 15:11:38 -0500 Subject: [PATCH] #9047 - ProviderAccount --- docs/development/models.md | 1 + docs/development/search.md | 2 +- docs/features/circuits.md | 6 +- docs/features/contacts.md | 1 + docs/getting-started/planning.md | 2 +- docs/models/circuits/circuit.md | 4 +- docs/models/circuits/provider.md | 4 - docs/models/circuits/provideraccount.md | 17 +++ netbox/circuits/api/nested_serializers.py | 13 ++ netbox/circuits/api/serializers.py | 28 +++- netbox/circuits/api/urls.py | 1 + netbox/circuits/api/views.py | 14 +- netbox/circuits/filtersets.py | 52 +++++-- netbox/circuits/forms/bulk_edit.py | 40 +++++- netbox/circuits/forms/bulk_import.py | 25 +++- netbox/circuits/forms/filtersets.py | 27 ++++ netbox/circuits/forms/model_forms.py | 52 +++++-- netbox/circuits/graphql/schema.py | 3 + netbox/circuits/graphql/types.py | 9 ++ .../migrations/0042_provideraccount.py | 129 ++++++++++++++++++ netbox/circuits/models/circuits.py | 18 +-- netbox/circuits/models/providers.py | 63 +++++++-- netbox/circuits/search.py | 10 +- netbox/circuits/tables/circuits.py | 8 +- netbox/circuits/tables/providers.py | 36 ++++- netbox/circuits/tests/test_api.py | 74 ++++++++-- netbox/circuits/tests/test_filtersets.py | 110 +++++++++++---- netbox/circuits/tests/test_views.py | 85 ++++++++++-- netbox/circuits/urls.py | 8 ++ netbox/circuits/views.py | 70 +++++++++- netbox/dcim/tests/test_cablepaths.py | 7 +- netbox/dcim/tests/test_models.py | 5 +- netbox/netbox/navigation/menu.py | 1 + netbox/netbox/tests/test_staging.py | 41 ++++-- netbox/templates/circuits/circuit.html | 6 +- .../circuits/circuittermination_edit.html | 1 + netbox/templates/circuits/provider.html | 8 ++ .../templates/circuits/provideraccount.html | 58 ++++++++ 38 files changed, 900 insertions(+), 139 deletions(-) create mode 100644 docs/models/circuits/provideraccount.md create mode 100644 netbox/circuits/migrations/0042_provideraccount.py create mode 100644 netbox/templates/circuits/provideraccount.html diff --git a/docs/development/models.md b/docs/development/models.md index 6f3998977..53368e103 100644 --- a/docs/development/models.md +++ b/docs/development/models.md @@ -32,6 +32,7 @@ These are considered the "core" application models which are used to model netwo * [circuits.Circuit](../models/circuits/circuit.md) * [circuits.Provider](../models/circuits/provider.md) +* [circuits.ProviderAccount](../models/circuits/provideracount.md) * [circuits.ProviderNetwork](../models/circuits/providernetwork.md) * [core.DataSource](../models/core/datasource.md) * [dcim.Cable](../models/dcim/cable.md) diff --git a/docs/development/search.md b/docs/development/search.md index 02bcaa898..6ccffa7af 100644 --- a/docs/development/search.md +++ b/docs/development/search.md @@ -29,7 +29,7 @@ A SearchIndex subclass defines both its model and a list of two-tuples specifyin | 60 | Unique serialized attribute (per related object) | Device.serial | | 100 | Primary human identifier | Device.name, Circuit.cid, Cable.label | | 110 | Slug | Site.slug | -| 200 | Secondary identifier | Provider.account, DeviceType.part_number | +| 200 | Secondary identifier | ProviderAccount.account, DeviceType.part_number | | 300 | Highly unique descriptive attribute | CircuitTermination.xconnect_id, IPAddress.dns_name | | 500 | Description | Site.description | | 1000 | Custom field default | - | diff --git a/docs/features/circuits.md b/docs/features/circuits.md index 7739efb4c..f5bbe95f4 100644 --- a/docs/features/circuits.md +++ b/docs/features/circuits.md @@ -5,13 +5,15 @@ NetBox is ideal for managing your network's transit and peering providers and ci ```mermaid flowchart TD ASN --> Provider - Provider --> ProviderNetwork & Circuit + Provider --> ProviderAccount --> Circuit + Provider --> ProviderNetwork CircuitType --> Circuit click ASN "../../models/circuits/asn/" click Circuit "../../models/circuits/circuit/" click CircuitType "../../models/circuits/circuittype/" click Provider "../../models/circuits/provider/" +click ProviderAccount "../../models/circuits/provideraccount/" click ProviderNetwork "../../models/circuits/providernetwork/" ``` @@ -25,7 +27,7 @@ Sometimes you'll need to model provider networks into which you don't have full A circuit is a physical connection between two points, which is installed and maintained by an external provider. For example, an Internet connection delivered as a fiber optic cable would be modeled as a circuit in NetBox. -Each circuit is associated with a provider and assigned a circuit ID, which must be unique to that provider. A circuit is also assigned a user-defined type, operational status, and various other operating characteristics. +Each circuit is associated with a provider account and assigned a circuit ID, which must be unique to that provider. A circuit is also assigned a user-defined type, operational status, and various other operating characteristics. Each circuit may have up to two terminations (A and Z) defined. Each termination can be associated with a particular site or provider network. In the case of the former, a cable can be connected between the circuit termination and a device component to map its physical connectivity. diff --git a/docs/features/contacts.md b/docs/features/contacts.md index 40e8dd12c..ed182134c 100644 --- a/docs/features/contacts.md +++ b/docs/features/contacts.md @@ -31,6 +31,7 @@ The following models support the assignment of contacts: * circuits.Circuit * circuits.Provider +* circuits.ProviderAccount * dcim.Device * dcim.Location * dcim.Manufacturer diff --git a/docs/getting-started/planning.md b/docs/getting-started/planning.md index 5dbe6e54e..64cc04ec9 100644 --- a/docs/getting-started/planning.md +++ b/docs/getting-started/planning.md @@ -56,7 +56,7 @@ Below is the (rough) recommended order in which NetBox objects should be created 4. Manufacturers, device types, and module types 5. Platforms and device roles 6. Devices and modules -7. Providers and provider networks +7. Providers, provider accounts and provider networks 8. Circuit types and circuits 9. Wireless LAN groups and wireless LANs 10. Route targets and VRFs diff --git a/docs/models/circuits/circuit.md b/docs/models/circuits/circuit.md index 50637ab4e..83c3264f1 100644 --- a/docs/models/circuits/circuit.md +++ b/docs/models/circuits/circuit.md @@ -4,9 +4,9 @@ A circuit represents a physical point-to-point data connection, typically used t ## Fields -### Provider +### Provider Account -The [provider](./provider.md) to which this circuit belongs. +The [provider account](./provideraccount.md) to which this circuit belongs. ### Circuit ID diff --git a/docs/models/circuits/provider.md b/docs/models/circuits/provider.md index 2598eeec4..4caa02a77 100644 --- a/docs/models/circuits/provider.md +++ b/docs/models/circuits/provider.md @@ -16,10 +16,6 @@ A unique URL-friendly identifier. (This value can be used for filtering.) The [AS numbers](../ipam/asn.md) assigned to this provider (optional). -### Account Number - -The administrative account identifier tied to this provider for your organization. - ### Portal URL The URL for the provider's customer service portal. diff --git a/docs/models/circuits/provideraccount.md b/docs/models/circuits/provideraccount.md new file mode 100644 index 000000000..f906c657e --- /dev/null +++ b/docs/models/circuits/provideraccount.md @@ -0,0 +1,17 @@ +# Provider Accounts + +This model can be used to represent individual accounts associated with a provider. + +## Fields + +### Provider + +The [provider](./provider.md) the account belongs to. + +### Name + +A human-friendly name, unique to the provider. + +### Account Number + +The administrative account identifier tied to this provider for your organization. \ No newline at end of file diff --git a/netbox/circuits/api/nested_serializers.py b/netbox/circuits/api/nested_serializers.py index 8fc1bfaf7..0f19ccffc 100644 --- a/netbox/circuits/api/nested_serializers.py +++ b/netbox/circuits/api/nested_serializers.py @@ -9,6 +9,7 @@ __all__ = [ 'NestedCircuitTypeSerializer', 'NestedProviderNetworkSerializer', 'NestedProviderSerializer', + 'NestedProviderAccountSerializer', ] @@ -37,6 +38,18 @@ class NestedProviderSerializer(WritableNestedSerializer): fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count'] +# +# Provider Accounts +# + +class NestedProviderAccountSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail') + + class Meta: + model = ProviderAccount + fields = ['id', 'url', 'display', 'name', 'account'] + + # # Circuits # diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 816c696d2..fefe6c19d 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -18,6 +18,12 @@ from .nested_serializers import * class ProviderSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') + accounts = SerializedPKRelatedField( + queryset=ProviderAccount.objects.all(), + serializer=NestedProviderAccountSerializer, + required=False, + many=True + ) asns = SerializedPKRelatedField( queryset=ASN.objects.all(), serializer=NestedASNSerializer, @@ -31,11 +37,27 @@ class ProviderSerializer(NetBoxModelSerializer): class Meta: model = Provider fields = [ - 'id', 'url', 'display', 'name', 'slug', 'account', 'description', 'comments', 'asns', 'tags', + 'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', ] +# +# Provider Accounts +# + +class ProviderAccountSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail') + provider = NestedProviderSerializer() + + class Meta: + model = ProviderAccount + fields = [ + 'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', + ] + + # # Provider networks # @@ -83,7 +105,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer): class CircuitSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') - provider = NestedProviderSerializer() + provider_account = NestedProviderAccountSerializer() status = ChoiceField(choices=CircuitStatusChoices, required=False) type = NestedCircuitTypeSerializer() tenant = NestedTenantSerializer(required=False, allow_null=True) @@ -93,7 +115,7 @@ class CircuitSerializer(NetBoxModelSerializer): class Meta: model = Circuit fields = [ - 'id', 'url', 'display', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', + 'id', 'url', 'display', 'cid', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index 9d75009d5..69d531a3d 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -14,6 +14,7 @@ router.register('circuits', views.CircuitViewSet) router.register('circuit-terminations', views.CircuitTerminationViewSet) # Provider networks +router.register('provider-accounts', views.ProviderAccountViewSet) router.register('provider-networks', views.ProviderNetworkViewSet) app_name = 'circuits-api' diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index f5f3f0fab..1c9d2198d 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -22,7 +22,7 @@ class CircuitsRootView(APIRootView): class ProviderViewSet(NetBoxModelViewSet): queryset = Provider.objects.prefetch_related('asns', 'tags').annotate( - circuit_count=count_related(Circuit, 'provider') + circuit_count=count_related(Circuit, 'provider_account__provider') ) serializer_class = serializers.ProviderSerializer filterset_class = filtersets.ProviderFilterSet @@ -46,7 +46,7 @@ class CircuitTypeViewSet(NetBoxModelViewSet): class CircuitViewSet(NetBoxModelViewSet): queryset = Circuit.objects.prefetch_related( - 'type', 'tenant', 'provider', 'termination_a', 'termination_z' + 'type', 'tenant', 'provider_account', 'provider_account__provider', 'termination_a', 'termination_z' ).prefetch_related('tags') serializer_class = serializers.CircuitSerializer filterset_class = filtersets.CircuitFilterSet @@ -65,6 +65,16 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet): brief_prefetch_fields = ['circuit'] +# +# Provider networks +# + +class ProviderAccountViewSet(NetBoxModelViewSet): + queryset = ProviderAccount.objects.prefetch_related('tags') + serializer_class = serializers.ProviderAccountSerializer + filterset_class = filtersets.ProviderAccountFilterSet + + # # Provider networks # diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index 8e4c9ab06..6668cc647 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -16,6 +16,7 @@ __all__ = ( 'CircuitTerminationFilterSet', 'CircuitTypeFilterSet', 'ProviderNetworkFilterSet', + 'ProviderAccountFilterSet', 'ProviderFilterSet', ) @@ -23,37 +24,37 @@ __all__ = ( class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='circuits__terminations__site__region', + field_name='accounts__circuits__terminations__site__region', lookup_expr='in', label=_('Region (ID)'), ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='circuits__terminations__site__region', + field_name='accounts__circuits__terminations__site__region', lookup_expr='in', to_field_name='slug', label=_('Region (slug)'), ) site_group_id = TreeNodeMultipleChoiceFilter( queryset=SiteGroup.objects.all(), - field_name='circuits__terminations__site__group', + field_name='accounts__circuits__terminations__site__group', lookup_expr='in', label=_('Site group (ID)'), ) site_group = TreeNodeMultipleChoiceFilter( queryset=SiteGroup.objects.all(), - field_name='circuits__terminations__site__group', + field_name='accounts__circuits__terminations__site__group', lookup_expr='in', to_field_name='slug', label=_('Site group (slug)'), ) site_id = django_filters.ModelMultipleChoiceFilter( - field_name='circuits__terminations__site', + field_name='accounts__circuits__terminations__site', queryset=Site.objects.all(), label=_('Site'), ) site = django_filters.ModelMultipleChoiceFilter( - field_name='circuits__terminations__site__slug', + field_name='accounts__circuits__terminations__site__slug', queryset=Site.objects.all(), to_field_name='slug', label=_('Site (slug)'), @@ -66,7 +67,34 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): class Meta: model = Provider - fields = ['id', 'name', 'slug', 'account'] + fields = ['id', 'name', 'slug', ] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(accounts__account__icontains=value) | + Q(accounts__name__icontains=value) | + Q(comments__icontains=value) + ) + + +class ProviderAccountFilterSet(NetBoxModelFilterSet): + provider_id = django_filters.ModelMultipleChoiceFilter( + queryset=Provider.objects.all(), + label=_('Provider (ID)'), + ) + provider = django_filters.ModelMultipleChoiceFilter( + field_name='provider__slug', + queryset=Provider.objects.all(), + to_field_name='slug', + label=_('Provider (slug)'), + ) + + class Meta: + model = ProviderAccount + fields = ['id', 'name', 'account', 'description'] def search(self, queryset, name, value): if not value.strip(): @@ -75,7 +103,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): Q(name__icontains=value) | Q(account__icontains=value) | Q(comments__icontains=value) - ) + ).distinct() class ProviderNetworkFilterSet(NetBoxModelFilterSet): @@ -114,15 +142,21 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet): class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): provider_id = django_filters.ModelMultipleChoiceFilter( + field_name='provider_account__provider', queryset=Provider.objects.all(), label=_('Provider (ID)'), ) provider = django_filters.ModelMultipleChoiceFilter( - field_name='provider__slug', + field_name='provider_account__provider__slug', queryset=Provider.objects.all(), to_field_name='slug', label=_('Provider (slug)'), ) + provider_account_id = django_filters.ModelMultipleChoiceFilter( + field_name='provider_account', + queryset=ProviderAccount.objects.all(), + label=_('ProviderAccount (ID)'), + ) provider_network_id = django_filters.ModelMultipleChoiceFilter( field_name='terminations__provider_network', queryset=ProviderNetwork.objects.all(), diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index 8fd09d3ee..c199bf989 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -14,6 +14,7 @@ __all__ = ( 'CircuitBulkEditForm', 'CircuitTypeBulkEditForm', 'ProviderBulkEditForm', + 'ProviderAccountBulkEditForm', 'ProviderNetworkBulkEditForm', ) @@ -24,11 +25,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): label=_('ASNs'), required=False ) - account = forms.CharField( - max_length=30, - required=False, - label=_('Account number') - ) description = forms.CharField( max_length=200, required=False @@ -39,10 +35,33 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): model = Provider fieldsets = ( - (None, ('asns', 'account', )), + (None, ('asns', )), ) nullable_fields = ( - 'asns', 'account', 'description', 'comments', + 'asns', 'description', 'comments', + ) + + +class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm): + provider = DynamicModelChoiceField( + queryset=Provider.objects.all(), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + comments = CommentField( + label=_('Comments') + ) + + model = ProviderAccount + fieldsets = ( + (None, ('provider', 'description')), + ) + nullable_fields = ( + 'description', + 'comments', ) @@ -95,6 +114,13 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): queryset=Provider.objects.all(), required=False ) + provider_account = DynamicModelChoiceField( + queryset=ProviderAccount.objects.all(), + required=False, + query_params={ + 'provider': '$provider' + } + ) status = forms.ChoiceField( choices=add_blank_choice(CircuitStatusChoices), required=False, diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index c1a0056c4..7b7de2277 100644 --- a/netbox/circuits/forms/bulk_import.py +++ b/netbox/circuits/forms/bulk_import.py @@ -13,6 +13,7 @@ __all__ = ( 'CircuitTerminationImportForm', 'CircuitTypeImportForm', 'ProviderImportForm', + 'ProviderAccountImportForm', 'ProviderNetworkImportForm', ) @@ -23,7 +24,21 @@ class ProviderImportForm(NetBoxModelImportForm): class Meta: model = Provider fields = ( - 'name', 'slug', 'account', 'description', 'comments', 'tags', + 'name', 'slug', 'description', 'comments', 'tags', + ) + + +class ProviderAccountImportForm(NetBoxModelImportForm): + provider = CSVModelChoiceField( + queryset=Provider.objects.all(), + to_field_name='name', + help_text=_('Assigned provider') + ) + + class Meta: + model = ProviderAccount + fields = ( + 'provider', 'name', 'account', 'comments', 'tags', ) @@ -50,10 +65,10 @@ class CircuitTypeImportForm(NetBoxModelImportForm): class CircuitImportForm(NetBoxModelImportForm): - provider = CSVModelChoiceField( - queryset=Provider.objects.all(), + provider_account = CSVModelChoiceField( + queryset=ProviderAccount.objects.all(), to_field_name='name', - help_text=_('Assigned provider') + help_text=_('Assigned provider account') ) type = CSVModelChoiceField( queryset=CircuitType.objects.all(), @@ -74,7 +89,7 @@ class CircuitImportForm(NetBoxModelImportForm): class Meta: model = Circuit fields = [ - 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', + 'cid', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description', 'comments', 'tags' ] diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 05dacfd38..7f98ee4b1 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -13,6 +13,7 @@ __all__ = ( 'CircuitFilterForm', 'CircuitTypeFilterForm', 'ProviderFilterForm', + 'ProviderAccountFilterForm', 'ProviderNetworkFilterForm', ) @@ -56,6 +57,24 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) +class ProviderAccountFilterForm(NetBoxModelFilterSetForm): + model = ProviderAccount + fieldsets = ( + (None, ('q', 'filter_id', 'tag')), + ('Attributes', ('provider_id', 'account')), + ) + provider_id = DynamicModelMultipleChoiceField( + queryset=Provider.objects.all(), + required=False, + label=_('Provider') + ) + account = forms.CharField( + max_length=100, + required=False + ) + tag = TagFilterField(model) + + class ProviderNetworkFilterForm(NetBoxModelFilterSetForm): model = ProviderNetwork fieldsets = ( @@ -99,6 +118,14 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi required=False, label=_('Provider') ) + provider_account_id = DynamicModelMultipleChoiceField( + queryset=Provider.objects.all(), + required=False, + query_params={ + 'provider_id': '$provider_id' + }, + label=_('Provider') + ) provider_network_id = DynamicModelMultipleChoiceField( queryset=ProviderNetwork.objects.all(), required=False, diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 537db50df..729aaa95b 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -14,6 +14,7 @@ __all__ = ( 'CircuitTerminationForm', 'CircuitTypeForm', 'ProviderForm', + 'ProviderAccountForm', 'ProviderNetworkForm', ) @@ -29,13 +30,25 @@ class ProviderForm(NetBoxModelForm): fieldsets = ( ('Provider', ('name', 'slug', 'asns', 'description', 'tags')), - ('Support Info', ('account',)), ) class Meta: model = Provider fields = [ - 'name', 'slug', 'account', 'asns', 'description', 'comments', 'tags', + 'name', 'slug', 'asns', 'description', 'comments', 'tags', + ] + + +class ProviderAccountForm(NetBoxModelForm): + provider = DynamicModelChoiceField( + queryset=Provider.objects.all() + ) + comments = CommentField() + + class Meta: + model = ProviderAccount + fields = [ + 'provider', 'name', 'account', 'description', 'comments', 'tags', ] @@ -74,7 +87,20 @@ class CircuitTypeForm(NetBoxModelForm): class CircuitForm(TenancyForm, NetBoxModelForm): provider = DynamicModelChoiceField( - queryset=Provider.objects.all() + required=False, + queryset=Provider.objects.all(), + initial_params={ + 'accounts': '$provider_account' + }, + ) + provider_account = DynamicModelChoiceField( + queryset=ProviderAccount.objects.all(), + initial_params={ + 'circuits': '$circuit' + }, + query_params={ + 'provider': '$provider', + } ) type = DynamicModelChoiceField( queryset=CircuitType.objects.all() @@ -82,7 +108,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - ('Circuit', ('provider', 'cid', 'type', 'status', 'description', 'tags')), + ('Circuit', ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')), ('Service Parameters', ('install_date', 'termination_date', 'commit_rate')), ('Tenancy', ('tenant_group', 'tenant')), ) @@ -90,8 +116,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm): class Meta: model = Circuit fields = [ - 'cid', 'type', 'provider', 'status', 'install_date', 'termination_date', 'commit_rate', 'description', - 'tenant_group', 'tenant', 'comments', 'tags', + 'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate', + 'description', 'tenant_group', 'tenant', 'comments', 'tags', ] widgets = { 'install_date': DatePicker(), @@ -104,8 +130,18 @@ class CircuitTerminationForm(NetBoxModelForm): provider = DynamicModelChoiceField( queryset=Provider.objects.all(), required=False, + initial_params={ + 'accounts': '$provider_account' + } + ) + provider_account = DynamicModelChoiceField( + queryset=ProviderAccount.objects.all(), + required=False, initial_params={ 'circuits': '$circuit' + }, + query_params={ + 'provider': '$provider', } ) circuit = DynamicModelChoiceField( @@ -128,8 +164,8 @@ class CircuitTerminationForm(NetBoxModelForm): class Meta: model = CircuitTermination fields = [ - 'provider', 'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed', - 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'tags', + 'provider', 'provider_account', 'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', + 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'tags', ] widgets = { 'port_speed': SelectSpeedWidget(), diff --git a/netbox/circuits/graphql/schema.py b/netbox/circuits/graphql/schema.py index 32b73e258..3d85f2512 100644 --- a/netbox/circuits/graphql/schema.py +++ b/netbox/circuits/graphql/schema.py @@ -31,6 +31,9 @@ class CircuitsQuery(graphene.ObjectType): def resolve_provider_list(root, info, **kwargs): return gql_query_optimizer(models.Provider.objects.all(), info) + provider_account = ObjectField(ProviderAccountType) + provider_account_list = ObjectListField(ProviderAccountType) + provider_network = ObjectField(ProviderNetworkType) provider_network_list = ObjectListField(ProviderNetworkType) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 5582de798..baa135e00 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -10,6 +10,7 @@ __all__ = ( 'CircuitType', 'CircuitTypeType', 'ProviderType', + 'ProviderAccountType', 'ProviderNetworkType', ) @@ -45,6 +46,14 @@ class ProviderType(NetBoxObjectType, ContactsMixin): filterset_class = filtersets.ProviderFilterSet +class ProviderAccountType(NetBoxObjectType): + + class Meta: + model = models.ProviderAccount + fields = '__all__' + filterset_class = filtersets.ProviderAccountFilterSet + + class ProviderNetworkType(NetBoxObjectType): class Meta: diff --git a/netbox/circuits/migrations/0042_provideraccount.py b/netbox/circuits/migrations/0042_provideraccount.py new file mode 100644 index 000000000..00e26510b --- /dev/null +++ b/netbox/circuits/migrations/0042_provideraccount.py @@ -0,0 +1,129 @@ +# Generated by Django 4.1.4 on 2023-03-14 16:02 + +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers +import utilities.json + + +# +# Migrate Account in Provider model to separate account model +# +def create_provideraccounts_from_providers(apps, schema_editor): + Provider = apps.get_model('circuits', 'Provider') + ProviderAccount = apps.get_model('circuits', 'ProviderAccount') + + for provider in Provider.objects.all(): + if provider.account is not None: + provideraccount = ProviderAccount.objects.create( + name=f'{provider.name} {provider.account}' if provider.account else f'{provider.name}', + account=provider.account, + provider=provider, + ) + + +# +# Unmigrate ProviderAccount to Provider model +# +def revert_provideraccounts_from_providers(apps, schema_editor): + ProviderAccount = apps.get_model('circuits', 'ProviderAccount') + provideraccounts = ProviderAccount.objects.all().order_by('pk') + for provideraccount in provideraccounts: + if provideraccounts.filter(provider=provideraccount.provider)[0] == provideraccount: + provideraccount.provider.account = provideraccount.account + provideraccount.provider.save() + + +def migrate_circuits_to_provideraccount(apps, schema_editor): + Circuit = apps.get_model('circuits', 'Circuit') + circuits = Circuit.objects.all() + for circuit in circuits: + circuit.provider_account = circuit.provider.accounts.order_by('pk').first() + circuit.save() + + +def migrate_circuits_from_provideraccount(apps, schema_editor): + Circuit = apps.get_model('circuits', 'Circuit') + circuits = Circuit.objects.all().order_by('pk') + for circuit in circuits: + circuit.provider = circuit.provider_account.provider + circuit.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0084_staging'), + ('circuits', '0041_standardize_description_comments'), + ] + + operations = [ + migrations.CreateModel( + name='ProviderAccount', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('description', models.CharField(blank=True, max_length=200)), + ('comments', models.TextField(blank=True)), + ('account', models.CharField(max_length=30)), + ('name', models.CharField(blank=True, max_length=100)), + ('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'ordering': ('provider', 'account'), + }, + ), + migrations.AddConstraint( + model_name='provideraccount', + constraint=models.UniqueConstraint(condition=models.Q(('account', ''), _negated=True), fields=('provider', 'name'), name='circuits_provideraccount_unique_provider_name'), + ), + migrations.AddConstraint( + model_name='provideraccount', + constraint=models.UniqueConstraint(fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'), + ), + migrations.RunPython( + create_provideraccounts_from_providers, revert_provideraccounts_from_providers + ), + migrations.RemoveField( + model_name='provider', + name='account', + ), + migrations.AddField( + model_name='circuit', + name='provider_account', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount', null=True, blank=True), + preserve_default=False, + ), + migrations.AlterField( + model_name='circuit', + name='provider', + field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, related_name='circuits', to='circuits.provider', null=True, blank=True), + ), + migrations.RunPython( + migrate_circuits_to_provideraccount, migrate_circuits_from_provideraccount + ), + migrations.AlterField( + model_name='circuit', + name='provider_account', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount'), + ), + migrations.RemoveConstraint( + model_name='circuit', + name='circuits_circuit_unique_provider_cid', + ), + migrations.AlterModelOptions( + name='circuit', + options={'ordering': ['provider_account', 'cid']}, + ), + migrations.AddConstraint( + model_name='circuit', + constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_circuit_unique_provider_cid'), + ), + migrations.RemoveField( + model_name='circuit', + name='provider', + ), + ] diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index 1c9f9682e..56385b186 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -28,17 +28,17 @@ class CircuitType(OrganizationalModel): class Circuit(PrimaryModel): """ - A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple - circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured - in Kbps. + A communications circuit connects two points. Each Circuit belongs to a Provider Account; ProviderAccounts may have + multiple circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are + measured in Kbps. """ cid = models.CharField( max_length=100, verbose_name='Circuit ID', help_text=_("Unique circuit ID") ) - provider = models.ForeignKey( - to='circuits.Provider', + provider_account = models.ForeignKey( + to='circuits.ProviderAccount', on_delete=models.PROTECT, related_name='circuits' ) @@ -103,18 +103,18 @@ class Circuit(PrimaryModel): ) clone_fields = ( - 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description', + 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description', ) prerequisite_models = ( 'circuits.CircuitType', - 'circuits.Provider', + 'circuits.ProviderAccount', ) class Meta: - ordering = ['provider', 'cid'] + ordering = ['provider_account', 'cid'] constraints = ( models.UniqueConstraint( - fields=('provider', 'cid'), + fields=('provider_account', 'cid'), name='%(app_label)s_%(class)s_unique_provider_cid' ), ) diff --git a/netbox/circuits/models/providers.py b/netbox/circuits/models/providers.py index abd5cc7a1..a0bd395af 100644 --- a/netbox/circuits/models/providers.py +++ b/netbox/circuits/models/providers.py @@ -1,5 +1,6 @@ from django.contrib.contenttypes.fields import GenericRelation from django.db import models +from django.db.models import Q from django.urls import reverse from django.utils.translation import gettext as _ @@ -8,13 +9,14 @@ from netbox.models import PrimaryModel __all__ = ( 'ProviderNetwork', 'Provider', + 'ProviderAccount', ) class Provider(PrimaryModel): """ - Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model - stores information pertinent to the user's relationship with the Provider. + This is usually a telecommunications company or similar organization. This model stores information pertinent to + the user's relationship with the Provider. """ name = models.CharField( max_length=100, @@ -30,20 +32,13 @@ class Provider(PrimaryModel): related_name='providers', blank=True ) - account = models.CharField( - max_length=30, - blank=True, - verbose_name='Account number' - ) # Generic relations contacts = GenericRelation( to='tenancy.ContactAssignment' ) - clone_fields = ( - 'account', - ) + clone_fields = () class Meta: ordering = ['name'] @@ -55,6 +50,54 @@ class Provider(PrimaryModel): return reverse('circuits:provider', args=[self.pk]) +class ProviderAccount(PrimaryModel): + """ + This is a discrete account within a provider. Each Circuit belongs to a Provider Account. + """ + account = models.CharField( + max_length=30, + verbose_name='Account number' + ) + name = models.CharField( + max_length=100, + blank=True + ) + provider = models.ForeignKey( + to='circuits.Provider', + on_delete=models.PROTECT, + related_name='accounts' + ) + + # Generic relations + contacts = GenericRelation( + to='tenancy.ContactAssignment' + ) + + clone_fields = ('provider', ) + + class Meta: + ordering = ('provider', 'account') + constraints = ( + models.UniqueConstraint( + fields=('provider', 'account'), + name='%(app_label)s_%(class)s_unique_provider_account' + ), + models.UniqueConstraint( + fields=('provider', 'name'), + name='%(app_label)s_%(class)s_unique_provider_name', + condition=~Q(account="") + ), + ) + + def __str__(self): + if self.name: + return f'{self.account} ({self.name})' + return f'{self.account}' + + def get_absolute_url(self): + return reverse('circuits:provideraccount', args=[self.pk]) + + class ProviderNetwork(PrimaryModel): """ This represents a provider network which exists outside of NetBox, the details of which are unknown or diff --git a/netbox/circuits/search.py b/netbox/circuits/search.py index 2859295d5..b80f92d4d 100644 --- a/netbox/circuits/search.py +++ b/netbox/circuits/search.py @@ -39,12 +39,20 @@ class ProviderIndex(SearchIndex): model = models.Provider fields = ( ('name', 100), - ('account', 200), ('description', 500), ('comments', 5000), ) +class ProviderAccountIndex(SearchIndex): + model = models.ProviderAccount + fields = ( + ('name', 100), + ('account', 200), + ('comments', 5000), + ) + + @register_search class ProviderNetworkIndex(SearchIndex): model = models.ProviderNetwork diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index b3f62d5fc..94dc73b4a 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -1,4 +1,6 @@ import django_tables2 as tables +from django_tables2.utils import Accessor + from circuits.models import * from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin @@ -48,6 +50,10 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): verbose_name='Circuit ID' ) provider = tables.Column( + accessor=Accessor('provider_account__provider'), + linkify=True + ) + provider_account = tables.Column( linkify=True ) status = columns.ChoiceFieldColumn() @@ -68,7 +74,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = Circuit fields = ( - 'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'tenant_group', 'termination_a', 'termination_z', + 'pk', 'id', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'tenant_group', 'termination_a', 'termination_z', 'install_date', 'termination_date', 'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated', ) diff --git a/netbox/circuits/tables/providers.py b/netbox/circuits/tables/providers.py index 9de8d25b2..21c541e83 100644 --- a/netbox/circuits/tables/providers.py +++ b/netbox/circuits/tables/providers.py @@ -7,6 +7,7 @@ from netbox.tables import NetBoxTable, columns __all__ = ( 'ProviderTable', + 'ProviderAccountTable', 'ProviderNetworkTable', ) @@ -15,6 +16,16 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable): name = tables.Column( linkify=True ) + accounts = columns.ManyToManyColumn( + linkify_item=True, + verbose_name='Accounts' + ) + account_count = columns.LinkedCountColumn( + accessor=tables.A('accounts__count'), + viewname='circuits:provideraccount_list', + url_params={'account_id': 'pk'}, + verbose_name='Account Count' + ) asns = columns.ManyToManyColumn( linkify_item=True, verbose_name='ASNs' @@ -39,10 +50,31 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = Provider fields = ( - 'pk', 'id', 'name', 'asns', 'account', 'asn_count', 'circuit_count', 'description', 'comments', 'contacts', + 'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated', ) - default_columns = ('pk', 'name', 'account', 'circuit_count') + default_columns = ('pk', 'name', 'account_count', 'circuit_count') + + +class ProviderAccountTable(ContactsColumnMixin, NetBoxTable): + account = tables.Column( + linkify=True + ) + name = tables.Column() + provider = tables.Column( + linkify=True + ) + comments = columns.MarkdownColumn() + tags = columns.TagColumn( + url_name='circuits:provideraccount_list' + ) + + class Meta(NetBoxTable.Meta): + model = ProviderAccount + fields = ( + 'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated', + ) + default_columns = ('pk', 'account', 'name', 'provider', 'circuit_count') class ProviderNetworkTable(NetBoxTable): diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index c9d2cfc40..722055111 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -20,7 +20,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase): model = Provider brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url'] bulk_update_data = { - 'account': '1234', + 'comments': 'New comments', } @classmethod @@ -106,6 +106,12 @@ class CircuitTest(APIViewTestCases.APIViewTestCase): ) Provider.objects.bulk_create(providers) + provider_accounts = ( + ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'), + ProviderAccount(name='Provider Account 2', provider=providers[1], account='2345'), + ) + ProviderAccount.objects.bulk_create(provider_accounts) + circuit_types = ( CircuitType(name='Circuit Type 1', slug='circuit-type-1'), CircuitType(name='Circuit Type 2', slug='circuit-type-2'), @@ -113,26 +119,26 @@ class CircuitTest(APIViewTestCases.APIViewTestCase): CircuitType.objects.bulk_create(circuit_types) circuits = ( - Circuit(cid='Circuit 1', provider=providers[0], type=circuit_types[0]), - Circuit(cid='Circuit 2', provider=providers[0], type=circuit_types[0]), - Circuit(cid='Circuit 3', provider=providers[0], type=circuit_types[0]), + Circuit(cid='Circuit 1', provider_account=provider_accounts[0], type=circuit_types[0]), + Circuit(cid='Circuit 2', provider_account=provider_accounts[0], type=circuit_types[0]), + Circuit(cid='Circuit 3', provider_account=provider_accounts[0], type=circuit_types[0]), ) Circuit.objects.bulk_create(circuits) cls.create_data = [ { 'cid': 'Circuit 4', - 'provider': providers[1].pk, + 'provider_account': provider_accounts[1].pk, 'type': circuit_types[1].pk, }, { 'cid': 'Circuit 5', - 'provider': providers[1].pk, + 'provider_account': provider_accounts[1].pk, 'type': circuit_types[1].pk, }, { 'cid': 'Circuit 6', - 'provider': providers[1].pk, + 'provider_account': provider_accounts[1].pk, 'type': circuit_types[1].pk, }, ] @@ -149,6 +155,11 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase): provider = Provider.objects.create(name='Provider 1', slug='provider-1') circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') + provider_account = ProviderAccount.objects.create( + name='Provider Account 2', + provider=provider, + account='2345' + ) sites = ( Site(name='Site 1', slug='site-1'), @@ -163,9 +174,9 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase): ProviderNetwork.objects.bulk_create(provider_networks) circuits = ( - Circuit(cid='Circuit 1', provider=provider, type=circuit_type), - Circuit(cid='Circuit 2', provider=provider, type=circuit_type), - Circuit(cid='Circuit 3', provider=provider, type=circuit_type), + Circuit(cid='Circuit 1', provider_account=provider_account, type=circuit_type), + Circuit(cid='Circuit 2', provider_account=provider_account, type=circuit_type), + Circuit(cid='Circuit 3', provider_account=provider_account, type=circuit_type), ) Circuit.objects.bulk_create(circuits) @@ -197,6 +208,49 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase): } +class ProviderAccountTest(APIViewTestCases.APIViewTestCase): + model = ProviderAccount + brief_fields = ['account', 'display', 'id', 'name', 'url'] + + @classmethod + def setUpTestData(cls): + providers = ( + Provider(name='Provider 1', slug='provider-1'), + Provider(name='Provider 2', slug='provider-2'), + ) + Provider.objects.bulk_create(providers) + + provider_accounts = ( + ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'), + ProviderAccount(name='Provider Account 2', provider=providers[0], account='2345'), + ProviderAccount(name='Provider Account 3', provider=providers[0], account='3456'), + ) + ProviderAccount.objects.bulk_create(provider_accounts) + + cls.create_data = [ + { + 'name': 'Provider Account 4', + 'provider': providers[0].pk, + 'account': '4567', + }, + { + 'name': 'Provider Account 5', + 'provider': providers[0].pk, + 'account': '5678', + }, + { + 'name': 'Provider Account 6', + 'provider': providers[0].pk, + 'account': '6789', + }, + ] + + cls.bulk_update_data = { + 'provider': providers[1].pk, + 'description': 'New description', + } + + class ProviderNetworkTest(APIViewTestCases.APIViewTestCase): model = ProviderNetwork brief_fields = ['display', 'id', 'name', 'url'] diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py index 897c87c05..0cb898a98 100644 --- a/netbox/circuits/tests/test_filtersets.py +++ b/netbox/circuits/tests/test_filtersets.py @@ -25,17 +25,26 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests): ASN.objects.bulk_create(asns) providers = ( - Provider(name='Provider 1', slug='provider-1', account='1234'), - Provider(name='Provider 2', slug='provider-2', account='2345'), - Provider(name='Provider 3', slug='provider-3', account='3456'), - Provider(name='Provider 4', slug='provider-4', account='4567'), - Provider(name='Provider 5', slug='provider-5', account='5678'), + Provider(name='Provider 1', slug='provider-1'), + Provider(name='Provider 2', slug='provider-2'), + Provider(name='Provider 3', slug='provider-3'), + Provider(name='Provider 4', slug='provider-4'), + Provider(name='Provider 5', slug='provider-5'), ) Provider.objects.bulk_create(providers) providers[0].asns.set([asns[0]]) providers[1].asns.set([asns[1]]) providers[2].asns.set([asns[2]]) + provider_accounts = ( + ProviderAccount(name='Account A', account='AAAA', provider=providers[0]), + ProviderAccount(name='Account B', account='BBBB', provider=providers[1]), + ProviderAccount(name='Account C', account='CCCC', provider=providers[2]), + ProviderAccount(name='Account D', account='DDDD', provider=providers[3]), + ProviderAccount(name='Account E', account='EEEE', provider=providers[4]), + ) + ProviderAccount.objects.bulk_create(provider_accounts) + regions = ( Region(name='Test Region 1', slug='test-region-1'), Region(name='Test Region 2', slug='test-region-2'), @@ -64,8 +73,8 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests): CircuitType.objects.bulk_create(circuit_types) circuits = ( - Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1'), - Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 1'), + Circuit(provider_account=provider_accounts[0], type=circuit_types[0], cid='Test Circuit 1'), + Circuit(provider_account=provider_accounts[1], type=circuit_types[1], cid='Test Circuit 1'), ) Circuit.objects.bulk_create(circuits) @@ -87,10 +96,6 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'asn_id': [asns[0].pk, asns[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_account(self): - params = {'account': ['1234', '2345']} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_region(self): regions = Region.objects.all()[:2] params = {'region_id': [regions[0].pk, regions[1].pk]} @@ -196,6 +201,12 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests): ) Provider.objects.bulk_create(providers) + provider_accounts = ( + ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'), + ProviderAccount(name='Provider Account 2', provider=providers[1], account='2345'), + ) + ProviderAccount.objects.bulk_create(provider_accounts) + provider_networks = ( ProviderNetwork(name='Provider Network 1', provider=providers[1]), ProviderNetwork(name='Provider Network 2', provider=providers[1]), @@ -204,12 +215,12 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests): ProviderNetwork.objects.bulk_create(provider_networks) circuits = ( - Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'), - Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'), - Circuit(provider=providers[0], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED), - Circuit(provider=providers[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED), - Circuit(provider=providers[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE), - Circuit(provider=providers[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE), + Circuit(provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'), + Circuit(provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'), + Circuit(provider_account=provider_accounts[0], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED), + Circuit(provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED), + Circuit(provider_account=provider_accounts[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE), + Circuit(provider_account=provider_accounts[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE), ) Circuit.objects.bulk_create(circuits) @@ -246,6 +257,11 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'provider': [provider.slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + def test_provider_account(self): + provider_account = ProviderAccount.objects.first() + params = {'provider_account_id': [provider_account.pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + def test_provider_network(self): provider_networks = ProviderNetwork.objects.all()[:2] params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]} @@ -326,6 +342,11 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): ) Provider.objects.bulk_create(providers) + provider_accounts = ( + ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'), + ) + ProviderAccount.objects.bulk_create(provider_accounts) + provider_networks = ( ProviderNetwork(name='Provider Network 1', provider=providers[0]), ProviderNetwork(name='Provider Network 2', provider=providers[0]), @@ -334,13 +355,13 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): ProviderNetwork.objects.bulk_create(provider_networks) circuits = ( - Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'), - Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 2'), - Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 3'), - Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 4'), - Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 5'), - Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 6'), - Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 7'), + Circuit(provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 1'), + Circuit(provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 2'), + Circuit(provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 3'), + Circuit(provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 4'), + Circuit(provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 5'), + Circuit(provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 6'), + Circuit(provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 7'), ) Circuit.objects.bulk_create(circuits) @@ -445,3 +466,44 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) params = {'provider': [providers[0].slug, providers[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests): + queryset = ProviderAccount.objects.all() + filterset = ProviderAccountFilterSet + + @classmethod + def setUpTestData(cls): + + providers = ( + Provider(name='Provider 1', slug='provider-1'), + Provider(name='Provider 2', slug='provider-2'), + Provider(name='Provider 3', slug='provider-3'), + ) + Provider.objects.bulk_create(providers) + + provider_accounts = ( + ProviderAccount(name='Provider Account 1', provider=providers[0], description='foobar1', account='1234'), + ProviderAccount(name='Provider Account 2', provider=providers[1], description='foobar2', account='2345'), + ProviderAccount(name='Provider Account 3', provider=providers[2], account='3456'), + ) + ProviderAccount.objects.bulk_create(provider_accounts) + + def test_name(self): + params = {'name': ['Provider Account 1', 'Provider Account 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_account(self): + params = {'account': ['1234', '3456']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_description(self): + params = {'description': ['foobar1', 'foobar2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_provider(self): + providers = Provider.objects.all()[:2] + params = {'provider_id': [providers[0].pk, providers[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'provider': [providers[0].slug, providers[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index 231d6a43c..12af6f311 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -38,7 +38,6 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'name': 'Provider X', 'slug': 'provider-x', 'asns': [asns[6].pk, asns[7].pk], - 'account': '1234', 'comments': 'Another provider', 'tags': [t.pk for t in tags], } @@ -58,7 +57,6 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) cls.bulk_edit_data = { - 'account': '5678', 'comments': 'New comments', } @@ -124,6 +122,12 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) Provider.objects.bulk_create(providers) + provider_accounts = ( + ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'), + ProviderAccount(name='Provider Account 2', provider=providers[1], account='2345'), + ) + ProviderAccount.objects.bulk_create(provider_accounts) + circuittypes = ( CircuitType(name='Circuit Type 1', slug='circuit-type-1'), CircuitType(name='Circuit Type 2', slug='circuit-type-2'), @@ -131,9 +135,9 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): CircuitType.objects.bulk_create(circuittypes) circuits = ( - Circuit(cid='Circuit 1', provider=providers[0], type=circuittypes[0]), - Circuit(cid='Circuit 2', provider=providers[0], type=circuittypes[0]), - Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]), + Circuit(cid='Circuit 1', provider_account=provider_accounts[0], type=circuittypes[0]), + Circuit(cid='Circuit 2', provider_account=provider_accounts[0], type=circuittypes[0]), + Circuit(cid='Circuit 3', provider_account=provider_accounts[0], type=circuittypes[0]), ) Circuit.objects.bulk_create(circuits) @@ -142,7 +146,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): cls.form_data = { 'cid': 'Circuit X', - 'provider': providers[1].pk, + 'provider_account': provider_accounts[1].pk, 'type': circuittypes[1].pk, 'status': CircuitStatusChoices.STATUS_DECOMMISSIONED, 'tenant': None, @@ -155,10 +159,10 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "cid,provider,type,status", - "Circuit 4,Provider 1,Circuit Type 1,active", - "Circuit 5,Provider 1,Circuit Type 1,active", - "Circuit 6,Provider 1,Circuit Type 1,active", + "cid,provider_account,type,status", + "Circuit 4,Provider Account 1,Circuit Type 1,active", + "Circuit 5,Provider Account 1,Circuit Type 1,active", + "Circuit 6,Provider Account 1,Circuit Type 1,active", ) cls.csv_update_data = ( @@ -169,7 +173,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) cls.bulk_edit_data = { - 'provider': providers[1].pk, + 'provider_account': provider_accounts[1].pk, 'type': circuittypes[1].pk, 'status': CircuitStatusChoices.STATUS_DECOMMISSIONED, 'tenant': None, @@ -179,6 +183,58 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): } +class ProviderAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = ProviderAccount + + @classmethod + def setUpTestData(cls): + + providers = ( + Provider(name='Provider 1', slug='provider-1'), + Provider(name='Provider 2', slug='provider-2'), + ) + Provider.objects.bulk_create(providers) + + provider_accounts = ( + ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'), + ProviderAccount(name='Provider Account 2', provider=providers[0], account='2345'), + ProviderAccount(name='Provider Account 3', provider=providers[0], account='3456'), + ) + + ProviderAccount.objects.bulk_create(provider_accounts) + + tags = create_tags('Alpha', 'Bravo', 'Charlie') + + cls.form_data = { + 'name': 'Provider Account X', + 'provider': providers[1].pk, + 'account': 'XXXX', + 'description': 'A new provider network', + 'comments': 'Longer description goes here', + 'tags': [t.pk for t in tags], + } + + cls.csv_data = ( + "name,provider,account,description", + "Provider Account 4,Provider 1,4567,Foo", + "Provider Account 5,Provider 1,5678,Bar", + "Provider Account 6,Provider 1,6789,Baz", + ) + + cls.csv_update_data = ( + "id,name,account,description", + f"{provider_accounts[0].pk},Provider Network 7,7890,New description7", + f"{provider_accounts[1].pk},Provider Network 8,8901,New description8", + f"{provider_accounts[2].pk},Provider Network 9,9012,New description9", + ) + + cls.bulk_edit_data = { + 'provider': providers[1].pk, + 'description': 'New description', + 'comments': 'New comments', + } + + class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = ProviderNetwork @@ -247,11 +303,12 @@ class CircuitTerminationTestCase( Site.objects.bulk_create(sites) provider = Provider.objects.create(name='Provider 1', slug='provider-1') circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') + account = ProviderAccount.objects.create(name='Provider Account 1', provider=provider, account='1234') circuits = ( - Circuit(cid='Circuit 1', provider=provider, type=circuittype), - Circuit(cid='Circuit 2', provider=provider, type=circuittype), - Circuit(cid='Circuit 3', provider=provider, type=circuittype), + Circuit(cid='Circuit 1', provider_account=account, type=circuittype), + Circuit(cid='Circuit 2', provider_account=account, type=circuittype), + Circuit(cid='Circuit 3', provider_account=account, type=circuittype), ) Circuit.objects.bulk_create(circuits) diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index d8c5ea276..5b5a3de8a 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -14,6 +14,14 @@ urlpatterns = [ path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), path('providers//', include(get_model_urls('circuits', 'provider'))), + # Provider networks + path('provider-accounts/', views.ProviderAccountListView.as_view(), name='provideraccount_list'), + path('provider-accounts/add/', views.ProviderAccountEditView.as_view(), name='provideraccount_add'), + path('provider-accounts/import/', views.ProviderAccountBulkImportView.as_view(), name='provideraccount_import'), + path('provider-accounts/edit/', views.ProviderAccountBulkEditView.as_view(), name='provideraccount_bulk_edit'), + path('provider-accounts/delete/', views.ProviderAccountBulkDeleteView.as_view(), name='provideraccount_bulk_delete'), + path('provider-accounts//', include(get_model_urls('circuits', 'provideraccount'))), + # Provider networks path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'), path('provider-networks/add/', views.ProviderNetworkEditView.as_view(), name='providernetwork_add'), diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 25620a852..6fe79d716 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -18,7 +18,7 @@ from .models import * class ProviderListView(generic.ObjectListView): queryset = Provider.objects.annotate( - count_circuits=count_related(Circuit, 'provider') + count_circuits=count_related(Circuit, 'provider_account__provider') ) filterset = filtersets.ProviderFilterSet filterset_form = forms.ProviderFilterForm @@ -31,7 +31,8 @@ class ProviderView(generic.ObjectView): def get_extra_context(self, request, instance): related_models = ( - (Circuit.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'), + (ProviderAccount.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'), + (Circuit.objects.restrict(request.user, 'view').filter(provider_account__provider=instance), 'provider_id'), ) return { @@ -57,7 +58,7 @@ class ProviderBulkImportView(generic.BulkImportView): class ProviderBulkEditView(generic.BulkEditView): queryset = Provider.objects.annotate( - count_circuits=count_related(Circuit, 'provider') + count_circuits=count_related(Circuit, 'provider_account__provider') ) filterset = filtersets.ProviderFilterSet table = tables.ProviderTable @@ -66,12 +67,73 @@ class ProviderBulkEditView(generic.BulkEditView): class ProviderBulkDeleteView(generic.BulkDeleteView): queryset = Provider.objects.annotate( - count_circuits=count_related(Circuit, 'provider') + count_circuits=count_related(Circuit, 'provider_account__provider') ) filterset = filtersets.ProviderFilterSet table = tables.ProviderTable +# +# ProviderAccounts +# + +class ProviderAccountListView(generic.ObjectListView): + queryset = ProviderAccount.objects.annotate( + count_circuits=count_related(Circuit, 'provider_account') + ) + filterset = filtersets.ProviderAccountFilterSet + filterset_form = forms.ProviderAccountFilterForm + table = tables.ProviderAccountTable + + +@register_model_view(ProviderAccount) +class ProviderAccountView(generic.ObjectView): + queryset = ProviderAccount.objects.all() + + def get_extra_context(self, request, instance): + related_models = ( + (Circuit.objects.restrict(request.user, 'view').filter(provider_account=instance), 'provider_account_id'), + ) + + return { + 'related_models': related_models, + } + + +@register_model_view(ProviderAccount, 'edit') +class ProviderAccountEditView(generic.ObjectEditView): + queryset = ProviderAccount.objects.all() + form = forms.ProviderAccountForm + + +@register_model_view(ProviderAccount, 'delete') +class ProviderAccountDeleteView(generic.ObjectDeleteView): + queryset = ProviderAccount.objects.all() + + +class ProviderAccountBulkImportView(generic.BulkImportView): + queryset = ProviderAccount.objects.all() + model_form = forms.ProviderAccountImportForm + table = tables.ProviderAccountTable + + +class ProviderAccountBulkEditView(generic.BulkEditView): + queryset = ProviderAccount.objects.annotate( + count_circuits=count_related(Circuit, 'provider_account') + ) + filterset = filtersets.ProviderAccountFilterSet + table = tables.ProviderAccountTable + form = forms.ProviderAccountBulkEditForm + + +class ProviderAccountBulkDeleteView(generic.BulkDeleteView): + queryset = ProviderAccount.objects.annotate( + count_circuits=count_related(Circuit, 'provider_account') + ) + filterset = filtersets.ProviderAccountFilterSet + table = tables.ProviderAccountTable + + # # Provider networks # diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 3367a3efe..c7b435a81 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -30,8 +30,9 @@ class CablePathTestCase(TestCase): cls.powerpanel = PowerPanel.objects.create(site=cls.site, name='Power Panel') provider = Provider.objects.create(name='Provider', slug='provider') + provider_account = ProviderAccount.objects.create(name='Account', account='AAAA1111', provider=provider) circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type') - cls.circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 1') + cls.circuit = Circuit.objects.create(provider_account=provider_account, type=circuit_type, cid='Circuit 1') def assertPathExists(self, nodes, **kwargs): """ @@ -1308,7 +1309,7 @@ class CablePathTestCase(TestCase): [IF1] --C1-- [CT1] [CT2] --> [PN1] """ interface1 = Interface.objects.create(device=self.device, name='Interface 1') - providernetwork = ProviderNetwork.objects.create(name='Provider Network 1', provider=self.circuit.provider) + providernetwork = ProviderNetwork.objects.create(name='Provider Network 1', provider=self.circuit.provider_account.provider) circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A') circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, provider_network=providernetwork, term_side='Z') @@ -1436,7 +1437,7 @@ class CablePathTestCase(TestCase): """ interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface2 = Interface.objects.create(device=self.device, name='Interface 2') - circuit2 = Circuit.objects.create(provider=self.circuit.provider, type=self.circuit.type, cid='Circuit 2') + circuit2 = Circuit.objects.create(provider_account=self.circuit.provider_account, type=self.circuit.type, cid='Circuit 2') circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A') circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z') circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, site=self.site, term_side='A') diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index e9a577648..d911b1404 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -504,10 +504,11 @@ class CableTestCase(TestCase): device=patch_pannel, name='FP4', type='8p8c', rear_port=rear_port4, rear_port_position=1 ) provider = Provider.objects.create(name='Provider 1', slug='provider-1') + provider_account = ProviderAccount.objects.create(name='Provider Account 1', account='A1', provider=provider) provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider) circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') - circuit1 = Circuit.objects.create(provider=provider, type=circuittype, cid='1') - circuit2 = Circuit.objects.create(provider=provider, type=circuittype, cid='2') + circuit1 = Circuit.objects.create(provider_account=provider_account, type=circuittype, cid='1') + circuit2 = Circuit.objects.create(provider_account=provider_account, type=circuittype, cid='2') circuittermination1 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A') circuittermination2 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z') circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A') diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index f5d15b3d8..54be4c637 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -245,6 +245,7 @@ CIRCUITS_MENU = Menu( label=_('Providers'), items=( get_model_item('circuits', 'provider', _('Providers')), + get_model_item('circuits', 'provideraccount', _('Provider Accounts')), get_model_item('circuits', 'providernetwork', _('Provider Networks')), ), ), diff --git a/netbox/netbox/tests/test_staging.py b/netbox/netbox/tests/test_staging.py index ed3a69f10..f6b6d62ae 100644 --- a/netbox/netbox/tests/test_staging.py +++ b/netbox/netbox/tests/test_staging.py @@ -1,6 +1,6 @@ from django.test import TransactionTestCase -from circuits.models import Provider, Circuit, CircuitType +from circuits.models import Provider, Circuit, CircuitType, ProviderAccount from extras.choices import ChangeActionChoices from extras.models import Branch, StagedChange, Tag from ipam.models import ASN, RIR @@ -28,18 +28,25 @@ class StagingTestCase(TransactionTestCase): ) Provider.objects.bulk_create(providers) + provider_accounts = ( + ProviderAccount(name='Account A', provider=providers[0], account='AAAA'), + ProviderAccount(name='Account B', provider=providers[1], account='BBBB'), + ProviderAccount(name='Account C', provider=providers[2], account='CCCC'), + ) + ProviderAccount.objects.bulk_create(provider_accounts) + circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') Circuit.objects.bulk_create(( - Circuit(provider=providers[0], cid='Circuit A1', type=circuit_type), - Circuit(provider=providers[0], cid='Circuit A2', type=circuit_type), - Circuit(provider=providers[0], cid='Circuit A3', type=circuit_type), - Circuit(provider=providers[1], cid='Circuit B1', type=circuit_type), - Circuit(provider=providers[1], cid='Circuit B2', type=circuit_type), - Circuit(provider=providers[1], cid='Circuit B3', type=circuit_type), - Circuit(provider=providers[2], cid='Circuit C1', type=circuit_type), - Circuit(provider=providers[2], cid='Circuit C2', type=circuit_type), - Circuit(provider=providers[2], cid='Circuit C3', type=circuit_type), + Circuit(provider_account=provider_accounts[0], cid='Circuit A1', type=circuit_type), + Circuit(provider_account=provider_accounts[0], cid='Circuit A2', type=circuit_type), + Circuit(provider_account=provider_accounts[0], cid='Circuit A3', type=circuit_type), + Circuit(provider_account=provider_accounts[1], cid='Circuit B1', type=circuit_type), + Circuit(provider_account=provider_accounts[1], cid='Circuit B2', type=circuit_type), + Circuit(provider_account=provider_accounts[1], cid='Circuit B3', type=circuit_type), + Circuit(provider_account=provider_accounts[2], cid='Circuit C1', type=circuit_type), + Circuit(provider_account=provider_accounts[2], cid='Circuit C2', type=circuit_type), + Circuit(provider_account=provider_accounts[2], cid='Circuit C3', type=circuit_type), )) def test_object_creation(self): @@ -50,7 +57,8 @@ class StagingTestCase(TransactionTestCase): with checkout(branch): provider = Provider.objects.create(name='Provider D', slug='provider-d') provider.asns.set(asns) - circuit = Circuit.objects.create(provider=provider, cid='Circuit D1', type=CircuitType.objects.first()) + provider_account = ProviderAccount.objects.create(name='Account D', provider=provider, account='DDDD') + circuit = Circuit.objects.create(provider_account=provider_account, cid='Circuit D1', type=CircuitType.objects.first()) circuit.tags.set(tags) # Sanity-checking @@ -62,7 +70,7 @@ class StagingTestCase(TransactionTestCase): # Verify that changes have been rolled back after exiting the context self.assertEqual(Provider.objects.count(), 3) self.assertEqual(Circuit.objects.count(), 9) - self.assertEqual(StagedChange.objects.count(), 5) + self.assertEqual(StagedChange.objects.count(), 6) # Verify that changes are replayed upon entering the context with checkout(branch): @@ -145,26 +153,31 @@ class StagingTestCase(TransactionTestCase): with checkout(branch): provider = Provider.objects.get(name='Provider A') - provider.circuits.all().delete() + Circuit.objects.filter(provider_account__provider=provider).delete() + provider.accounts.all().delete() provider.delete() # Sanity-checking self.assertEqual(Provider.objects.count(), 2) + self.assertEqual(ProviderAccount.objects.count(), 2) self.assertEqual(Circuit.objects.count(), 6) # Verify that changes have been rolled back after exiting the context self.assertEqual(Provider.objects.count(), 3) + self.assertEqual(ProviderAccount.objects.count(), 3) self.assertEqual(Circuit.objects.count(), 9) - self.assertEqual(StagedChange.objects.count(), 4) + self.assertEqual(StagedChange.objects.count(), 5) # Verify that changes are replayed upon entering the context with checkout(branch): self.assertEqual(Provider.objects.count(), 2) + self.assertEqual(ProviderAccount.objects.count(), 2) self.assertEqual(Circuit.objects.count(), 6) # Verify that changes are applied and deleted upon branch merge branch.merge() self.assertEqual(Provider.objects.count(), 2) + self.assertEqual(ProviderAccount.objects.count(), 2) self.assertEqual(Circuit.objects.count(), 6) self.assertEqual(StagedChange.objects.count(), 0) diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 81ff6e912..b7b3f81c5 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -16,7 +16,11 @@ - + + + + + diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html index acfb8d0ca..e50f21786 100644 --- a/netbox/templates/circuits/circuittermination_edit.html +++ b/netbox/templates/circuits/circuittermination_edit.html @@ -8,6 +8,7 @@
Circuit Termination
{% render_field form.provider %} + {% render_field form.provider_account %} {% render_field form.circuit %} {% render_field form.term_side %} {% render_field form.tags %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 3973d2867..2c076543c 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -52,6 +52,14 @@
+
+
+
Provider Accounts
+
+
Circuits
diff --git a/netbox/templates/circuits/provideraccount.html b/netbox/templates/circuits/provideraccount.html new file mode 100644 index 000000000..7fe02eaed --- /dev/null +++ b/netbox/templates/circuits/provideraccount.html @@ -0,0 +1,58 @@ +{% extends 'generic/object.html' %} +{% load static %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} + +{% block breadcrumbs %} + {{ block.super }} + +{% endblock %} + +{% block content %} +
+
+
+
+ Provider Account +
+
+
Provider{{ object.provider|linkify }}{{ object.provider_account.provider|linkify }}
Provider Account{{ object.provider_account|linkify }}
Circuit ID
+ + + + + + + + + + + + +
Provider{{ object.provider|linkify }}
Name{{ object.name }}
Account{{ object.account|placeholder }}
+ + + {% include 'inc/panels/tags.html' %} + {% plugin_left_page object %} + +
+ {% include 'inc/panels/related_objects.html' %} + {% include 'inc/panels/comments.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/contacts.html' %} + {% plugin_right_page object %} +
+ +
+
+
Circuits
+
+
+ {% plugin_full_width_page object %} +
+ +{% endblock %}