object_create.py 15 KB

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