sites.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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 django.utils.translation import gettext_lazy as _
  6. from timezone_field import TimeZoneField
  7. from dcim.choices import *
  8. from dcim.constants import *
  9. from netbox.models import NestedGroupModel, PrimaryModel
  10. from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
  11. from utilities.fields import NaturalOrderingField
  12. __all__ = (
  13. 'Location',
  14. 'Region',
  15. 'Site',
  16. 'SiteGroup',
  17. )
  18. #
  19. # Regions
  20. #
  21. class Region(ContactsMixin, NestedGroupModel):
  22. """
  23. A region represents a geographic collection of sites. For example, you might create regions representing countries,
  24. states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
  25. also considered to be members of its parent and ancestor region(s).
  26. """
  27. vlan_groups = GenericRelation(
  28. to='ipam.VLANGroup',
  29. content_type_field='scope_type',
  30. object_id_field='scope_id',
  31. related_query_name='region'
  32. )
  33. class Meta:
  34. constraints = (
  35. models.UniqueConstraint(
  36. fields=('parent', 'name'),
  37. name='%(app_label)s_%(class)s_parent_name'
  38. ),
  39. models.UniqueConstraint(
  40. fields=('name',),
  41. name='%(app_label)s_%(class)s_name',
  42. condition=Q(parent__isnull=True),
  43. violation_error_message=_("A top-level region with this name already exists.")
  44. ),
  45. models.UniqueConstraint(
  46. fields=('parent', 'slug'),
  47. name='%(app_label)s_%(class)s_parent_slug'
  48. ),
  49. models.UniqueConstraint(
  50. fields=('slug',),
  51. name='%(app_label)s_%(class)s_slug',
  52. condition=Q(parent__isnull=True),
  53. violation_error_message=_("A top-level region with this slug already exists.")
  54. ),
  55. )
  56. verbose_name = _('region')
  57. verbose_name_plural = _('regions')
  58. def get_absolute_url(self):
  59. return reverse('dcim:region', args=[self.pk])
  60. def get_site_count(self):
  61. return Site.objects.filter(
  62. Q(region=self) |
  63. Q(region__in=self.get_descendants())
  64. ).count()
  65. #
  66. # Site groups
  67. #
  68. class SiteGroup(ContactsMixin, NestedGroupModel):
  69. """
  70. A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
  71. within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
  72. nested recursively to form a hierarchy.
  73. """
  74. vlan_groups = GenericRelation(
  75. to='ipam.VLANGroup',
  76. content_type_field='scope_type',
  77. object_id_field='scope_id',
  78. related_query_name='site_group'
  79. )
  80. class Meta:
  81. constraints = (
  82. models.UniqueConstraint(
  83. fields=('parent', 'name'),
  84. name='%(app_label)s_%(class)s_parent_name'
  85. ),
  86. models.UniqueConstraint(
  87. fields=('name',),
  88. name='%(app_label)s_%(class)s_name',
  89. condition=Q(parent__isnull=True),
  90. violation_error_message=_("A top-level site group with this name already exists.")
  91. ),
  92. models.UniqueConstraint(
  93. fields=('parent', 'slug'),
  94. name='%(app_label)s_%(class)s_parent_slug'
  95. ),
  96. models.UniqueConstraint(
  97. fields=('slug',),
  98. name='%(app_label)s_%(class)s_slug',
  99. condition=Q(parent__isnull=True),
  100. violation_error_message=_("A top-level site group with this slug already exists.")
  101. ),
  102. )
  103. verbose_name = _('site group')
  104. verbose_name_plural = _('site groups')
  105. def get_absolute_url(self):
  106. return reverse('dcim:sitegroup', args=[self.pk])
  107. def get_site_count(self):
  108. return Site.objects.filter(
  109. Q(group=self) |
  110. Q(group__in=self.get_descendants())
  111. ).count()
  112. #
  113. # Sites
  114. #
  115. class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
  116. """
  117. A Site represents a geographic location within a network; typically a building or campus. The optional facility
  118. field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
  119. """
  120. name = models.CharField(
  121. verbose_name=_('name'),
  122. max_length=100,
  123. unique=True,
  124. help_text=_("Full name of the site")
  125. )
  126. _name = NaturalOrderingField(
  127. target_field='name',
  128. max_length=100,
  129. blank=True
  130. )
  131. slug = models.SlugField(
  132. verbose_name=_('slug'),
  133. max_length=100,
  134. unique=True
  135. )
  136. status = models.CharField(
  137. verbose_name=_('status'),
  138. max_length=50,
  139. choices=SiteStatusChoices,
  140. default=SiteStatusChoices.STATUS_ACTIVE
  141. )
  142. region = models.ForeignKey(
  143. to='dcim.Region',
  144. on_delete=models.SET_NULL,
  145. related_name='sites',
  146. blank=True,
  147. null=True
  148. )
  149. group = models.ForeignKey(
  150. to='dcim.SiteGroup',
  151. on_delete=models.SET_NULL,
  152. related_name='sites',
  153. blank=True,
  154. null=True
  155. )
  156. tenant = models.ForeignKey(
  157. to='tenancy.Tenant',
  158. on_delete=models.PROTECT,
  159. related_name='sites',
  160. blank=True,
  161. null=True
  162. )
  163. facility = models.CharField(
  164. verbose_name=_('facility'),
  165. max_length=50,
  166. blank=True,
  167. help_text=_('Local facility ID or description')
  168. )
  169. asns = models.ManyToManyField(
  170. to='ipam.ASN',
  171. related_name='sites',
  172. blank=True
  173. )
  174. time_zone = TimeZoneField(
  175. blank=True
  176. )
  177. physical_address = models.CharField(
  178. verbose_name=_('physical address'),
  179. max_length=200,
  180. blank=True,
  181. help_text=_('Physical location of the building')
  182. )
  183. shipping_address = models.CharField(
  184. verbose_name=_('shipping address'),
  185. max_length=200,
  186. blank=True,
  187. help_text=_('If different from the physical address')
  188. )
  189. latitude = models.DecimalField(
  190. verbose_name=_('latitude'),
  191. max_digits=8,
  192. decimal_places=6,
  193. blank=True,
  194. null=True,
  195. help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
  196. )
  197. longitude = models.DecimalField(
  198. verbose_name=_('longitude'),
  199. max_digits=9,
  200. decimal_places=6,
  201. blank=True,
  202. null=True,
  203. help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
  204. )
  205. # Generic relations
  206. vlan_groups = GenericRelation(
  207. to='ipam.VLANGroup',
  208. content_type_field='scope_type',
  209. object_id_field='scope_id',
  210. related_query_name='site'
  211. )
  212. clone_fields = (
  213. 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'physical_address', 'shipping_address',
  214. 'latitude', 'longitude', 'description',
  215. )
  216. class Meta:
  217. ordering = ('_name',)
  218. verbose_name = _('site')
  219. verbose_name_plural = _('sites')
  220. def __str__(self):
  221. return self.name
  222. def get_absolute_url(self):
  223. return reverse('dcim:site', args=[self.pk])
  224. def get_status_color(self):
  225. return SiteStatusChoices.colors.get(self.status)
  226. #
  227. # Locations
  228. #
  229. class Location(ContactsMixin, ImageAttachmentsMixin, NestedGroupModel):
  230. """
  231. A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
  232. site, or a room within a building, for example.
  233. """
  234. site = models.ForeignKey(
  235. to='dcim.Site',
  236. on_delete=models.CASCADE,
  237. related_name='locations'
  238. )
  239. status = models.CharField(
  240. verbose_name=_('status'),
  241. max_length=50,
  242. choices=LocationStatusChoices,
  243. default=LocationStatusChoices.STATUS_ACTIVE
  244. )
  245. tenant = models.ForeignKey(
  246. to='tenancy.Tenant',
  247. on_delete=models.PROTECT,
  248. related_name='locations',
  249. blank=True,
  250. null=True
  251. )
  252. facility = models.CharField(
  253. verbose_name=_('facility'),
  254. max_length=50,
  255. blank=True,
  256. help_text=_('Local facility ID or description')
  257. )
  258. # Generic relations
  259. vlan_groups = GenericRelation(
  260. to='ipam.VLANGroup',
  261. content_type_field='scope_type',
  262. object_id_field='scope_id',
  263. related_query_name='location'
  264. )
  265. clone_fields = ('site', 'parent', 'status', 'tenant', 'facility', 'description')
  266. prerequisite_models = (
  267. 'dcim.Site',
  268. )
  269. class Meta:
  270. ordering = ['site', 'name']
  271. constraints = (
  272. models.UniqueConstraint(
  273. fields=('site', 'parent', 'name'),
  274. name='%(app_label)s_%(class)s_parent_name'
  275. ),
  276. models.UniqueConstraint(
  277. fields=('site', 'name'),
  278. name='%(app_label)s_%(class)s_name',
  279. condition=Q(parent__isnull=True),
  280. violation_error_message=_("A location with this name already exists within the specified site.")
  281. ),
  282. models.UniqueConstraint(
  283. fields=('site', 'parent', 'slug'),
  284. name='%(app_label)s_%(class)s_parent_slug'
  285. ),
  286. models.UniqueConstraint(
  287. fields=('site', 'slug'),
  288. name='%(app_label)s_%(class)s_slug',
  289. condition=Q(parent__isnull=True),
  290. violation_error_message=_("A location with this slug already exists within the specified site.")
  291. ),
  292. )
  293. verbose_name = _('location')
  294. verbose_name_plural = _('locations')
  295. def get_absolute_url(self):
  296. return reverse('dcim:location', args=[self.pk])
  297. def get_status_color(self):
  298. return LocationStatusChoices.colors.get(self.status)
  299. def clean(self):
  300. super().clean()
  301. # Parent Location (if any) must belong to the same Site
  302. if self.parent and self.parent.site != self.site:
  303. raise ValidationError(_(
  304. "Parent location ({parent}) must belong to the same site ({site})."
  305. ).format(parent=self.parent, site=self.site))