models.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. from django.contrib.contenttypes.fields import GenericRelation
  2. from django.core.exceptions import ValidationError
  3. from django.db import models
  4. from django.urls import reverse
  5. from dcim.fields import ASNField
  6. from dcim.models import CableTermination, PathEndpoint
  7. from extras.models import ObjectChange
  8. from extras.utils import extras_features
  9. from netbox.models import BigIDModel, ChangeLoggedModel, OrganizationalModel, PrimaryModel
  10. from utilities.querysets import RestrictedQuerySet
  11. from .choices import *
  12. __all__ = (
  13. 'Circuit',
  14. 'CircuitTermination',
  15. 'CircuitType',
  16. 'ProviderNetwork',
  17. 'Provider',
  18. )
  19. @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
  20. class Provider(PrimaryModel):
  21. """
  22. Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
  23. stores information pertinent to the user's relationship with the Provider.
  24. """
  25. name = models.CharField(
  26. max_length=100,
  27. unique=True
  28. )
  29. slug = models.SlugField(
  30. max_length=100,
  31. unique=True
  32. )
  33. asn = ASNField(
  34. blank=True,
  35. null=True,
  36. verbose_name='ASN',
  37. help_text='32-bit autonomous system number'
  38. )
  39. account = models.CharField(
  40. max_length=30,
  41. blank=True,
  42. verbose_name='Account number'
  43. )
  44. portal_url = models.URLField(
  45. blank=True,
  46. verbose_name='Portal URL'
  47. )
  48. noc_contact = models.TextField(
  49. blank=True,
  50. verbose_name='NOC contact'
  51. )
  52. admin_contact = models.TextField(
  53. blank=True,
  54. verbose_name='Admin contact'
  55. )
  56. comments = models.TextField(
  57. blank=True
  58. )
  59. # Generic relations
  60. contacts = GenericRelation(
  61. to='tenancy.ContactAssignment'
  62. )
  63. objects = RestrictedQuerySet.as_manager()
  64. clone_fields = [
  65. 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
  66. ]
  67. class Meta:
  68. ordering = ['name']
  69. def __str__(self):
  70. return self.name
  71. def get_absolute_url(self):
  72. return reverse('circuits:provider', args=[self.pk])
  73. #
  74. # Provider networks
  75. #
  76. @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
  77. class ProviderNetwork(PrimaryModel):
  78. """
  79. This represents a provider network which exists outside of NetBox, the details of which are unknown or
  80. unimportant to the user.
  81. """
  82. name = models.CharField(
  83. max_length=100
  84. )
  85. provider = models.ForeignKey(
  86. to='circuits.Provider',
  87. on_delete=models.PROTECT,
  88. related_name='networks'
  89. )
  90. description = models.CharField(
  91. max_length=200,
  92. blank=True
  93. )
  94. comments = models.TextField(
  95. blank=True
  96. )
  97. objects = RestrictedQuerySet.as_manager()
  98. class Meta:
  99. ordering = ('provider', 'name')
  100. constraints = (
  101. models.UniqueConstraint(
  102. fields=('provider', 'name'),
  103. name='circuits_providernetwork_provider_name'
  104. ),
  105. )
  106. unique_together = ('provider', 'name')
  107. def __str__(self):
  108. return self.name
  109. def get_absolute_url(self):
  110. return reverse('circuits:providernetwork', args=[self.pk])
  111. @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
  112. class CircuitType(OrganizationalModel):
  113. """
  114. Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
  115. "Long Haul," "Metro," or "Out-of-Band".
  116. """
  117. name = models.CharField(
  118. max_length=100,
  119. unique=True
  120. )
  121. slug = models.SlugField(
  122. max_length=100,
  123. unique=True
  124. )
  125. description = models.CharField(
  126. max_length=200,
  127. blank=True,
  128. )
  129. objects = RestrictedQuerySet.as_manager()
  130. class Meta:
  131. ordering = ['name']
  132. def __str__(self):
  133. return self.name
  134. def get_absolute_url(self):
  135. return reverse('circuits:circuittype', args=[self.pk])
  136. @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
  137. class Circuit(PrimaryModel):
  138. """
  139. A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
  140. circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
  141. in Kbps.
  142. """
  143. cid = models.CharField(
  144. max_length=100,
  145. verbose_name='Circuit ID'
  146. )
  147. provider = models.ForeignKey(
  148. to='circuits.Provider',
  149. on_delete=models.PROTECT,
  150. related_name='circuits'
  151. )
  152. type = models.ForeignKey(
  153. to='CircuitType',
  154. on_delete=models.PROTECT,
  155. related_name='circuits'
  156. )
  157. status = models.CharField(
  158. max_length=50,
  159. choices=CircuitStatusChoices,
  160. default=CircuitStatusChoices.STATUS_ACTIVE
  161. )
  162. tenant = models.ForeignKey(
  163. to='tenancy.Tenant',
  164. on_delete=models.PROTECT,
  165. related_name='circuits',
  166. blank=True,
  167. null=True
  168. )
  169. install_date = models.DateField(
  170. blank=True,
  171. null=True,
  172. verbose_name='Date installed'
  173. )
  174. commit_rate = models.PositiveIntegerField(
  175. blank=True,
  176. null=True,
  177. verbose_name='Commit rate (Kbps)')
  178. description = models.CharField(
  179. max_length=200,
  180. blank=True
  181. )
  182. comments = models.TextField(
  183. blank=True
  184. )
  185. # Generic relations
  186. contacts = GenericRelation(
  187. to='tenancy.ContactAssignment'
  188. )
  189. images = GenericRelation(
  190. to='extras.ImageAttachment'
  191. )
  192. # Cache associated CircuitTerminations
  193. termination_a = models.ForeignKey(
  194. to='circuits.CircuitTermination',
  195. on_delete=models.SET_NULL,
  196. related_name='+',
  197. editable=False,
  198. blank=True,
  199. null=True
  200. )
  201. termination_z = models.ForeignKey(
  202. to='circuits.CircuitTermination',
  203. on_delete=models.SET_NULL,
  204. related_name='+',
  205. editable=False,
  206. blank=True,
  207. null=True
  208. )
  209. objects = RestrictedQuerySet.as_manager()
  210. clone_fields = [
  211. 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
  212. ]
  213. class Meta:
  214. ordering = ['provider', 'cid']
  215. unique_together = ['provider', 'cid']
  216. def __str__(self):
  217. return self.cid
  218. def get_absolute_url(self):
  219. return reverse('circuits:circuit', args=[self.pk])
  220. def get_status_class(self):
  221. return CircuitStatusChoices.CSS_CLASSES.get(self.status)
  222. @extras_features('webhooks')
  223. class CircuitTermination(ChangeLoggedModel, CableTermination):
  224. circuit = models.ForeignKey(
  225. to='circuits.Circuit',
  226. on_delete=models.CASCADE,
  227. related_name='terminations'
  228. )
  229. term_side = models.CharField(
  230. max_length=1,
  231. choices=CircuitTerminationSideChoices,
  232. verbose_name='Termination'
  233. )
  234. site = models.ForeignKey(
  235. to='dcim.Site',
  236. on_delete=models.PROTECT,
  237. related_name='circuit_terminations',
  238. blank=True,
  239. null=True
  240. )
  241. provider_network = models.ForeignKey(
  242. to=ProviderNetwork,
  243. on_delete=models.PROTECT,
  244. related_name='circuit_terminations',
  245. blank=True,
  246. null=True
  247. )
  248. port_speed = models.PositiveIntegerField(
  249. verbose_name='Port speed (Kbps)',
  250. blank=True,
  251. null=True
  252. )
  253. upstream_speed = models.PositiveIntegerField(
  254. blank=True,
  255. null=True,
  256. verbose_name='Upstream speed (Kbps)',
  257. help_text='Upstream speed, if different from port speed'
  258. )
  259. xconnect_id = models.CharField(
  260. max_length=50,
  261. blank=True,
  262. verbose_name='Cross-connect ID'
  263. )
  264. pp_info = models.CharField(
  265. max_length=100,
  266. blank=True,
  267. verbose_name='Patch panel/port(s)'
  268. )
  269. description = models.CharField(
  270. max_length=200,
  271. blank=True
  272. )
  273. objects = RestrictedQuerySet.as_manager()
  274. class Meta:
  275. ordering = ['circuit', 'term_side']
  276. unique_together = ['circuit', 'term_side']
  277. def __str__(self):
  278. return f'Termination {self.term_side}: {self.site or self.provider_network}'
  279. def get_absolute_url(self):
  280. if self.site:
  281. return self.site.get_absolute_url()
  282. return self.provider_network.get_absolute_url()
  283. def clean(self):
  284. super().clean()
  285. # Must define either site *or* provider network
  286. if self.site is None and self.provider_network is None:
  287. raise ValidationError("A circuit termination must attach to either a site or a provider network.")
  288. if self.site and self.provider_network:
  289. raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
  290. def to_objectchange(self, action):
  291. # Annotate the parent Circuit
  292. try:
  293. circuit = self.circuit
  294. except Circuit.DoesNotExist:
  295. # Parent circuit has been deleted
  296. circuit = None
  297. return super().to_objectchange(action, related_object=circuit)
  298. @property
  299. def parent_object(self):
  300. return self.circuit
  301. def get_peer_termination(self):
  302. peer_side = 'Z' if self.term_side == 'A' else 'A'
  303. try:
  304. return CircuitTermination.objects.prefetch_related('site').get(
  305. circuit=self.circuit,
  306. term_side=peer_side
  307. )
  308. except CircuitTermination.DoesNotExist:
  309. return None