object_create.py 20 KB

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