Initial work for #9047 - Adding provider accounts

This commit is contained in:
Daniel Sheppard 2023-03-21 14:15:38 -05:00
parent 7d32b881c4
commit 850e77395e
16 changed files with 328 additions and 21 deletions

View File

@ -9,6 +9,7 @@ __all__ = [
'NestedCircuitTypeSerializer', 'NestedCircuitTypeSerializer',
'NestedProviderNetworkSerializer', 'NestedProviderNetworkSerializer',
'NestedProviderSerializer', 'NestedProviderSerializer',
'NestedProviderAccountSerializer',
] ]
@ -37,6 +38,18 @@ class NestedProviderSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count'] 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 # Circuits
# #

View File

@ -18,6 +18,12 @@ from .nested_serializers import *
class ProviderSerializer(NetBoxModelSerializer): class ProviderSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
accounts = SerializedPKRelatedField(
queryset=ProviderAccount.objects.all(),
serializer=NestedProviderAccountSerializer,
required=False,
many=True
)
asns = SerializedPKRelatedField( asns = SerializedPKRelatedField(
queryset=ASN.objects.all(), queryset=ASN.objects.all(),
serializer=NestedASNSerializer, serializer=NestedASNSerializer,
@ -31,11 +37,26 @@ class ProviderSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Provider model = Provider
fields = [ 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', '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 # Provider networks
# #

View File

@ -14,6 +14,7 @@ router.register('circuits', views.CircuitViewSet)
router.register('circuit-terminations', views.CircuitTerminationViewSet) router.register('circuit-terminations', views.CircuitTerminationViewSet)
# Provider networks # Provider networks
router.register('provider-accounts', views.ProviderAccountViewSet)
router.register('provider-networks', views.ProviderNetworkViewSet) router.register('provider-networks', views.ProviderNetworkViewSet)
app_name = 'circuits-api' app_name = 'circuits-api'

View File

@ -65,6 +65,16 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
brief_prefetch_fields = ['circuit'] 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 # Provider networks
# #

View File

@ -16,6 +16,7 @@ __all__ = (
'CircuitTerminationFilterSet', 'CircuitTerminationFilterSet',
'CircuitTypeFilterSet', 'CircuitTypeFilterSet',
'ProviderNetworkFilterSet', 'ProviderNetworkFilterSet',
'ProviderAccountFilterSet',
'ProviderFilterSet', 'ProviderFilterSet',
) )
@ -66,7 +67,34 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class Meta: class Meta:
model = Provider 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): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -75,7 +103,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
Q(name__icontains=value) | Q(name__icontains=value) |
Q(account__icontains=value) | Q(account__icontains=value) |
Q(comments__icontains=value) Q(comments__icontains=value)
) ).distinct()
class ProviderNetworkFilterSet(NetBoxModelFilterSet): class ProviderNetworkFilterSet(NetBoxModelFilterSet):

View File

@ -15,6 +15,7 @@ __all__ = (
'CircuitBulkEditForm', 'CircuitBulkEditForm',
'CircuitTypeBulkEditForm', 'CircuitTypeBulkEditForm',
'ProviderBulkEditForm', 'ProviderBulkEditForm',
'ProviderAccountBulkEditForm',
'ProviderNetworkBulkEditForm', 'ProviderNetworkBulkEditForm',
) )
@ -25,11 +26,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
label=_('ASNs'), label=_('ASNs'),
required=False required=False
) )
account = forms.CharField(
max_length=30,
required=False,
label=_('Account number')
)
description = forms.CharField( description = forms.CharField(
max_length=200, max_length=200,
required=False required=False
@ -41,10 +37,23 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
model = Provider model = Provider
fieldsets = ( fieldsets = (
(None, ('asns', 'account', )), (None, ('asns', )),
) )
nullable_fields = ( nullable_fields = (
'asns', 'account', 'description', 'comments', 'asns', 'description', 'comments',
)
class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
comments = CommentField(
widget=SmallTextarea,
label=_('Comments')
)
model = ProviderAccount
fieldsets = ()
nullable_fields = (
'comments',
) )

View File

@ -13,6 +13,7 @@ __all__ = (
'CircuitTerminationImportForm', 'CircuitTerminationImportForm',
'CircuitTypeImportForm', 'CircuitTypeImportForm',
'ProviderImportForm', 'ProviderImportForm',
'ProviderAccountImportForm',
'ProviderNetworkImportForm', 'ProviderNetworkImportForm',
) )
@ -23,7 +24,21 @@ class ProviderImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = Provider model = Provider
fields = ( 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',
) )

View File

@ -13,6 +13,7 @@ __all__ = (
'CircuitFilterForm', 'CircuitFilterForm',
'CircuitTypeFilterForm', 'CircuitTypeFilterForm',
'ProviderFilterForm', 'ProviderFilterForm',
'ProviderAccountFilterForm',
'ProviderNetworkFilterForm', 'ProviderNetworkFilterForm',
) )
@ -56,6 +57,24 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model) 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): class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
model = ProviderNetwork model = ProviderNetwork
fieldsets = ( fieldsets = (

View File

@ -15,6 +15,7 @@ __all__ = (
'CircuitTerminationForm', 'CircuitTerminationForm',
'CircuitTypeForm', 'CircuitTypeForm',
'ProviderForm', 'ProviderForm',
'ProviderAccountForm',
'ProviderNetworkForm', 'ProviderNetworkForm',
) )
@ -30,19 +31,28 @@ class ProviderForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
('Provider', ('name', 'slug', 'asns', 'description', 'tags')), ('Provider', ('name', 'slug', 'asns', 'description', 'tags')),
('Support Info', ('account',)),
) )
class Meta: class Meta:
model = Provider model = Provider
fields = [ fields = [
'name', 'slug', 'account', 'asns', 'description', 'comments', 'tags', 'name', 'slug', 'asns', 'description', 'comments', 'tags',
] ]
help_texts = { help_texts = {
'name': _("Full name of the provider"), '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): class ProviderNetworkForm(NetBoxModelForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
queryset=Provider.objects.all() queryset=Provider.objects.all()

View File

@ -7,6 +7,7 @@ from netbox.models import PrimaryModel
__all__ = ( __all__ = (
'ProviderNetwork', 'ProviderNetwork',
'Provider', 'Provider',
'ProviderAccount',
) )
@ -28,6 +29,36 @@ class Provider(PrimaryModel):
related_name='providers', related_name='providers',
blank=True 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( account = models.CharField(
max_length=30, max_length=30,
blank=True, blank=True,
@ -39,18 +70,26 @@ class Provider(PrimaryModel):
to='tenancy.ContactAssignment' to='tenancy.ContactAssignment'
) )
clone_fields = ( clone_fields = ('provider', )
'account',
)
class Meta: 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): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('circuits:provider', args=[self.pk]) return reverse('circuits:provideraccount', args=[self.pk])
class ProviderNetwork(PrimaryModel): class ProviderNetwork(PrimaryModel):

View File

@ -39,12 +39,20 @@ class ProviderIndex(SearchIndex):
model = models.Provider model = models.Provider
fields = ( fields = (
('name', 100), ('name', 100),
('account', 200),
('description', 500), ('description', 500),
('comments', 5000), ('comments', 5000),
) )
class ProviderAccountIndex(SearchIndex):
model = models.ProviderAccount
fields = (
('name', 100),
('account', 200),
('comments', 5000),
)
@register_search @register_search
class ProviderNetworkIndex(SearchIndex): class ProviderNetworkIndex(SearchIndex):
model = models.ProviderNetwork model = models.ProviderNetwork

View File

@ -7,6 +7,7 @@ from netbox.tables import NetBoxTable, columns
__all__ = ( __all__ = (
'ProviderTable', 'ProviderTable',
'ProviderAccountTable',
'ProviderNetworkTable', 'ProviderNetworkTable',
) )
@ -15,6 +16,16 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True 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( asns = columns.ManyToManyColumn(
linkify_item=True, linkify_item=True,
verbose_name='ASNs' verbose_name='ASNs'
@ -39,10 +50,31 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Provider model = Provider
fields = ( 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', '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): class ProviderNetworkTable(NetBoxTable):

View File

@ -14,6 +14,14 @@ urlpatterns = [
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
path('providers/<int:pk>/', include(get_model_urls('circuits', 'provider'))), path('providers/<int:pk>/', 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/<int:pk>/', include(get_model_urls('circuits', 'provideraccount'))),
# Provider networks # Provider networks
path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'), path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'),
path('provider-networks/add/', views.ProviderNetworkEditView.as_view(), name='providernetwork_add'), path('provider-networks/add/', views.ProviderNetworkEditView.as_view(), name='providernetwork_add'),

View File

@ -73,6 +73,52 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
table = tables.ProviderTable 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 # Provider networks
# #

View File

@ -244,6 +244,7 @@ CIRCUITS_MENU = Menu(
label=_('Providers'), label=_('Providers'),
items=( items=(
get_model_item('circuits', 'provider', _('Providers')), get_model_item('circuits', 'provider', _('Providers')),
get_model_item('circuits', 'provideraccount', _('Provider Accounts')),
get_model_item('circuits', 'providernetwork', _('Provider Networks')), get_model_item('circuits', 'providernetwork', _('Provider Networks')),
), ),
), ),

View File

@ -0,0 +1,47 @@
{% extends 'generic/object.html' %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'circuits:provideraccount_list' %}?provider_id={{ object.provider_id }}">{{ object.provider }}</a></li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Provider Account
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Provider</th>
<td>{{ object.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Account</th>
<td>{{ object.account|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% 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 %}
</div>
</div>
{% endblock %}