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..03d282eaa 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,26 @@ 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') + + class Meta: + model = ProviderAccount + fields = [ + 'id', 'url', 'display', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', + ] + + # # Provider networks # 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..7b2b6b706 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -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..826350975 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -16,6 +16,7 @@ __all__ = ( 'CircuitTerminationFilterSet', 'CircuitTypeFilterSet', 'ProviderNetworkFilterSet', + 'ProviderAccountFilterSet', 'ProviderFilterSet', ) @@ -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): diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index e1fe6338d..eda319e31 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -15,6 +15,7 @@ __all__ = ( 'CircuitBulkEditForm', 'CircuitTypeBulkEditForm', 'ProviderBulkEditForm', + 'ProviderAccountBulkEditForm', 'ProviderNetworkBulkEditForm', ) @@ -25,11 +26,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 @@ -41,10 +37,23 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): model = Provider fieldsets = ( - (None, ('asns', 'account', )), + (None, ('asns', )), ) nullable_fields = ( - 'asns', 'account', 'description', 'comments', + 'asns', 'description', 'comments', + ) + + +class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm): + comments = CommentField( + widget=SmallTextarea, + label=_('Comments') + ) + + model = ProviderAccount + fieldsets = () + nullable_fields = ( + 'comments', ) diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index b61fb1bc7..9e79b79ba 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', ) diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index d7cfc494d..ad9953277 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 = ( diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index cd73780fa..9cb9eb305 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -15,6 +15,7 @@ __all__ = ( 'CircuitTerminationForm', 'CircuitTypeForm', 'ProviderForm', + 'ProviderAccountForm', 'ProviderNetworkForm', ) @@ -30,19 +31,28 @@ 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', ] help_texts = { 'name': _("Full name of the provider"), } +class ProviderAccountForm(NetBoxModelForm): + comments = CommentField() + + class Meta: + model = ProviderAccount + fields = [ + 'name', 'account', 'provider', 'description', 'comments', 'tags', + ] + + class ProviderNetworkForm(NetBoxModelForm): provider = DynamicModelChoiceField( queryset=Provider.objects.all() diff --git a/netbox/circuits/models/providers.py b/netbox/circuits/models/providers.py index 18a81dcef..f2ac6c032 100644 --- a/netbox/circuits/models/providers.py +++ b/netbox/circuits/models/providers.py @@ -7,6 +7,7 @@ from netbox.models import PrimaryModel __all__ = ( 'ProviderNetwork', 'Provider', + 'ProviderAccount', ) @@ -28,6 +29,36 @@ class Provider(PrimaryModel): related_name='providers', blank=True ) + + # Generic relations + contacts = GenericRelation( + to='tenancy.ContactAssignment' + ) + + clone_fields = () + + class Meta: + ordering = ['name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('circuits:provider', args=[self.pk]) + + +class ProviderAccount(PrimaryModel): + """ + This represents a provider account + """ + name = models.CharField( + max_length=100 + ) + provider = models.ForeignKey( + to='circuits.Provider', + on_delete=models.PROTECT, + related_name='accounts' + ) account = models.CharField( max_length=30, blank=True, @@ -39,18 +70,26 @@ class Provider(PrimaryModel): to='tenancy.ContactAssignment' ) - clone_fields = ( - 'account', - ) + clone_fields = ('provider', ) class Meta: - ordering = ['name'] + ordering = ('provider', 'name') + constraints = ( + models.UniqueConstraint( + fields=('provider', 'name'), + name='%(app_label)s_%(class)s_unique_provider_name' + ), + models.UniqueConstraint( + fields=('provider', 'account'), + name='%(app_label)s_%(class)s_unique_provider_account' + ), + ) def __str__(self): return self.name def get_absolute_url(self): - return reverse('circuits:provider', args=[self.pk]) + return reverse('circuits:provideraccount', args=[self.pk]) class ProviderNetwork(PrimaryModel): 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/providers.py b/netbox/circuits/tables/providers.py index 9de8d25b2..447249d7c 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): + name = tables.Column( + linkify=True + ) + account = 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', 'name', 'account', 'provider', 'comments', 'contacts', 'tags', 'created', 'last_updated', + ) + default_columns = ('pk', 'name', 'account', 'provider') class ProviderNetworkTable(NetBoxTable): 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 228b70bb1..ee1871d16 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -73,6 +73,52 @@ class ProviderBulkDeleteView(generic.BulkDeleteView): table = tables.ProviderTable +# +# ProviderAccounts +# + +class ProviderAccountListView(generic.ObjectListView): + queryset = ProviderAccount.objects.all() + filterset = filtersets.ProviderAccountFilterSet + filterset_form = forms.ProviderAccountFilterForm + table = tables.ProviderAccountTable + + +@register_model_view(ProviderAccount) +class ProviderAccountView(generic.ObjectView): + queryset = ProviderAccount.objects.all() + + +@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.all() + filterset = filtersets.ProviderAccountFilterSet + table = tables.ProviderAccountTable + form = forms.ProviderAccountBulkEditForm + + +class ProviderAccountBulkDeleteView(generic.BulkDeleteView): + queryset = ProviderAccount.objects.all() + filterset = filtersets.ProviderAccountFilterSet + table = tables.ProviderAccountTable + + # # Provider networks # diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 83a81690f..933fab113 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -244,6 +244,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/templates/circuits/provideraccount.html b/netbox/templates/circuits/provideraccount.html new file mode 100644 index 000000000..ec9efaf55 --- /dev/null +++ b/netbox/templates/circuits/provideraccount.html @@ -0,0 +1,47 @@ +{% 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 %} +
+
+{% endblock %}