models.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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. 'Cloud',
  16. 'Provider',
  17. )
  18. @extras_features('custom_fields', 'custom_links', 'export_templates', '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. # Clouds
  84. #
  85. @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
  86. class Cloud(PrimaryModel):
  87. name = models.CharField(
  88. max_length=100
  89. )
  90. provider = models.ForeignKey(
  91. to='circuits.Provider',
  92. on_delete=models.PROTECT,
  93. related_name='clouds'
  94. )
  95. description = models.CharField(
  96. max_length=200,
  97. blank=True
  98. )
  99. comments = models.TextField(
  100. blank=True
  101. )
  102. csv_headers = [
  103. 'provider', 'name', 'description', 'comments',
  104. ]
  105. objects = RestrictedQuerySet.as_manager()
  106. class Meta:
  107. ordering = ('provider', 'name')
  108. constraints = (
  109. models.UniqueConstraint(
  110. fields=('provider', 'name'),
  111. name='circuits_cloud_provider_name'
  112. ),
  113. )
  114. unique_together = ('provider', 'name')
  115. def __str__(self):
  116. return self.name
  117. def get_absolute_url(self):
  118. return reverse('circuits:cloud', args=[self.pk])
  119. def to_csv(self):
  120. return (
  121. self.provider.name,
  122. self.name,
  123. self.description,
  124. self.comments,
  125. )
  126. @extras_features('custom_fields', 'export_templates', 'webhooks')
  127. class CircuitType(OrganizationalModel):
  128. """
  129. Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
  130. "Long Haul," "Metro," or "Out-of-Band".
  131. """
  132. name = models.CharField(
  133. max_length=100,
  134. unique=True
  135. )
  136. slug = models.SlugField(
  137. max_length=100,
  138. unique=True
  139. )
  140. description = models.CharField(
  141. max_length=200,
  142. blank=True,
  143. )
  144. objects = RestrictedQuerySet.as_manager()
  145. csv_headers = ['name', 'slug', 'description']
  146. class Meta:
  147. ordering = ['name']
  148. def __str__(self):
  149. return self.name
  150. def get_absolute_url(self):
  151. return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
  152. def to_csv(self):
  153. return (
  154. self.name,
  155. self.slug,
  156. self.description,
  157. )
  158. @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
  159. class Circuit(PrimaryModel):
  160. """
  161. A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
  162. circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
  163. in Kbps.
  164. """
  165. cid = models.CharField(
  166. max_length=100,
  167. verbose_name='Circuit ID'
  168. )
  169. provider = models.ForeignKey(
  170. to='circuits.Provider',
  171. on_delete=models.PROTECT,
  172. related_name='circuits'
  173. )
  174. type = models.ForeignKey(
  175. to='CircuitType',
  176. on_delete=models.PROTECT,
  177. related_name='circuits'
  178. )
  179. status = models.CharField(
  180. max_length=50,
  181. choices=CircuitStatusChoices,
  182. default=CircuitStatusChoices.STATUS_ACTIVE
  183. )
  184. tenant = models.ForeignKey(
  185. to='tenancy.Tenant',
  186. on_delete=models.PROTECT,
  187. related_name='circuits',
  188. blank=True,
  189. null=True
  190. )
  191. install_date = models.DateField(
  192. blank=True,
  193. null=True,
  194. verbose_name='Date installed'
  195. )
  196. commit_rate = models.PositiveIntegerField(
  197. blank=True,
  198. null=True,
  199. verbose_name='Commit rate (Kbps)')
  200. description = models.CharField(
  201. max_length=200,
  202. blank=True
  203. )
  204. comments = models.TextField(
  205. blank=True
  206. )
  207. # Cache associated CircuitTerminations
  208. termination_a = models.ForeignKey(
  209. to='circuits.CircuitTermination',
  210. on_delete=models.SET_NULL,
  211. related_name='+',
  212. editable=False,
  213. blank=True,
  214. null=True
  215. )
  216. termination_z = models.ForeignKey(
  217. to='circuits.CircuitTermination',
  218. on_delete=models.SET_NULL,
  219. related_name='+',
  220. editable=False,
  221. blank=True,
  222. null=True
  223. )
  224. objects = RestrictedQuerySet.as_manager()
  225. csv_headers = [
  226. 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
  227. ]
  228. clone_fields = [
  229. 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
  230. ]
  231. class Meta:
  232. ordering = ['provider', 'cid']
  233. unique_together = ['provider', 'cid']
  234. def __str__(self):
  235. return self.cid
  236. def get_absolute_url(self):
  237. return reverse('circuits:circuit', args=[self.pk])
  238. def to_csv(self):
  239. return (
  240. self.cid,
  241. self.provider.name,
  242. self.type.name,
  243. self.get_status_display(),
  244. self.tenant.name if self.tenant else None,
  245. self.install_date,
  246. self.commit_rate,
  247. self.description,
  248. self.comments,
  249. )
  250. def get_status_class(self):
  251. return CircuitStatusChoices.CSS_CLASSES.get(self.status)
  252. @extras_features('webhooks')
  253. class CircuitTermination(ChangeLoggedModel, PathEndpoint, CableTermination):
  254. circuit = models.ForeignKey(
  255. to='circuits.Circuit',
  256. on_delete=models.CASCADE,
  257. related_name='terminations'
  258. )
  259. term_side = models.CharField(
  260. max_length=1,
  261. choices=CircuitTerminationSideChoices,
  262. verbose_name='Termination'
  263. )
  264. site = models.ForeignKey(
  265. to='dcim.Site',
  266. on_delete=models.PROTECT,
  267. related_name='circuit_terminations',
  268. blank=True,
  269. null=True
  270. )
  271. cloud = models.ForeignKey(
  272. to=Cloud,
  273. on_delete=models.PROTECT,
  274. related_name='circuit_terminations',
  275. blank=True,
  276. null=True
  277. )
  278. port_speed = models.PositiveIntegerField(
  279. verbose_name='Port speed (Kbps)',
  280. blank=True,
  281. null=True
  282. )
  283. upstream_speed = models.PositiveIntegerField(
  284. blank=True,
  285. null=True,
  286. verbose_name='Upstream speed (Kbps)',
  287. help_text='Upstream speed, if different from port speed'
  288. )
  289. xconnect_id = models.CharField(
  290. max_length=50,
  291. blank=True,
  292. verbose_name='Cross-connect ID'
  293. )
  294. pp_info = models.CharField(
  295. max_length=100,
  296. blank=True,
  297. verbose_name='Patch panel/port(s)'
  298. )
  299. description = models.CharField(
  300. max_length=200,
  301. blank=True
  302. )
  303. objects = RestrictedQuerySet.as_manager()
  304. class Meta:
  305. ordering = ['circuit', 'term_side']
  306. unique_together = ['circuit', 'term_side']
  307. def __str__(self):
  308. if self.site:
  309. return str(self.site)
  310. return str(self.cloud)
  311. def get_absolute_url(self):
  312. if self.site:
  313. return self.site.get_absolute_url()
  314. return self.cloud.get_absolute_url()
  315. def clean(self):
  316. super().clean()
  317. # Must define either site *or* cloud
  318. if self.site is None and self.cloud is None:
  319. raise ValidationError("A circuit termination must attach to either a site or a cloud.")
  320. if self.site and self.cloud:
  321. raise ValidationError("A circuit termination cannot attach to both a site and a cloud.")
  322. def to_objectchange(self, action):
  323. # Annotate the parent Circuit
  324. try:
  325. circuit = self.circuit
  326. except Circuit.DoesNotExist:
  327. # Parent circuit has been deleted
  328. circuit = None
  329. return super().to_objectchange(action, related_object=circuit)
  330. @property
  331. def parent_object(self):
  332. return self.circuit
  333. def get_peer_termination(self):
  334. peer_side = 'Z' if self.term_side == 'A' else 'A'
  335. try:
  336. return CircuitTermination.objects.prefetch_related('site').get(
  337. circuit=self.circuit,
  338. term_side=peer_side
  339. )
  340. except CircuitTermination.DoesNotExist:
  341. return None