object_create.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. from django import forms
  2. from dcim.choices import *
  3. from dcim.constants import *
  4. from dcim.models import *
  5. from extras.forms import CustomFieldModelForm, CustomFieldsMixin
  6. from extras.models import Tag
  7. from ipam.models import VLAN
  8. from utilities.forms import (
  9. add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
  10. ExpandableNameField, StaticSelect,
  11. )
  12. from .common import InterfaceCommonForm
  13. __all__ = (
  14. 'ConsolePortCreateForm',
  15. 'ConsolePortTemplateCreateForm',
  16. 'ConsoleServerPortCreateForm',
  17. 'ConsoleServerPortTemplateCreateForm',
  18. 'DeviceBayCreateForm',
  19. 'DeviceBayTemplateCreateForm',
  20. 'FrontPortCreateForm',
  21. 'FrontPortTemplateCreateForm',
  22. 'InterfaceCreateForm',
  23. 'InterfaceTemplateCreateForm',
  24. 'InventoryItemCreateForm',
  25. 'PowerOutletCreateForm',
  26. 'PowerOutletTemplateCreateForm',
  27. 'PowerPortCreateForm',
  28. 'PowerPortTemplateCreateForm',
  29. 'RearPortCreateForm',
  30. 'RearPortTemplateCreateForm',
  31. 'VirtualChassisCreateForm',
  32. )
  33. class ComponentForm(forms.Form):
  34. """
  35. Subclass this form when facilitating the creation of one or more device component or component templates based on
  36. a name pattern.
  37. """
  38. name_pattern = ExpandableNameField(
  39. label='Name'
  40. )
  41. label_pattern = ExpandableNameField(
  42. label='Label',
  43. required=False,
  44. help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
  45. )
  46. def clean(self):
  47. super().clean()
  48. # Validate that the number of components being created from both the name_pattern and label_pattern are equal
  49. if self.cleaned_data['label_pattern']:
  50. name_pattern_count = len(self.cleaned_data['name_pattern'])
  51. label_pattern_count = len(self.cleaned_data['label_pattern'])
  52. if name_pattern_count != label_pattern_count:
  53. raise forms.ValidationError({
  54. 'label_pattern': f'The provided name pattern will create {name_pattern_count} components, however '
  55. f'{label_pattern_count} labels will be generated. These counts must match.'
  56. }, code='label_pattern_mismatch')
  57. class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
  58. region = DynamicModelChoiceField(
  59. queryset=Region.objects.all(),
  60. required=False,
  61. initial_params={
  62. 'sites': '$site'
  63. }
  64. )
  65. site_group = DynamicModelChoiceField(
  66. queryset=SiteGroup.objects.all(),
  67. required=False,
  68. initial_params={
  69. 'sites': '$site'
  70. }
  71. )
  72. site = DynamicModelChoiceField(
  73. queryset=Site.objects.all(),
  74. required=False,
  75. query_params={
  76. 'region_id': '$region',
  77. 'group_id': '$site_group',
  78. }
  79. )
  80. rack = DynamicModelChoiceField(
  81. queryset=Rack.objects.all(),
  82. required=False,
  83. null_option='None',
  84. query_params={
  85. 'site_id': '$site'
  86. }
  87. )
  88. members = DynamicModelMultipleChoiceField(
  89. queryset=Device.objects.all(),
  90. required=False,
  91. query_params={
  92. 'site_id': '$site',
  93. 'rack_id': '$rack',
  94. }
  95. )
  96. initial_position = forms.IntegerField(
  97. initial=1,
  98. required=False,
  99. help_text='Position of the first member device. Increases by one for each additional member.'
  100. )
  101. tags = DynamicModelMultipleChoiceField(
  102. queryset=Tag.objects.all(),
  103. required=False
  104. )
  105. class Meta:
  106. model = VirtualChassis
  107. fields = [
  108. 'name', 'domain', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
  109. ]
  110. def save(self, *args, **kwargs):
  111. instance = super().save(*args, **kwargs)
  112. # Assign VC members
  113. if instance.pk:
  114. initial_position = self.cleaned_data.get('initial_position') or 1
  115. for i, member in enumerate(self.cleaned_data['members'], start=initial_position):
  116. member.virtual_chassis = instance
  117. member.vc_position = i
  118. member.save()
  119. return instance
  120. #
  121. # Component templates
  122. #
  123. class ComponentTemplateCreateForm(BootstrapMixin, ComponentForm):
  124. """
  125. Base form for the creation of device component templates (subclassed from ComponentTemplateModel).
  126. """
  127. manufacturer = DynamicModelChoiceField(
  128. queryset=Manufacturer.objects.all(),
  129. required=False,
  130. initial_params={
  131. 'device_types': 'device_type'
  132. }
  133. )
  134. device_type = DynamicModelChoiceField(
  135. queryset=DeviceType.objects.all(),
  136. query_params={
  137. 'manufacturer_id': '$manufacturer'
  138. }
  139. )
  140. description = forms.CharField(
  141. required=False
  142. )
  143. class ConsolePortTemplateCreateForm(ComponentTemplateCreateForm):
  144. type = forms.ChoiceField(
  145. choices=add_blank_choice(ConsolePortTypeChoices),
  146. widget=StaticSelect()
  147. )
  148. field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description')
  149. class ConsoleServerPortTemplateCreateForm(ComponentTemplateCreateForm):
  150. type = forms.ChoiceField(
  151. choices=add_blank_choice(ConsolePortTypeChoices),
  152. widget=StaticSelect()
  153. )
  154. field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description')
  155. class PowerPortTemplateCreateForm(ComponentTemplateCreateForm):
  156. type = forms.ChoiceField(
  157. choices=add_blank_choice(PowerPortTypeChoices),
  158. required=False
  159. )
  160. maximum_draw = forms.IntegerField(
  161. min_value=1,
  162. required=False,
  163. help_text="Maximum power draw (watts)"
  164. )
  165. allocated_draw = forms.IntegerField(
  166. min_value=1,
  167. required=False,
  168. help_text="Allocated power draw (watts)"
  169. )
  170. field_order = (
  171. 'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw',
  172. 'description',
  173. )
  174. class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm):
  175. type = forms.ChoiceField(
  176. choices=add_blank_choice(PowerOutletTypeChoices),
  177. required=False
  178. )
  179. power_port = forms.ModelChoiceField(
  180. queryset=PowerPortTemplate.objects.all(),
  181. required=False
  182. )
  183. feed_leg = forms.ChoiceField(
  184. choices=add_blank_choice(PowerOutletFeedLegChoices),
  185. required=False,
  186. widget=StaticSelect()
  187. )
  188. field_order = (
  189. 'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg',
  190. 'description',
  191. )
  192. def __init__(self, *args, **kwargs):
  193. super().__init__(*args, **kwargs)
  194. # Limit power_port choices to current DeviceType
  195. device_type = DeviceType.objects.get(
  196. pk=self.initial.get('device_type') or self.data.get('device_type')
  197. )
  198. self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
  199. device_type=device_type
  200. )
  201. class InterfaceTemplateCreateForm(ComponentTemplateCreateForm):
  202. type = forms.ChoiceField(
  203. choices=InterfaceTypeChoices,
  204. widget=StaticSelect()
  205. )
  206. mgmt_only = forms.BooleanField(
  207. required=False,
  208. label='Management only'
  209. )
  210. field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only', 'description')
  211. class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
  212. type = forms.ChoiceField(
  213. choices=PortTypeChoices,
  214. widget=StaticSelect()
  215. )
  216. color = ColorField(
  217. required=False
  218. )
  219. rear_port_set = forms.MultipleChoiceField(
  220. choices=[],
  221. label='Rear ports',
  222. help_text='Select one rear port assignment for each front port being created.',
  223. )
  224. field_order = (
  225. 'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'description',
  226. )
  227. def __init__(self, *args, **kwargs):
  228. super().__init__(*args, **kwargs)
  229. device_type = DeviceType.objects.get(
  230. pk=self.initial.get('device_type') or self.data.get('device_type')
  231. )
  232. # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
  233. occupied_port_positions = [
  234. (front_port.rear_port_id, front_port.rear_port_position)
  235. for front_port in device_type.frontporttemplates.all()
  236. ]
  237. # Populate rear port choices
  238. choices = []
  239. rear_ports = RearPortTemplate.objects.filter(device_type=device_type)
  240. for rear_port in rear_ports:
  241. for i in range(1, rear_port.positions + 1):
  242. if (rear_port.pk, i) not in occupied_port_positions:
  243. choices.append(
  244. ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
  245. )
  246. self.fields['rear_port_set'].choices = choices
  247. def clean(self):
  248. super().clean()
  249. # Validate that the number of ports being created equals the number of selected (rear port, position) tuples
  250. front_port_count = len(self.cleaned_data['name_pattern'])
  251. rear_port_count = len(self.cleaned_data['rear_port_set'])
  252. if front_port_count != rear_port_count:
  253. raise forms.ValidationError({
  254. 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
  255. 'were selected. These counts must match.'.format(front_port_count, rear_port_count)
  256. })
  257. def get_iterative_data(self, iteration):
  258. # Assign rear port and position from selected set
  259. rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
  260. return {
  261. 'rear_port': int(rear_port),
  262. 'rear_port_position': int(position),
  263. }
  264. class RearPortTemplateCreateForm(ComponentTemplateCreateForm):
  265. type = forms.ChoiceField(
  266. choices=PortTypeChoices,
  267. widget=StaticSelect(),
  268. )
  269. color = ColorField(
  270. required=False
  271. )
  272. positions = forms.IntegerField(
  273. min_value=REARPORT_POSITIONS_MIN,
  274. max_value=REARPORT_POSITIONS_MAX,
  275. initial=1,
  276. help_text='The number of front ports which may be mapped to each rear port'
  277. )
  278. field_order = (
  279. 'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'description',
  280. )
  281. class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm):
  282. field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
  283. #
  284. # Device components
  285. #
  286. class ComponentCreateForm(BootstrapMixin, CustomFieldsMixin, ComponentForm):
  287. """
  288. Base form for the creation of device components (models subclassed from ComponentModel).
  289. """
  290. device = DynamicModelChoiceField(
  291. queryset=Device.objects.all()
  292. )
  293. description = forms.CharField(
  294. max_length=200,
  295. required=False
  296. )
  297. tags = DynamicModelMultipleChoiceField(
  298. queryset=Tag.objects.all(),
  299. required=False
  300. )
  301. class ConsolePortCreateForm(ComponentCreateForm):
  302. model = ConsolePort
  303. type = forms.ChoiceField(
  304. choices=add_blank_choice(ConsolePortTypeChoices),
  305. required=False,
  306. widget=StaticSelect()
  307. )
  308. speed = forms.ChoiceField(
  309. choices=add_blank_choice(ConsolePortSpeedChoices),
  310. required=False,
  311. widget=StaticSelect()
  312. )
  313. field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
  314. class ConsoleServerPortCreateForm(ComponentCreateForm):
  315. model = ConsoleServerPort
  316. type = forms.ChoiceField(
  317. choices=add_blank_choice(ConsolePortTypeChoices),
  318. required=False,
  319. widget=StaticSelect()
  320. )
  321. speed = forms.ChoiceField(
  322. choices=add_blank_choice(ConsolePortSpeedChoices),
  323. required=False,
  324. widget=StaticSelect()
  325. )
  326. field_order = ('device', 'name_pattern', 'label_pattern', 'type', 'speed', 'mark_connected', 'description', 'tags')
  327. class PowerPortCreateForm(ComponentCreateForm):
  328. model = PowerPort
  329. type = forms.ChoiceField(
  330. choices=add_blank_choice(PowerPortTypeChoices),
  331. required=False,
  332. widget=StaticSelect()
  333. )
  334. maximum_draw = forms.IntegerField(
  335. min_value=1,
  336. required=False,
  337. help_text="Maximum draw in watts"
  338. )
  339. allocated_draw = forms.IntegerField(
  340. min_value=1,
  341. required=False,
  342. help_text="Allocated draw in watts"
  343. )
  344. field_order = (
  345. 'device', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
  346. 'description', 'tags',
  347. )
  348. class PowerOutletCreateForm(ComponentCreateForm):
  349. model = PowerOutlet
  350. type = forms.ChoiceField(
  351. choices=add_blank_choice(PowerOutletTypeChoices),
  352. required=False,
  353. widget=StaticSelect()
  354. )
  355. power_port = forms.ModelChoiceField(
  356. queryset=PowerPort.objects.all(),
  357. required=False
  358. )
  359. feed_leg = forms.ChoiceField(
  360. choices=add_blank_choice(PowerOutletFeedLegChoices),
  361. required=False
  362. )
  363. field_order = (
  364. 'device', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
  365. 'tags',
  366. )
  367. def __init__(self, *args, **kwargs):
  368. super().__init__(*args, **kwargs)
  369. # Limit power_port queryset to PowerPorts which belong to the parent Device
  370. device = Device.objects.get(
  371. pk=self.initial.get('device') or self.data.get('device')
  372. )
  373. self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
  374. class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
  375. model = Interface
  376. type = forms.ChoiceField(
  377. choices=InterfaceTypeChoices,
  378. widget=StaticSelect(),
  379. )
  380. enabled = forms.BooleanField(
  381. required=False,
  382. initial=True
  383. )
  384. parent = DynamicModelChoiceField(
  385. queryset=Interface.objects.all(),
  386. required=False,
  387. query_params={
  388. 'device_id': '$device',
  389. }
  390. )
  391. lag = DynamicModelChoiceField(
  392. queryset=Interface.objects.all(),
  393. required=False,
  394. query_params={
  395. 'device_id': '$device',
  396. 'type': 'lag',
  397. }
  398. )
  399. mac_address = forms.CharField(
  400. required=False,
  401. label='MAC Address'
  402. )
  403. mgmt_only = forms.BooleanField(
  404. required=False,
  405. label='Management only',
  406. help_text='This interface is used only for out-of-band management'
  407. )
  408. mode = forms.ChoiceField(
  409. choices=add_blank_choice(InterfaceModeChoices),
  410. required=False,
  411. widget=StaticSelect(),
  412. )
  413. untagged_vlan = DynamicModelChoiceField(
  414. queryset=VLAN.objects.all(),
  415. required=False
  416. )
  417. tagged_vlans = DynamicModelMultipleChoiceField(
  418. queryset=VLAN.objects.all(),
  419. required=False
  420. )
  421. field_order = (
  422. 'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
  423. 'description', 'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
  424. )
  425. def __init__(self, *args, **kwargs):
  426. super().__init__(*args, **kwargs)
  427. # Limit VLAN choices by device
  428. device_id = self.initial.get('device') or self.data.get('device')
  429. self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device_id)
  430. self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device_id)
  431. class FrontPortCreateForm(ComponentCreateForm):
  432. model = FrontPort
  433. type = forms.ChoiceField(
  434. choices=PortTypeChoices,
  435. widget=StaticSelect(),
  436. )
  437. color = ColorField(
  438. required=False
  439. )
  440. rear_port_set = forms.MultipleChoiceField(
  441. choices=[],
  442. label='Rear ports',
  443. help_text='Select one rear port assignment for each front port being created.',
  444. )
  445. field_order = (
  446. 'device', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'mark_connected', 'description',
  447. 'tags',
  448. )
  449. def __init__(self, *args, **kwargs):
  450. super().__init__(*args, **kwargs)
  451. device = Device.objects.get(
  452. pk=self.initial.get('device') or self.data.get('device')
  453. )
  454. # Determine which rear port positions are occupied. These will be excluded from the list of available
  455. # mappings.
  456. occupied_port_positions = [
  457. (front_port.rear_port_id, front_port.rear_port_position)
  458. for front_port in device.frontports.all()
  459. ]
  460. # Populate rear port choices
  461. choices = []
  462. rear_ports = RearPort.objects.filter(device=device)
  463. for rear_port in rear_ports:
  464. for i in range(1, rear_port.positions + 1):
  465. if (rear_port.pk, i) not in occupied_port_positions:
  466. choices.append(
  467. ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
  468. )
  469. self.fields['rear_port_set'].choices = choices
  470. def clean(self):
  471. super().clean()
  472. # Validate that the number of ports being created equals the number of selected (rear port, position) tuples
  473. front_port_count = len(self.cleaned_data['name_pattern'])
  474. rear_port_count = len(self.cleaned_data['rear_port_set'])
  475. if front_port_count != rear_port_count:
  476. raise forms.ValidationError({
  477. 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
  478. 'were selected. These counts must match.'.format(front_port_count, rear_port_count)
  479. })
  480. def get_iterative_data(self, iteration):
  481. # Assign rear port and position from selected set
  482. rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
  483. return {
  484. 'rear_port': int(rear_port),
  485. 'rear_port_position': int(position),
  486. }
  487. class RearPortCreateForm(ComponentCreateForm):
  488. model = RearPort
  489. type = forms.ChoiceField(
  490. choices=PortTypeChoices,
  491. widget=StaticSelect(),
  492. )
  493. color = ColorField(
  494. required=False
  495. )
  496. positions = forms.IntegerField(
  497. min_value=REARPORT_POSITIONS_MIN,
  498. max_value=REARPORT_POSITIONS_MAX,
  499. initial=1,
  500. help_text='The number of front ports which may be mapped to each rear port'
  501. )
  502. field_order = (
  503. 'device', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'mark_connected', 'description',
  504. 'tags',
  505. )
  506. class DeviceBayCreateForm(ComponentCreateForm):
  507. model = DeviceBay
  508. field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')
  509. class InventoryItemCreateForm(ComponentCreateForm):
  510. model = InventoryItem
  511. manufacturer = DynamicModelChoiceField(
  512. queryset=Manufacturer.objects.all(),
  513. required=False
  514. )
  515. parent = DynamicModelChoiceField(
  516. queryset=InventoryItem.objects.all(),
  517. required=False,
  518. query_params={
  519. 'device_id': '$device'
  520. }
  521. )
  522. part_id = forms.CharField(
  523. max_length=50,
  524. required=False,
  525. label='Part ID'
  526. )
  527. serial = forms.CharField(
  528. max_length=50,
  529. required=False,
  530. )
  531. asset_tag = forms.CharField(
  532. max_length=50,
  533. required=False,
  534. )
  535. field_order = (
  536. 'device', 'parent', 'name_pattern', 'label_pattern', 'manufacturer', 'part_id', 'serial', 'asset_tag',
  537. 'description', 'tags',
  538. )