models.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. from django import forms
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.core.exceptions import ValidationError
  4. from dcim.forms.common import InterfaceCommonForm
  5. from dcim.forms.models import INTERFACE_MODE_HELP_TEXT
  6. from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
  7. from ipam.models import IPAddress, VLAN, VLANGroup, VRF
  8. from netbox.forms import NetBoxModelForm
  9. from tenancy.forms import TenancyForm
  10. from utilities.forms import (
  11. BootstrapMixin, CommentField, ConfirmationForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
  12. JSONField, SlugField, StaticSelect,
  13. )
  14. from virtualization.models import *
  15. __all__ = (
  16. 'ClusterAddDevicesForm',
  17. 'ClusterForm',
  18. 'ClusterGroupForm',
  19. 'ClusterRemoveDevicesForm',
  20. 'ClusterTypeForm',
  21. 'VirtualMachineForm',
  22. 'VMInterfaceForm',
  23. )
  24. class ClusterTypeForm(NetBoxModelForm):
  25. slug = SlugField()
  26. class Meta:
  27. model = ClusterType
  28. fields = (
  29. 'name', 'slug', 'description', 'tags',
  30. )
  31. class ClusterGroupForm(NetBoxModelForm):
  32. slug = SlugField()
  33. class Meta:
  34. model = ClusterGroup
  35. fields = (
  36. 'name', 'slug', 'description', 'tags',
  37. )
  38. class ClusterForm(TenancyForm, NetBoxModelForm):
  39. type = DynamicModelChoiceField(
  40. queryset=ClusterType.objects.all()
  41. )
  42. group = DynamicModelChoiceField(
  43. queryset=ClusterGroup.objects.all(),
  44. required=False
  45. )
  46. region = DynamicModelChoiceField(
  47. queryset=Region.objects.all(),
  48. required=False,
  49. initial_params={
  50. 'sites': '$site'
  51. }
  52. )
  53. site_group = DynamicModelChoiceField(
  54. queryset=SiteGroup.objects.all(),
  55. required=False,
  56. initial_params={
  57. 'sites': '$site'
  58. }
  59. )
  60. site = DynamicModelChoiceField(
  61. queryset=Site.objects.all(),
  62. required=False,
  63. query_params={
  64. 'region_id': '$region',
  65. 'group_id': '$site_group',
  66. }
  67. )
  68. comments = CommentField()
  69. fieldsets = (
  70. ('Cluster', ('name', 'type', 'group', 'status', 'tags')),
  71. ('Site', ('region', 'site_group', 'site')),
  72. ('Tenancy', ('tenant_group', 'tenant')),
  73. )
  74. class Meta:
  75. model = Cluster
  76. fields = (
  77. 'name', 'type', 'group', 'status', 'tenant', 'region', 'site_group', 'site', 'comments', 'tags',
  78. )
  79. widgets = {
  80. 'status': StaticSelect(),
  81. }
  82. class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
  83. region = DynamicModelChoiceField(
  84. queryset=Region.objects.all(),
  85. required=False,
  86. null_option='None'
  87. )
  88. site_group = DynamicModelChoiceField(
  89. queryset=SiteGroup.objects.all(),
  90. required=False,
  91. null_option='None'
  92. )
  93. site = DynamicModelChoiceField(
  94. queryset=Site.objects.all(),
  95. required=False,
  96. query_params={
  97. 'region_id': '$region',
  98. 'group_id': '$site_group',
  99. }
  100. )
  101. rack = DynamicModelChoiceField(
  102. queryset=Rack.objects.all(),
  103. required=False,
  104. null_option='None',
  105. query_params={
  106. 'site_id': '$site'
  107. }
  108. )
  109. devices = DynamicModelMultipleChoiceField(
  110. queryset=Device.objects.all(),
  111. query_params={
  112. 'site_id': '$site',
  113. 'rack_id': '$rack',
  114. 'cluster_id': 'null',
  115. }
  116. )
  117. class Meta:
  118. fields = [
  119. 'region', 'site', 'rack', 'devices',
  120. ]
  121. def __init__(self, cluster, *args, **kwargs):
  122. self.cluster = cluster
  123. super().__init__(*args, **kwargs)
  124. self.fields['devices'].choices = []
  125. def clean(self):
  126. super().clean()
  127. # If the Cluster is assigned to a Site, all Devices must be assigned to that Site.
  128. if self.cluster.site is not None:
  129. for device in self.cleaned_data.get('devices', []):
  130. if device.site != self.cluster.site:
  131. raise ValidationError({
  132. 'devices': "{} belongs to a different site ({}) than the cluster ({})".format(
  133. device, device.site, self.cluster.site
  134. )
  135. })
  136. class ClusterRemoveDevicesForm(ConfirmationForm):
  137. pk = forms.ModelMultipleChoiceField(
  138. queryset=Device.objects.all(),
  139. widget=forms.MultipleHiddenInput()
  140. )
  141. class VirtualMachineForm(TenancyForm, NetBoxModelForm):
  142. site = DynamicModelChoiceField(
  143. queryset=Site.objects.all(),
  144. required=False
  145. )
  146. cluster_group = DynamicModelChoiceField(
  147. queryset=ClusterGroup.objects.all(),
  148. required=False,
  149. null_option='None',
  150. initial_params={
  151. 'clusters': '$cluster'
  152. }
  153. )
  154. cluster = DynamicModelChoiceField(
  155. queryset=Cluster.objects.all(),
  156. required=False,
  157. query_params={
  158. 'site_id': '$site',
  159. 'group_id': '$cluster_group',
  160. }
  161. )
  162. device = DynamicModelChoiceField(
  163. queryset=Device.objects.all(),
  164. required=False,
  165. query_params={
  166. 'cluster_id': '$cluster',
  167. 'site_id': '$site',
  168. },
  169. help_text="Optionally pin this VM to a specific host device within the cluster"
  170. )
  171. role = DynamicModelChoiceField(
  172. queryset=DeviceRole.objects.all(),
  173. required=False,
  174. query_params={
  175. "vm_role": "True"
  176. }
  177. )
  178. platform = DynamicModelChoiceField(
  179. queryset=Platform.objects.all(),
  180. required=False
  181. )
  182. local_context_data = JSONField(
  183. required=False,
  184. label=''
  185. )
  186. fieldsets = (
  187. ('Virtual Machine', ('name', 'role', 'status', 'tags')),
  188. ('Site/Cluster', ('site', 'cluster_group', 'cluster', 'device')),
  189. ('Tenancy', ('tenant_group', 'tenant')),
  190. ('Management', ('platform', 'primary_ip4', 'primary_ip6')),
  191. ('Resources', ('vcpus', 'memory', 'disk')),
  192. ('Config Context', ('local_context_data',)),
  193. )
  194. class Meta:
  195. model = VirtualMachine
  196. fields = [
  197. 'name', 'status', 'site', 'cluster_group', 'cluster', 'device', 'role', 'tenant_group', 'tenant',
  198. 'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags',
  199. 'local_context_data',
  200. ]
  201. help_texts = {
  202. 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
  203. "config context",
  204. }
  205. widgets = {
  206. "status": StaticSelect(),
  207. 'primary_ip4': StaticSelect(),
  208. 'primary_ip6': StaticSelect(),
  209. }
  210. def __init__(self, *args, **kwargs):
  211. super().__init__(*args, **kwargs)
  212. if self.instance.pk:
  213. # Compile list of choices for primary IPv4 and IPv6 addresses
  214. for family in [4, 6]:
  215. ip_choices = [(None, '---------')]
  216. # Gather PKs of all interfaces belonging to this VM
  217. interface_ids = self.instance.interfaces.values_list('pk', flat=True)
  218. # Collect interface IPs
  219. interface_ips = IPAddress.objects.filter(
  220. address__family=family,
  221. assigned_object_type=ContentType.objects.get_for_model(VMInterface),
  222. assigned_object_id__in=interface_ids
  223. )
  224. if interface_ips:
  225. ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
  226. ip_choices.append(('Interface IPs', ip_list))
  227. # Collect NAT IPs
  228. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  229. address__family=family,
  230. nat_inside__assigned_object_type=ContentType.objects.get_for_model(VMInterface),
  231. nat_inside__assigned_object_id__in=interface_ids
  232. )
  233. if nat_ips:
  234. ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
  235. ip_choices.append(('NAT IPs', ip_list))
  236. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  237. else:
  238. # An object that doesn't exist yet can't have any IPs assigned to it
  239. self.fields['primary_ip4'].choices = []
  240. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  241. self.fields['primary_ip6'].choices = []
  242. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  243. class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
  244. virtual_machine = DynamicModelChoiceField(
  245. queryset=VirtualMachine.objects.all()
  246. )
  247. parent = DynamicModelChoiceField(
  248. queryset=VMInterface.objects.all(),
  249. required=False,
  250. label='Parent interface',
  251. query_params={
  252. 'virtual_machine_id': '$virtual_machine',
  253. }
  254. )
  255. bridge = DynamicModelChoiceField(
  256. queryset=VMInterface.objects.all(),
  257. required=False,
  258. label='Bridged interface',
  259. query_params={
  260. 'virtual_machine_id': '$virtual_machine',
  261. }
  262. )
  263. vlan_group = DynamicModelChoiceField(
  264. queryset=VLANGroup.objects.all(),
  265. required=False,
  266. label='VLAN group'
  267. )
  268. untagged_vlan = DynamicModelChoiceField(
  269. queryset=VLAN.objects.all(),
  270. required=False,
  271. label='Untagged VLAN',
  272. query_params={
  273. 'group_id': '$vlan_group',
  274. 'available_on_virtualmachine': '$virtual_machine',
  275. }
  276. )
  277. tagged_vlans = DynamicModelMultipleChoiceField(
  278. queryset=VLAN.objects.all(),
  279. required=False,
  280. label='Tagged VLANs',
  281. query_params={
  282. 'group_id': '$vlan_group',
  283. 'available_on_virtualmachine': '$virtual_machine',
  284. }
  285. )
  286. vrf = DynamicModelChoiceField(
  287. queryset=VRF.objects.all(),
  288. required=False,
  289. label='VRF'
  290. )
  291. fieldsets = (
  292. ('Interface', ('virtual_machine', 'name', 'description', 'tags')),
  293. ('Addressing', ('vrf', 'mac_address')),
  294. ('Operation', ('mtu', 'enabled')),
  295. ('Related Interfaces', ('parent', 'bridge')),
  296. ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
  297. )
  298. class Meta:
  299. model = VMInterface
  300. fields = [
  301. 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
  302. 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
  303. ]
  304. widgets = {
  305. 'mode': StaticSelect()
  306. }
  307. labels = {
  308. 'mode': '802.1Q Mode',
  309. }
  310. help_texts = {
  311. 'mode': INTERFACE_MODE_HELP_TEXT,
  312. }
  313. def __init__(self, *args, **kwargs):
  314. super().__init__(*args, **kwargs)
  315. # Disable reassignment of VirtualMachine when editing an existing instance
  316. if self.instance.pk:
  317. self.fields['virtual_machine'].disabled = True