forms.py 36 KB

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