2
0

forms.py 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237
  1. from django import forms
  2. from django.core.validators import MaxValueValidator, MinValueValidator
  3. from dcim.models import Device, Interface, Rack, Region, Site
  4. from extras.forms import (
  5. AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
  6. )
  7. from extras.models import Tag
  8. from tenancy.forms import TenancyFilterForm, TenancyForm
  9. from tenancy.models import Tenant
  10. from utilities.forms import (
  11. add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, CSVModelChoiceField, CSVModelForm,
  12. DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, ReturnURLForm,
  13. SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
  14. )
  15. from virtualization.models import VirtualMachine, VMInterface
  16. from .choices import *
  17. from .constants import *
  18. from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
  19. PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([
  20. (i, i) for i in range(PREFIX_LENGTH_MIN, PREFIX_LENGTH_MAX + 1)
  21. ])
  22. IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
  23. (i, i) for i in range(IPADDRESS_MASK_LENGTH_MIN, IPADDRESS_MASK_LENGTH_MAX + 1)
  24. ])
  25. #
  26. # VRFs
  27. #
  28. class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  29. tags = DynamicModelMultipleChoiceField(
  30. queryset=Tag.objects.all(),
  31. required=False
  32. )
  33. class Meta:
  34. model = VRF
  35. fields = [
  36. 'name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant', 'tags',
  37. ]
  38. labels = {
  39. 'rd': "RD",
  40. }
  41. help_texts = {
  42. 'rd': "Route distinguisher in any format",
  43. }
  44. class VRFCSVForm(CustomFieldModelCSVForm):
  45. tenant = CSVModelChoiceField(
  46. queryset=Tenant.objects.all(),
  47. required=False,
  48. to_field_name='name',
  49. help_text='Assigned tenant'
  50. )
  51. class Meta:
  52. model = VRF
  53. fields = VRF.csv_headers
  54. class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  55. pk = forms.ModelMultipleChoiceField(
  56. queryset=VRF.objects.all(),
  57. widget=forms.MultipleHiddenInput()
  58. )
  59. tenant = DynamicModelChoiceField(
  60. queryset=Tenant.objects.all(),
  61. required=False
  62. )
  63. enforce_unique = forms.NullBooleanField(
  64. required=False,
  65. widget=BulkEditNullBooleanSelect(),
  66. label='Enforce unique space'
  67. )
  68. description = forms.CharField(
  69. max_length=100,
  70. required=False
  71. )
  72. class Meta:
  73. nullable_fields = [
  74. 'tenant', 'description',
  75. ]
  76. class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  77. model = VRF
  78. field_order = ['q', 'tenant_group', 'tenant']
  79. q = forms.CharField(
  80. required=False,
  81. label='Search'
  82. )
  83. tag = TagFilterField(model)
  84. #
  85. # RIRs
  86. #
  87. class RIRForm(BootstrapMixin, forms.ModelForm):
  88. slug = SlugField()
  89. class Meta:
  90. model = RIR
  91. fields = [
  92. 'name', 'slug', 'is_private', 'description',
  93. ]
  94. class RIRCSVForm(CSVModelForm):
  95. slug = SlugField()
  96. class Meta:
  97. model = RIR
  98. fields = RIR.csv_headers
  99. help_texts = {
  100. 'name': 'RIR name',
  101. }
  102. class RIRFilterForm(BootstrapMixin, forms.Form):
  103. is_private = forms.NullBooleanField(
  104. required=False,
  105. label='Private',
  106. widget=StaticSelect2(
  107. choices=BOOLEAN_WITH_BLANK_CHOICES
  108. )
  109. )
  110. #
  111. # Aggregates
  112. #
  113. class AggregateForm(BootstrapMixin, CustomFieldModelForm):
  114. rir = DynamicModelChoiceField(
  115. queryset=RIR.objects.all()
  116. )
  117. tags = DynamicModelMultipleChoiceField(
  118. queryset=Tag.objects.all(),
  119. required=False
  120. )
  121. class Meta:
  122. model = Aggregate
  123. fields = [
  124. 'prefix', 'rir', 'date_added', 'description', 'tags',
  125. ]
  126. help_texts = {
  127. 'prefix': "IPv4 or IPv6 network",
  128. 'rir': "Regional Internet Registry responsible for this prefix",
  129. }
  130. widgets = {
  131. 'date_added': DatePicker(),
  132. }
  133. class AggregateCSVForm(CustomFieldModelCSVForm):
  134. rir = CSVModelChoiceField(
  135. queryset=RIR.objects.all(),
  136. to_field_name='name',
  137. help_text='Assigned RIR'
  138. )
  139. class Meta:
  140. model = Aggregate
  141. fields = Aggregate.csv_headers
  142. class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  143. pk = forms.ModelMultipleChoiceField(
  144. queryset=Aggregate.objects.all(),
  145. widget=forms.MultipleHiddenInput()
  146. )
  147. rir = DynamicModelChoiceField(
  148. queryset=RIR.objects.all(),
  149. required=False,
  150. label='RIR'
  151. )
  152. date_added = forms.DateField(
  153. required=False
  154. )
  155. description = forms.CharField(
  156. max_length=100,
  157. required=False
  158. )
  159. class Meta:
  160. nullable_fields = [
  161. 'date_added', 'description',
  162. ]
  163. widgets = {
  164. 'date_added': DatePicker(),
  165. }
  166. class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
  167. model = Aggregate
  168. q = forms.CharField(
  169. required=False,
  170. label='Search'
  171. )
  172. family = forms.ChoiceField(
  173. required=False,
  174. choices=add_blank_choice(IPAddressFamilyChoices),
  175. label='Address family',
  176. widget=StaticSelect2()
  177. )
  178. rir = DynamicModelMultipleChoiceField(
  179. queryset=RIR.objects.all(),
  180. to_field_name='slug',
  181. required=False,
  182. label='RIR'
  183. )
  184. tag = TagFilterField(model)
  185. #
  186. # Roles
  187. #
  188. class RoleForm(BootstrapMixin, forms.ModelForm):
  189. slug = SlugField()
  190. class Meta:
  191. model = Role
  192. fields = [
  193. 'name', 'slug', 'weight', 'description',
  194. ]
  195. class RoleCSVForm(CSVModelForm):
  196. slug = SlugField()
  197. class Meta:
  198. model = Role
  199. fields = Role.csv_headers
  200. #
  201. # Prefixes
  202. #
  203. class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  204. vrf = DynamicModelChoiceField(
  205. queryset=VRF.objects.all(),
  206. required=False,
  207. label='VRF',
  208. display_field='display_name'
  209. )
  210. site = DynamicModelChoiceField(
  211. queryset=Site.objects.all(),
  212. required=False,
  213. null_option='None'
  214. )
  215. vlan_group = DynamicModelChoiceField(
  216. queryset=VLANGroup.objects.all(),
  217. required=False,
  218. label='VLAN group',
  219. null_option='None',
  220. query_params={
  221. 'site_id': '$site'
  222. }
  223. )
  224. vlan = DynamicModelChoiceField(
  225. queryset=VLAN.objects.all(),
  226. required=False,
  227. label='VLAN',
  228. display_field='display_name',
  229. query_params={
  230. 'site_id': '$site',
  231. 'group_id': '$vlan_group',
  232. }
  233. )
  234. role = DynamicModelChoiceField(
  235. queryset=Role.objects.all(),
  236. required=False
  237. )
  238. tags = DynamicModelMultipleChoiceField(
  239. queryset=Tag.objects.all(),
  240. required=False
  241. )
  242. class Meta:
  243. model = Prefix
  244. fields = [
  245. 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant',
  246. 'tags',
  247. ]
  248. widgets = {
  249. 'status': StaticSelect2(),
  250. }
  251. def __init__(self, *args, **kwargs):
  252. # Initialize helper selectors
  253. instance = kwargs.get('instance')
  254. initial = kwargs.get('initial', {}).copy()
  255. if instance and instance.vlan is not None:
  256. initial['vlan_group'] = instance.vlan.group
  257. kwargs['initial'] = initial
  258. super().__init__(*args, **kwargs)
  259. self.fields['vrf'].empty_label = 'Global'
  260. class PrefixCSVForm(CustomFieldModelCSVForm):
  261. vrf = CSVModelChoiceField(
  262. queryset=VRF.objects.all(),
  263. to_field_name='name',
  264. required=False,
  265. help_text='Assigned VRF'
  266. )
  267. tenant = CSVModelChoiceField(
  268. queryset=Tenant.objects.all(),
  269. required=False,
  270. to_field_name='name',
  271. help_text='Assigned tenant'
  272. )
  273. site = CSVModelChoiceField(
  274. queryset=Site.objects.all(),
  275. required=False,
  276. to_field_name='name',
  277. help_text='Assigned site'
  278. )
  279. vlan_group = CSVModelChoiceField(
  280. queryset=VLANGroup.objects.all(),
  281. required=False,
  282. to_field_name='name',
  283. help_text="VLAN's group (if any)"
  284. )
  285. vlan = CSVModelChoiceField(
  286. queryset=VLAN.objects.all(),
  287. required=False,
  288. to_field_name='vid',
  289. help_text="Assigned VLAN"
  290. )
  291. status = CSVChoiceField(
  292. choices=PrefixStatusChoices,
  293. help_text='Operational status'
  294. )
  295. role = CSVModelChoiceField(
  296. queryset=Role.objects.all(),
  297. required=False,
  298. to_field_name='name',
  299. help_text='Functional role'
  300. )
  301. class Meta:
  302. model = Prefix
  303. fields = Prefix.csv_headers
  304. def __init__(self, data=None, *args, **kwargs):
  305. super().__init__(data, *args, **kwargs)
  306. if data:
  307. # Limit vlan queryset by assigned site and group
  308. params = {
  309. f"site__{self.fields['site'].to_field_name}": data.get('site'),
  310. f"group__{self.fields['vlan_group'].to_field_name}": data.get('vlan_group'),
  311. }
  312. self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params)
  313. class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  314. pk = forms.ModelMultipleChoiceField(
  315. queryset=Prefix.objects.all(),
  316. widget=forms.MultipleHiddenInput()
  317. )
  318. site = DynamicModelChoiceField(
  319. queryset=Site.objects.all(),
  320. required=False
  321. )
  322. vrf = DynamicModelChoiceField(
  323. queryset=VRF.objects.all(),
  324. required=False,
  325. label='VRF'
  326. )
  327. prefix_length = forms.IntegerField(
  328. min_value=PREFIX_LENGTH_MIN,
  329. max_value=PREFIX_LENGTH_MAX,
  330. required=False
  331. )
  332. tenant = DynamicModelChoiceField(
  333. queryset=Tenant.objects.all(),
  334. required=False
  335. )
  336. status = forms.ChoiceField(
  337. choices=add_blank_choice(PrefixStatusChoices),
  338. required=False,
  339. widget=StaticSelect2()
  340. )
  341. role = DynamicModelChoiceField(
  342. queryset=Role.objects.all(),
  343. required=False
  344. )
  345. is_pool = forms.NullBooleanField(
  346. required=False,
  347. widget=BulkEditNullBooleanSelect(),
  348. label='Is a pool'
  349. )
  350. description = forms.CharField(
  351. max_length=100,
  352. required=False
  353. )
  354. class Meta:
  355. nullable_fields = [
  356. 'site', 'vrf', 'tenant', 'role', 'description',
  357. ]
  358. class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  359. model = Prefix
  360. field_order = [
  361. 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'region', 'site', 'role', 'tenant_group',
  362. 'tenant', 'is_pool', 'expand',
  363. ]
  364. mask_length__lte = forms.IntegerField(
  365. widget=forms.HiddenInput()
  366. )
  367. q = forms.CharField(
  368. required=False,
  369. label='Search'
  370. )
  371. within_include = forms.CharField(
  372. required=False,
  373. widget=forms.TextInput(
  374. attrs={
  375. 'placeholder': 'Prefix',
  376. }
  377. ),
  378. label='Search within'
  379. )
  380. family = forms.ChoiceField(
  381. required=False,
  382. choices=add_blank_choice(IPAddressFamilyChoices),
  383. label='Address family',
  384. widget=StaticSelect2()
  385. )
  386. mask_length = forms.ChoiceField(
  387. required=False,
  388. choices=PREFIX_MASK_LENGTH_CHOICES,
  389. label='Mask length',
  390. widget=StaticSelect2()
  391. )
  392. vrf_id = DynamicModelMultipleChoiceField(
  393. queryset=VRF.objects.all(),
  394. required=False,
  395. label='VRF',
  396. null_option='Global'
  397. )
  398. status = forms.MultipleChoiceField(
  399. choices=PrefixStatusChoices,
  400. required=False,
  401. widget=StaticSelect2Multiple()
  402. )
  403. region = DynamicModelMultipleChoiceField(
  404. queryset=Region.objects.all(),
  405. to_field_name='slug',
  406. required=False
  407. )
  408. site = DynamicModelMultipleChoiceField(
  409. queryset=Site.objects.all(),
  410. to_field_name='slug',
  411. required=False,
  412. null_option='None',
  413. query_params={
  414. 'region': '$region'
  415. }
  416. )
  417. role = DynamicModelMultipleChoiceField(
  418. queryset=Role.objects.all(),
  419. to_field_name='slug',
  420. required=False,
  421. null_option='None'
  422. )
  423. is_pool = forms.NullBooleanField(
  424. required=False,
  425. label='Is a pool',
  426. widget=StaticSelect2(
  427. choices=BOOLEAN_WITH_BLANK_CHOICES
  428. )
  429. )
  430. tag = TagFilterField(model)
  431. #
  432. # IP addresses
  433. #
  434. class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
  435. device = DynamicModelChoiceField(
  436. queryset=Device.objects.all(),
  437. required=False,
  438. display_field='display_name'
  439. )
  440. interface = DynamicModelChoiceField(
  441. queryset=Interface.objects.all(),
  442. required=False,
  443. query_params={
  444. 'device_id': '$device'
  445. }
  446. )
  447. virtual_machine = DynamicModelChoiceField(
  448. queryset=VirtualMachine.objects.all(),
  449. required=False
  450. )
  451. vminterface = DynamicModelChoiceField(
  452. queryset=VMInterface.objects.all(),
  453. required=False,
  454. label='Interface',
  455. query_params={
  456. 'virtual_machine_id': '$virtual_machine'
  457. }
  458. )
  459. vrf = DynamicModelChoiceField(
  460. queryset=VRF.objects.all(),
  461. required=False,
  462. label='VRF'
  463. )
  464. nat_site = DynamicModelChoiceField(
  465. queryset=Site.objects.all(),
  466. required=False,
  467. label='Site'
  468. )
  469. nat_rack = DynamicModelChoiceField(
  470. queryset=Rack.objects.all(),
  471. required=False,
  472. label='Rack',
  473. display_field='display_name',
  474. null_option='None',
  475. query_params={
  476. 'site_id': '$site'
  477. }
  478. )
  479. nat_device = DynamicModelChoiceField(
  480. queryset=Device.objects.all(),
  481. required=False,
  482. label='Device',
  483. display_field='display_name',
  484. query_params={
  485. 'site_id': '$site',
  486. 'rack_id': '$nat_rack',
  487. }
  488. )
  489. nat_vrf = DynamicModelChoiceField(
  490. queryset=VRF.objects.all(),
  491. required=False,
  492. label='VRF'
  493. )
  494. nat_inside = DynamicModelChoiceField(
  495. queryset=IPAddress.objects.all(),
  496. required=False,
  497. label='IP Address',
  498. display_field='address',
  499. query_params={
  500. 'device_id': '$nat_device',
  501. 'vrf_id': '$nat_vrf',
  502. }
  503. )
  504. primary_for_parent = forms.BooleanField(
  505. required=False,
  506. label='Make this the primary IP for the device/VM'
  507. )
  508. tags = DynamicModelMultipleChoiceField(
  509. queryset=Tag.objects.all(),
  510. required=False
  511. )
  512. class Meta:
  513. model = IPAddress
  514. fields = [
  515. 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack',
  516. 'nat_inside', 'tenant_group', 'tenant', 'tags',
  517. ]
  518. widgets = {
  519. 'status': StaticSelect2(),
  520. 'role': StaticSelect2(),
  521. }
  522. def __init__(self, *args, **kwargs):
  523. # Initialize helper selectors
  524. instance = kwargs.get('instance')
  525. initial = kwargs.get('initial', {}).copy()
  526. if instance:
  527. if type(instance.assigned_object) is Interface:
  528. initial['device'] = instance.assigned_object.device
  529. initial['interface'] = instance.assigned_object
  530. elif type(instance.assigned_object) is VMInterface:
  531. initial['virtual_machine'] = instance.assigned_object.virtual_machine
  532. initial['vminterface'] = instance.assigned_object
  533. if instance.nat_inside and instance.nat_inside.device is not None:
  534. initial['nat_site'] = instance.nat_inside.device.site
  535. initial['nat_rack'] = instance.nat_inside.device.rack
  536. initial['nat_device'] = instance.nat_inside.device
  537. kwargs['initial'] = initial
  538. super().__init__(*args, **kwargs)
  539. self.fields['vrf'].empty_label = 'Global'
  540. # Initialize primary_for_parent if IP address is already assigned
  541. if self.instance.pk and self.instance.assigned_object:
  542. parent = self.instance.assigned_object.parent
  543. if (
  544. self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
  545. self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
  546. ):
  547. self.initial['primary_for_parent'] = True
  548. def clean(self):
  549. super().clean()
  550. # Cannot select both a device interface and a VM interface
  551. if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'):
  552. raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface")
  553. # Primary IP assignment is only available if an interface has been assigned.
  554. interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
  555. if self.cleaned_data.get('primary_for_parent') and not interface:
  556. self.add_error(
  557. 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
  558. )
  559. def save(self, *args, **kwargs):
  560. # Set assigned object
  561. interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
  562. if interface:
  563. self.instance.assigned_object = interface
  564. ipaddress = super().save(*args, **kwargs)
  565. # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
  566. if interface and self.cleaned_data['primary_for_parent']:
  567. if ipaddress.address.version == 4:
  568. interface.parent.primary_ip4 = ipaddress
  569. else:
  570. interface.primary_ip6 = ipaddress
  571. interface.parent.save()
  572. elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress:
  573. interface.parent.primary_ip4 = None
  574. interface.parent.save()
  575. elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress:
  576. interface.parent.primary_ip4 = None
  577. interface.parent.save()
  578. return ipaddress
  579. class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
  580. pattern = ExpandableIPAddressField(
  581. label='Address pattern'
  582. )
  583. class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  584. vrf = DynamicModelChoiceField(
  585. queryset=VRF.objects.all(),
  586. required=False,
  587. label='VRF'
  588. )
  589. tags = DynamicModelMultipleChoiceField(
  590. queryset=Tag.objects.all(),
  591. required=False
  592. )
  593. class Meta:
  594. model = IPAddress
  595. fields = [
  596. 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
  597. ]
  598. widgets = {
  599. 'status': StaticSelect2(),
  600. 'role': StaticSelect2(),
  601. }
  602. def __init__(self, *args, **kwargs):
  603. super().__init__(*args, **kwargs)
  604. self.fields['vrf'].empty_label = 'Global'
  605. class IPAddressCSVForm(CustomFieldModelCSVForm):
  606. vrf = CSVModelChoiceField(
  607. queryset=VRF.objects.all(),
  608. to_field_name='name',
  609. required=False,
  610. help_text='Assigned VRF'
  611. )
  612. tenant = CSVModelChoiceField(
  613. queryset=Tenant.objects.all(),
  614. to_field_name='name',
  615. required=False,
  616. help_text='Assigned tenant'
  617. )
  618. status = CSVChoiceField(
  619. choices=IPAddressStatusChoices,
  620. help_text='Operational status'
  621. )
  622. role = CSVChoiceField(
  623. choices=IPAddressRoleChoices,
  624. required=False,
  625. help_text='Functional role'
  626. )
  627. device = CSVModelChoiceField(
  628. queryset=Device.objects.all(),
  629. required=False,
  630. to_field_name='name',
  631. help_text='Parent device of assigned interface (if any)'
  632. )
  633. virtual_machine = CSVModelChoiceField(
  634. queryset=VirtualMachine.objects.all(),
  635. required=False,
  636. to_field_name='name',
  637. help_text='Parent VM of assigned interface (if any)'
  638. )
  639. interface = CSVModelChoiceField(
  640. queryset=Interface.objects.none(), # Can also refer to VMInterface
  641. required=False,
  642. to_field_name='name',
  643. help_text='Assigned interface'
  644. )
  645. is_primary = forms.BooleanField(
  646. help_text='Make this the primary IP for the assigned device',
  647. required=False
  648. )
  649. class Meta:
  650. model = IPAddress
  651. fields = IPAddress.csv_headers
  652. def __init__(self, data=None, *args, **kwargs):
  653. super().__init__(data, *args, **kwargs)
  654. if data:
  655. # Limit interface queryset by assigned device
  656. if data.get('device'):
  657. self.fields['interface'].queryset = Interface.objects.filter(
  658. **{f"device__{self.fields['device'].to_field_name}": data['device']}
  659. )
  660. # Limit interface queryset by assigned device
  661. elif data.get('virtual_machine'):
  662. self.fields['interface'].queryset = VMInterface.objects.filter(
  663. **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
  664. )
  665. def clean(self):
  666. super().clean()
  667. device = self.cleaned_data.get('device')
  668. virtual_machine = self.cleaned_data.get('virtual_machine')
  669. is_primary = self.cleaned_data.get('is_primary')
  670. # Validate is_primary
  671. if is_primary and not device and not virtual_machine:
  672. raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP")
  673. def save(self, *args, **kwargs):
  674. # Set interface assignment
  675. if self.cleaned_data['interface']:
  676. self.instance.assigned_object = self.cleaned_data['interface']
  677. ipaddress = super().save(*args, **kwargs)
  678. # Set as primary for device/VM
  679. if self.cleaned_data['is_primary']:
  680. parent = self.cleaned_data['device'] or self.cleaned_data['virtual_machine']
  681. if self.instance.address.version == 4:
  682. parent.primary_ip4 = ipaddress
  683. elif self.instance.address.version == 6:
  684. parent.primary_ip6 = ipaddress
  685. parent.save()
  686. return ipaddress
  687. class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  688. pk = forms.ModelMultipleChoiceField(
  689. queryset=IPAddress.objects.all(),
  690. widget=forms.MultipleHiddenInput()
  691. )
  692. vrf = DynamicModelChoiceField(
  693. queryset=VRF.objects.all(),
  694. required=False,
  695. label='VRF'
  696. )
  697. mask_length = forms.IntegerField(
  698. min_value=IPADDRESS_MASK_LENGTH_MIN,
  699. max_value=IPADDRESS_MASK_LENGTH_MAX,
  700. required=False
  701. )
  702. tenant = DynamicModelChoiceField(
  703. queryset=Tenant.objects.all(),
  704. required=False
  705. )
  706. status = forms.ChoiceField(
  707. choices=add_blank_choice(IPAddressStatusChoices),
  708. required=False,
  709. widget=StaticSelect2()
  710. )
  711. role = forms.ChoiceField(
  712. choices=add_blank_choice(IPAddressRoleChoices),
  713. required=False,
  714. widget=StaticSelect2()
  715. )
  716. dns_name = forms.CharField(
  717. max_length=255,
  718. required=False
  719. )
  720. description = forms.CharField(
  721. max_length=100,
  722. required=False
  723. )
  724. class Meta:
  725. nullable_fields = [
  726. 'vrf', 'role', 'tenant', 'dns_name', 'description',
  727. ]
  728. class IPAddressAssignForm(BootstrapMixin, forms.Form):
  729. vrf_id = DynamicModelChoiceField(
  730. queryset=VRF.objects.all(),
  731. required=False,
  732. label='VRF',
  733. empty_label='Global'
  734. )
  735. q = forms.CharField(
  736. required=False,
  737. label='Search',
  738. )
  739. class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  740. model = IPAddress
  741. field_order = [
  742. 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'assigned_to_interface', 'tenant_group',
  743. 'tenant',
  744. ]
  745. q = forms.CharField(
  746. required=False,
  747. label='Search'
  748. )
  749. parent = forms.CharField(
  750. required=False,
  751. widget=forms.TextInput(
  752. attrs={
  753. 'placeholder': 'Prefix',
  754. }
  755. ),
  756. label='Parent Prefix'
  757. )
  758. family = forms.ChoiceField(
  759. required=False,
  760. choices=add_blank_choice(IPAddressFamilyChoices),
  761. label='Address family',
  762. widget=StaticSelect2()
  763. )
  764. mask_length = forms.ChoiceField(
  765. required=False,
  766. choices=IPADDRESS_MASK_LENGTH_CHOICES,
  767. label='Mask length',
  768. widget=StaticSelect2()
  769. )
  770. vrf_id = DynamicModelMultipleChoiceField(
  771. queryset=VRF.objects.all(),
  772. required=False,
  773. label='VRF',
  774. null_option='Global'
  775. )
  776. status = forms.MultipleChoiceField(
  777. choices=IPAddressStatusChoices,
  778. required=False,
  779. widget=StaticSelect2Multiple()
  780. )
  781. role = forms.MultipleChoiceField(
  782. choices=IPAddressRoleChoices,
  783. required=False,
  784. widget=StaticSelect2Multiple()
  785. )
  786. assigned_to_interface = forms.NullBooleanField(
  787. required=False,
  788. label='Assigned to an interface',
  789. widget=StaticSelect2(
  790. choices=BOOLEAN_WITH_BLANK_CHOICES
  791. )
  792. )
  793. tag = TagFilterField(model)
  794. #
  795. # VLAN groups
  796. #
  797. class VLANGroupForm(BootstrapMixin, forms.ModelForm):
  798. site = DynamicModelChoiceField(
  799. queryset=Site.objects.all(),
  800. required=False
  801. )
  802. slug = SlugField()
  803. class Meta:
  804. model = VLANGroup
  805. fields = [
  806. 'site', 'name', 'slug', 'description',
  807. ]
  808. class VLANGroupCSVForm(CSVModelForm):
  809. site = CSVModelChoiceField(
  810. queryset=Site.objects.all(),
  811. required=False,
  812. to_field_name='name',
  813. help_text='Assigned site'
  814. )
  815. slug = SlugField()
  816. class Meta:
  817. model = VLANGroup
  818. fields = VLANGroup.csv_headers
  819. class VLANGroupFilterForm(BootstrapMixin, forms.Form):
  820. region = DynamicModelMultipleChoiceField(
  821. queryset=Region.objects.all(),
  822. to_field_name='slug',
  823. required=False
  824. )
  825. site = DynamicModelMultipleChoiceField(
  826. queryset=Site.objects.all(),
  827. to_field_name='slug',
  828. required=False,
  829. null_option='None',
  830. query_params={
  831. 'region': '$region'
  832. }
  833. )
  834. #
  835. # VLANs
  836. #
  837. class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  838. site = DynamicModelChoiceField(
  839. queryset=Site.objects.all(),
  840. required=False,
  841. null_option='None'
  842. )
  843. group = DynamicModelChoiceField(
  844. queryset=VLANGroup.objects.all(),
  845. required=False,
  846. query_params={
  847. 'site_id': '$site'
  848. }
  849. )
  850. role = DynamicModelChoiceField(
  851. queryset=Role.objects.all(),
  852. required=False
  853. )
  854. tags = DynamicModelMultipleChoiceField(
  855. queryset=Tag.objects.all(),
  856. required=False
  857. )
  858. class Meta:
  859. model = VLAN
  860. fields = [
  861. 'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags',
  862. ]
  863. help_texts = {
  864. 'site': "Leave blank if this VLAN spans multiple sites",
  865. 'group': "VLAN group (optional)",
  866. 'vid': "Configured VLAN ID",
  867. 'name': "Configured VLAN name",
  868. 'status': "Operational status of this VLAN",
  869. 'role': "The primary function of this VLAN",
  870. }
  871. widgets = {
  872. 'status': StaticSelect2(),
  873. }
  874. class VLANCSVForm(CustomFieldModelCSVForm):
  875. site = CSVModelChoiceField(
  876. queryset=Site.objects.all(),
  877. required=False,
  878. to_field_name='name',
  879. help_text='Assigned site'
  880. )
  881. group = CSVModelChoiceField(
  882. queryset=VLANGroup.objects.all(),
  883. required=False,
  884. to_field_name='name',
  885. help_text='Assigned VLAN group'
  886. )
  887. tenant = CSVModelChoiceField(
  888. queryset=Tenant.objects.all(),
  889. to_field_name='name',
  890. required=False,
  891. help_text='Assigned tenant'
  892. )
  893. status = CSVChoiceField(
  894. choices=VLANStatusChoices,
  895. help_text='Operational status'
  896. )
  897. role = CSVModelChoiceField(
  898. queryset=Role.objects.all(),
  899. required=False,
  900. to_field_name='name',
  901. help_text='Functional role'
  902. )
  903. class Meta:
  904. model = VLAN
  905. fields = VLAN.csv_headers
  906. help_texts = {
  907. 'vid': 'Numeric VLAN ID (1-4095)',
  908. 'name': 'VLAN name',
  909. }
  910. def __init__(self, data=None, *args, **kwargs):
  911. super().__init__(data, *args, **kwargs)
  912. if data:
  913. # Limit vlan queryset by assigned group
  914. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  915. self.fields['group'].queryset = self.fields['group'].queryset.filter(**params)
  916. class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  917. pk = forms.ModelMultipleChoiceField(
  918. queryset=VLAN.objects.all(),
  919. widget=forms.MultipleHiddenInput()
  920. )
  921. site = DynamicModelChoiceField(
  922. queryset=Site.objects.all(),
  923. required=False
  924. )
  925. group = DynamicModelChoiceField(
  926. queryset=VLANGroup.objects.all(),
  927. required=False,
  928. query_params={
  929. 'site_id': '$site'
  930. }
  931. )
  932. tenant = DynamicModelChoiceField(
  933. queryset=Tenant.objects.all(),
  934. required=False
  935. )
  936. status = forms.ChoiceField(
  937. choices=add_blank_choice(VLANStatusChoices),
  938. required=False,
  939. widget=StaticSelect2()
  940. )
  941. role = DynamicModelChoiceField(
  942. queryset=Role.objects.all(),
  943. required=False
  944. )
  945. description = forms.CharField(
  946. max_length=100,
  947. required=False
  948. )
  949. class Meta:
  950. nullable_fields = [
  951. 'site', 'group', 'tenant', 'role', 'description',
  952. ]
  953. class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  954. model = VLAN
  955. field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
  956. q = forms.CharField(
  957. required=False,
  958. label='Search'
  959. )
  960. region = DynamicModelMultipleChoiceField(
  961. queryset=Region.objects.all(),
  962. to_field_name='slug',
  963. required=False
  964. )
  965. site = DynamicModelMultipleChoiceField(
  966. queryset=Site.objects.all(),
  967. to_field_name='slug',
  968. required=False,
  969. null_option='None',
  970. query_params={
  971. 'region': '$region'
  972. }
  973. )
  974. group_id = DynamicModelMultipleChoiceField(
  975. queryset=VLANGroup.objects.all(),
  976. required=False,
  977. label='VLAN group',
  978. null_option='None',
  979. query_params={
  980. 'region': '$region'
  981. }
  982. )
  983. status = forms.MultipleChoiceField(
  984. choices=VLANStatusChoices,
  985. required=False,
  986. widget=StaticSelect2Multiple()
  987. )
  988. role = DynamicModelMultipleChoiceField(
  989. queryset=Role.objects.all(),
  990. to_field_name='slug',
  991. required=False,
  992. null_option='None'
  993. )
  994. tag = TagFilterField(model)
  995. #
  996. # Services
  997. #
  998. class ServiceForm(BootstrapMixin, CustomFieldModelForm):
  999. port = forms.IntegerField(
  1000. min_value=SERVICE_PORT_MIN,
  1001. max_value=SERVICE_PORT_MAX
  1002. )
  1003. tags = DynamicModelMultipleChoiceField(
  1004. queryset=Tag.objects.all(),
  1005. required=False
  1006. )
  1007. class Meta:
  1008. model = Service
  1009. fields = [
  1010. 'name', 'protocol', 'port', 'ipaddresses', 'description', 'tags',
  1011. ]
  1012. help_texts = {
  1013. 'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
  1014. "reachable via all IPs assigned to the device.",
  1015. }
  1016. widgets = {
  1017. 'protocol': StaticSelect2(),
  1018. 'ipaddresses': StaticSelect2Multiple(),
  1019. }
  1020. def __init__(self, *args, **kwargs):
  1021. super().__init__(*args, **kwargs)
  1022. # Limit IP address choices to those assigned to interfaces of the parent device/VM
  1023. if self.instance.device:
  1024. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  1025. interface__in=self.instance.device.vc_interfaces.values_list('id', flat=True)
  1026. )
  1027. elif self.instance.virtual_machine:
  1028. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  1029. vminterface__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True)
  1030. )
  1031. else:
  1032. self.fields['ipaddresses'].choices = []
  1033. class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
  1034. model = Service
  1035. q = forms.CharField(
  1036. required=False,
  1037. label='Search'
  1038. )
  1039. protocol = forms.ChoiceField(
  1040. choices=add_blank_choice(ServiceProtocolChoices),
  1041. required=False,
  1042. widget=StaticSelect2Multiple()
  1043. )
  1044. port = forms.IntegerField(
  1045. required=False,
  1046. )
  1047. tag = TagFilterField(model)
  1048. class ServiceCSVForm(CustomFieldModelCSVForm):
  1049. device = CSVModelChoiceField(
  1050. queryset=Device.objects.all(),
  1051. required=False,
  1052. to_field_name='name',
  1053. help_text='Required if not assigned to a VM'
  1054. )
  1055. virtual_machine = CSVModelChoiceField(
  1056. queryset=VirtualMachine.objects.all(),
  1057. required=False,
  1058. to_field_name='name',
  1059. help_text='Required if not assigned to a device'
  1060. )
  1061. protocol = CSVChoiceField(
  1062. choices=ServiceProtocolChoices,
  1063. help_text='IP protocol'
  1064. )
  1065. class Meta:
  1066. model = Service
  1067. fields = Service.csv_headers
  1068. class ServiceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  1069. pk = forms.ModelMultipleChoiceField(
  1070. queryset=Service.objects.all(),
  1071. widget=forms.MultipleHiddenInput()
  1072. )
  1073. protocol = forms.ChoiceField(
  1074. choices=add_blank_choice(ServiceProtocolChoices),
  1075. required=False,
  1076. widget=StaticSelect2()
  1077. )
  1078. port = forms.IntegerField(
  1079. validators=[
  1080. MinValueValidator(1),
  1081. MaxValueValidator(65535),
  1082. ],
  1083. required=False
  1084. )
  1085. description = forms.CharField(
  1086. max_length=100,
  1087. required=False
  1088. )
  1089. class Meta:
  1090. nullable_fields = [
  1091. 'description',
  1092. ]