from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from dcim.fields import ASNField from dcim.models import LinkTermination, PathEndpoint from extras.models import ObjectChange from extras.utils import extras_features from netbox.models import BigIDModel, ChangeLoggedModel, OrganizationalModel, PrimaryModel from utilities.querysets import RestrictedQuerySet from .choices import * __all__ = ( 'Circuit', 'CircuitTermination', 'CircuitType', 'ProviderNetwork', 'Provider', ) @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Provider(PrimaryModel): """ Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model stores information pertinent to the user's relationship with the Provider. """ name = models.CharField( max_length=100, unique=True ) slug = models.SlugField( max_length=100, unique=True ) asn = ASNField( blank=True, null=True, verbose_name='ASN', help_text='32-bit autonomous system number' ) account = models.CharField( max_length=30, blank=True, verbose_name='Account number' ) portal_url = models.URLField( blank=True, verbose_name='Portal URL' ) noc_contact = models.TextField( blank=True, verbose_name='NOC contact' ) admin_contact = models.TextField( blank=True, verbose_name='Admin contact' ) comments = models.TextField( blank=True ) # Generic relations contacts = GenericRelation( to='tenancy.ContactAssignment' ) objects = RestrictedQuerySet.as_manager() clone_fields = [ 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', ] class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('circuits:provider', args=[self.pk]) # # Provider networks # @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') 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='networks' ) description = models.CharField( max_length=200, blank=True ) comments = models.TextField( blank=True ) objects = RestrictedQuerySet.as_manager() class Meta: ordering = ('provider', 'name') constraints = ( models.UniqueConstraint( fields=('provider', 'name'), name='circuits_providernetwork_provider_name' ), ) unique_together = ('provider', 'name') def __str__(self): return self.name def get_absolute_url(self): return reverse('circuits:providernetwork', args=[self.pk]) @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class CircuitType(OrganizationalModel): """ Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named "Long Haul," "Metro," or "Out-of-Band". """ name = models.CharField( max_length=100, unique=True ) slug = models.SlugField( max_length=100, unique=True ) description = models.CharField( max_length=200, blank=True, ) objects = RestrictedQuerySet.as_manager() class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('circuits:circuittype', args=[self.pk]) @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Circuit(PrimaryModel): """ A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured in Kbps. """ cid = models.CharField( max_length=100, verbose_name='Circuit ID' ) provider = models.ForeignKey( to='circuits.Provider', on_delete=models.PROTECT, related_name='circuits' ) type = models.ForeignKey( to='CircuitType', on_delete=models.PROTECT, related_name='circuits' ) status = models.CharField( max_length=50, choices=CircuitStatusChoices, default=CircuitStatusChoices.STATUS_ACTIVE ) tenant = models.ForeignKey( to='tenancy.Tenant', on_delete=models.PROTECT, related_name='circuits', blank=True, null=True ) install_date = models.DateField( blank=True, null=True, verbose_name='Date installed' ) commit_rate = models.PositiveIntegerField( blank=True, null=True, verbose_name='Commit rate (Kbps)') description = models.CharField( max_length=200, blank=True ) comments = models.TextField( blank=True ) # Generic relations contacts = GenericRelation( to='tenancy.ContactAssignment' ) images = GenericRelation( to='extras.ImageAttachment' ) # Cache associated CircuitTerminations termination_a = models.ForeignKey( to='circuits.CircuitTermination', on_delete=models.SET_NULL, related_name='+', editable=False, blank=True, null=True ) termination_z = models.ForeignKey( to='circuits.CircuitTermination', on_delete=models.SET_NULL, related_name='+', editable=False, blank=True, null=True ) objects = RestrictedQuerySet.as_manager() clone_fields = [ 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', ] class Meta: ordering = ['provider', 'cid'] unique_together = ['provider', 'cid'] def __str__(self): return self.cid def get_absolute_url(self): return reverse('circuits:circuit', args=[self.pk]) def get_status_class(self): return CircuitStatusChoices.CSS_CLASSES.get(self.status) @extras_features('webhooks') class CircuitTermination(ChangeLoggedModel, LinkTermination): circuit = models.ForeignKey( to='circuits.Circuit', on_delete=models.CASCADE, related_name='terminations' ) term_side = models.CharField( max_length=1, choices=CircuitTerminationSideChoices, verbose_name='Termination' ) site = models.ForeignKey( to='dcim.Site', on_delete=models.PROTECT, related_name='circuit_terminations', blank=True, null=True ) provider_network = models.ForeignKey( to=ProviderNetwork, on_delete=models.PROTECT, related_name='circuit_terminations', blank=True, null=True ) port_speed = models.PositiveIntegerField( verbose_name='Port speed (Kbps)', blank=True, null=True ) upstream_speed = models.PositiveIntegerField( blank=True, null=True, verbose_name='Upstream speed (Kbps)', help_text='Upstream speed, if different from port speed' ) xconnect_id = models.CharField( max_length=50, blank=True, verbose_name='Cross-connect ID' ) pp_info = models.CharField( max_length=100, blank=True, verbose_name='Patch panel/port(s)' ) description = models.CharField( max_length=200, blank=True ) objects = RestrictedQuerySet.as_manager() class Meta: ordering = ['circuit', 'term_side'] unique_together = ['circuit', 'term_side'] def __str__(self): return f'Termination {self.term_side}: {self.site or self.provider_network}' def get_absolute_url(self): if self.site: return self.site.get_absolute_url() return self.provider_network.get_absolute_url() def clean(self): super().clean() # 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 try: circuit = self.circuit except Circuit.DoesNotExist: # Parent circuit has been deleted circuit = None return super().to_objectchange(action, related_object=circuit) @property def parent_object(self): return self.circuit def get_peer_termination(self): peer_side = 'Z' if self.term_side == 'A' else 'A' try: return CircuitTermination.objects.prefetch_related('site').get( circuit=self.circuit, term_side=peer_side ) except CircuitTermination.DoesNotExist: return None