object_create.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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 netbox.forms.mixins import OwnerMixin
  6. from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
  7. from utilities.forms.rendering import FieldSet, TabbedGroups
  8. from utilities.forms.widgets import APISelect
  9. from . import model_forms
  10. __all__ = (
  11. 'ComponentCreateForm',
  12. 'ConsolePortCreateForm',
  13. 'ConsolePortTemplateCreateForm',
  14. 'ConsoleServerPortCreateForm',
  15. 'ConsoleServerPortTemplateCreateForm',
  16. 'DeviceBayCreateForm',
  17. 'DeviceBayTemplateCreateForm',
  18. 'FrontPortCreateForm',
  19. 'FrontPortTemplateCreateForm',
  20. 'InterfaceCreateForm',
  21. 'InterfaceTemplateCreateForm',
  22. 'InventoryItemCreateForm',
  23. 'InventoryItemTemplateCreateForm',
  24. 'ModuleBayCreateForm',
  25. 'ModuleBayTemplateCreateForm',
  26. 'PowerOutletCreateForm',
  27. 'PowerOutletTemplateCreateForm',
  28. 'PowerPortCreateForm',
  29. 'PowerPortTemplateCreateForm',
  30. 'RearPortCreateForm',
  31. 'RearPortTemplateCreateForm',
  32. 'VirtualChassisCreateForm',
  33. )
  34. class ComponentCreateForm(forms.Form):
  35. """
  36. Subclass this form when facilitating the creation of one or more component or component template objects based on
  37. a name pattern.
  38. """
  39. name = ExpandableNameField(
  40. label=_('Name'),
  41. )
  42. label = ExpandableNameField(
  43. label=_('Label'),
  44. required=False,
  45. help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
  46. )
  47. # Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by
  48. # ComponentCreateView when creating objects.
  49. replication_fields = ('name', 'label')
  50. def clean(self):
  51. super().clean()
  52. # Validate that all replication fields generate an equal number of values (or a single value)
  53. if not (patterns := self.cleaned_data.get(self.replication_fields[0])):
  54. return
  55. pattern_count = len(patterns)
  56. for field_name in self.replication_fields:
  57. # A field is absent from cleaned_data if it failed its own validation, e.g. an inverted numeric range
  58. if not (values := self.cleaned_data.get(field_name)):
  59. continue
  60. value_count = len(values)
  61. if value_count == 1:
  62. # If the field resolves to a single value (because no pattern was used), multiply it by the number
  63. # of expected values. This allows us to reuse the same label when creating multiple components.
  64. self.cleaned_data[field_name] = values * pattern_count
  65. elif value_count != pattern_count:
  66. raise forms.ValidationError({
  67. field_name: _(
  68. "The provided pattern specifies {value_count} values, but {pattern_count} are expected."
  69. ).format(value_count=value_count, pattern_count=pattern_count)
  70. }, code='label_pattern_mismatch')
  71. #
  72. # Device component templates
  73. #
  74. class ConsolePortTemplateCreateForm(ComponentCreateForm, model_forms.ConsolePortTemplateForm):
  75. class Meta(model_forms.ConsolePortTemplateForm.Meta):
  76. exclude = ('name', 'label')
  77. class ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm):
  78. class Meta(model_forms.ConsoleServerPortTemplateForm.Meta):
  79. exclude = ('name', 'label')
  80. class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm):
  81. class Meta(model_forms.PowerPortTemplateForm.Meta):
  82. exclude = ('name', 'label')
  83. class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm):
  84. class Meta(model_forms.PowerOutletTemplateForm.Meta):
  85. exclude = ('name', 'label')
  86. class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemplateForm):
  87. class Meta(model_forms.InterfaceTemplateForm.Meta):
  88. exclude = ('name', 'label')
  89. class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
  90. # Override fieldsets from FrontPortTemplateForm
  91. fieldsets = (
  92. FieldSet(
  93. TabbedGroups(
  94. FieldSet('device_type', name=_('Device Type')),
  95. FieldSet('module_type', name=_('Module Type')),
  96. ),
  97. 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description',
  98. ),
  99. )
  100. class Meta:
  101. model = FrontPortTemplate
  102. fields = (
  103. 'device_type', 'module_type', 'type', 'color', 'positions', 'description',
  104. )
  105. def get_iterative_data(self, iteration):
  106. positions = self.cleaned_data['positions']
  107. offset = positions * iteration
  108. return {
  109. 'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
  110. }
  111. class RearPortTemplateCreateForm(ComponentCreateForm, model_forms.RearPortTemplateForm):
  112. class Meta(model_forms.RearPortTemplateForm.Meta):
  113. exclude = ('name', 'label')
  114. class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemplateForm):
  115. class Meta(model_forms.DeviceBayTemplateForm.Meta):
  116. exclude = ('name', 'label')
  117. class ModuleBayTemplateCreateForm(ComponentCreateForm, model_forms.ModuleBayTemplateForm):
  118. position = ExpandableNameField(
  119. label=_('Position'),
  120. required=False,
  121. help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
  122. )
  123. replication_fields = ('name', 'label', 'position')
  124. class Meta(model_forms.ModuleBayTemplateForm.Meta):
  125. exclude = ('name', 'label', 'position')
  126. class InventoryItemTemplateCreateForm(ComponentCreateForm, model_forms.InventoryItemTemplateForm):
  127. class Meta(model_forms.InventoryItemTemplateForm.Meta):
  128. exclude = ('name', 'label')
  129. #
  130. # Device components
  131. #
  132. class ConsolePortCreateForm(ComponentCreateForm, model_forms.ConsolePortForm):
  133. class Meta(model_forms.ConsolePortForm.Meta):
  134. exclude = ('name', 'label')
  135. class ConsoleServerPortCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortForm):
  136. class Meta(model_forms.ConsoleServerPortForm.Meta):
  137. exclude = ('name', 'label')
  138. class PowerPortCreateForm(ComponentCreateForm, model_forms.PowerPortForm):
  139. class Meta(model_forms.PowerPortForm.Meta):
  140. exclude = ('name', 'label')
  141. class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
  142. class Meta(model_forms.PowerOutletForm.Meta):
  143. exclude = ('name', 'label')
  144. class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
  145. class Meta(model_forms.InterfaceForm.Meta):
  146. exclude = ('name', 'label')
  147. class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
  148. device = DynamicModelChoiceField(
  149. label=_('Device'),
  150. queryset=Device.objects.all(),
  151. selector=True,
  152. widget=APISelect(
  153. # TODO: Clean up the application of HTMXSelect attributes
  154. attrs={
  155. 'hx-get': '.',
  156. 'hx-include': '#form_fields',
  157. 'hx-target': '#form_fields',
  158. }
  159. )
  160. )
  161. # Override fieldsets from FrontPortForm to omit rear_port_position
  162. fieldsets = (
  163. FieldSet(
  164. 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
  165. 'description', 'tags',
  166. ),
  167. )
  168. class Meta:
  169. model = FrontPort
  170. fields = [
  171. 'device', 'module', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', 'tags',
  172. ]
  173. def get_iterative_data(self, iteration):
  174. positions = self.cleaned_data['positions']
  175. offset = positions * iteration
  176. return {
  177. 'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
  178. }
  179. class RearPortCreateForm(ComponentCreateForm, model_forms.RearPortForm):
  180. class Meta(model_forms.RearPortForm.Meta):
  181. exclude = ('name', 'label')
  182. class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm):
  183. class Meta(model_forms.DeviceBayForm.Meta):
  184. exclude = ('name', 'label')
  185. class ModuleBayCreateForm(ComponentCreateForm, model_forms.ModuleBayForm):
  186. position = ExpandableNameField(
  187. label=_('Position'),
  188. required=False,
  189. help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
  190. )
  191. replication_fields = ('name', 'label', 'position')
  192. class Meta(model_forms.ModuleBayForm.Meta):
  193. exclude = ('name', 'label', 'position')
  194. class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm):
  195. class Meta(model_forms.InventoryItemForm.Meta):
  196. exclude = ('name', 'label')
  197. #
  198. # Virtual chassis
  199. #
  200. class VirtualChassisCreateForm(OwnerMixin, NetBoxModelForm):
  201. region = DynamicModelChoiceField(
  202. label=_('Region'),
  203. queryset=Region.objects.all(),
  204. required=False,
  205. initial_params={
  206. 'sites': '$site'
  207. }
  208. )
  209. site_group = DynamicModelChoiceField(
  210. label=_('Site group'),
  211. queryset=SiteGroup.objects.all(),
  212. required=False,
  213. initial_params={
  214. 'sites': '$site'
  215. }
  216. )
  217. site = DynamicModelChoiceField(
  218. label=_('Site'),
  219. queryset=Site.objects.all(),
  220. required=False,
  221. query_params={
  222. 'region_id': '$region',
  223. 'group_id': '$site_group',
  224. }
  225. )
  226. rack = DynamicModelChoiceField(
  227. label=_('Rack'),
  228. queryset=Rack.objects.all(),
  229. required=False,
  230. null_option='None',
  231. query_params={
  232. 'site_id': '$site'
  233. }
  234. )
  235. members = DynamicModelMultipleChoiceField(
  236. label=_('Members'),
  237. queryset=Device.objects.all(),
  238. required=False,
  239. query_params={
  240. 'virtual_chassis_id': 'null',
  241. 'site_id': '$site',
  242. 'rack_id': '$rack',
  243. }
  244. )
  245. initial_position = forms.IntegerField(
  246. label=_('Initial position'),
  247. initial=1,
  248. required=False,
  249. help_text=_('Position of the first member device. Increases by one for each additional member.')
  250. )
  251. fieldsets = (
  252. FieldSet('name', 'domain', 'description', 'tags', name=_('Virtual Chassis')),
  253. FieldSet('region', 'site_group', 'site', 'rack', 'members', 'initial_position', name=_('Member Devices')),
  254. )
  255. class Meta:
  256. model = VirtualChassis
  257. fields = [
  258. 'name', 'domain', 'description', 'region', 'site_group', 'site', 'rack', 'owner', 'members',
  259. 'initial_position', 'tags',
  260. ]
  261. def clean(self):
  262. super().clean()
  263. if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
  264. raise forms.ValidationError({
  265. 'initial_position': _("A position must be specified for the first VC member.")
  266. })
  267. def save(self, *args, **kwargs):
  268. instance = super().save(*args, **kwargs)
  269. # Assign VC members
  270. if instance.pk and self.cleaned_data['members']:
  271. initial_position = self.cleaned_data.get('initial_position', 1)
  272. for i, member in enumerate(self.cleaned_data['members'], start=initial_position):
  273. member.snapshot()
  274. member.virtual_chassis = instance
  275. member.vc_position = i
  276. member.save()
  277. return instance