9604 Add Termination to CircuitTermination (#17821)

* 9604 add scope type to CircuitTermination

* 9604 add scope type to CircuitTermination

* 9604 add scope type to CircuitTermination

* 9604 model_forms

* 9604 form filtersets

* 9604 form bulk_import

* 9604 form bulk_edit

* 9604 serializers

* 9604 graphql

* 9604 tests and detail template

* 9604 fix migration merge

* 9604 fix tests

* 9604 fix tests

* 9604 fix table

* 9604 updates

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* 9604 remove provider_network

* 9604 fix tests

* 9604 fix tests

* 9604 fix forms

* 9604 review changes

* 9604 scope->termination

* 9604 fix _circuit_terminations

* 9604 fix _circuit_terminations

* 9604 sitegroup -> site_group

* 9604 update docs

* 9604 fix form termination side reference

* Misc cleanup

* Fix terminations in circuits table

* Fix missing imports

* Clean up termination attrs display

* Add termination & type to CircuitTerminationTable

* Update cable tracing logic

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
Arthur Hanson
2024-10-31 06:55:08 -07:00
committed by GitHub
parent f74a9a1c76
commit a8eb455f3e
24 changed files with 649 additions and 211 deletions
+16 -1
View File
@@ -462,6 +462,10 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
children: List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
return self.circuit_terminations.all()
@strawberry_django.type(
models.Manufacturer,
@@ -705,6 +709,10 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
def parent(self) -> Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None:
return self.parent
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
return self.circuit_terminations.all()
@strawberry_django.type(
models.Site,
@@ -726,10 +734,13 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
locations: List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]
asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
circuit_terminations: List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
clusters: List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]
vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
return self.circuit_terminations.all()
@strawberry_django.type(
models.SiteGroup,
@@ -746,6 +757,10 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
def parent(self) -> Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None:
return self.parent
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
return self.circuit_terminations.all()
@strawberry_django.type(
models.VirtualChassis,
+6 -6
View File
@@ -344,7 +344,7 @@ class CableTermination(ChangeLoggedModel):
)
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
if self.termination_type.model == 'circuittermination' and self.termination._provider_network is not None:
raise ValidationError(_("Circuit terminations attached to a provider network may not be cabled."))
def save(self, *args, **kwargs):
@@ -690,19 +690,19 @@ class CablePath(models.Model):
).first()
if circuit_termination is None:
break
elif circuit_termination.provider_network:
elif circuit_termination._provider_network:
# Circuit terminates to a ProviderNetwork
path.extend([
[object_to_path_node(circuit_termination)],
[object_to_path_node(circuit_termination.provider_network)],
[object_to_path_node(circuit_termination._provider_network)],
])
is_complete = True
break
elif circuit_termination.site and not circuit_termination.cable:
# Circuit terminates to a Site
elif circuit_termination.termination and not circuit_termination.cable:
# Circuit terminates to a Region/Site/etc.
path.extend([
[object_to_path_node(circuit_termination)],
[object_to_path_node(circuit_termination.site)],
[object_to_path_node(circuit_termination.termination)],
])
break
+15 -15
View File
@@ -1167,7 +1167,7 @@ class CablePathTestCase(TestCase):
[IF1] --C1-- [CT1]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
# Create cable 1
cable1 = Cable(
@@ -1198,7 +1198,7 @@ class CablePathTestCase(TestCase):
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
# Create cable 1
cable1 = Cable(
@@ -1214,7 +1214,7 @@ class CablePathTestCase(TestCase):
)
# Create CT2
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
# Check for partial path to site
self.assertPathExists(
@@ -1266,7 +1266,7 @@ class CablePathTestCase(TestCase):
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
# Create cable 1
cable1 = Cable(
@@ -1282,7 +1282,7 @@ class CablePathTestCase(TestCase):
)
# Create CT2
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
# Check for partial path to site
self.assertPathExists(
@@ -1335,8 +1335,8 @@ class CablePathTestCase(TestCase):
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
site2 = Site.objects.create(name='Site 2', slug='site-2')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=site2, term_side='Z')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=site2, term_side='Z')
# Create cable 1
cable1 = Cable(
@@ -1365,8 +1365,8 @@ class CablePathTestCase(TestCase):
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
providernetwork = ProviderNetwork.objects.create(name='Provider Network 1', provider=self.circuit.provider)
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, provider_network=providernetwork, term_side='Z')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=providernetwork, term_side='Z')
# Create cable 1
cable1 = Cable(
@@ -1413,8 +1413,8 @@ class CablePathTestCase(TestCase):
frontport2_2 = FrontPort.objects.create(
device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
)
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
# Create cables
cable1 = Cable(
@@ -1499,10 +1499,10 @@ class CablePathTestCase(TestCase):
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
circuit2 = Circuit.objects.create(provider=self.circuit.provider, type=self.circuit.type, cid='Circuit 2')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, site=self.site, term_side='A')
circuittermination4 = CircuitTermination.objects.create(circuit=circuit2, site=self.site, term_side='Z')
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, termination=self.site, term_side='A')
circuittermination4 = CircuitTermination.objects.create(circuit=circuit2, termination=self.site, term_side='Z')
# Create cables
cable1 = Cable(
+3 -3
View File
@@ -5135,7 +5135,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuit = Circuit.objects.create(cid='Circuit 1', provider=provider, type=circuit_type)
circuit_termination = CircuitTermination.objects.create(circuit=circuit, term_side='A', site=sites[0])
circuit_termination = CircuitTermination.objects.create(circuit=circuit, term_side='A', termination=sites[0])
# Cables
cables = (
@@ -5308,9 +5308,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_site(self):
site = Site.objects.all()[:2]
params = {'site_id': [site[0].pk, site[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11)
params = {'site': [site[0].slug, site[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11)
def test_tenant(self):
tenant = Tenant.objects.all()[:2]
+3 -3
View File
@@ -762,9 +762,9 @@ class CableTestCase(TestCase):
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuit1 = Circuit.objects.create(provider=provider, type=circuittype, cid='1')
circuit2 = Circuit.objects.create(provider=provider, type=circuittype, cid='2')
CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A')
CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z')
CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A')
CircuitTermination.objects.create(circuit=circuit1, termination=site, term_side='A')
CircuitTermination.objects.create(circuit=circuit1, termination=site, term_side='Z')
CircuitTermination.objects.create(circuit=circuit2, termination=provider_network, term_side='A')
def test_cable_creation(self):
"""
+23 -3
View File
@@ -242,6 +242,10 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView):
extra=(
(Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
(Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
(
Circuit.objects.restrict(request.user, 'view').filter(terminations___region=instance).distinct(),
'region_id'
),
),
),
}
@@ -324,6 +328,10 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
extra=(
(Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
(Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
(
Circuit.objects.restrict(request.user, 'view').filter(terminations___site_group=instance).distinct(),
'site_group_id'
),
),
),
}
@@ -404,8 +412,10 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView):
scope_id=instance.pk
), 'site'),
(ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'),
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(),
'site_id'),
(
Circuit.objects.restrict(request.user, 'view').filter(terminations___site=instance).distinct(),
'site_id'
),
),
),
}
@@ -475,7 +485,17 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView):
def get_extra_context(self, request, instance):
locations = instance.get_descendants(include_self=True)
return {
'related_models': self.get_related_models(request, locations, [CableTermination]),
'related_models': self.get_related_models(
request,
locations,
[CableTermination],
(
(
Circuit.objects.restrict(request.user, 'view').filter(terminations___location=instance).distinct(),
'location_id'
),
),
),
}