models.py 10 KB

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