forms.py 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268
  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. ).prefetch_related(
  551. 'device__primary_ip4',
  552. 'device__primary_ip6',
  553. 'virtual_machine__primary_ip4',
  554. 'virtual_machine__primary_ip6',
  555. ) # We prefetch the primary address fields to ensure cache invalidation does not balk on the save()
  556. else:
  557. self.fields['interface'].choices = []
  558. # Initialize primary_for_parent if IP address is already assigned
  559. if self.instance.pk and self.instance.interface is not None:
  560. parent = self.instance.interface.parent
  561. if (
  562. self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
  563. self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
  564. ):
  565. self.initial['primary_for_parent'] = True
  566. def clean(self):
  567. super().clean()
  568. # Primary IP assignment is only available if an interface has been assigned.
  569. if self.cleaned_data.get('primary_for_parent') and not self.cleaned_data.get('interface'):
  570. self.add_error(
  571. 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
  572. )
  573. def save(self, *args, **kwargs):
  574. ipaddress = super().save(*args, **kwargs)
  575. # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
  576. if self.cleaned_data['primary_for_parent']:
  577. parent = self.cleaned_data['interface'].parent
  578. if ipaddress.address.version == 4:
  579. parent.primary_ip4 = ipaddress
  580. else:
  581. parent.primary_ip6 = ipaddress
  582. parent.save()
  583. elif self.cleaned_data['interface']:
  584. parent = self.cleaned_data['interface'].parent
  585. if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
  586. parent.primary_ip4 = None
  587. parent.save()
  588. elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
  589. parent.primary_ip6 = None
  590. parent.save()
  591. return ipaddress
  592. class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
  593. pattern = ExpandableIPAddressField(
  594. label='Address pattern'
  595. )
  596. class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  597. vrf = DynamicModelChoiceField(
  598. queryset=VRF.objects.all(),
  599. required=False,
  600. label='VRF'
  601. )
  602. tags = TagField(
  603. required=False
  604. )
  605. class Meta:
  606. model = IPAddress
  607. fields = [
  608. 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
  609. ]
  610. widgets = {
  611. 'status': StaticSelect2(),
  612. 'role': StaticSelect2(),
  613. }
  614. def __init__(self, *args, **kwargs):
  615. super().__init__(*args, **kwargs)
  616. self.fields['vrf'].empty_label = 'Global'
  617. class IPAddressCSVForm(CustomFieldModelCSVForm):
  618. vrf = CSVModelChoiceField(
  619. queryset=VRF.objects.all(),
  620. to_field_name='name',
  621. required=False,
  622. help_text='Assigned VRF'
  623. )
  624. tenant = CSVModelChoiceField(
  625. queryset=Tenant.objects.all(),
  626. to_field_name='name',
  627. required=False,
  628. help_text='Assigned tenant'
  629. )
  630. status = CSVChoiceField(
  631. choices=IPAddressStatusChoices,
  632. help_text='Operational status'
  633. )
  634. role = CSVChoiceField(
  635. choices=IPAddressRoleChoices,
  636. required=False,
  637. help_text='Functional role'
  638. )
  639. device = CSVModelChoiceField(
  640. queryset=Device.objects.all(),
  641. required=False,
  642. to_field_name='name',
  643. help_text='Parent device of assigned interface (if any)'
  644. )
  645. virtual_machine = CSVModelChoiceField(
  646. queryset=VirtualMachine.objects.all(),
  647. required=False,
  648. to_field_name='name',
  649. help_text='Parent VM of assigned interface (if any)'
  650. )
  651. interface = CSVModelChoiceField(
  652. queryset=Interface.objects.all(),
  653. required=False,
  654. to_field_name='name',
  655. help_text='Assigned interface'
  656. )
  657. is_primary = forms.BooleanField(
  658. help_text='Make this the primary IP for the assigned device',
  659. required=False
  660. )
  661. class Meta:
  662. model = IPAddress
  663. fields = IPAddress.csv_headers
  664. def __init__(self, data=None, *args, **kwargs):
  665. super().__init__(data, *args, **kwargs)
  666. if data:
  667. # Limit interface queryset by assigned device or virtual machine
  668. if data.get('device'):
  669. params = {
  670. f"device__{self.fields['device'].to_field_name}": data.get('device')
  671. }
  672. elif data.get('virtual_machine'):
  673. params = {
  674. f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data.get('virtual_machine')
  675. }
  676. else:
  677. params = {
  678. 'device': None,
  679. 'virtual_machine': None,
  680. }
  681. self.fields['interface'].queryset = self.fields['interface'].queryset.filter(**params)
  682. def clean(self):
  683. super().clean()
  684. device = self.cleaned_data.get('device')
  685. virtual_machine = self.cleaned_data.get('virtual_machine')
  686. is_primary = self.cleaned_data.get('is_primary')
  687. # Validate is_primary
  688. if is_primary and not device and not virtual_machine:
  689. raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP")
  690. def save(self, *args, **kwargs):
  691. ipaddress = super().save(*args, **kwargs)
  692. # Set as primary for device/VM
  693. if self.cleaned_data['is_primary']:
  694. parent = self.cleaned_data['device'] or self.cleaned_data['virtual_machine']
  695. if self.instance.address.version == 4:
  696. parent.primary_ip4 = ipaddress
  697. elif self.instance.address.version == 6:
  698. parent.primary_ip6 = ipaddress
  699. parent.save()
  700. return ipaddress
  701. class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  702. pk = forms.ModelMultipleChoiceField(
  703. queryset=IPAddress.objects.all(),
  704. widget=forms.MultipleHiddenInput()
  705. )
  706. vrf = DynamicModelChoiceField(
  707. queryset=VRF.objects.all(),
  708. required=False,
  709. label='VRF'
  710. )
  711. mask_length = forms.IntegerField(
  712. min_value=IPADDRESS_MASK_LENGTH_MIN,
  713. max_value=IPADDRESS_MASK_LENGTH_MAX,
  714. required=False
  715. )
  716. tenant = DynamicModelChoiceField(
  717. queryset=Tenant.objects.all(),
  718. required=False
  719. )
  720. status = forms.ChoiceField(
  721. choices=add_blank_choice(IPAddressStatusChoices),
  722. required=False,
  723. widget=StaticSelect2()
  724. )
  725. role = forms.ChoiceField(
  726. choices=add_blank_choice(IPAddressRoleChoices),
  727. required=False,
  728. widget=StaticSelect2()
  729. )
  730. dns_name = forms.CharField(
  731. max_length=255,
  732. required=False
  733. )
  734. description = forms.CharField(
  735. max_length=100,
  736. required=False
  737. )
  738. class Meta:
  739. nullable_fields = [
  740. 'vrf', 'role', 'tenant', 'dns_name', 'description',
  741. ]
  742. class IPAddressAssignForm(BootstrapMixin, forms.Form):
  743. vrf_id = DynamicModelChoiceField(
  744. queryset=VRF.objects.all(),
  745. required=False,
  746. label='VRF',
  747. empty_label='Global'
  748. )
  749. q = forms.CharField(
  750. required=False,
  751. label='Search',
  752. )
  753. class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  754. model = IPAddress
  755. field_order = [
  756. 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'assigned_to_interface', 'tenant_group',
  757. 'tenant',
  758. ]
  759. q = forms.CharField(
  760. required=False,
  761. label='Search'
  762. )
  763. parent = forms.CharField(
  764. required=False,
  765. widget=forms.TextInput(
  766. attrs={
  767. 'placeholder': 'Prefix',
  768. }
  769. ),
  770. label='Parent Prefix'
  771. )
  772. family = forms.ChoiceField(
  773. required=False,
  774. choices=add_blank_choice(IPAddressFamilyChoices),
  775. label='Address family',
  776. widget=StaticSelect2()
  777. )
  778. mask_length = forms.ChoiceField(
  779. required=False,
  780. choices=IPADDRESS_MASK_LENGTH_CHOICES,
  781. label='Mask length',
  782. widget=StaticSelect2()
  783. )
  784. vrf_id = DynamicModelMultipleChoiceField(
  785. queryset=VRF.objects.all(),
  786. required=False,
  787. label='VRF',
  788. widget=APISelectMultiple(
  789. null_option=True,
  790. )
  791. )
  792. status = forms.MultipleChoiceField(
  793. choices=IPAddressStatusChoices,
  794. required=False,
  795. widget=StaticSelect2Multiple()
  796. )
  797. role = forms.MultipleChoiceField(
  798. choices=IPAddressRoleChoices,
  799. required=False,
  800. widget=StaticSelect2Multiple()
  801. )
  802. assigned_to_interface = forms.NullBooleanField(
  803. required=False,
  804. label='Assigned to an interface',
  805. widget=StaticSelect2(
  806. choices=BOOLEAN_WITH_BLANK_CHOICES
  807. )
  808. )
  809. tag = TagFilterField(model)
  810. #
  811. # VLAN groups
  812. #
  813. class VLANGroupForm(BootstrapMixin, forms.ModelForm):
  814. site = DynamicModelChoiceField(
  815. queryset=Site.objects.all(),
  816. required=False
  817. )
  818. slug = SlugField()
  819. class Meta:
  820. model = VLANGroup
  821. fields = [
  822. 'site', 'name', 'slug', 'description',
  823. ]
  824. class VLANGroupCSVForm(CSVModelForm):
  825. site = CSVModelChoiceField(
  826. queryset=Site.objects.all(),
  827. required=False,
  828. to_field_name='name',
  829. help_text='Assigned site'
  830. )
  831. slug = SlugField()
  832. class Meta:
  833. model = VLANGroup
  834. fields = VLANGroup.csv_headers
  835. class VLANGroupFilterForm(BootstrapMixin, forms.Form):
  836. region = DynamicModelMultipleChoiceField(
  837. queryset=Region.objects.all(),
  838. to_field_name='slug',
  839. required=False,
  840. widget=APISelectMultiple(
  841. value_field="slug",
  842. filter_for={
  843. 'site': 'region',
  844. }
  845. )
  846. )
  847. site = DynamicModelMultipleChoiceField(
  848. queryset=Site.objects.all(),
  849. to_field_name='slug',
  850. required=False,
  851. widget=APISelectMultiple(
  852. value_field="slug",
  853. null_option=True,
  854. )
  855. )
  856. #
  857. # VLANs
  858. #
  859. class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  860. site = DynamicModelChoiceField(
  861. queryset=Site.objects.all(),
  862. required=False,
  863. widget=APISelect(
  864. filter_for={
  865. 'group': 'site_id'
  866. },
  867. attrs={
  868. 'nullable': 'true',
  869. }
  870. )
  871. )
  872. group = DynamicModelChoiceField(
  873. queryset=VLANGroup.objects.all(),
  874. required=False
  875. )
  876. role = DynamicModelChoiceField(
  877. queryset=Role.objects.all(),
  878. required=False
  879. )
  880. tags = TagField(required=False)
  881. class Meta:
  882. model = VLAN
  883. fields = [
  884. 'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags',
  885. ]
  886. help_texts = {
  887. 'site': "Leave blank if this VLAN spans multiple sites",
  888. 'group': "VLAN group (optional)",
  889. 'vid': "Configured VLAN ID",
  890. 'name': "Configured VLAN name",
  891. 'status': "Operational status of this VLAN",
  892. 'role': "The primary function of this VLAN",
  893. }
  894. widgets = {
  895. 'status': StaticSelect2(),
  896. }
  897. class VLANCSVForm(CustomFieldModelCSVForm):
  898. site = CSVModelChoiceField(
  899. queryset=Site.objects.all(),
  900. required=False,
  901. to_field_name='name',
  902. help_text='Assigned site'
  903. )
  904. group = CSVModelChoiceField(
  905. queryset=VLANGroup.objects.all(),
  906. required=False,
  907. to_field_name='name',
  908. help_text='Assigned VLAN group'
  909. )
  910. tenant = CSVModelChoiceField(
  911. queryset=Tenant.objects.all(),
  912. to_field_name='name',
  913. required=False,
  914. help_text='Assigned tenant'
  915. )
  916. status = CSVChoiceField(
  917. choices=VLANStatusChoices,
  918. help_text='Operational status'
  919. )
  920. role = CSVModelChoiceField(
  921. queryset=Role.objects.all(),
  922. required=False,
  923. to_field_name='name',
  924. help_text='Functional role'
  925. )
  926. class Meta:
  927. model = VLAN
  928. fields = VLAN.csv_headers
  929. help_texts = {
  930. 'vid': 'Numeric VLAN ID (1-4095)',
  931. 'name': 'VLAN name',
  932. }
  933. def __init__(self, data=None, *args, **kwargs):
  934. super().__init__(data, *args, **kwargs)
  935. if data:
  936. # Limit vlan queryset by assigned group
  937. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  938. self.fields['group'].queryset = self.fields['group'].queryset.filter(**params)
  939. class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  940. pk = forms.ModelMultipleChoiceField(
  941. queryset=VLAN.objects.all(),
  942. widget=forms.MultipleHiddenInput()
  943. )
  944. site = DynamicModelChoiceField(
  945. queryset=Site.objects.all(),
  946. required=False,
  947. widget=APISelect(
  948. filter_for={
  949. 'group': 'site_id'
  950. }
  951. )
  952. )
  953. group = DynamicModelChoiceField(
  954. queryset=VLANGroup.objects.all(),
  955. required=False
  956. )
  957. tenant = DynamicModelChoiceField(
  958. queryset=Tenant.objects.all(),
  959. required=False
  960. )
  961. status = forms.ChoiceField(
  962. choices=add_blank_choice(VLANStatusChoices),
  963. required=False,
  964. widget=StaticSelect2()
  965. )
  966. role = DynamicModelChoiceField(
  967. queryset=Role.objects.all(),
  968. required=False
  969. )
  970. description = forms.CharField(
  971. max_length=100,
  972. required=False
  973. )
  974. class Meta:
  975. nullable_fields = [
  976. 'site', 'group', 'tenant', 'role', 'description',
  977. ]
  978. class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  979. model = VLAN
  980. field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
  981. q = forms.CharField(
  982. required=False,
  983. label='Search'
  984. )
  985. region = DynamicModelMultipleChoiceField(
  986. queryset=Region.objects.all(),
  987. to_field_name='slug',
  988. required=False,
  989. widget=APISelectMultiple(
  990. value_field="slug",
  991. filter_for={
  992. 'site': 'region',
  993. 'group_id': 'region'
  994. }
  995. )
  996. )
  997. site = DynamicModelMultipleChoiceField(
  998. queryset=Site.objects.all(),
  999. to_field_name='slug',
  1000. required=False,
  1001. widget=APISelectMultiple(
  1002. value_field="slug",
  1003. null_option=True,
  1004. )
  1005. )
  1006. group_id = DynamicModelMultipleChoiceField(
  1007. queryset=VLANGroup.objects.all(),
  1008. required=False,
  1009. label='VLAN group',
  1010. widget=APISelectMultiple(
  1011. null_option=True,
  1012. )
  1013. )
  1014. status = forms.MultipleChoiceField(
  1015. choices=VLANStatusChoices,
  1016. required=False,
  1017. widget=StaticSelect2Multiple()
  1018. )
  1019. role = DynamicModelMultipleChoiceField(
  1020. queryset=Role.objects.all(),
  1021. to_field_name='slug',
  1022. required=False,
  1023. widget=APISelectMultiple(
  1024. value_field="slug",
  1025. null_option=True,
  1026. )
  1027. )
  1028. tag = TagFilterField(model)
  1029. #
  1030. # Services
  1031. #
  1032. class ServiceForm(BootstrapMixin, CustomFieldModelForm):
  1033. port = forms.IntegerField(
  1034. min_value=SERVICE_PORT_MIN,
  1035. max_value=SERVICE_PORT_MAX
  1036. )
  1037. tags = TagField(
  1038. required=False
  1039. )
  1040. class Meta:
  1041. model = Service
  1042. fields = [
  1043. 'name', 'protocol', 'port', 'ipaddresses', 'description', 'tags',
  1044. ]
  1045. help_texts = {
  1046. 'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
  1047. "reachable via all IPs assigned to the device.",
  1048. }
  1049. widgets = {
  1050. 'protocol': StaticSelect2(),
  1051. 'ipaddresses': StaticSelect2Multiple(),
  1052. }
  1053. def __init__(self, *args, **kwargs):
  1054. super().__init__(*args, **kwargs)
  1055. # Limit IP address choices to those assigned to interfaces of the parent device/VM
  1056. if self.instance.device:
  1057. vc_interface_ids = [i['id'] for i in self.instance.device.vc_interfaces.values('id')]
  1058. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  1059. interface_id__in=vc_interface_ids
  1060. )
  1061. elif self.instance.virtual_machine:
  1062. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  1063. interface__virtual_machine=self.instance.virtual_machine
  1064. )
  1065. else:
  1066. self.fields['ipaddresses'].choices = []
  1067. class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
  1068. model = Service
  1069. q = forms.CharField(
  1070. required=False,
  1071. label='Search'
  1072. )
  1073. protocol = forms.ChoiceField(
  1074. choices=add_blank_choice(ServiceProtocolChoices),
  1075. required=False,
  1076. widget=StaticSelect2Multiple()
  1077. )
  1078. port = forms.IntegerField(
  1079. required=False,
  1080. )
  1081. tag = TagFilterField(model)
  1082. class ServiceCSVForm(CustomFieldModelCSVForm):
  1083. device = CSVModelChoiceField(
  1084. queryset=Device.objects.all(),
  1085. required=False,
  1086. to_field_name='name',
  1087. help_text='Required if not assigned to a VM'
  1088. )
  1089. virtual_machine = CSVModelChoiceField(
  1090. queryset=VirtualMachine.objects.all(),
  1091. required=False,
  1092. to_field_name='name',
  1093. help_text='Required if not assigned to a device'
  1094. )
  1095. protocol = CSVChoiceField(
  1096. choices=ServiceProtocolChoices,
  1097. help_text='IP protocol'
  1098. )
  1099. class Meta:
  1100. model = Service
  1101. fields = Service.csv_headers
  1102. class ServiceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  1103. pk = forms.ModelMultipleChoiceField(
  1104. queryset=Service.objects.all(),
  1105. widget=forms.MultipleHiddenInput()
  1106. )
  1107. protocol = forms.ChoiceField(
  1108. choices=add_blank_choice(ServiceProtocolChoices),
  1109. required=False,
  1110. widget=StaticSelect2()
  1111. )
  1112. port = forms.IntegerField(
  1113. validators=[
  1114. MinValueValidator(1),
  1115. MaxValueValidator(65535),
  1116. ],
  1117. required=False
  1118. )
  1119. description = forms.CharField(
  1120. max_length=100,
  1121. required=False
  1122. )
  1123. class Meta:
  1124. nullable_fields = [
  1125. 'description',
  1126. ]