1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Introduce the Cloud model

This commit is contained in:
Jeremy Stretch
2021-03-18 11:10:48 -04:00
parent 433c48a1a3
commit 6ff8a267e9
17 changed files with 523 additions and 16 deletions

View File

@@ -1,16 +1,29 @@
from rest_framework import serializers
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from circuits.models import *
from netbox.api import WritableNestedSerializer
__all__ = [
'NestedCircuitSerializer',
'NestedCircuitTerminationSerializer',
'NestedCircuitTypeSerializer',
'NestedCloudSerializer',
'NestedProviderSerializer',
]
#
# Clouds
#
class NestedCloudSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:cloud-detail')
class Meta:
model = Provider
fields = ['id', 'url', 'display', 'name']
#
# Providers
#

View File

@@ -1,7 +1,7 @@
from rest_framework import serializers
from circuits.choices import CircuitStatusChoices
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from circuits.models import *
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
from netbox.api import ChoiceField
@@ -28,6 +28,22 @@ class ProviderSerializer(PrimaryModelSerializer):
]
#
# Clouds
#
class CloudSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:cloud-detail')
provider = NestedProviderSerializer()
class Meta:
model = Cloud
fields = [
'id', 'url', 'display', 'provider', 'name', 'description', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
]
#
# Circuits
#

View File

@@ -13,5 +13,8 @@ router.register('circuit-types', views.CircuitTypeViewSet)
router.register('circuits', views.CircuitViewSet)
router.register('circuit-terminations', views.CircuitTerminationViewSet)
# Clouds
router.register('clouds', views.CloudViewSet)
app_name = 'circuits-api'
urlpatterns = router.urls

View File

@@ -2,7 +2,7 @@ from django.db.models import Prefetch
from rest_framework.routers import APIRootView
from circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from circuits.models import *
from dcim.api.views import PathEndpointMixin
from extras.api.views import CustomFieldModelViewSet
from netbox.api.views import ModelViewSet
@@ -66,3 +66,13 @@ class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
serializer_class = serializers.CircuitTerminationSerializer
filterset_class = filters.CircuitTerminationFilterSet
brief_prefetch_fields = ['circuit']
#
# Clouds
#
class CloudViewSet(CustomFieldModelViewSet):
queryset = Cloud.objects.prefetch_related('tags')
serializer_class = serializers.CloudSerializer
filterset_class = filters.CloudFilterSet

View File

@@ -9,12 +9,13 @@ from utilities.filters import (
BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
)
from .choices import *
from .models import Circuit, CircuitTermination, CircuitType, Provider
from .models import *
__all__ = (
'CircuitFilterSet',
'CircuitTerminationFilterSet',
'CircuitTypeFilterSet',
'CloudFilterSet',
'ProviderFilterSet',
)
@@ -79,6 +80,36 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated
)
class CloudFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
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)',
)
tag = TagFilter()
class Meta:
model = Cloud
fields = ['id', 'name']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(description__icontains=value) |
Q(comments__icontains=value)
).distinct()
class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
class Meta:

View File

@@ -14,7 +14,7 @@ from utilities.forms import (
StaticSelect2, StaticSelect2Multiple, TagFilterField,
)
from .choices import CircuitStatusChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider
from .models import *
#
@@ -128,6 +128,83 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
tag = TagFilterField(model)
#
# Clouds
#
class CloudForm(BootstrapMixin, CustomFieldModelForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all()
)
comments = CommentField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = Cloud
fields = [
'provider', 'name', 'description', 'comments', 'tags',
]
fieldsets = (
('Cloud', ('provider', 'name', 'description', 'tags')),
)
class CloudCSVForm(CustomFieldModelCSVForm):
provider = CSVModelChoiceField(
queryset=Provider.objects.all(),
to_field_name='name',
help_text='Assigned provider'
)
class Meta:
model = Cloud
fields = [
'provider', 'name', 'description', 'comments',
]
class CloudBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Cloud.objects.all(),
widget=forms.MultipleHiddenInput
)
provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
required=False
)
description = forms.CharField(
max_length=100,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
class Meta:
nullable_fields = [
'description', 'comments',
]
class CloudFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Cloud
field_order = ['q', 'provider_id']
q = forms.CharField(
required=False,
label=_('Search')
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
)
tag = TagFilterField(model)
#
# Circuit types
#

View File

@@ -0,0 +1,40 @@
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('extras', '0058_journalentry'),
('circuits', '0026_mark_connected'),
]
operations = [
migrations.CreateModel(
name='Cloud',
fields=[
('created', models.DateField(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=django.core.serializers.json.DjangoJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='clouds', to='circuits.provider')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ('provider', 'name'),
},
),
migrations.AddConstraint(
model_name='cloud',
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_cloud_provider_name'),
),
migrations.AlterUniqueTogether(
name='cloud',
unique_together={('provider', 'name')},
),
]

View File

@@ -15,6 +15,7 @@ __all__ = (
'Circuit',
'CircuitTermination',
'CircuitType',
'Cloud',
'Provider',
)
@@ -91,6 +92,59 @@ class Provider(PrimaryModel):
)
#
# Clouds
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Cloud(PrimaryModel):
name = models.CharField(
max_length=100
)
provider = models.ForeignKey(
to='circuits.Provider',
on_delete=models.PROTECT,
related_name='clouds'
)
description = models.CharField(
max_length=200,
blank=True
)
comments = models.TextField(
blank=True
)
csv_headers = [
'provider', 'name', 'description', 'comments',
]
objects = RestrictedQuerySet.as_manager()
class Meta:
ordering = ('provider', 'name')
constraints = (
models.UniqueConstraint(
fields=('provider', 'name'),
name='circuits_cloud_provider_name'
),
)
unique_together = ('provider', 'name')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('circuits:cloud', args=[self.pk])
def to_csv(self):
return (
self.provider.name,
self.name,
self.description,
self.comments,
)
@extras_features('custom_fields', 'export_templates', 'webhooks')
class CircuitType(OrganizationalModel):
"""

View File

@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
from tenancy.tables import TenantColumn
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn
from .models import Circuit, CircuitType, Provider
from .models import *
#
@@ -29,6 +29,28 @@ class ProviderTable(BaseTable):
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
#
# Clouds
#
class CloudTable(BaseTable):
pk = ToggleColumn()
name = tables.Column(
linkify=True
)
provider = tables.Column(
linkify=True
)
tags = TagColumn(
url_name='circuits:cloud_list'
)
class Meta(BaseTable.Meta):
model = Cloud
fields = ('pk', 'name', 'provider', 'description', 'tags')
default_columns = ('pk', 'name', 'provider', 'description')
#
# Circuit types
#

View File

@@ -1,7 +1,7 @@
from django.urls import reverse
from circuits.choices import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from circuits.models import *
from dcim.models import Site
from utilities.testing import APITestCase, APIViewTestCases
@@ -178,3 +178,43 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
cls.bulk_update_data = {
'port_speed': 123456
}
class CloudTest(APIViewTestCases.APIViewTestCase):
model = Cloud
brief_fields = ['display', 'id', 'name', 'url']
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
)
Provider.objects.bulk_create(providers)
clouds = (
Cloud(name='Cloud 1', provider=providers[0]),
Cloud(name='Cloud 2', provider=providers[0]),
Cloud(name='Cloud 3', provider=providers[0]),
)
Cloud.objects.bulk_create(clouds)
cls.create_data = [
{
'name': 'Cloud 4',
'provider': providers[0].pk,
},
{
'name': 'Cloud 5',
'provider': providers[0].pk,
},
{
'name': 'Cloud 6',
'provider': providers[0].pk,
},
]
cls.bulk_update_data = {
'provider': providers[1].pk,
'description': 'New description',
}

View File

@@ -2,7 +2,7 @@ from django.test import TestCase
from circuits.choices import *
from circuits.filters import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from circuits.models import *
from dcim.models import Cable, Region, Site, SiteGroup
from tenancy.models import Tenant, TenantGroup
@@ -353,3 +353,40 @@ class CircuitTerminationTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class CloudTestCase(TestCase):
queryset = Cloud.objects.all()
filterset = CloudFilterSet
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
Provider(name='Provider 3', slug='provider-3'),
)
Provider.objects.bulk_create(providers)
clouds = (
Cloud(name='Cloud 1', provider=providers[0]),
Cloud(name='Cloud 2', provider=providers[1]),
Cloud(name='Cloud 3', provider=providers[2]),
)
Cloud.objects.bulk_create(clouds)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self):
params = {'name': ['Cloud 1', 'Cloud 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_provider(self):
providers = Provider.objects.all()[:2]
params = {'provider_id': [providers[0].pk, providers[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'provider': [providers[0].slug, providers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@@ -1,7 +1,7 @@
import datetime
from circuits.choices import *
from circuits.models import Circuit, CircuitType, Provider
from circuits.models import *
from utilities.testing import ViewTestCases
@@ -133,3 +133,45 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'description': 'New description',
'comments': 'New comments',
}
class CloudTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Cloud
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
)
Provider.objects.bulk_create(providers)
Cloud.objects.bulk_create([
Cloud(name='Cloud 1', provider=providers[0]),
Cloud(name='Cloud 2', provider=providers[0]),
Cloud(name='Cloud 3', provider=providers[0]),
])
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Cloud X',
'provider': providers[1].pk,
'description': 'A new cloud',
'comments': 'Longer description goes here',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,provider,description",
"Cloud 4,Provider 1,Foo",
"Cloud 5,Provider 1,Bar",
"Cloud 6,Provider 1,Baz",
)
cls.bulk_edit_data = {
'provider': providers[1].pk,
'description': 'New description',
'comments': 'New comments',
}

View File

@@ -3,7 +3,7 @@ from django.urls import path
from dcim.views import CableCreateView, PathTraceView
from extras.views import ObjectChangeLogView, ObjectJournalView
from . import views
from .models import Circuit, CircuitTermination, CircuitType, Provider
from .models import *
app_name = 'circuits'
urlpatterns = [
@@ -20,6 +20,18 @@ urlpatterns = [
path('providers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
path('providers/<int:pk>/journal/', ObjectJournalView.as_view(), name='provider_journal', kwargs={'model': Provider}),
# Clouds
path('clouds/', views.CloudListView.as_view(), name='cloud_list'),
path('clouds/add/', views.CloudEditView.as_view(), name='cloud_add'),
path('clouds/import/', views.CloudBulkImportView.as_view(), name='cloud_import'),
path('clouds/edit/', views.CloudBulkEditView.as_view(), name='cloud_bulk_edit'),
path('clouds/delete/', views.CloudBulkDeleteView.as_view(), name='cloud_bulk_delete'),
path('clouds/<int:pk>/', views.CloudView.as_view(), name='cloud'),
path('clouds/<int:pk>/edit/', views.CloudEditView.as_view(), name='cloud_edit'),
path('clouds/<int:pk>/delete/', views.CloudDeleteView.as_view(), name='cloud_delete'),
path('clouds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cloud_changelog', kwargs={'model': Cloud}),
path('clouds/<int:pk>/journal/', ObjectJournalView.as_view(), name='cloud_journal', kwargs={'model': Cloud}),
# Circuit types
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'),

View File

@@ -9,7 +9,7 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.utils import count_related
from . import filters, forms, tables
from .choices import CircuitTerminationSideChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider
from .models import *
#
@@ -81,6 +81,49 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
table = tables.ProviderTable
#
# Clouds
#
class CloudListView(generic.ObjectListView):
queryset = Cloud.objects.all()
filterset = filters.CloudFilterSet
filterset_form = forms.CloudFilterForm
table = tables.CloudTable
class CloudView(generic.ObjectView):
queryset = Cloud.objects.all()
class CloudEditView(generic.ObjectEditView):
queryset = Cloud.objects.all()
model_form = forms.CloudForm
class CloudDeleteView(generic.ObjectDeleteView):
queryset = Cloud.objects.all()
class CloudBulkImportView(generic.BulkImportView):
queryset = Cloud.objects.all()
model_form = forms.CloudCSVForm
table = tables.CloudTable
class CloudBulkEditView(generic.BulkEditView):
queryset = Cloud.objects.all()
filterset = filters.CloudFilterSet
table = tables.CloudTable
form = forms.CloudBulkEditForm
class CloudBulkDeleteView(generic.BulkDeleteView):
queryset = Cloud.objects.all()
filterset = filters.CloudFilterSet
table = tables.CloudTable
#
# Circuit Types
#