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

Rename Cloud to ProviderNetwork

This commit is contained in:
Jeremy Stretch
2021-04-01 10:21:41 -04:00
parent 83c4577f6d
commit d57222328b
27 changed files with 219 additions and 217 deletions

View File

@ -1,7 +1,7 @@
# Circuits
{!docs/models/circuits/provider.md!}
{!docs/models/circuits/cloud.md!}
{!docs/models/circuits/providernetwork.md!}
---

View File

@ -2,9 +2,9 @@
The association of a circuit with a particular site and/or device is modeled separately as a circuit termination. A circuit may have up to two terminations, labeled A and Z. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites.
Each circuit termination is attached to either a site or a cloud. Site terminations may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details.
Each circuit termination is attached to either a site or to a provider network. Site terminations may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details.
In adherence with NetBox's philosophy of closely modeling the real world, a circuit may be connected only to a physical interface. For example, circuits may not terminate to LAG interfaces, which are virtual in nature. In such cases, a separate physical circuit is associated with each LAG member interface and each needs to be modeled discretely.
!!! note
A circuit in NetBox represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit, with one end terminating within the provider's infrastructure. The cloud model is ideal for representing these networks.
A circuit in NetBox represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit, with one end terminating within the provider's infrastructure. The provider network model is ideal for representing these networks.

View File

@ -1,5 +0,0 @@
# Clouds
A cloud represents an abstract portion of network topology, just like in a topology diagram. For example, a cloud may be used to represent a provider's MPLS network.
Each cloud must be assigned to a provider. A circuit may terminate to either a cloud or to a site.

View File

@ -0,0 +1,5 @@
# Provider Network
This model can be used to represent the boundary of a provider network, the details of which are unknown or unimportant to the NetBox user. For example, it might represent a provider's regional MPLS network to which multiple circuits provide connectivity.
Each provider network must be assigned to a provider. A circuit may terminate to either a provider network or to a site.

View File

@ -78,9 +78,9 @@ This release introduces the new SiteGroup model, which can be used to organize s
The ObjectChange model (which is used to record the creation, modification, and deletion of NetBox objects) now explicitly records the pre-change and post-change state of each object, rather than only the post-change state. This was done to present a more clear depiction of each change being made, and to prevent the erroneous association of a previous unlogged change with its successor.
#### Cloud Modeling for Circuits ([#5986](https://github.com/netbox-community/netbox/issues/5986))
#### Provider Network Modeling ([#5986](https://github.com/netbox-community/netbox/issues/5986))
A new Cloud model has been introduced to represent the boundary of a network that exists outside the scope of NetBox. This is analogous to using a cloud icon on a topology drawing to represent an abstracted network. Each cloud must be assigned to a provider, and circuits can terminate to either clouds or sites. The use of this model will likely be extended by future releases to support overlay and virtual circuit modeling.
A new provider network model has been introduced to represent the boundary of a network that exists outside the scope of NetBox. Each instance of this model must be assigned to a provider, and circuits can now terminate to either provider networks or to sites. The use of this model will likely be extended by future releases to support overlay and virtual circuit modeling.
### Enhancements
@ -130,9 +130,9 @@ A new Cloud model has been introduced to represent the boundary of a network tha
* Renamed RackGroup to Location
* The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
* circuits.CircuitTermination
* Added the `cloud` field
* circuits.Cloud
* Added the `/api/circuits/clouds/` endpoint
* Added the `provider_network` field
* circuits.ProviderNetwork
* Added the `/api/circuits/provider-networks/` endpoint
* dcim.Device
* Added the `location` field
* dcim.Interface

View File

@ -7,17 +7,17 @@ __all__ = [
'NestedCircuitSerializer',
'NestedCircuitTerminationSerializer',
'NestedCircuitTypeSerializer',
'NestedCloudSerializer',
'NestedProviderNetworkSerializer',
'NestedProviderSerializer',
]
#
# Clouds
# Provider networks
#
class NestedCloudSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:cloud-detail')
class NestedProviderNetworkSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
class Meta:
model = Provider

View File

@ -29,15 +29,15 @@ class ProviderSerializer(PrimaryModelSerializer):
#
# Clouds
# Provider networks
#
class CloudSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:cloud-detail')
class ProviderNetworkSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
provider = NestedProviderSerializer()
class Meta:
model = Cloud
model = ProviderNetwork
fields = [
'id', 'url', 'display', 'provider', 'name', 'description', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
@ -63,12 +63,12 @@ class CircuitTypeSerializer(OrganizationalModelSerializer):
class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = NestedSiteSerializer()
cloud = NestedCloudSerializer()
provider_network = NestedProviderNetworkSerializer()
class Meta:
model = CircuitTermination
fields = [
'id', 'url', 'display', 'site', 'cloud', 'port_speed', 'upstream_speed', 'xconnect_id',
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable',
]
@ -95,13 +95,13 @@ class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializ
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer()
site = NestedSiteSerializer(required=False)
cloud = NestedCloudSerializer(required=False)
provider_network = NestedProviderNetworkSerializer(required=False)
cable = NestedCableSerializer(read_only=True)
class Meta:
model = CircuitTermination
fields = [
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'cloud', 'port_speed', 'upstream_speed',
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', '_occupied',
]

View File

@ -13,8 +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)
# Provider networks
router.register('provider-networks', views.ProviderNetworkViewSet)
app_name = 'circuits-api'
urlpatterns = router.urls

View File

@ -68,10 +68,10 @@ class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
#
# Clouds
# Provider networks
#
class CloudViewSet(CustomFieldModelViewSet):
queryset = Cloud.objects.prefetch_related('tags')
serializer_class = serializers.CloudSerializer
filterset_class = filters.CloudFilterSet
class ProviderNetworkViewSet(CustomFieldModelViewSet):
queryset = ProviderNetwork.objects.prefetch_related('tags')
serializer_class = serializers.ProviderNetworkSerializer
filterset_class = filters.ProviderNetworkFilterSet

View File

@ -15,7 +15,7 @@ __all__ = (
'CircuitFilterSet',
'CircuitTerminationFilterSet',
'CircuitTypeFilterSet',
'CloudFilterSet',
'ProviderNetworkFilterSet',
'ProviderFilterSet',
)
@ -80,7 +80,7 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated
)
class CloudFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
class ProviderNetworkFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@ -98,7 +98,7 @@ class CloudFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFil
tag = TagFilter()
class Meta:
model = Cloud
model = ProviderNetwork
fields = ['id', 'name']
def search(self, queryset, name, value):
@ -132,10 +132,10 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
to_field_name='slug',
label='Provider (slug)',
)
cloud_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__cloud',
queryset=Cloud.objects.all(),
label='Cloud (ID)',
provider_network_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__provider_network',
queryset=ProviderNetwork.objects.all(),
label='ProviderNetwork (ID)',
)
type_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitType.objects.all(),
@ -226,9 +226,9 @@ class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, Path
to_field_name='slug',
label='Site (slug)',
)
cloud_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cloud.objects.all(),
label='Cloud (ID)',
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
label='ProviderNetwork (ID)',
)
class Meta:

View File

@ -129,10 +129,10 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
#
# Clouds
# Provider networks
#
class CloudForm(BootstrapMixin, CustomFieldModelForm):
class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all()
)
@ -143,16 +143,16 @@ class CloudForm(BootstrapMixin, CustomFieldModelForm):
)
class Meta:
model = Cloud
model = ProviderNetwork
fields = [
'provider', 'name', 'description', 'comments', 'tags',
]
fieldsets = (
('Cloud', ('provider', 'name', 'description', 'tags')),
('Provider Network', ('provider', 'name', 'description', 'tags')),
)
class CloudCSVForm(CustomFieldModelCSVForm):
class ProviderNetworkCSVForm(CustomFieldModelCSVForm):
provider = CSVModelChoiceField(
queryset=Provider.objects.all(),
to_field_name='name',
@ -160,15 +160,15 @@ class CloudCSVForm(CustomFieldModelCSVForm):
)
class Meta:
model = Cloud
model = ProviderNetwork
fields = [
'provider', 'name', 'description', 'comments',
]
class CloudBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Cloud.objects.all(),
queryset=ProviderNetwork.objects.all(),
widget=forms.MultipleHiddenInput
)
provider = DynamicModelChoiceField(
@ -190,8 +190,8 @@ class CloudBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFo
]
class CloudFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Cloud
class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = ProviderNetwork
field_order = ['q', 'provider_id']
q = forms.CharField(
required=False,
@ -357,7 +357,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Circuit
field_order = [
'q', 'type_id', 'provider_id', 'cloud_id', 'status', 'region_id', 'site_id', 'tenant_group_id', 'tenant_id',
'q', 'type_id', 'provider_id', 'provider_network_id', 'status', 'region_id', 'site_id', 'tenant_group_id', 'tenant_id',
'commit_rate',
]
q = forms.CharField(
@ -374,13 +374,13 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
required=False,
label=_('Provider')
)
cloud_id = DynamicModelMultipleChoiceField(
queryset=Cloud.objects.all(),
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Cloud')
label=_('Provider network')
)
status = forms.MultipleChoiceField(
choices=CircuitStatusChoices,
@ -435,16 +435,16 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
},
required=False
)
cloud = DynamicModelChoiceField(
queryset=Cloud.objects.all(),
provider_network = DynamicModelChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False
)
class Meta:
model = CircuitTermination
fields = [
'term_side', 'region', 'site_group', 'site', 'cloud', 'mark_connected', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description',
'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected', 'port_speed',
'upstream_speed', 'xconnect_id', 'pp_info', 'description',
]
help_texts = {
'port_speed': "Physical circuit speed",
@ -460,4 +460,4 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['cloud'].widget.add_query_param('provider_id', self.instance.circuit.provider_id)
self.fields['provider_network'].widget.add_query_param('provider_id', self.instance.circuit.provider_id)

View File

@ -12,9 +12,9 @@ class Migration(migrations.Migration):
]
operations = [
# Create the new Cloud model
# Create the new ProviderNetwork model
migrations.CreateModel(
name='Cloud',
name='ProviderNetwork',
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
@ -23,7 +23,7 @@ class Migration(migrations.Migration):
('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')),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='networks', to='circuits.provider')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
@ -31,19 +31,19 @@ class Migration(migrations.Migration):
},
),
migrations.AddConstraint(
model_name='cloud',
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_cloud_provider_name'),
model_name='providernetwork',
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_provider_name'),
),
migrations.AlterUniqueTogether(
name='cloud',
name='providernetwork',
unique_together={('provider', 'name')},
),
# Add cloud FK to CircuitTermination
# Add ProviderNetwork FK to CircuitTermination
migrations.AddField(
model_name='circuittermination',
name='cloud',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='circuits.cloud'),
name='provider_network',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='circuits.providernetwork'),
),
migrations.AlterField(
model_name='circuittermination',

View File

@ -26,7 +26,7 @@ def cache_circuit_terminations(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('circuits', '0027_cloud'),
('circuits', '0027_providernetwork'),
]
operations = [

View File

@ -15,7 +15,7 @@ __all__ = (
'Circuit',
'CircuitTermination',
'CircuitType',
'Cloud',
'ProviderNetwork',
'Provider',
)
@ -93,18 +93,22 @@ class Provider(PrimaryModel):
#
# Clouds
# Provider networks
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Cloud(PrimaryModel):
class ProviderNetwork(PrimaryModel):
"""
This represents a provider network which exists outside of NetBox, the details of which are unknown or
unimportant to the user.
"""
name = models.CharField(
max_length=100
)
provider = models.ForeignKey(
to='circuits.Provider',
on_delete=models.PROTECT,
related_name='clouds'
related_name='networks'
)
description = models.CharField(
max_length=200,
@ -125,7 +129,7 @@ class Cloud(PrimaryModel):
constraints = (
models.UniqueConstraint(
fields=('provider', 'name'),
name='circuits_cloud_provider_name'
name='circuits_providernetwork_provider_name'
),
)
unique_together = ('provider', 'name')
@ -134,7 +138,7 @@ class Cloud(PrimaryModel):
return self.name
def get_absolute_url(self):
return reverse('circuits:cloud', args=[self.pk])
return reverse('circuits:providernetwork', args=[self.pk])
def to_csv(self):
return (
@ -308,8 +312,8 @@ class CircuitTermination(ChangeLoggedModel, PathEndpoint, CableTermination):
blank=True,
null=True
)
cloud = models.ForeignKey(
to=Cloud,
provider_network = models.ForeignKey(
to=ProviderNetwork,
on_delete=models.PROTECT,
related_name='circuit_terminations',
blank=True,
@ -348,23 +352,21 @@ class CircuitTermination(ChangeLoggedModel, PathEndpoint, CableTermination):
unique_together = ['circuit', 'term_side']
def __str__(self):
if self.site:
return str(self.site)
return str(self.cloud)
return str(self.site or self.provider_network)
def get_absolute_url(self):
if self.site:
return self.site.get_absolute_url()
return self.cloud.get_absolute_url()
return self.provider_network.get_absolute_url()
def clean(self):
super().clean()
# Must define either site *or* cloud
if self.site is None and self.cloud is None:
raise ValidationError("A circuit termination must attach to either a site or a cloud.")
if self.site and self.cloud:
raise ValidationError("A circuit termination cannot attach to both a site and a cloud.")
# Must define either site *or* provider network
if self.site is None and self.provider_network is None:
raise ValidationError("A circuit termination must attach to either a site or a provider network.")
if self.site and self.provider_network:
raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
def to_objectchange(self, action):
# Annotate the parent Circuit

View File

@ -30,10 +30,10 @@ class ProviderTable(BaseTable):
#
# Clouds
# Provider networks
#
class CloudTable(BaseTable):
class ProviderNetworkTable(BaseTable):
pk = ToggleColumn()
name = tables.Column(
linkify=True
@ -42,11 +42,11 @@ class CloudTable(BaseTable):
linkify=True
)
tags = TagColumn(
url_name='circuits:cloud_list'
url_name='circuits:providernetwork_list'
)
class Meta(BaseTable.Meta):
model = Cloud
model = ProviderNetwork
fields = ('pk', 'name', 'provider', 'description', 'tags')
default_columns = ('pk', 'name', 'provider', 'description')

View File

@ -180,8 +180,8 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
}
class CloudTest(APIViewTestCases.APIViewTestCase):
model = Cloud
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
model = ProviderNetwork
brief_fields = ['display', 'id', 'name', 'url']
@classmethod
@ -192,24 +192,24 @@ class CloudTest(APIViewTestCases.APIViewTestCase):
)
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]),
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
)
Cloud.objects.bulk_create(clouds)
ProviderNetwork.objects.bulk_create(provider_networks)
cls.create_data = [
{
'name': 'Cloud 4',
'name': 'Provider Network 4',
'provider': providers[0].pk,
},
{
'name': 'Cloud 5',
'name': 'Provider Network 5',
'provider': providers[0].pk,
},
{
'name': 'Cloud 6',
'name': 'Provider Network 6',
'provider': providers[0].pk,
},
]

View File

@ -186,12 +186,12 @@ class CircuitTestCase(TestCase):
)
Provider.objects.bulk_create(providers)
clouds = (
Cloud(name='Cloud 1', provider=providers[1]),
Cloud(name='Cloud 2', provider=providers[1]),
Cloud(name='Cloud 3', provider=providers[1]),
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[1]),
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
ProviderNetwork(name='Provider Network 3', provider=providers[1]),
)
Cloud.objects.bulk_create(clouds)
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = (
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE),
@ -207,9 +207,9 @@ class CircuitTestCase(TestCase):
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A'),
CircuitTermination(circuit=circuits[3], cloud=clouds[0], term_side='A'),
CircuitTermination(circuit=circuits[4], cloud=clouds[1], term_side='A'),
CircuitTermination(circuit=circuits[5], cloud=clouds[2], term_side='A'),
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
))
CircuitTermination.objects.bulk_create(circuit_terminations)
@ -236,9 +236,9 @@ class CircuitTestCase(TestCase):
params = {'provider': [provider.slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_cloud(self):
clouds = Cloud.objects.all()[:2]
params = {'cloud_id': [clouds[0].pk, clouds[1].pk]}
def test_provider_network(self):
provider_networks = ProviderNetwork.objects.all()[:2]
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
@ -312,12 +312,12 @@ class CircuitTerminationTestCase(TestCase):
)
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]),
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
)
Cloud.objects.bulk_create(clouds)
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = (
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
@ -336,9 +336,9 @@ class CircuitTerminationTestCase(TestCase):
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
CircuitTermination(circuit=circuits[3], cloud=clouds[0], term_side='A'),
CircuitTermination(circuit=circuits[4], cloud=clouds[1], term_side='A'),
CircuitTermination(circuit=circuits[5], cloud=clouds[2], term_side='A'),
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
))
CircuitTermination.objects.bulk_create(circuit_terminations)
@ -372,9 +372,9 @@ class CircuitTerminationTestCase(TestCase):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_cloud(self):
clouds = Cloud.objects.all()[:2]
params = {'cloud_id': [clouds[0].pk, clouds[1].pk]}
def test_provider_network(self):
provider_networks = ProviderNetwork.objects.all()[:2]
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self):
@ -388,9 +388,9 @@ class CircuitTerminationTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
class CloudTestCase(TestCase):
queryset = Cloud.objects.all()
filterset = CloudFilterSet
class ProviderNetworkTestCase(TestCase):
queryset = ProviderNetwork.objects.all()
filterset = ProviderNetworkFilterSet
@classmethod
def setUpTestData(cls):
@ -402,19 +402,19 @@ class CloudTestCase(TestCase):
)
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]),
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
)
Cloud.objects.bulk_create(clouds)
ProviderNetwork.objects.bulk_create(provider_networks)
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']}
params = {'name': ['Provider Network 1', 'Provider Network 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_provider(self):

View File

@ -135,8 +135,8 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
class CloudTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Cloud
class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = ProviderNetwork
@classmethod
def setUpTestData(cls):
@ -147,27 +147,27 @@ class CloudTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
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]),
ProviderNetwork.objects.bulk_create([
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
])
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Cloud X',
'name': 'Provider Network X',
'provider': providers[1].pk,
'description': 'A new cloud',
'description': 'A new provider network',
'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",
"Provider Network 4,Provider 1,Foo",
"Provider Network 5,Provider 1,Bar",
"Provider Network 6,Provider 1,Baz",
)
cls.bulk_edit_data = {

View File

@ -20,17 +20,17 @@ 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}),
# Provider networks
path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'),
path('provider-networks/add/', views.ProviderNetworkEditView.as_view(), name='providernetwork_add'),
path('provider-networks/import/', views.ProviderNetworkBulkImportView.as_view(), name='providernetwork_import'),
path('provider-networks/edit/', views.ProviderNetworkBulkEditView.as_view(), name='providernetwork_bulk_edit'),
path('provider-networks/delete/', views.ProviderNetworkBulkDeleteView.as_view(), name='providernetwork_bulk_delete'),
path('provider-networks/<int:pk>/', views.ProviderNetworkView.as_view(), name='providernetwork'),
path('provider-networks/<int:pk>/edit/', views.ProviderNetworkEditView.as_view(), name='providernetwork_edit'),
path('provider-networks/<int:pk>/delete/', views.ProviderNetworkDeleteView.as_view(), name='providernetwork_delete'),
path('provider-networks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='providernetwork_changelog', kwargs={'model': ProviderNetwork}),
path('provider-networks/<int:pk>/journal/', ObjectJournalView.as_view(), name='providernetwork_journal', kwargs={'model': ProviderNetwork}),
# Circuit types
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),

View File

@ -77,23 +77,23 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
#
# Clouds
# Provider networks
#
class CloudListView(generic.ObjectListView):
queryset = Cloud.objects.all()
filterset = filters.CloudFilterSet
filterset_form = forms.CloudFilterForm
table = tables.CloudTable
class ProviderNetworkListView(generic.ObjectListView):
queryset = ProviderNetwork.objects.all()
filterset = filters.ProviderNetworkFilterSet
filterset_form = forms.ProviderNetworkFilterForm
table = tables.ProviderNetworkTable
class CloudView(generic.ObjectView):
queryset = Cloud.objects.all()
class ProviderNetworkView(generic.ObjectView):
queryset = ProviderNetwork.objects.all()
def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(
Q(termination_a__cloud=instance.pk) |
Q(termination_z__cloud=instance.pk)
Q(termination_a__provider_network=instance.pk) |
Q(termination_z__provider_network=instance.pk)
).prefetch_related(
'type', 'tenant', 'terminations__site'
)
@ -108,32 +108,32 @@ class CloudView(generic.ObjectView):
}
class CloudEditView(generic.ObjectEditView):
queryset = Cloud.objects.all()
model_form = forms.CloudForm
class ProviderNetworkEditView(generic.ObjectEditView):
queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkForm
class CloudDeleteView(generic.ObjectDeleteView):
queryset = Cloud.objects.all()
class ProviderNetworkDeleteView(generic.ObjectDeleteView):
queryset = ProviderNetwork.objects.all()
class CloudBulkImportView(generic.BulkImportView):
queryset = Cloud.objects.all()
model_form = forms.CloudCSVForm
table = tables.CloudTable
class ProviderNetworkBulkImportView(generic.BulkImportView):
queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkCSVForm
table = tables.ProviderNetworkTable
class CloudBulkEditView(generic.BulkEditView):
queryset = Cloud.objects.all()
filterset = filters.CloudFilterSet
table = tables.CloudTable
form = forms.CloudBulkEditForm
class ProviderNetworkBulkEditView(generic.BulkEditView):
queryset = ProviderNetwork.objects.all()
filterset = filters.ProviderNetworkFilterSet
table = tables.ProviderNetworkTable
form = forms.ProviderNetworkBulkEditForm
class CloudBulkDeleteView(generic.BulkDeleteView):
queryset = Cloud.objects.all()
filterset = filters.CloudFilterSet
table = tables.CloudTable
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderNetwork.objects.all()
filterset = filters.ProviderNetworkFilterSet
table = tables.ProviderNetworkTable
#

View File

@ -242,14 +242,14 @@ class Cable(PrimaryModel):
):
raise ValidationError("A front port cannot be connected to it corresponding rear port")
# A CircuitTermination attached to a Cloud cannot have a Cable
if isinstance(self.termination_a, CircuitTermination) and self.termination_a.cloud is not None:
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
if isinstance(self.termination_a, CircuitTermination) and self.termination_a.provider_network is not None:
raise ValidationError({
'termination_a_id': "Circuit terminations attached to a cloud may not be cabled."
'termination_a_id': "Circuit terminations attached to a provider network may not be cabled."
})
if isinstance(self.termination_b, CircuitTermination) and self.termination_b.cloud is not None:
if isinstance(self.termination_b, CircuitTermination) and self.termination_b.provider_network is not None:
raise ValidationError({
'termination_b_id': "Circuit terminations attached to a cloud may not be cabled."
'termination_b_id': "Circuit terminations attached to a provider network may not be cabled."
})
# Check for an existing Cable connected to either termination object

View File

@ -479,13 +479,13 @@ class CableTestCase(TestCase):
device=self.patch_pannel, name='FP4', type='8p8c', rear_port=self.rear_port4, rear_port_position=1
)
self.provider = Provider.objects.create(name='Provider 1', slug='provider-1')
cloud = Cloud.objects.create(name='Cloud 1', provider=self.provider)
provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=self.provider)
self.circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
self.circuit1 = Circuit.objects.create(provider=self.provider, type=self.circuittype, cid='1')
self.circuit2 = Circuit.objects.create(provider=self.provider, type=self.circuittype, cid='2')
self.circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit1, site=site, term_side='A')
self.circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit1, site=site, term_side='Z')
self.circuittermination3 = CircuitTermination.objects.create(circuit=self.circuit2, cloud=cloud, term_side='A')
self.circuittermination3 = CircuitTermination.objects.create(circuit=self.circuit2, provider_network=provider_network, term_side='A')
def test_cable_creation(self):
"""
@ -555,9 +555,9 @@ class CableTestCase(TestCase):
with self.assertRaises(ValidationError):
cable.clean()
def test_cable_cannot_terminate_to_a_cloud_circuittermination(self):
def test_cable_cannot_terminate_to_a_provider_network_circuittermination(self):
"""
Neither side of a cable can be terminated to a CircuitTermination which is attached to a Cloud
Neither side of a cable can be terminated to a CircuitTermination which is attached to a ProviderNetwork
"""
cable = Cable(termination_a=self.interface3, termination_b=self.circuittermination3)
with self.assertRaises(ValidationError):

View File

@ -1,8 +1,8 @@
from collections import OrderedDict
from circuits.filters import CircuitFilterSet, CloudFilterSet, ProviderFilterSet
from circuits.models import Circuit, Cloud, Provider
from circuits.tables import CircuitTable, CloudTable, ProviderTable
from circuits.filters import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
from circuits.models import Circuit, ProviderNetwork, Provider
from circuits.tables import CircuitTable, ProviderNetworkTable, ProviderTable
from dcim.filters import (
CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, LocationFilterSet,
SiteFilterSet, VirtualChassisFilterSet,
@ -45,11 +45,11 @@ SEARCH_TYPES = OrderedDict((
'table': CircuitTable,
'url': 'circuits:circuit_list',
}),
('cloud', {
'queryset': Cloud.objects.prefetch_related('provider'),
'filterset': CloudFilterSet,
'table': CloudTable,
'url': 'circuits:cloud_list',
('providernetwork', {
'queryset': ProviderNetwork.objects.prefetch_related('provider'),
'filterset': ProviderNetworkFilterSet,
'table': ProviderNetworkTable,
'url': 'circuits:providernetwork_list',
}),
# DCIM
('site', {

View File

@ -26,19 +26,19 @@
<p class="form-control-static">{{ form.term_side.value }}</p>
</div>
</div>
{% with cloud_tab_active=form.initial.cloud %}
{% with providernetwork_tab_active=form.initial.provider_network %}
<ul class="nav nav-tabs" role="tablist">
<li role="presentation"{% if not cloud_tab_active %} class="active"{% endif %}><a href="#site" role="tab" data-toggle="tab">Site</a></li>
<li role="presentation"{% if cloud_tab_active %} class="active"{% endif %}><a href="#cloud" role="tab" data-toggle="tab">Cloud</a></li>
<li role="presentation"{% if not providernetwork_tab_active %} class="active"{% endif %}><a href="#site" role="tab" data-toggle="tab">Site</a></li>
<li role="presentation"{% if providernetwork_tab_active %} class="active"{% endif %}><a href="#providernetwork" role="tab" data-toggle="tab">Provider Network</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane{% if not cloud_tab_active %} active{% endif %}" id="site">
<div class="tab-pane{% if not providernetwork_tab_active %} active{% endif %}" id="site">
{% render_field form.region %}
{% render_field form.site_group %}
{% render_field form.site %}
</div>
<div class="tab-pane{% if cloud_tab_active %} active{% endif %}" id="cloud">
{% render_field form.cloud %}
<div class="tab-pane{% if providernetwork_tab_active %} active{% endif %}" id="providernetwork">
{% render_field form.provider_network %}
</div>
</div>
{% endwith %}

View File

@ -85,9 +85,9 @@
</tr>
{% else %}
<tr>
<td>Cloud</td>
<td>Provider Network</td>
<td>
<a href="{{ termination.cloud.get_absolute_url }}">{{ termination.cloud }}</a>
<a href="{{ termination.provider_network.get_absolute_url }}">{{ termination.provider_network }}</a>
</td>
</tr>
{% endif %}

View File

@ -4,8 +4,8 @@
{% 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><a href="{% url 'circuits:providernetwork_list' %}">Provider Networks</a></li>
<li><a href="{% url 'circuits:providernetwork_list' %}?provider_id={{ object.provider_id }}">{{ object.provider }}</a></li>
<li>{{ object }}</li>
{% endblock %}
@ -14,7 +14,7 @@
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Cloud</strong>
<strong>Provider Network</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
@ -46,7 +46,7 @@
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:cloud_list' %}
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:providernetwork_list' %}
{% plugin_left_page object %}
</div>
<div class="col-md-8">

View File

@ -465,14 +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 %}
<li{% if not perms.circuits.view_providernetwork %} class="disabled"{% endif %}>
{% if perms.circuits.add_providernetwork %}
<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>
<a href="{% url 'circuits:providernetwork_add' %}" class="btn btn-xs btn-success" title="Add"><i class="mdi mdi-plus-thick"></i></a>
<a href="{% url 'circuits:providernetwork_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>
<a href="{% url 'circuits:providernetwork_list' %}">Provider Networks</a>
</li>
</ul>
</li>