forms.py 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256
  1. from django import forms
  2. from django.core.validators import MaxValueValidator, MinValueValidator
  3. from dcim.models import Device, Interface, Rack, Region, Site
  4. from extras.forms import (
  5. AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
  6. )
  7. from extras.models import Tag
  8. from tenancy.forms import TenancyFilterForm, TenancyForm
  9. from tenancy.models import Tenant
  10. from utilities.forms import (
  11. add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, CSVModelChoiceField, CSVModelForm,
  12. DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, ReturnURLForm,
  13. SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
  14. )
  15. from virtualization.models import 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. initial_params={
  224. 'vlans': '$vlan'
  225. }
  226. )
  227. vlan = DynamicModelChoiceField(
  228. queryset=VLAN.objects.all(),
  229. required=False,
  230. label='VLAN',
  231. display_field='display_name',
  232. query_params={
  233. 'site_id': '$site',
  234. 'group_id': '$vlan_group',
  235. }
  236. )
  237. role = DynamicModelChoiceField(
  238. queryset=Role.objects.all(),
  239. required=False
  240. )
  241. tags = DynamicModelMultipleChoiceField(
  242. queryset=Tag.objects.all(),
  243. required=False
  244. )
  245. class Meta:
  246. model = Prefix
  247. fields = [
  248. 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant',
  249. 'tags',
  250. ]
  251. widgets = {
  252. 'status': StaticSelect2(),
  253. }
  254. def __init__(self, *args, **kwargs):
  255. super().__init__(*args, **kwargs)
  256. self.fields['vrf'].empty_label = 'Global'
  257. class PrefixCSVForm(CustomFieldModelCSVForm):
  258. vrf = CSVModelChoiceField(
  259. queryset=VRF.objects.all(),
  260. to_field_name='name',
  261. required=False,
  262. help_text='Assigned VRF'
  263. )
  264. tenant = CSVModelChoiceField(
  265. queryset=Tenant.objects.all(),
  266. required=False,
  267. to_field_name='name',
  268. help_text='Assigned tenant'
  269. )
  270. site = CSVModelChoiceField(
  271. queryset=Site.objects.all(),
  272. required=False,
  273. to_field_name='name',
  274. help_text='Assigned site'
  275. )
  276. vlan_group = CSVModelChoiceField(
  277. queryset=VLANGroup.objects.all(),
  278. required=False,
  279. to_field_name='name',
  280. help_text="VLAN's group (if any)"
  281. )
  282. vlan = CSVModelChoiceField(
  283. queryset=VLAN.objects.all(),
  284. required=False,
  285. to_field_name='vid',
  286. help_text="Assigned VLAN"
  287. )
  288. status = CSVChoiceField(
  289. choices=PrefixStatusChoices,
  290. help_text='Operational status'
  291. )
  292. role = CSVModelChoiceField(
  293. queryset=Role.objects.all(),
  294. required=False,
  295. to_field_name='name',
  296. help_text='Functional role'
  297. )
  298. class Meta:
  299. model = Prefix
  300. fields = Prefix.csv_headers
  301. def __init__(self, data=None, *args, **kwargs):
  302. super().__init__(data, *args, **kwargs)
  303. if data:
  304. # Limit vlan queryset by assigned site and group
  305. params = {
  306. f"site__{self.fields['site'].to_field_name}": data.get('site'),
  307. f"group__{self.fields['vlan_group'].to_field_name}": data.get('vlan_group'),
  308. }
  309. self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params)
  310. class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  311. pk = forms.ModelMultipleChoiceField(
  312. queryset=Prefix.objects.all(),
  313. widget=forms.MultipleHiddenInput()
  314. )
  315. site = DynamicModelChoiceField(
  316. queryset=Site.objects.all(),
  317. required=False
  318. )
  319. vrf = DynamicModelChoiceField(
  320. queryset=VRF.objects.all(),
  321. required=False,
  322. label='VRF',
  323. display_field='display_name'
  324. )
  325. prefix_length = forms.IntegerField(
  326. min_value=PREFIX_LENGTH_MIN,
  327. max_value=PREFIX_LENGTH_MAX,
  328. required=False
  329. )
  330. tenant = DynamicModelChoiceField(
  331. queryset=Tenant.objects.all(),
  332. required=False
  333. )
  334. status = forms.ChoiceField(
  335. choices=add_blank_choice(PrefixStatusChoices),
  336. required=False,
  337. widget=StaticSelect2()
  338. )
  339. role = DynamicModelChoiceField(
  340. queryset=Role.objects.all(),
  341. required=False
  342. )
  343. is_pool = forms.NullBooleanField(
  344. required=False,
  345. widget=BulkEditNullBooleanSelect(),
  346. label='Is a pool'
  347. )
  348. description = forms.CharField(
  349. max_length=100,
  350. required=False
  351. )
  352. class Meta:
  353. nullable_fields = [
  354. 'site', 'vrf', 'tenant', 'role', 'description',
  355. ]
  356. class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  357. model = Prefix
  358. field_order = [
  359. 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'region', 'site', 'role', 'tenant_group',
  360. 'tenant', 'is_pool', 'expand',
  361. ]
  362. mask_length__lte = forms.IntegerField(
  363. widget=forms.HiddenInput()
  364. )
  365. q = forms.CharField(
  366. required=False,
  367. label='Search'
  368. )
  369. within_include = forms.CharField(
  370. required=False,
  371. widget=forms.TextInput(
  372. attrs={
  373. 'placeholder': 'Prefix',
  374. }
  375. ),
  376. label='Search within'
  377. )
  378. family = forms.ChoiceField(
  379. required=False,
  380. choices=add_blank_choice(IPAddressFamilyChoices),
  381. label='Address family',
  382. widget=StaticSelect2()
  383. )
  384. mask_length = forms.ChoiceField(
  385. required=False,
  386. choices=PREFIX_MASK_LENGTH_CHOICES,
  387. label='Mask length',
  388. widget=StaticSelect2()
  389. )
  390. vrf_id = DynamicModelMultipleChoiceField(
  391. queryset=VRF.objects.all(),
  392. required=False,
  393. label='VRF',
  394. null_option='Global'
  395. )
  396. status = forms.MultipleChoiceField(
  397. choices=PrefixStatusChoices,
  398. required=False,
  399. widget=StaticSelect2Multiple()
  400. )
  401. region = DynamicModelMultipleChoiceField(
  402. queryset=Region.objects.all(),
  403. to_field_name='slug',
  404. required=False
  405. )
  406. site = DynamicModelMultipleChoiceField(
  407. queryset=Site.objects.all(),
  408. to_field_name='slug',
  409. required=False,
  410. null_option='None',
  411. query_params={
  412. 'region': '$region'
  413. }
  414. )
  415. role = DynamicModelMultipleChoiceField(
  416. queryset=Role.objects.all(),
  417. to_field_name='slug',
  418. required=False,
  419. null_option='None'
  420. )
  421. is_pool = forms.NullBooleanField(
  422. required=False,
  423. label='Is a pool',
  424. widget=StaticSelect2(
  425. choices=BOOLEAN_WITH_BLANK_CHOICES
  426. )
  427. )
  428. tag = TagFilterField(model)
  429. #
  430. # IP addresses
  431. #
  432. class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
  433. device = DynamicModelChoiceField(
  434. queryset=Device.objects.all(),
  435. required=False,
  436. display_field='display_name',
  437. initial_params={
  438. 'interfaces': '$interface'
  439. }
  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. initial_params={
  452. 'interfaces': '$vminterface'
  453. }
  454. )
  455. vminterface = DynamicModelChoiceField(
  456. queryset=VMInterface.objects.all(),
  457. required=False,
  458. label='Interface',
  459. query_params={
  460. 'virtual_machine_id': '$virtual_machine'
  461. }
  462. )
  463. vrf = DynamicModelChoiceField(
  464. queryset=VRF.objects.all(),
  465. required=False,
  466. label='VRF',
  467. display_field='display_name'
  468. )
  469. nat_site = DynamicModelChoiceField(
  470. queryset=Site.objects.all(),
  471. required=False,
  472. label='Site'
  473. )
  474. nat_rack = DynamicModelChoiceField(
  475. queryset=Rack.objects.all(),
  476. required=False,
  477. label='Rack',
  478. display_field='display_name',
  479. null_option='None',
  480. query_params={
  481. 'site_id': '$site'
  482. }
  483. )
  484. nat_device = DynamicModelChoiceField(
  485. queryset=Device.objects.all(),
  486. required=False,
  487. label='Device',
  488. display_field='display_name',
  489. query_params={
  490. 'site_id': '$site',
  491. 'rack_id': '$nat_rack',
  492. }
  493. )
  494. nat_cluster = DynamicModelChoiceField(
  495. queryset=Cluster.objects.all(),
  496. required=False,
  497. label='Cluster'
  498. )
  499. nat_virtual_machine = DynamicModelChoiceField(
  500. queryset=VirtualMachine.objects.all(),
  501. required=False,
  502. label='Virtual Machine',
  503. query_params={
  504. 'cluster_id': '$nat_cluster',
  505. }
  506. )
  507. nat_vrf = DynamicModelChoiceField(
  508. queryset=VRF.objects.all(),
  509. required=False,
  510. label='VRF',
  511. display_field='display_name'
  512. )
  513. nat_inside = DynamicModelChoiceField(
  514. queryset=IPAddress.objects.all(),
  515. required=False,
  516. label='IP Address',
  517. display_field='address',
  518. query_params={
  519. 'device_id': '$nat_device',
  520. 'virtual_machine_id': '$nat_virtual_machine',
  521. 'vrf_id': '$nat_vrf',
  522. }
  523. )
  524. primary_for_parent = forms.BooleanField(
  525. required=False,
  526. label='Make this the primary IP for the device/VM'
  527. )
  528. tags = DynamicModelMultipleChoiceField(
  529. queryset=Tag.objects.all(),
  530. required=False
  531. )
  532. class Meta:
  533. model = IPAddress
  534. fields = [
  535. 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack',
  536. 'nat_device', 'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant',
  537. 'tags',
  538. ]
  539. widgets = {
  540. 'status': StaticSelect2(),
  541. 'role': StaticSelect2(),
  542. }
  543. def __init__(self, *args, **kwargs):
  544. # Initialize helper selectors
  545. instance = kwargs.get('instance')
  546. initial = kwargs.get('initial', {}).copy()
  547. if instance:
  548. if type(instance.assigned_object) is Interface:
  549. initial['interface'] = instance.assigned_object
  550. elif type(instance.assigned_object) is VMInterface:
  551. initial['vminterface'] = instance.assigned_object
  552. if instance.nat_inside:
  553. nat_inside_parent = instance.nat_inside.assigned_object
  554. if type(nat_inside_parent) is Interface:
  555. initial['nat_site'] = nat_inside_parent.device.site.pk
  556. initial['nat_rack'] = nat_inside_parent.device.rack.pk
  557. initial['nat_device'] = nat_inside_parent.device.pk
  558. elif type(nat_inside_parent) is VMInterface:
  559. initial['nat_cluster'] = nat_inside_parent.virtual_machine.cluster.pk
  560. initial['nat_virtual_machine'] = nat_inside_parent.virtual_machine.pk
  561. kwargs['initial'] = initial
  562. super().__init__(*args, **kwargs)
  563. self.fields['vrf'].empty_label = 'Global'
  564. # Initialize primary_for_parent if IP address is already assigned
  565. if self.instance.pk and self.instance.assigned_object:
  566. parent = self.instance.assigned_object.parent
  567. if (
  568. self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
  569. self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
  570. ):
  571. self.initial['primary_for_parent'] = True
  572. def clean(self):
  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. self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
  577. # Primary IP assignment is only available if an interface has been assigned.
  578. interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
  579. if self.cleaned_data.get('primary_for_parent') and not interface:
  580. self.add_error(
  581. 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
  582. )
  583. def save(self, *args, **kwargs):
  584. ipaddress = super().save(*args, **kwargs)
  585. # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
  586. interface = self.instance.assigned_object
  587. if interface and self.cleaned_data['primary_for_parent']:
  588. if ipaddress.address.version == 4:
  589. interface.parent.primary_ip4 = ipaddress
  590. else:
  591. interface.parent.primary_ip6 = ipaddress
  592. interface.parent.save()
  593. elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress:
  594. interface.parent.primary_ip4 = None
  595. interface.parent.save()
  596. elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress:
  597. interface.parent.primary_ip6 = None
  598. interface.parent.save()
  599. return ipaddress
  600. class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
  601. pattern = ExpandableIPAddressField(
  602. label='Address pattern'
  603. )
  604. class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  605. vrf = DynamicModelChoiceField(
  606. queryset=VRF.objects.all(),
  607. required=False,
  608. label='VRF',
  609. display_field='display_name'
  610. )
  611. tags = DynamicModelMultipleChoiceField(
  612. queryset=Tag.objects.all(),
  613. required=False
  614. )
  615. class Meta:
  616. model = IPAddress
  617. fields = [
  618. 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
  619. ]
  620. widgets = {
  621. 'status': StaticSelect2(),
  622. 'role': StaticSelect2(),
  623. }
  624. def __init__(self, *args, **kwargs):
  625. super().__init__(*args, **kwargs)
  626. self.fields['vrf'].empty_label = 'Global'
  627. class IPAddressCSVForm(CustomFieldModelCSVForm):
  628. vrf = CSVModelChoiceField(
  629. queryset=VRF.objects.all(),
  630. to_field_name='name',
  631. required=False,
  632. help_text='Assigned VRF'
  633. )
  634. tenant = CSVModelChoiceField(
  635. queryset=Tenant.objects.all(),
  636. to_field_name='name',
  637. required=False,
  638. help_text='Assigned tenant'
  639. )
  640. status = CSVChoiceField(
  641. choices=IPAddressStatusChoices,
  642. help_text='Operational status'
  643. )
  644. role = CSVChoiceField(
  645. choices=IPAddressRoleChoices,
  646. required=False,
  647. help_text='Functional role'
  648. )
  649. device = CSVModelChoiceField(
  650. queryset=Device.objects.all(),
  651. required=False,
  652. to_field_name='name',
  653. help_text='Parent device of assigned interface (if any)'
  654. )
  655. virtual_machine = CSVModelChoiceField(
  656. queryset=VirtualMachine.objects.all(),
  657. required=False,
  658. to_field_name='name',
  659. help_text='Parent VM of assigned interface (if any)'
  660. )
  661. interface = CSVModelChoiceField(
  662. queryset=Interface.objects.none(), # Can also refer to VMInterface
  663. required=False,
  664. to_field_name='name',
  665. help_text='Assigned interface'
  666. )
  667. is_primary = forms.BooleanField(
  668. help_text='Make this the primary IP for the assigned device',
  669. required=False
  670. )
  671. class Meta:
  672. model = IPAddress
  673. fields = IPAddress.csv_headers
  674. def __init__(self, data=None, *args, **kwargs):
  675. super().__init__(data, *args, **kwargs)
  676. if data:
  677. # Limit interface queryset by assigned device
  678. if data.get('device'):
  679. self.fields['interface'].queryset = Interface.objects.filter(
  680. **{f"device__{self.fields['device'].to_field_name}": data['device']}
  681. )
  682. # Limit interface queryset by assigned device
  683. elif data.get('virtual_machine'):
  684. self.fields['interface'].queryset = VMInterface.objects.filter(
  685. **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
  686. )
  687. def clean(self):
  688. super().clean()
  689. device = self.cleaned_data.get('device')
  690. virtual_machine = self.cleaned_data.get('virtual_machine')
  691. is_primary = self.cleaned_data.get('is_primary')
  692. # Validate is_primary
  693. if is_primary and not device and not virtual_machine:
  694. raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP")
  695. def save(self, *args, **kwargs):
  696. # Set interface assignment
  697. if self.cleaned_data['interface']:
  698. self.instance.assigned_object = self.cleaned_data['interface']
  699. ipaddress = super().save(*args, **kwargs)
  700. # Set as primary for device/VM
  701. if self.cleaned_data['is_primary']:
  702. parent = self.cleaned_data['device'] or self.cleaned_data['virtual_machine']
  703. if self.instance.address.version == 4:
  704. parent.primary_ip4 = ipaddress
  705. elif self.instance.address.version == 6:
  706. parent.primary_ip6 = ipaddress
  707. parent.save()
  708. return ipaddress
  709. class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  710. pk = forms.ModelMultipleChoiceField(
  711. queryset=IPAddress.objects.all(),
  712. widget=forms.MultipleHiddenInput()
  713. )
  714. vrf = DynamicModelChoiceField(
  715. queryset=VRF.objects.all(),
  716. required=False,
  717. label='VRF',
  718. display_field='display_name'
  719. )
  720. mask_length = forms.IntegerField(
  721. min_value=IPADDRESS_MASK_LENGTH_MIN,
  722. max_value=IPADDRESS_MASK_LENGTH_MAX,
  723. required=False
  724. )
  725. tenant = DynamicModelChoiceField(
  726. queryset=Tenant.objects.all(),
  727. required=False
  728. )
  729. status = forms.ChoiceField(
  730. choices=add_blank_choice(IPAddressStatusChoices),
  731. required=False,
  732. widget=StaticSelect2()
  733. )
  734. role = forms.ChoiceField(
  735. choices=add_blank_choice(IPAddressRoleChoices),
  736. required=False,
  737. widget=StaticSelect2()
  738. )
  739. dns_name = forms.CharField(
  740. max_length=255,
  741. required=False
  742. )
  743. description = forms.CharField(
  744. max_length=100,
  745. required=False
  746. )
  747. class Meta:
  748. nullable_fields = [
  749. 'vrf', 'role', 'tenant', 'dns_name', 'description',
  750. ]
  751. class IPAddressAssignForm(BootstrapMixin, forms.Form):
  752. vrf_id = DynamicModelChoiceField(
  753. queryset=VRF.objects.all(),
  754. required=False,
  755. label='VRF',
  756. empty_label='Global'
  757. )
  758. q = forms.CharField(
  759. required=False,
  760. label='Search',
  761. )
  762. class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  763. model = IPAddress
  764. field_order = [
  765. 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'assigned_to_interface', 'tenant_group',
  766. 'tenant',
  767. ]
  768. q = forms.CharField(
  769. required=False,
  770. label='Search'
  771. )
  772. parent = forms.CharField(
  773. required=False,
  774. widget=forms.TextInput(
  775. attrs={
  776. 'placeholder': 'Prefix',
  777. }
  778. ),
  779. label='Parent Prefix'
  780. )
  781. family = forms.ChoiceField(
  782. required=False,
  783. choices=add_blank_choice(IPAddressFamilyChoices),
  784. label='Address family',
  785. widget=StaticSelect2()
  786. )
  787. mask_length = forms.ChoiceField(
  788. required=False,
  789. choices=IPADDRESS_MASK_LENGTH_CHOICES,
  790. label='Mask length',
  791. widget=StaticSelect2()
  792. )
  793. vrf_id = DynamicModelMultipleChoiceField(
  794. queryset=VRF.objects.all(),
  795. required=False,
  796. label='VRF',
  797. null_option='Global'
  798. )
  799. status = forms.MultipleChoiceField(
  800. choices=IPAddressStatusChoices,
  801. required=False,
  802. widget=StaticSelect2Multiple()
  803. )
  804. role = forms.MultipleChoiceField(
  805. choices=IPAddressRoleChoices,
  806. required=False,
  807. widget=StaticSelect2Multiple()
  808. )
  809. assigned_to_interface = forms.NullBooleanField(
  810. required=False,
  811. label='Assigned to an interface',
  812. widget=StaticSelect2(
  813. choices=BOOLEAN_WITH_BLANK_CHOICES
  814. )
  815. )
  816. tag = TagFilterField(model)
  817. #
  818. # VLAN groups
  819. #
  820. class VLANGroupForm(BootstrapMixin, forms.ModelForm):
  821. site = DynamicModelChoiceField(
  822. queryset=Site.objects.all(),
  823. required=False
  824. )
  825. slug = SlugField()
  826. class Meta:
  827. model = VLANGroup
  828. fields = [
  829. 'site', 'name', 'slug', 'description',
  830. ]
  831. class VLANGroupCSVForm(CSVModelForm):
  832. site = CSVModelChoiceField(
  833. queryset=Site.objects.all(),
  834. required=False,
  835. to_field_name='name',
  836. help_text='Assigned site'
  837. )
  838. slug = SlugField()
  839. class Meta:
  840. model = VLANGroup
  841. fields = VLANGroup.csv_headers
  842. class VLANGroupFilterForm(BootstrapMixin, forms.Form):
  843. region = DynamicModelMultipleChoiceField(
  844. queryset=Region.objects.all(),
  845. to_field_name='slug',
  846. required=False
  847. )
  848. site = DynamicModelMultipleChoiceField(
  849. queryset=Site.objects.all(),
  850. to_field_name='slug',
  851. required=False,
  852. null_option='None',
  853. query_params={
  854. 'region': '$region'
  855. }
  856. )
  857. #
  858. # VLANs
  859. #
  860. class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  861. site = DynamicModelChoiceField(
  862. queryset=Site.objects.all(),
  863. required=False,
  864. null_option='None'
  865. )
  866. group = DynamicModelChoiceField(
  867. queryset=VLANGroup.objects.all(),
  868. required=False,
  869. query_params={
  870. 'site_id': '$site'
  871. }
  872. )
  873. role = DynamicModelChoiceField(
  874. queryset=Role.objects.all(),
  875. required=False
  876. )
  877. tags = DynamicModelMultipleChoiceField(
  878. queryset=Tag.objects.all(),
  879. required=False
  880. )
  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. )
  948. group = DynamicModelChoiceField(
  949. queryset=VLANGroup.objects.all(),
  950. required=False,
  951. query_params={
  952. 'site_id': '$site'
  953. }
  954. )
  955. tenant = DynamicModelChoiceField(
  956. queryset=Tenant.objects.all(),
  957. required=False
  958. )
  959. status = forms.ChoiceField(
  960. choices=add_blank_choice(VLANStatusChoices),
  961. required=False,
  962. widget=StaticSelect2()
  963. )
  964. role = DynamicModelChoiceField(
  965. queryset=Role.objects.all(),
  966. required=False
  967. )
  968. description = forms.CharField(
  969. max_length=100,
  970. required=False
  971. )
  972. class Meta:
  973. nullable_fields = [
  974. 'site', 'group', 'tenant', 'role', 'description',
  975. ]
  976. class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  977. model = VLAN
  978. field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
  979. q = forms.CharField(
  980. required=False,
  981. label='Search'
  982. )
  983. region = DynamicModelMultipleChoiceField(
  984. queryset=Region.objects.all(),
  985. to_field_name='slug',
  986. required=False
  987. )
  988. site = DynamicModelMultipleChoiceField(
  989. queryset=Site.objects.all(),
  990. to_field_name='slug',
  991. required=False,
  992. null_option='None',
  993. query_params={
  994. 'region': '$region'
  995. }
  996. )
  997. group_id = DynamicModelMultipleChoiceField(
  998. queryset=VLANGroup.objects.all(),
  999. required=False,
  1000. label='VLAN group',
  1001. null_option='None',
  1002. query_params={
  1003. 'region': '$region'
  1004. }
  1005. )
  1006. status = forms.MultipleChoiceField(
  1007. choices=VLANStatusChoices,
  1008. required=False,
  1009. widget=StaticSelect2Multiple()
  1010. )
  1011. role = DynamicModelMultipleChoiceField(
  1012. queryset=Role.objects.all(),
  1013. to_field_name='slug',
  1014. required=False,
  1015. null_option='None'
  1016. )
  1017. tag = TagFilterField(model)
  1018. #
  1019. # Services
  1020. #
  1021. class ServiceForm(BootstrapMixin, CustomFieldModelForm):
  1022. port = forms.IntegerField(
  1023. min_value=SERVICE_PORT_MIN,
  1024. max_value=SERVICE_PORT_MAX
  1025. )
  1026. tags = DynamicModelMultipleChoiceField(
  1027. queryset=Tag.objects.all(),
  1028. required=False
  1029. )
  1030. class Meta:
  1031. model = Service
  1032. fields = [
  1033. 'name', 'protocol', 'port', 'ipaddresses', 'description', 'tags',
  1034. ]
  1035. help_texts = {
  1036. 'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
  1037. "reachable via all IPs assigned to the device.",
  1038. }
  1039. widgets = {
  1040. 'protocol': StaticSelect2(),
  1041. 'ipaddresses': StaticSelect2Multiple(),
  1042. }
  1043. def __init__(self, *args, **kwargs):
  1044. super().__init__(*args, **kwargs)
  1045. # Limit IP address choices to those assigned to interfaces of the parent device/VM
  1046. if self.instance.device:
  1047. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  1048. interface__in=self.instance.device.vc_interfaces.values_list('id', flat=True)
  1049. )
  1050. elif self.instance.virtual_machine:
  1051. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  1052. vminterface__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True)
  1053. )
  1054. else:
  1055. self.fields['ipaddresses'].choices = []
  1056. class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
  1057. model = Service
  1058. q = forms.CharField(
  1059. required=False,
  1060. label='Search'
  1061. )
  1062. protocol = forms.ChoiceField(
  1063. choices=add_blank_choice(ServiceProtocolChoices),
  1064. required=False,
  1065. widget=StaticSelect2Multiple()
  1066. )
  1067. port = forms.IntegerField(
  1068. required=False,
  1069. )
  1070. tag = TagFilterField(model)
  1071. class ServiceCSVForm(CustomFieldModelCSVForm):
  1072. device = CSVModelChoiceField(
  1073. queryset=Device.objects.all(),
  1074. required=False,
  1075. to_field_name='name',
  1076. help_text='Required if not assigned to a VM'
  1077. )
  1078. virtual_machine = CSVModelChoiceField(
  1079. queryset=VirtualMachine.objects.all(),
  1080. required=False,
  1081. to_field_name='name',
  1082. help_text='Required if not assigned to a device'
  1083. )
  1084. protocol = CSVChoiceField(
  1085. choices=ServiceProtocolChoices,
  1086. help_text='IP protocol'
  1087. )
  1088. class Meta:
  1089. model = Service
  1090. fields = Service.csv_headers
  1091. class ServiceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  1092. pk = forms.ModelMultipleChoiceField(
  1093. queryset=Service.objects.all(),
  1094. widget=forms.MultipleHiddenInput()
  1095. )
  1096. protocol = forms.ChoiceField(
  1097. choices=add_blank_choice(ServiceProtocolChoices),
  1098. required=False,
  1099. widget=StaticSelect2()
  1100. )
  1101. port = forms.IntegerField(
  1102. validators=[
  1103. MinValueValidator(1),
  1104. MaxValueValidator(65535),
  1105. ],
  1106. required=False
  1107. )
  1108. description = forms.CharField(
  1109. max_length=100,
  1110. required=False
  1111. )
  1112. class Meta:
  1113. nullable_fields = [
  1114. 'description',
  1115. ]