forms.py 35 KB

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