diff --git a/docs/features/circuits.md b/docs/features/circuits.md index c8eda141a..5afbcafb5 100644 --- a/docs/features/circuits.md +++ b/docs/features/circuits.md @@ -5,8 +5,8 @@ NetBox is ideal for managing your network's transit and peering providers and ci ```mermaid flowchart TD ASN --> Provider - Provider --> ProviderAccount --> Circuit - Provider --> ProviderNetwork & Circuit + Provider --> ProviderNetwork & ProviderAccount & Circuit + ProviderAccount --> Circuit CircuitType --> Circuit click ASN "../../models/circuits/asn/" @@ -27,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 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 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. Provider accounts can also be employed to further categorize circuits belonging to a common provider: These may represent different business units or technologies. 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/getting-started/planning.md b/docs/getting-started/planning.md index 64cc04ec9..9641cd98b 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, provider accounts 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 83c3264f1..19fd8c882 100644 --- a/docs/models/circuits/circuit.md +++ b/docs/models/circuits/circuit.md @@ -4,9 +4,13 @@ A circuit represents a physical point-to-point data connection, typically used t ## Fields +### Provider + +The [provider](./provider.md) to which this circuit belongs. + ### Provider Account -The [provider account](./provideraccount.md) to which this circuit belongs. +Circuits may optionally be assigned to a specific [provider account](./provideraccount.md). ### Circuit ID diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index 69d531a3d..fcb7a1a51 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -7,15 +7,13 @@ router.APIRootView = views.CircuitsRootView # Providers router.register('providers', views.ProviderViewSet) +router.register('provider-accounts', views.ProviderAccountViewSet) +router.register('provider-networks', views.ProviderNetworkViewSet) # Circuits router.register('circuit-types', views.CircuitTypeViewSet) 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' urlpatterns = router.urls diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 1c9d2198d..bd9431887 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_account__provider') + circuit_count=count_related(Circuit, '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_account', 'provider_account__provider', 'termination_a', 'termination_z' + 'type', 'tenant', 'provider', 'provider_account', 'termination_a', 'termination_z' ).prefetch_related('tags') serializer_class = serializers.CircuitSerializer filterset_class = filtersets.CircuitFilterSet @@ -66,11 +66,11 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet): # -# Provider networks +# Provider accounts # class ProviderAccountViewSet(NetBoxModelViewSet): - queryset = ProviderAccount.objects.prefetch_related('tags') + queryset = ProviderAccount.objects.prefetch_related('provider', 'tags') serializer_class = serializers.ProviderAccountSerializer filterset_class = filtersets.ProviderAccountFilterSet diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index eea1438f3..e28238fea 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -24,37 +24,37 @@ __all__ = ( class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='accounts__circuits__terminations__site__region', + field_name='circuits__terminations__site__region', lookup_expr='in', label=_('Region (ID)'), ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='accounts__circuits__terminations__site__region', + field_name='circuits__terminations__site__region', lookup_expr='in', to_field_name='slug', label=_('Region (slug)'), ) site_group_id = TreeNodeMultipleChoiceFilter( queryset=SiteGroup.objects.all(), - field_name='accounts__circuits__terminations__site__group', + field_name='circuits__terminations__site__group', lookup_expr='in', label=_('Site group (ID)'), ) site_group = TreeNodeMultipleChoiceFilter( queryset=SiteGroup.objects.all(), - field_name='accounts__circuits__terminations__site__group', + field_name='circuits__terminations__site__group', lookup_expr='in', to_field_name='slug', label=_('Site group (slug)'), ) site_id = django_filters.ModelMultipleChoiceFilter( - field_name='accounts__circuits__terminations__site', + field_name='circuits__terminations__site', queryset=Site.objects.all(), label=_('Site'), ) site = django_filters.ModelMultipleChoiceFilter( - field_name='accounts__circuits__terminations__site__slug', + field_name='circuits__terminations__site__slug', queryset=Site.objects.all(), to_field_name='slug', label=_('Site (slug)'), @@ -67,7 +67,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): class Meta: model = Provider - fields = ['id', 'name', 'slug', ] + fields = ['id', 'name', 'slug'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index 03e042e89..39cdd85d0 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -35,7 +35,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): model = Provider fieldsets = ( - (None, ('asns', )), + (None, ('asns', 'description')), ) nullable_fields = ( 'asns', 'description', 'comments', @@ -60,8 +60,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm): (None, ('provider', 'description')), ) nullable_fields = ( - 'description', - 'comments', + 'description', 'comments', ) diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index 60c9be2e6..690cea828 100644 --- a/netbox/circuits/forms/bulk_import.py +++ b/netbox/circuits/forms/bulk_import.py @@ -38,7 +38,7 @@ class ProviderAccountImportForm(NetBoxModelImportForm): class Meta: model = ProviderAccount fields = ( - 'provider', 'name', 'account', 'comments', 'tags', + 'provider', 'name', 'account', 'description', 'comments', 'tags', ) diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 7f98ee4b1..01ebb79bb 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -69,7 +69,6 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm): label=_('Provider') ) account = forms.CharField( - max_length=100, required=False ) tag = TagFilterField(model) diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 827a95be5..8aeaa9619 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -41,8 +41,7 @@ class ProviderForm(NetBoxModelForm): class ProviderAccountForm(NetBoxModelForm): provider = DynamicModelChoiceField( - queryset=Provider.objects.all(), - selector=True + queryset=Provider.objects.all() ) comments = CommentField() @@ -93,14 +92,10 @@ class CircuitForm(TenancyForm, NetBoxModelForm): ) provider_account = DynamicModelChoiceField( queryset=ProviderAccount.objects.all(), - initial_params={ - 'circuits': '$circuit' - }, + required=False, query_params={ 'provider_id': '$provider', - }, - selector=True, - required=False + } ) type = DynamicModelChoiceField( queryset=CircuitType.objects.all() @@ -129,9 +124,6 @@ class CircuitForm(TenancyForm, NetBoxModelForm): class CircuitTerminationForm(NetBoxModelForm): circuit = DynamicModelChoiceField( queryset=Circuit.objects.all(), - query_params={ - 'provider_id': '$provider', - }, selector=True ) site = DynamicModelChoiceField( diff --git a/netbox/circuits/migrations/0042_provideraccount.py b/netbox/circuits/migrations/0042_provideraccount.py index fa67474ec..3e583844e 100644 --- a/netbox/circuits/migrations/0042_provideraccount.py +++ b/netbox/circuits/migrations/0042_provideraccount.py @@ -1,92 +1,91 @@ -# 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: - provideraccount = ProviderAccount.objects.create( - name=f'{provider.name} {provider.account}', - 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() - - -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.AlterModelOptions( - name='circuit', - options={'ordering': ['provider', 'provider_account', 'cid']}, - ), - migrations.AddConstraint( - model_name='circuit', - constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'), - ), - ] +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers +import utilities.json + + +def create_provideraccounts_from_providers(apps, schema_editor): + """ + Migrate Account in Provider model to separate account model + """ + Provider = apps.get_model('circuits', 'Provider') + ProviderAccount = apps.get_model('circuits', 'ProviderAccount') + + provider_accounts = [] + for provider in Provider.objects.all(): + if provider.account: + provider_accounts.append(ProviderAccount( + provider=provider, + account=provider.account + )) + ProviderAccount.objects.bulk_create(provider_accounts, batch_size=100) + + +def restore_providers_from_provideraccounts(apps, schema_editor): + """ + Restore Provider account values from auto-generated ProviderAccounts + """ + ProviderAccount = apps.get_model('circuits', 'ProviderAccount') + provider_accounts = ProviderAccount.objects.order_by('pk') + for provideraccount in provider_accounts: + if provider_accounts.filter(provider=provideraccount.provider)[0] == provideraccount: + provideraccount.provider.account = provideraccount.account + provideraccount.provider.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=100)), + ('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(('name', ''), _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, restore_providers_from_provideraccounts + ), + 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.AlterModelOptions( + name='circuit', + options={'ordering': ['provider', 'provider_account', 'cid']}, + ), + migrations.AddConstraint( + model_name='circuit', + constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'), + ), + ] diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index 5e5a16ab9..f629c0b30 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -28,9 +28,9 @@ class CircuitType(OrganizationalModel): class Circuit(PrimaryModel): """ - 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. + 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, and may optionally be assigned to a particular + ProviderAccount. Circuit port speed and commit rate are measured in Kbps. """ cid = models.CharField( max_length=100, @@ -116,7 +116,6 @@ class Circuit(PrimaryModel): prerequisite_models = ( 'circuits.CircuitType', 'circuits.Provider', - 'circuits.ProviderAccount', ) class Meta: @@ -143,8 +142,9 @@ class Circuit(PrimaryModel): def clean(self): super().clean() + if self.provider_account and self.provider != self.provider_account.provider: - raise ValidationError("Provider must match ProviderAccount's provider") + raise ValidationError({'provider_account': "The assigned account must belong to the assigned provider."}) class CircuitTermination( diff --git a/netbox/circuits/models/providers.py b/netbox/circuits/models/providers.py index a0bd395af..52eb26c98 100644 --- a/netbox/circuits/models/providers.py +++ b/netbox/circuits/models/providers.py @@ -15,8 +15,8 @@ __all__ = ( class Provider(PrimaryModel): """ - This is usually a telecommunications company or similar organization. This model stores information pertinent to - the user's relationship with the Provider. + 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. """ name = models.CharField( max_length=100, @@ -54,19 +54,19 @@ 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' ) + account = models.CharField( + max_length=100, + verbose_name='Account ID' + ) + name = models.CharField( + max_length=100, + blank=True + ) # Generic relations contacts = GenericRelation( @@ -85,7 +85,7 @@ class ProviderAccount(PrimaryModel): models.UniqueConstraint( fields=('provider', 'name'), name='%(app_label)s_%(class)s_unique_provider_name', - condition=~Q(account="") + condition=~Q(name="") ), ) diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index 85ee6b8a9..e8bdf6a92 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -1,5 +1,4 @@ import django_tables2 as tables -from django_tables2.utils import Accessor from circuits.models import * from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin @@ -53,7 +52,8 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): linkify=True ) provider_account = tables.Column( - linkify=True + linkify=True, + verbose_name='Account' ) status = columns.ChoiceFieldColumn() termination_a = tables.TemplateColumn( diff --git a/netbox/circuits/tables/providers.py b/netbox/circuits/tables/providers.py index 21c541e83..ef8a6cd38 100644 --- a/netbox/circuits/tables/providers.py +++ b/netbox/circuits/tables/providers.py @@ -50,8 +50,8 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = Provider fields = ( - 'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description', 'comments', 'contacts', - 'tags', 'created', 'last_updated', + 'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description', + 'comments', 'contacts', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'account_count', 'circuit_count') @@ -64,6 +64,12 @@ class ProviderAccountTable(ContactsColumnMixin, NetBoxTable): provider = tables.Column( linkify=True ) + circuit_count = columns.LinkedCountColumn( + accessor=Accessor('count_circuits'), + viewname='circuits:circuit_list', + url_params={'provider_account_id': 'pk'}, + verbose_name='Circuits' + ) comments = columns.MarkdownColumn() tags = columns.TagColumn( url_name='circuits:provideraccount_list' @@ -72,7 +78,8 @@ class ProviderAccountTable(ContactsColumnMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = ProviderAccount fields = ( - 'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated', + 'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created', + 'last_updated', ) default_columns = ('pk', 'account', 'name', 'provider', 'circuit_count') diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index 75907c292..1969441eb 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -158,11 +158,6 @@ 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'), @@ -177,9 +172,9 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase): ProviderNetwork.objects.bulk_create(provider_networks) circuits = ( - Circuit(cid='Circuit 1', provider=provider, provider_account=provider_account, type=circuit_type), - Circuit(cid='Circuit 2', provider=provider, provider_account=provider_account, type=circuit_type), - Circuit(cid='Circuit 3', provider=provider, provider_account=provider_account, type=circuit_type), + 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.objects.bulk_create(circuits) diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py index 88a37472c..bfa4bee5b 100644 --- a/netbox/circuits/tests/test_filtersets.py +++ b/netbox/circuits/tests/test_filtersets.py @@ -36,15 +36,6 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests): 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'), @@ -73,8 +64,8 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests): CircuitType.objects.bulk_create(circuit_types) circuits = ( - Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Test Circuit 1'), - Circuit(provider=providers[1], provider_account=provider_accounts[1], type=circuit_types[1], cid='Test Circuit 1'), + Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'), + Circuit(provider=providers[1], type=circuit_types[1], cid='Circuit 2'), ) Circuit.objects.bulk_create(circuits) @@ -202,8 +193,9 @@ 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(name='Provider Account 1', provider=providers[0], account='A'), + ProviderAccount(name='Provider Account 2', provider=providers[1], account='B'), + ProviderAccount(name='Provider Account 3', provider=providers[2], account='C'), ) ProviderAccount.objects.bulk_create(provider_accounts) @@ -217,10 +209,10 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests): circuits = ( Circuit(provider=providers[0], 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=providers[0], 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=providers[0], 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=providers[0], provider_account=provider_accounts[1], 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], 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=providers[1], 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=providers[1], 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(provider=providers[1], provider_account=provider_accounts[2], 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], provider_account=provider_accounts[2], 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) @@ -258,9 +250,9 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests): 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) + provider_accounts = ProviderAccount.objects.all()[:2] + params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_provider_network(self): provider_networks = ProviderNetwork.objects.all()[:2] @@ -342,11 +334,6 @@ 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]), @@ -355,13 +342,13 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): ProviderNetwork.objects.bulk_create(provider_networks) circuits = ( - Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 1'), - Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 2'), - Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 3'), - Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 4'), - Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 5'), - Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 6'), - Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 7'), + Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 1'), + Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 2'), + Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 3'), + Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 4'), + Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 5'), + Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 6'), + Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 7'), ) Circuit.objects.bulk_create(circuits) diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index deb2a7b6b..85e2304cf 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -202,7 +202,6 @@ class ProviderAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase): 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') @@ -305,12 +304,11 @@ 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, provider_account=account, type=circuittype), - Circuit(cid='Circuit 2', provider=provider, provider_account=account, type=circuittype), - Circuit(cid='Circuit 3', provider=provider, provider_account=account, type=circuittype), + 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.objects.bulk_create(circuits) diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index 5b5a3de8a..55a192c64 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -14,7 +14,7 @@ urlpatterns = [ path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), path('providers//', include(get_model_urls('circuits', 'provider'))), - # Provider networks + # Provider accounts 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'), diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index ecd219d15..3367a3efe 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -30,9 +30,8 @@ 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, provider_account=provider_account, type=circuit_type, cid='Circuit 1') + cls.circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 1') def assertPathExists(self, nodes, **kwargs): """ @@ -1309,7 +1308,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_account.provider) + providernetwork = ProviderNetwork.objects.create(name='Provider Network 1', provider=self.circuit.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') @@ -1437,7 +1436,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, provider_account=self.circuit.provider_account, type=self.circuit.type, cid='Circuit 2') + circuit2 = Circuit.objects.create(provider=self.circuit.provider, 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 7718ce285..e9a577648 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -504,11 +504,10 @@ 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, provider_account=provider_account, type=circuittype, cid='1') - circuit2 = Circuit.objects.create(provider=provider, provider_account=provider_account, type=circuittype, cid='2') + circuit1 = Circuit.objects.create(provider=provider, type=circuittype, cid='1') + circuit2 = Circuit.objects.create(provider=provider, 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/tests/test_staging.py b/netbox/netbox/tests/test_staging.py index 4a0ea3f9b..f128ad379 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, ProviderAccount +from circuits.models import Provider, Circuit, CircuitType from extras.choices import ChangeActionChoices from extras.models import Branch, StagedChange, Tag from ipam.models import ASN, RIR @@ -28,25 +28,18 @@ 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], provider_account=provider_accounts[0], cid='Circuit A1', type=circuit_type), - Circuit(provider=providers[0], provider_account=provider_accounts[0], cid='Circuit A2', type=circuit_type), - Circuit(provider=providers[0], provider_account=provider_accounts[0], cid='Circuit A3', type=circuit_type), - Circuit(provider=providers[1], provider_account=provider_accounts[1], cid='Circuit B1', type=circuit_type), - Circuit(provider=providers[1], provider_account=provider_accounts[1], cid='Circuit B2', type=circuit_type), - Circuit(provider=providers[1], provider_account=provider_accounts[1], cid='Circuit B3', type=circuit_type), - Circuit(provider=providers[2], provider_account=provider_accounts[2], cid='Circuit C1', type=circuit_type), - Circuit(provider=providers[2], provider_account=provider_accounts[2], cid='Circuit C2', type=circuit_type), - Circuit(provider=providers[2], provider_account=provider_accounts[2], cid='Circuit C3', type=circuit_type), + 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), )) def test_object_creation(self): @@ -57,8 +50,7 @@ class StagingTestCase(TransactionTestCase): with checkout(branch): provider = Provider.objects.create(name='Provider D', slug='provider-d') provider.asns.set(asns) - provider_account = ProviderAccount.objects.create(name='Account D', provider=provider, account='DDDD') - circuit = Circuit.objects.create(provider=provider, provider_account=provider_account, cid='Circuit D1', type=CircuitType.objects.first()) + circuit = Circuit.objects.create(provider=provider, cid='Circuit D1', type=CircuitType.objects.first()) circuit.tags.set(tags) # Sanity-checking @@ -70,7 +62,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(), 6) + self.assertEqual(StagedChange.objects.count(), 5) # Verify that changes are replayed upon entering the context with checkout(branch): @@ -153,31 +145,25 @@ class StagingTestCase(TransactionTestCase): with checkout(branch): provider = Provider.objects.get(name='Provider A') - 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(), 5) + self.assertEqual(StagedChange.objects.count(), 4) # 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 e9809efc5..ee994e959 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -19,8 +19,8 @@ {{ object.provider|linkify }} - Provider Account - {{ object.provider_account|linkify }} + Account + {{ object.provider_account|linkify|placeholder }} Circuit ID diff --git a/netbox/templates/circuits/provideraccount.html b/netbox/templates/circuits/provideraccount.html index 7fe02eaed..63344ada1 100644 --- a/netbox/templates/circuits/provideraccount.html +++ b/netbox/templates/circuits/provideraccount.html @@ -1,58 +1,55 @@ -{% 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 }}
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 %} +{% 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 }}
Account{{ object.account }}
Name{{ object.name|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 %}