forms.py 38 KB

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