forms.py 14 KB

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