object_create.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. from django import forms
  2. from django.utils.translation import gettext_lazy as _
  3. from dcim.models import *
  4. from netbox.forms import NetBoxModelForm
  5. from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
  6. from utilities.forms.rendering import FieldSet
  7. from utilities.forms.widgets import APISelect
  8. from . import model_forms
  9. __all__ = (
  10. 'ComponentCreateForm',
  11. 'ConsolePortCreateForm',
  12. 'ConsolePortTemplateCreateForm',
  13. 'ConsoleServerPortCreateForm',
  14. 'ConsoleServerPortTemplateCreateForm',
  15. 'DeviceBayCreateForm',
  16. 'DeviceBayTemplateCreateForm',
  17. 'FrontPortCreateForm',
  18. 'FrontPortTemplateCreateForm',
  19. 'InterfaceCreateForm',
  20. 'InterfaceTemplateCreateForm',
  21. 'InventoryItemCreateForm',
  22. 'InventoryItemTemplateCreateForm',
  23. 'ModuleBayCreateForm',
  24. 'ModuleBayTemplateCreateForm',
  25. 'PowerOutletCreateForm',
  26. 'PowerOutletTemplateCreateForm',
  27. 'PowerPortCreateForm',
  28. 'PowerPortTemplateCreateForm',
  29. 'RearPortCreateForm',
  30. 'RearPortTemplateCreateForm',
  31. 'VirtualChassisCreateForm',
  32. )
  33. class ComponentCreateForm(forms.Form):
  34. """
  35. Subclass this form when facilitating the creation of one or more component or component template objects based on
  36. a name pattern.
  37. """
  38. name = ExpandableNameField(
  39. label=_('Name'),
  40. )
  41. label = ExpandableNameField(
  42. label=_('Label'),
  43. required=False,
  44. help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
  45. )
  46. # Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by
  47. # ComponentCreateView when creating objects.
  48. replication_fields = ('name', 'label')
  49. def clean(self):
  50. super().clean()
  51. # Validate that all replication fields generate an equal number of values
  52. if not (patterns := self.cleaned_data.get(self.replication_fields[0])):
  53. return
  54. pattern_count = len(patterns)
  55. for field_name in self.replication_fields:
  56. value_count = len(self.cleaned_data[field_name])
  57. if self.cleaned_data[field_name] and value_count != pattern_count:
  58. raise forms.ValidationError({
  59. field_name: _(
  60. "The provided pattern specifies {value_count} values, but {pattern_count} are expected."
  61. ).format(value_count=value_count, pattern_count=pattern_count)
  62. }, code='label_pattern_mismatch')
  63. #
  64. # Device component templates
  65. #
  66. class ConsolePortTemplateCreateForm(ComponentCreateForm, model_forms.ConsolePortTemplateForm):
  67. class Meta(model_forms.ConsolePortTemplateForm.Meta):
  68. exclude = ('name', 'label')
  69. class ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm):
  70. class Meta(model_forms.ConsoleServerPortTemplateForm.Meta):
  71. exclude = ('name', 'label')
  72. class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm):
  73. class Meta(model_forms.PowerPortTemplateForm.Meta):
  74. exclude = ('name', 'label')
  75. class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm):
  76. class Meta(model_forms.PowerOutletTemplateForm.Meta):
  77. exclude = ('name', 'label')
  78. class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemplateForm):
  79. class Meta(model_forms.InterfaceTemplateForm.Meta):
  80. exclude = ('name', 'label')
  81. class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
  82. rear_port = forms.MultipleChoiceField(
  83. choices=[],
  84. label=_('Rear ports'),
  85. help_text=_('Select one rear port assignment for each front port being created.'),
  86. widget=forms.SelectMultiple(attrs={'size': 6})
  87. )
  88. # Override fieldsets from FrontPortTemplateForm to omit rear_port_position
  89. fieldsets = (
  90. FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description'),
  91. )
  92. class Meta(model_forms.FrontPortTemplateForm.Meta):
  93. exclude = ('name', 'label', 'rear_port', 'rear_port_position')
  94. def __init__(self, *args, **kwargs):
  95. super().__init__(*args, **kwargs)
  96. # TODO: This needs better validation
  97. if 'device_type' in self.initial or self.data.get('device_type'):
  98. parent = DeviceType.objects.get(
  99. pk=self.initial.get('device_type') or self.data.get('device_type')
  100. )
  101. elif 'module_type' in self.initial or self.data.get('module_type'):
  102. parent = ModuleType.objects.get(
  103. pk=self.initial.get('module_type') or self.data.get('module_type')
  104. )
  105. else:
  106. return
  107. # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
  108. occupied_port_positions = [
  109. (front_port.rear_port_id, front_port.rear_port_position)
  110. for front_port in parent.frontporttemplates.all()
  111. ]
  112. # Populate rear port choices
  113. choices = []
  114. rear_ports = parent.rearporttemplates.all()
  115. for rear_port in rear_ports:
  116. for i in range(1, rear_port.positions + 1):
  117. if (rear_port.pk, i) not in occupied_port_positions:
  118. choices.append(
  119. ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
  120. )
  121. self.fields['rear_port'].choices = choices
  122. def clean(self):
  123. super().clean()
  124. # Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
  125. # positions
  126. frontport_count = len(self.cleaned_data['name'])
  127. rearport_count = len(self.cleaned_data['rear_port'])
  128. if frontport_count != rearport_count:
  129. raise forms.ValidationError({
  130. 'rear_port': _(
  131. "The number of front port templates to be created ({frontport_count}) must match the selected "
  132. "number of rear port positions ({rearport_count})."
  133. ).format(
  134. frontport_count=frontport_count,
  135. rearport_count=rearport_count
  136. )
  137. })
  138. def get_iterative_data(self, iteration):
  139. # Assign rear port and position from selected set
  140. rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
  141. return {
  142. 'rear_port': int(rear_port),
  143. 'rear_port_position': int(position),
  144. }
  145. class RearPortTemplateCreateForm(ComponentCreateForm, model_forms.RearPortTemplateForm):
  146. class Meta(model_forms.RearPortTemplateForm.Meta):
  147. exclude = ('name', 'label')
  148. class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemplateForm):
  149. class Meta(model_forms.DeviceBayTemplateForm.Meta):
  150. exclude = ('name', 'label')
  151. class ModuleBayTemplateCreateForm(ComponentCreateForm, model_forms.ModuleBayTemplateForm):
  152. position = ExpandableNameField(
  153. label=_('Position'),
  154. required=False,
  155. help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
  156. )
  157. replication_fields = ('name', 'label', 'position')
  158. class Meta(model_forms.ModuleBayTemplateForm.Meta):
  159. exclude = ('name', 'label', 'position')
  160. class InventoryItemTemplateCreateForm(ComponentCreateForm, model_forms.InventoryItemTemplateForm):
  161. class Meta(model_forms.InventoryItemTemplateForm.Meta):
  162. exclude = ('name', 'label')
  163. #
  164. # Device components
  165. #
  166. class ConsolePortCreateForm(ComponentCreateForm, model_forms.ConsolePortForm):
  167. class Meta(model_forms.ConsolePortForm.Meta):
  168. exclude = ('name', 'label')
  169. class ConsoleServerPortCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortForm):
  170. class Meta(model_forms.ConsoleServerPortForm.Meta):
  171. exclude = ('name', 'label')
  172. class PowerPortCreateForm(ComponentCreateForm, model_forms.PowerPortForm):
  173. class Meta(model_forms.PowerPortForm.Meta):
  174. exclude = ('name', 'label')
  175. class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
  176. class Meta(model_forms.PowerOutletForm.Meta):
  177. exclude = ('name', 'label')
  178. class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
  179. class Meta(model_forms.InterfaceForm.Meta):
  180. exclude = ('name', 'label')
  181. class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
  182. device = DynamicModelChoiceField(
  183. label=_('Device'),
  184. queryset=Device.objects.all(),
  185. selector=True,
  186. widget=APISelect(
  187. # TODO: Clean up the application of HTMXSelect attributes
  188. attrs={
  189. 'hx-get': '.',
  190. 'hx-include': '#form_fields',
  191. 'hx-target': '#form_fields',
  192. }
  193. )
  194. )
  195. rear_port = forms.MultipleChoiceField(
  196. choices=[],
  197. label=_('Rear ports'),
  198. help_text=_('Select one rear port assignment for each front port being created.'),
  199. widget=forms.SelectMultiple(attrs={'size': 6})
  200. )
  201. # Override fieldsets from FrontPortForm to omit rear_port_position
  202. fieldsets = (
  203. FieldSet(
  204. 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags',
  205. ),
  206. )
  207. class Meta(model_forms.FrontPortForm.Meta):
  208. exclude = ('name', 'label', 'rear_port', 'rear_port_position')
  209. def __init__(self, *args, **kwargs):
  210. super().__init__(*args, **kwargs)
  211. if device_id := self.data.get('device') or self.initial.get('device'):
  212. device = Device.objects.get(pk=device_id)
  213. else:
  214. return
  215. # Determine which rear port positions are occupied. These will be excluded from the list of available
  216. # mappings.
  217. occupied_port_positions = [
  218. (front_port.rear_port_id, front_port.rear_port_position)
  219. for front_port in device.frontports.all()
  220. ]
  221. # Populate rear port choices
  222. choices = []
  223. rear_ports = RearPort.objects.filter(device=device)
  224. for rear_port in rear_ports:
  225. for i in range(1, rear_port.positions + 1):
  226. if (rear_port.pk, i) not in occupied_port_positions:
  227. choices.append(
  228. ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
  229. )
  230. self.fields['rear_port'].choices = choices
  231. def clean(self):
  232. super().clean()
  233. # Check that the number of FrontPorts to be created matches the selected number of RearPort positions
  234. frontport_count = len(self.cleaned_data['name'])
  235. rearport_count = len(self.cleaned_data['rear_port'])
  236. if frontport_count != rearport_count:
  237. raise forms.ValidationError({
  238. 'rear_port': _(
  239. "The number of front ports to be created ({frontport_count}) must match the selected number of "
  240. "rear port positions ({rearport_count})."
  241. ).format(
  242. frontport_count=frontport_count,
  243. rearport_count=rearport_count
  244. )
  245. })
  246. def get_iterative_data(self, iteration):
  247. # Assign rear port and position from selected set
  248. rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
  249. return {
  250. 'rear_port': int(rear_port),
  251. 'rear_port_position': int(position),
  252. }
  253. class RearPortCreateForm(ComponentCreateForm, model_forms.RearPortForm):
  254. class Meta(model_forms.RearPortForm.Meta):
  255. exclude = ('name', 'label')
  256. class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm):
  257. class Meta(model_forms.DeviceBayForm.Meta):
  258. exclude = ('name', 'label')
  259. class ModuleBayCreateForm(ComponentCreateForm, model_forms.ModuleBayForm):
  260. position = ExpandableNameField(
  261. label=_('Position'),
  262. required=False,
  263. help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
  264. )
  265. replication_fields = ('name', 'label', 'position')
  266. class Meta(model_forms.ModuleBayForm.Meta):
  267. exclude = ('name', 'label', 'position')
  268. class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm):
  269. class Meta(model_forms.InventoryItemForm.Meta):
  270. exclude = ('name', 'label')
  271. #
  272. # Virtual chassis
  273. #
  274. class VirtualChassisCreateForm(NetBoxModelForm):
  275. region = DynamicModelChoiceField(
  276. label=_('Region'),
  277. queryset=Region.objects.all(),
  278. required=False,
  279. initial_params={
  280. 'sites': '$site'
  281. }
  282. )
  283. site_group = DynamicModelChoiceField(
  284. label=_('Site group'),
  285. queryset=SiteGroup.objects.all(),
  286. required=False,
  287. initial_params={
  288. 'sites': '$site'
  289. }
  290. )
  291. site = DynamicModelChoiceField(
  292. label=_('Site'),
  293. queryset=Site.objects.all(),
  294. required=False,
  295. query_params={
  296. 'region_id': '$region',
  297. 'group_id': '$site_group',
  298. }
  299. )
  300. rack = DynamicModelChoiceField(
  301. label=_('Rack'),
  302. queryset=Rack.objects.all(),
  303. required=False,
  304. null_option='None',
  305. query_params={
  306. 'site_id': '$site'
  307. }
  308. )
  309. members = DynamicModelMultipleChoiceField(
  310. label=_('Members'),
  311. queryset=Device.objects.all(),
  312. required=False,
  313. query_params={
  314. 'site_id': '$site',
  315. 'rack_id': '$rack',
  316. }
  317. )
  318. initial_position = forms.IntegerField(
  319. label=_('Initial position'),
  320. initial=1,
  321. required=False,
  322. help_text=_('Position of the first member device. Increases by one for each additional member.')
  323. )
  324. class Meta:
  325. model = VirtualChassis
  326. fields = [
  327. 'name', 'domain', 'description', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position',
  328. 'tags',
  329. ]
  330. def clean(self):
  331. super().clean()
  332. if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
  333. raise forms.ValidationError({
  334. 'initial_position': _("A position must be specified for the first VC member.")
  335. })
  336. def save(self, *args, **kwargs):
  337. instance = super().save(*args, **kwargs)
  338. # Assign VC members
  339. if instance.pk and self.cleaned_data['members']:
  340. initial_position = self.cleaned_data.get('initial_position', 1)
  341. for i, member in enumerate(self.cleaned_data['members'], start=initial_position):
  342. member.virtual_chassis = instance
  343. member.vc_position = i
  344. member.save()
  345. return instance