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',
'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
#

View File

@ -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
#

View File

@ -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'

View File

@ -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
#

View File

@ -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):

View File

@ -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',
)

View File

@ -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',
)

View File

@ -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 = (

View File

@ -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()

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

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

View File

@ -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
#

View File

@ -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')),
),
),

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 %}