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
#

View File

@ -1,10 +1,8 @@
from collections import OrderedDict
from django.db.models import Count
from circuits.filters import CircuitFilterSet, ProviderFilterSet
from circuits.models import Circuit, Provider
from circuits.tables import CircuitTable, ProviderTable
from circuits.filters import CircuitFilterSet, CloudFilterSet, ProviderFilterSet
from circuits.models import Circuit, Cloud, Provider
from circuits.tables import CircuitTable, CloudTable, ProviderTable
from dcim.filters import (
CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, LocationFilterSet,
SiteFilterSet, VirtualChassisFilterSet,
@ -47,6 +45,12 @@ SEARCH_TYPES = OrderedDict((
'table': CircuitTable,
'url': 'circuits:circuit_list',
}),
('cloud', {
'queryset': Cloud.objects.prefetch_related('provider'),
'filterset': CloudFilterSet,
'table': CloudTable,
'url': 'circuits:cloud_list',
}),
# DCIM
('site', {
'queryset': Site.objects.prefetch_related('region', 'tenant'),

View File

@ -0,0 +1,55 @@
{% extends 'generic/object.html' %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'circuits:cloud_list' %}">Clouds</a></li>
<li><a href="{% url 'circuits:cloud_list' %}?provider_id={{ object.provider_id }}">{{ object.provider }}</a></li>
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Cloud</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description }}</td>
</tr>
</table>
</div>
{% include 'inc/custom_fields_panel.html' %}
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:cloud_list' %}
{% plugin_left_page object %}
</div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body rendered-markdown">
{% if object.comments %}
{{ object.comments|render_markdown }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -465,6 +465,14 @@
</div>
{% endif %}
<a href="{% url 'circuits:provider_list' %}">Providers</a>
<li{% if not perms.circuits.view_cloud %} class="disabled"{% endif %}>
{% if perms.circuits.add_cloud %}
<div class="buttons pull-right">
<a href="{% url 'circuits:cloud_add' %}" class="btn btn-xs btn-success" title="Add"><i class="mdi mdi-plus-thick"></i></a>
<a href="{% url 'circuits:cloud_import' %}" class="btn btn-xs btn-info" title="Import"><i class="mdi mdi-database-import-outline"></i></a>
</div>
{% endif %}
<a href="{% url 'circuits:cloud_list' %}">Clouds</a>
</li>
</ul>
</li>