forms.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. from __future__ import unicode_literals
  2. from mptt.forms import TreeNodeChoiceField
  3. from django import forms
  4. from django.core.exceptions import ValidationError
  5. from django.db.models import Count
  6. from dcim.constants import IFACE_FF_VIRTUAL
  7. from dcim.formfields import MACAddressFormField
  8. from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
  9. from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
  10. from tenancy.forms import TenancyForm
  11. from tenancy.models import Tenant
  12. from utilities.forms import (
  13. add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
  14. ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm,
  15. ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, SlugField, SmallTextarea,
  16. )
  17. from .constants import STATUS_CHOICES
  18. from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
  19. VIFACE_FF_CHOICES = (
  20. (IFACE_FF_VIRTUAL, 'Virtual'),
  21. )
  22. #
  23. # Cluster types
  24. #
  25. class ClusterTypeForm(BootstrapMixin, forms.ModelForm):
  26. slug = SlugField()
  27. class Meta:
  28. model = ClusterType
  29. fields = ['name', 'slug']
  30. #
  31. # Cluster groups
  32. #
  33. class ClusterGroupForm(BootstrapMixin, forms.ModelForm):
  34. slug = SlugField()
  35. class Meta:
  36. model = ClusterGroup
  37. fields = ['name', 'slug']
  38. #
  39. # Clusters
  40. #
  41. class ClusterForm(BootstrapMixin, CustomFieldForm):
  42. comments = CommentField(widget=SmallTextarea)
  43. class Meta:
  44. model = Cluster
  45. fields = ['name', 'type', 'group', 'site', 'comments']
  46. class ClusterCSVForm(forms.ModelForm):
  47. type = forms.ModelChoiceField(
  48. queryset=ClusterType.objects.all(),
  49. to_field_name='name',
  50. help_text='Name of cluster type',
  51. error_messages={
  52. 'invalid_choice': 'Invalid cluster type name.',
  53. }
  54. )
  55. group = forms.ModelChoiceField(
  56. queryset=ClusterGroup.objects.all(),
  57. to_field_name='name',
  58. required=False,
  59. help_text='Name of cluster group',
  60. error_messages={
  61. 'invalid_choice': 'Invalid cluster group name.',
  62. }
  63. )
  64. site = forms.ModelChoiceField(
  65. queryset=Site.objects.all(),
  66. to_field_name='name',
  67. required=False,
  68. help_text='Name of assigned site',
  69. error_messages={
  70. 'invalid_choice': 'Invalid site name.',
  71. }
  72. )
  73. class Meta:
  74. model = Cluster
  75. fields = ['name', 'type', 'group', 'site', 'comments']
  76. class ClusterBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  77. pk = forms.ModelMultipleChoiceField(queryset=Cluster.objects.all(), widget=forms.MultipleHiddenInput)
  78. type = forms.ModelChoiceField(queryset=ClusterType.objects.all(), required=False)
  79. group = forms.ModelChoiceField(queryset=ClusterGroup.objects.all(), required=False)
  80. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
  81. comments = CommentField(widget=SmallTextarea)
  82. class Meta:
  83. nullable_fields = ['group', 'site', 'comments']
  84. class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
  85. model = Cluster
  86. q = forms.CharField(required=False, label='Search')
  87. type = FilterChoiceField(
  88. queryset=ClusterType.objects.annotate(filter_count=Count('clusters')),
  89. to_field_name='slug',
  90. required=False,
  91. )
  92. group = FilterChoiceField(
  93. queryset=ClusterGroup.objects.annotate(filter_count=Count('clusters')),
  94. to_field_name='slug',
  95. null_option=(0, 'None'),
  96. required=False,
  97. )
  98. site = FilterChoiceField(
  99. queryset=Site.objects.annotate(filter_count=Count('clusters')),
  100. to_field_name='slug',
  101. null_option=(0, 'None'),
  102. required=False,
  103. )
  104. class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
  105. region = TreeNodeChoiceField(
  106. queryset=Region.objects.all(),
  107. required=False,
  108. widget=forms.Select(
  109. attrs={'filter-for': 'site', 'nullable': 'true'}
  110. )
  111. )
  112. site = ChainedModelChoiceField(
  113. queryset=Site.objects.all(),
  114. chains=(
  115. ('region', 'region'),
  116. ),
  117. required=False,
  118. widget=APISelect(
  119. api_url='/api/dcim/sites/?region_id={{region}}',
  120. attrs={'filter-for': 'rack'}
  121. )
  122. )
  123. rack = ChainedModelChoiceField(
  124. queryset=Rack.objects.all(),
  125. chains=(
  126. ('site', 'site'),
  127. ),
  128. required=False,
  129. widget=APISelect(
  130. api_url='/api/dcim/racks/?site_id={{site}}',
  131. attrs={'filter-for': 'devices', 'nullable': 'true'}
  132. )
  133. )
  134. devices = ChainedModelMultipleChoiceField(
  135. queryset=Device.objects.filter(cluster__isnull=True),
  136. chains=(
  137. ('site', 'site'),
  138. ('rack', 'rack'),
  139. ),
  140. label='Device',
  141. widget=APISelectMultiple(
  142. api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
  143. display_field='display_name',
  144. disabled_indicator='cluster'
  145. )
  146. )
  147. class Meta:
  148. fields = ['region', 'site', 'rack', 'devices']
  149. def __init__(self, cluster, *args, **kwargs):
  150. self.cluster = cluster
  151. super(ClusterAddDevicesForm, self).__init__(*args, **kwargs)
  152. self.fields['devices'].choices = []
  153. def clean(self):
  154. super(ClusterAddDevicesForm, self).clean()
  155. # If the Cluster is assigned to a Site, all Devices must be assigned to that Site.
  156. if self.cluster.site is not None:
  157. for device in self.cleaned_data.get('devices'):
  158. if device.site != self.cluster.site:
  159. raise ValidationError({
  160. 'devices': "{} belongs to a different site ({}) than the cluster ({})".format(
  161. device, device.site, self.cluster.site
  162. )
  163. })
  164. class ClusterRemoveDevicesForm(ConfirmationForm):
  165. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  166. #
  167. # Virtual Machines
  168. #
  169. class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  170. cluster_group = forms.ModelChoiceField(
  171. queryset=ClusterGroup.objects.all(),
  172. required=False,
  173. widget=forms.Select(
  174. attrs={'filter-for': 'cluster', 'nullable': 'true'}
  175. )
  176. )
  177. cluster = ChainedModelChoiceField(
  178. queryset=Cluster.objects.all(),
  179. chains=(
  180. ('group', 'cluster_group'),
  181. ),
  182. widget=APISelect(
  183. api_url='/api/virtualization/clusters/?group_id={{cluster_group}}'
  184. )
  185. )
  186. class Meta:
  187. model = VirtualMachine
  188. fields = [
  189. 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
  190. 'comments',
  191. ]
  192. def __init__(self, *args, **kwargs):
  193. # Initialize helper selector
  194. instance = kwargs.get('instance')
  195. if instance.pk and instance.cluster is not None:
  196. initial = kwargs.get('initial', {}).copy()
  197. initial['cluster_group'] = instance.cluster.group
  198. kwargs['initial'] = initial
  199. super(VirtualMachineForm, self).__init__(*args, **kwargs)
  200. class VirtualMachineCSVForm(forms.ModelForm):
  201. status = CSVChoiceField(
  202. choices=STATUS_CHOICES,
  203. required=False,
  204. help_text='Operational status of device'
  205. )
  206. cluster = forms.ModelChoiceField(
  207. queryset=Cluster.objects.all(),
  208. to_field_name='name',
  209. help_text='Name of parent cluster',
  210. error_messages={
  211. 'invalid_choice': 'Invalid cluster name.',
  212. }
  213. )
  214. role = forms.ModelChoiceField(
  215. queryset=DeviceRole.objects.filter(vm_role=True),
  216. required=False,
  217. to_field_name='name',
  218. help_text='Name of functional role',
  219. error_messages={
  220. 'invalid_choice': 'Invalid role name.'
  221. }
  222. )
  223. tenant = forms.ModelChoiceField(
  224. queryset=Tenant.objects.all(),
  225. required=False,
  226. to_field_name='name',
  227. help_text='Name of assigned tenant',
  228. error_messages={
  229. 'invalid_choice': 'Tenant not found.'
  230. }
  231. )
  232. platform = forms.ModelChoiceField(
  233. queryset=Platform.objects.all(),
  234. required=False,
  235. to_field_name='name',
  236. help_text='Name of assigned platform',
  237. error_messages={
  238. 'invalid_choice': 'Invalid platform.',
  239. }
  240. )
  241. class Meta:
  242. model = VirtualMachine
  243. fields = ['name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments']
  244. class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  245. pk = forms.ModelMultipleChoiceField(queryset=VirtualMachine.objects.all(), widget=forms.MultipleHiddenInput)
  246. status = forms.ChoiceField(choices=add_blank_choice(STATUS_CHOICES), required=False, initial='')
  247. cluster = forms.ModelChoiceField(queryset=Cluster.objects.all(), required=False)
  248. role = forms.ModelChoiceField(queryset=DeviceRole.objects.filter(vm_role=True), required=False)
  249. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  250. platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
  251. vcpus = forms.IntegerField(required=False, label='vCPUs')
  252. memory = forms.IntegerField(required=False, label='Memory (MB)')
  253. disk = forms.IntegerField(required=False, label='Disk (GB)')
  254. comments = CommentField(widget=SmallTextarea)
  255. class Meta:
  256. nullable_fields = ['role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments']
  257. def vm_status_choices():
  258. status_counts = {}
  259. for status in VirtualMachine.objects.values('status').annotate(count=Count('status')).order_by('status'):
  260. status_counts[status['status']] = status['count']
  261. return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in STATUS_CHOICES]
  262. class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
  263. model = VirtualMachine
  264. q = forms.CharField(required=False, label='Search')
  265. cluster_group = FilterChoiceField(
  266. queryset=ClusterGroup.objects.all(),
  267. to_field_name='slug',
  268. null_option=(0, 'None')
  269. )
  270. cluster_id = FilterChoiceField(
  271. queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')),
  272. label='Cluster'
  273. )
  274. role = FilterChoiceField(
  275. queryset=DeviceRole.objects.filter(vm_role=True).annotate(filter_count=Count('virtual_machines')),
  276. to_field_name='slug',
  277. null_option=(0, 'None')
  278. )
  279. status = forms.MultipleChoiceField(choices=vm_status_choices, required=False)
  280. tenant = FilterChoiceField(
  281. queryset=Tenant.objects.annotate(filter_count=Count('virtual_machines')),
  282. to_field_name='slug',
  283. null_option=(0, 'None')
  284. )
  285. platform = FilterChoiceField(
  286. queryset=Platform.objects.annotate(filter_count=Count('virtual_machines')),
  287. to_field_name='slug',
  288. null_option=(0, 'None')
  289. )
  290. #
  291. # VM interfaces
  292. #
  293. class InterfaceForm(BootstrapMixin, forms.ModelForm):
  294. class Meta:
  295. model = Interface
  296. fields = ['virtual_machine', 'name', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description']
  297. widgets = {
  298. 'virtual_machine': forms.HiddenInput(),
  299. 'form_factor': forms.HiddenInput(),
  300. }
  301. class InterfaceCreateForm(ComponentForm):
  302. name_pattern = ExpandableNameField(label='Name')
  303. form_factor = forms.ChoiceField(choices=VIFACE_FF_CHOICES, initial=IFACE_FF_VIRTUAL, widget=forms.HiddenInput())
  304. enabled = forms.BooleanField(required=False)
  305. mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
  306. mac_address = MACAddressFormField(required=False, label='MAC Address')
  307. description = forms.CharField(max_length=100, required=False)
  308. def __init__(self, *args, **kwargs):
  309. # Set interfaces enabled by default
  310. kwargs['initial'] = kwargs.get('initial', {}).copy()
  311. kwargs['initial'].update({'enabled': True})
  312. super(InterfaceCreateForm, self).__init__(*args, **kwargs)
  313. class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
  314. pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
  315. virtual_machine = forms.ModelChoiceField(queryset=VirtualMachine.objects.all(), widget=forms.HiddenInput)
  316. enabled = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect)
  317. mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
  318. description = forms.CharField(max_length=100, required=False)
  319. class Meta:
  320. nullable_fields = ['mtu', 'description']
  321. #
  322. # Bulk VirtualMachine component creation
  323. #
  324. class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
  325. pk = forms.ModelMultipleChoiceField(queryset=VirtualMachine.objects.all(), widget=forms.MultipleHiddenInput)
  326. name_pattern = ExpandableNameField(label='Name')
  327. class VirtualMachineBulkAddInterfaceForm(VirtualMachineBulkAddComponentForm):
  328. form_factor = forms.ChoiceField(choices=VIFACE_FF_CHOICES, initial=IFACE_FF_VIRTUAL, widget=forms.HiddenInput())
  329. enabled = forms.BooleanField(required=False, initial=True)
  330. mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
  331. description = forms.CharField(max_length=100, required=False)