forms.py 36 KB

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