forms.py 38 KB

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