forms.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971
  1. from __future__ import unicode_literals
  2. from django import forms
  3. from django.core.exceptions import MultipleObjectsReturned
  4. from django.core.validators import MaxValueValidator, MinValueValidator
  5. from django.db.models import Count
  6. from taggit.forms import TagField
  7. from dcim.models import Site, Rack, Device, Interface
  8. from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
  9. from tenancy.forms import TenancyForm
  10. from tenancy.models import Tenant
  11. from utilities.forms import (
  12. AnnotatedMultipleChoiceField, APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
  13. CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm,
  14. SlugField, add_blank_choice,
  15. )
  16. from virtualization.models import VirtualMachine
  17. from .constants import (
  18. IP_PROTOCOL_CHOICES, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES,
  19. )
  20. from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
  21. IP_FAMILY_CHOICES = [
  22. ('', 'All'),
  23. (4, 'IPv4'),
  24. (6, 'IPv6'),
  25. ]
  26. PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 128)])
  27. IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 129)])
  28. #
  29. # VRFs
  30. #
  31. class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  32. tags = TagField(required=False)
  33. class Meta:
  34. model = VRF
  35. fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant', 'tags']
  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(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
  60. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  61. enforce_unique = forms.NullBooleanField(
  62. required=False, widget=BulkEditNullBooleanSelect, label='Enforce unique space'
  63. )
  64. description = forms.CharField(max_length=100, required=False)
  65. class Meta:
  66. nullable_fields = ['tenant', 'description']
  67. class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
  68. model = VRF
  69. q = forms.CharField(required=False, label='Search')
  70. tenant = FilterChoiceField(
  71. queryset=Tenant.objects.annotate(filter_count=Count('vrfs')),
  72. to_field_name='slug',
  73. null_label='-- None --'
  74. )
  75. #
  76. # RIRs
  77. #
  78. class RIRForm(BootstrapMixin, forms.ModelForm):
  79. slug = SlugField()
  80. class Meta:
  81. model = RIR
  82. fields = ['name', 'slug', 'is_private']
  83. class RIRCSVForm(forms.ModelForm):
  84. slug = SlugField()
  85. class Meta:
  86. model = RIR
  87. fields = RIR.csv_headers
  88. help_texts = {
  89. 'name': 'RIR name',
  90. }
  91. class RIRFilterForm(BootstrapMixin, forms.Form):
  92. is_private = forms.NullBooleanField(required=False, label='Private', widget=forms.Select(choices=[
  93. ('', '---------'),
  94. ('True', 'Yes'),
  95. ('False', 'No'),
  96. ]))
  97. #
  98. # Aggregates
  99. #
  100. class AggregateForm(BootstrapMixin, CustomFieldForm):
  101. tags = TagField(required=False)
  102. class Meta:
  103. model = Aggregate
  104. fields = ['prefix', 'rir', 'date_added', 'description', 'tags']
  105. help_texts = {
  106. 'prefix': "IPv4 or IPv6 network",
  107. 'rir': "Regional Internet Registry responsible for this prefix",
  108. 'date_added': "Format: YYYY-MM-DD",
  109. }
  110. class AggregateCSVForm(forms.ModelForm):
  111. rir = forms.ModelChoiceField(
  112. queryset=RIR.objects.all(),
  113. to_field_name='name',
  114. help_text='Name of parent RIR',
  115. error_messages={
  116. 'invalid_choice': 'RIR not found.',
  117. }
  118. )
  119. class Meta:
  120. model = Aggregate
  121. fields = Aggregate.csv_headers
  122. class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  123. pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
  124. rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
  125. date_added = forms.DateField(required=False)
  126. description = forms.CharField(max_length=100, required=False)
  127. class Meta:
  128. nullable_fields = ['date_added', 'description']
  129. class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
  130. model = Aggregate
  131. q = forms.CharField(required=False, label='Search')
  132. family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
  133. rir = FilterChoiceField(
  134. queryset=RIR.objects.annotate(filter_count=Count('aggregates')),
  135. to_field_name='slug',
  136. label='RIR'
  137. )
  138. #
  139. # Roles
  140. #
  141. class RoleForm(BootstrapMixin, forms.ModelForm):
  142. slug = SlugField()
  143. class Meta:
  144. model = Role
  145. fields = ['name', 'slug']
  146. class RoleCSVForm(forms.ModelForm):
  147. slug = SlugField()
  148. class Meta:
  149. model = Role
  150. fields = Role.csv_headers
  151. help_texts = {
  152. 'name': 'Role name',
  153. }
  154. #
  155. # Prefixes
  156. #
  157. class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  158. site = forms.ModelChoiceField(
  159. queryset=Site.objects.all(),
  160. required=False,
  161. label='Site',
  162. widget=forms.Select(
  163. attrs={'filter-for': 'vlan_group', 'nullable': 'true'}
  164. )
  165. )
  166. vlan_group = ChainedModelChoiceField(
  167. queryset=VLANGroup.objects.all(),
  168. chains=(
  169. ('site', 'site'),
  170. ),
  171. required=False,
  172. label='VLAN group',
  173. widget=APISelect(
  174. api_url='/api/ipam/vlan-groups/?site_id={{site}}',
  175. attrs={'filter-for': 'vlan', 'nullable': 'true'}
  176. )
  177. )
  178. vlan = ChainedModelChoiceField(
  179. queryset=VLAN.objects.all(),
  180. chains=(
  181. ('site', 'site'),
  182. ('group', 'vlan_group'),
  183. ),
  184. required=False,
  185. label='VLAN',
  186. widget=APISelect(
  187. api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name'
  188. )
  189. )
  190. tags = TagField(required=False)
  191. class Meta:
  192. model = Prefix
  193. fields = [
  194. 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant',
  195. 'tags',
  196. ]
  197. def __init__(self, *args, **kwargs):
  198. # Initialize helper selectors
  199. instance = kwargs.get('instance')
  200. initial = kwargs.get('initial', {}).copy()
  201. if instance and instance.vlan is not None:
  202. initial['vlan_group'] = instance.vlan.group
  203. kwargs['initial'] = initial
  204. super(PrefixForm, self).__init__(*args, **kwargs)
  205. self.fields['vrf'].empty_label = 'Global'
  206. class PrefixCSVForm(forms.ModelForm):
  207. vrf = forms.ModelChoiceField(
  208. queryset=VRF.objects.all(),
  209. required=False,
  210. to_field_name='rd',
  211. help_text='Route distinguisher of parent VRF',
  212. error_messages={
  213. 'invalid_choice': 'VRF not found.',
  214. }
  215. )
  216. tenant = forms.ModelChoiceField(
  217. queryset=Tenant.objects.all(),
  218. required=False,
  219. to_field_name='name',
  220. help_text='Name of assigned tenant',
  221. error_messages={
  222. 'invalid_choice': 'Tenant not found.',
  223. }
  224. )
  225. site = forms.ModelChoiceField(
  226. queryset=Site.objects.all(),
  227. required=False,
  228. to_field_name='name',
  229. help_text='Name of parent site',
  230. error_messages={
  231. 'invalid_choice': 'Site not found.',
  232. }
  233. )
  234. vlan_group = forms.CharField(
  235. help_text='Group name of assigned VLAN',
  236. required=False
  237. )
  238. vlan_vid = forms.IntegerField(
  239. help_text='Numeric ID of assigned VLAN',
  240. required=False
  241. )
  242. status = CSVChoiceField(
  243. choices=PREFIX_STATUS_CHOICES,
  244. help_text='Operational status'
  245. )
  246. role = forms.ModelChoiceField(
  247. queryset=Role.objects.all(),
  248. required=False,
  249. to_field_name='name',
  250. help_text='Functional role',
  251. error_messages={
  252. 'invalid_choice': 'Invalid role.',
  253. }
  254. )
  255. class Meta:
  256. model = Prefix
  257. fields = Prefix.csv_headers
  258. def clean(self):
  259. super(PrefixCSVForm, self).clean()
  260. site = self.cleaned_data.get('site')
  261. vlan_group = self.cleaned_data.get('vlan_group')
  262. vlan_vid = self.cleaned_data.get('vlan_vid')
  263. # Validate VLAN
  264. if vlan_group and vlan_vid:
  265. try:
  266. self.instance.vlan = VLAN.objects.get(site=site, group__name=vlan_group, vid=vlan_vid)
  267. except VLAN.DoesNotExist:
  268. if site:
  269. raise forms.ValidationError("VLAN {} not found in site {} group {}".format(
  270. vlan_vid, site, vlan_group
  271. ))
  272. else:
  273. raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group))
  274. except MultipleObjectsReturned:
  275. raise forms.ValidationError(
  276. "Multiple VLANs with VID {} found in group {}".format(vlan_vid, vlan_group)
  277. )
  278. elif vlan_vid:
  279. try:
  280. self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid)
  281. except VLAN.DoesNotExist:
  282. if site:
  283. raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site))
  284. else:
  285. raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid))
  286. except MultipleObjectsReturned:
  287. raise forms.ValidationError("Multiple VLANs with VID {} found".format(vlan_vid))
  288. class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  289. pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
  290. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
  291. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
  292. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  293. status = forms.ChoiceField(choices=add_blank_choice(PREFIX_STATUS_CHOICES), required=False)
  294. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
  295. is_pool = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a pool')
  296. description = forms.CharField(max_length=100, required=False)
  297. class Meta:
  298. nullable_fields = ['site', 'vrf', 'tenant', 'role', 'description']
  299. class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
  300. model = Prefix
  301. q = forms.CharField(required=False, label='Search')
  302. within_include = forms.CharField(required=False, label='Search within', widget=forms.TextInput(attrs={
  303. 'placeholder': 'Prefix',
  304. }))
  305. family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
  306. mask_length = forms.ChoiceField(required=False, choices=PREFIX_MASK_LENGTH_CHOICES, label='Mask length')
  307. vrf = FilterChoiceField(
  308. queryset=VRF.objects.annotate(filter_count=Count('prefixes')),
  309. to_field_name='rd',
  310. label='VRF',
  311. null_label='-- Global --'
  312. )
  313. tenant = FilterChoiceField(
  314. queryset=Tenant.objects.annotate(filter_count=Count('prefixes')),
  315. to_field_name='slug',
  316. null_label='-- None --'
  317. )
  318. status = AnnotatedMultipleChoiceField(
  319. choices=PREFIX_STATUS_CHOICES,
  320. annotate=Prefix.objects.all(),
  321. annotate_field='status',
  322. required=False
  323. )
  324. site = FilterChoiceField(
  325. queryset=Site.objects.annotate(filter_count=Count('prefixes')),
  326. to_field_name='slug',
  327. null_label='-- None --'
  328. )
  329. role = FilterChoiceField(
  330. queryset=Role.objects.annotate(filter_count=Count('prefixes')),
  331. to_field_name='slug',
  332. null_label='-- None --'
  333. )
  334. expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
  335. #
  336. # IP addresses
  337. #
  338. class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm):
  339. interface = forms.ModelChoiceField(
  340. queryset=Interface.objects.all(),
  341. required=False
  342. )
  343. nat_site = forms.ModelChoiceField(
  344. queryset=Site.objects.all(),
  345. required=False,
  346. label='Site',
  347. widget=forms.Select(
  348. attrs={'filter-for': 'nat_rack'}
  349. )
  350. )
  351. nat_rack = ChainedModelChoiceField(
  352. queryset=Rack.objects.all(),
  353. chains=(
  354. ('site', 'nat_site'),
  355. ),
  356. required=False,
  357. label='Rack',
  358. widget=APISelect(
  359. api_url='/api/dcim/racks/?site_id={{nat_site}}',
  360. display_field='display_name',
  361. attrs={'filter-for': 'nat_device', 'nullable': 'true'}
  362. )
  363. )
  364. nat_device = ChainedModelChoiceField(
  365. queryset=Device.objects.all(),
  366. chains=(
  367. ('site', 'nat_site'),
  368. ('rack', 'nat_rack'),
  369. ),
  370. required=False,
  371. label='Device',
  372. widget=APISelect(
  373. api_url='/api/dcim/devices/?site_id={{nat_site}}&rack_id={{nat_rack}}',
  374. display_field='display_name',
  375. attrs={'filter-for': 'nat_inside'}
  376. )
  377. )
  378. nat_inside = ChainedModelChoiceField(
  379. queryset=IPAddress.objects.all(),
  380. chains=(
  381. ('interface__device', 'nat_device'),
  382. ),
  383. required=False,
  384. label='IP Address',
  385. widget=APISelect(
  386. api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
  387. display_field='address'
  388. )
  389. )
  390. livesearch = forms.CharField(
  391. required=False,
  392. label='Search',
  393. widget=Livesearch(
  394. query_key='q',
  395. query_url='ipam-api:ipaddress-list',
  396. field_to_update='nat_inside',
  397. obj_label='address'
  398. )
  399. )
  400. primary_for_parent = forms.BooleanField(required=False, label='Make this the primary IP for the device/VM')
  401. tags = TagField(required=False)
  402. class Meta:
  403. model = IPAddress
  404. fields = [
  405. 'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_parent', 'nat_site',
  406. 'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags',
  407. ]
  408. def __init__(self, *args, **kwargs):
  409. # Initialize helper selectors
  410. instance = kwargs.get('instance')
  411. initial = kwargs.get('initial', {}).copy()
  412. if instance and instance.nat_inside and instance.nat_inside.device is not None:
  413. initial['nat_site'] = instance.nat_inside.device.site
  414. initial['nat_rack'] = instance.nat_inside.device.rack
  415. initial['nat_device'] = instance.nat_inside.device
  416. kwargs['initial'] = initial
  417. super(IPAddressForm, self).__init__(*args, **kwargs)
  418. self.fields['vrf'].empty_label = 'Global'
  419. # Limit interface selections to those belonging to the parent device/VM
  420. if self.instance and self.instance.interface:
  421. self.fields['interface'].queryset = Interface.objects.filter(
  422. device=self.instance.interface.device, virtual_machine=self.instance.interface.virtual_machine
  423. )
  424. else:
  425. self.fields['interface'].choices = []
  426. # Initialize primary_for_parent if IP address is already assigned
  427. if self.instance.pk and self.instance.interface is not None:
  428. parent = self.instance.interface.parent
  429. if (
  430. self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
  431. self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
  432. ):
  433. self.initial['primary_for_parent'] = True
  434. def clean(self):
  435. super(IPAddressForm, self).clean()
  436. # Primary IP assignment is only available if an interface has been assigned.
  437. if self.cleaned_data.get('primary_for_parent') and not self.cleaned_data.get('interface'):
  438. self.add_error(
  439. 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
  440. )
  441. def save(self, *args, **kwargs):
  442. ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
  443. # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
  444. if self.cleaned_data['primary_for_parent']:
  445. parent = self.cleaned_data['interface'].parent
  446. if ipaddress.address.version == 4:
  447. parent.primary_ip4 = ipaddress
  448. else:
  449. parent.primary_ip6 = ipaddress
  450. parent.save()
  451. elif self.cleaned_data['interface']:
  452. parent = self.cleaned_data['interface'].parent
  453. if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
  454. parent.primary_ip4 = None
  455. parent.save()
  456. elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
  457. parent.primary_ip6 = None
  458. parent.save()
  459. return ipaddress
  460. class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
  461. pattern = ExpandableIPAddressField(label='Address pattern')
  462. class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  463. class Meta:
  464. model = IPAddress
  465. fields = ['address', 'vrf', 'status', 'role', 'description', 'tenant_group', 'tenant']
  466. def __init__(self, *args, **kwargs):
  467. super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
  468. self.fields['vrf'].empty_label = 'Global'
  469. class IPAddressCSVForm(forms.ModelForm):
  470. vrf = forms.ModelChoiceField(
  471. queryset=VRF.objects.all(),
  472. required=False,
  473. to_field_name='rd',
  474. help_text='Route distinguisher of the assigned VRF',
  475. error_messages={
  476. 'invalid_choice': 'VRF not found.',
  477. }
  478. )
  479. tenant = forms.ModelChoiceField(
  480. queryset=Tenant.objects.all(),
  481. to_field_name='name',
  482. required=False,
  483. help_text='Name of the assigned tenant',
  484. error_messages={
  485. 'invalid_choice': 'Tenant not found.',
  486. }
  487. )
  488. status = CSVChoiceField(
  489. choices=IPADDRESS_STATUS_CHOICES,
  490. help_text='Operational status'
  491. )
  492. role = CSVChoiceField(
  493. choices=IPADDRESS_ROLE_CHOICES,
  494. required=False,
  495. help_text='Functional role'
  496. )
  497. device = FlexibleModelChoiceField(
  498. queryset=Device.objects.all(),
  499. required=False,
  500. to_field_name='name',
  501. help_text='Name or ID of assigned device',
  502. error_messages={
  503. 'invalid_choice': 'Device not found.',
  504. }
  505. )
  506. virtual_machine = forms.ModelChoiceField(
  507. queryset=VirtualMachine.objects.all(),
  508. required=False,
  509. to_field_name='name',
  510. help_text='Name of assigned virtual machine',
  511. error_messages={
  512. 'invalid_choice': 'Virtual machine not found.',
  513. }
  514. )
  515. interface_name = forms.CharField(
  516. help_text='Name of assigned interface',
  517. required=False
  518. )
  519. is_primary = forms.BooleanField(
  520. help_text='Make this the primary IP for the assigned device',
  521. required=False
  522. )
  523. class Meta:
  524. model = IPAddress
  525. fields = IPAddress.csv_headers
  526. def clean(self):
  527. super(IPAddressCSVForm, self).clean()
  528. device = self.cleaned_data.get('device')
  529. virtual_machine = self.cleaned_data.get('virtual_machine')
  530. interface_name = self.cleaned_data.get('interface_name')
  531. is_primary = self.cleaned_data.get('is_primary')
  532. # Validate interface
  533. if interface_name and device:
  534. try:
  535. self.instance.interface = Interface.objects.get(device=device, name=interface_name)
  536. except Interface.DoesNotExist:
  537. raise forms.ValidationError("Invalid interface {} for device {}".format(
  538. interface_name, device
  539. ))
  540. elif interface_name and virtual_machine:
  541. try:
  542. self.instance.interface = Interface.objects.get(virtual_machine=virtual_machine, name=interface_name)
  543. except Interface.DoesNotExist:
  544. raise forms.ValidationError("Invalid interface {} for virtual machine {}".format(
  545. interface_name, virtual_machine
  546. ))
  547. elif interface_name:
  548. raise forms.ValidationError("Interface given ({}) but parent device/virtual machine not specified".format(
  549. interface_name
  550. ))
  551. elif device:
  552. raise forms.ValidationError("Device specified ({}) but interface missing".format(device))
  553. elif virtual_machine:
  554. raise forms.ValidationError("Virtual machine specified ({}) but interface missing".format(virtual_machine))
  555. # Validate is_primary
  556. if is_primary and not device and not virtual_machine:
  557. raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP")
  558. def save(self, *args, **kwargs):
  559. # Set interface
  560. if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
  561. self.instance.interface = Interface.objects.get(
  562. device=self.cleaned_data['device'],
  563. name=self.cleaned_data['interface_name']
  564. )
  565. elif self.cleaned_data['virtual_machine'] and self.cleaned_data['interface_name']:
  566. self.instance.interface = Interface.objects.get(
  567. virtual_machine=self.cleaned_data['virtual_machine'],
  568. name=self.cleaned_data['interface_name']
  569. )
  570. ipaddress = super(IPAddressCSVForm, self).save(*args, **kwargs)
  571. # Set as primary for device/VM
  572. if self.cleaned_data['is_primary']:
  573. parent = self.cleaned_data['device'] or self.cleaned_data['virtual_machine']
  574. if self.instance.address.version == 4:
  575. parent.primary_ip4 = ipaddress
  576. elif self.instance.address.version == 6:
  577. parent.primary_ip6 = ipaddress
  578. parent.save()
  579. return ipaddress
  580. class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  581. pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
  582. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
  583. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  584. status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), required=False)
  585. role = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_ROLE_CHOICES), required=False)
  586. description = forms.CharField(max_length=100, required=False)
  587. class Meta:
  588. nullable_fields = ['vrf', 'role', 'tenant', 'description']
  589. class IPAddressAssignForm(BootstrapMixin, forms.Form):
  590. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global')
  591. address = forms.CharField(label='IP Address')
  592. class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
  593. model = IPAddress
  594. q = forms.CharField(required=False, label='Search')
  595. parent = forms.CharField(required=False, label='Parent Prefix', widget=forms.TextInput(attrs={
  596. 'placeholder': 'Prefix',
  597. }))
  598. family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
  599. mask_length = forms.ChoiceField(required=False, choices=IPADDRESS_MASK_LENGTH_CHOICES, label='Mask length')
  600. vrf = FilterChoiceField(
  601. queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')),
  602. to_field_name='rd',
  603. label='VRF',
  604. null_label='-- Global --'
  605. )
  606. tenant = FilterChoiceField(
  607. queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
  608. to_field_name='slug',
  609. null_label='-- None --'
  610. )
  611. status = AnnotatedMultipleChoiceField(
  612. choices=IPADDRESS_STATUS_CHOICES,
  613. annotate=IPAddress.objects.all(),
  614. annotate_field='status',
  615. required=False
  616. )
  617. role = AnnotatedMultipleChoiceField(
  618. choices=IPADDRESS_ROLE_CHOICES,
  619. annotate=IPAddress.objects.all(),
  620. annotate_field='role',
  621. required=False
  622. )
  623. #
  624. # VLAN groups
  625. #
  626. class VLANGroupForm(BootstrapMixin, forms.ModelForm):
  627. slug = SlugField()
  628. class Meta:
  629. model = VLANGroup
  630. fields = ['site', 'name', 'slug']
  631. class VLANGroupCSVForm(forms.ModelForm):
  632. site = forms.ModelChoiceField(
  633. queryset=Site.objects.all(),
  634. required=False,
  635. to_field_name='name',
  636. help_text='Name of parent site',
  637. error_messages={
  638. 'invalid_choice': 'Site not found.',
  639. }
  640. )
  641. slug = SlugField()
  642. class Meta:
  643. model = VLANGroup
  644. fields = VLANGroup.csv_headers
  645. help_texts = {
  646. 'name': 'Name of VLAN group',
  647. }
  648. class VLANGroupFilterForm(BootstrapMixin, forms.Form):
  649. site = FilterChoiceField(
  650. queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
  651. to_field_name='slug',
  652. null_label='-- Global --'
  653. )
  654. #
  655. # VLANs
  656. #
  657. class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  658. site = forms.ModelChoiceField(
  659. queryset=Site.objects.all(),
  660. required=False,
  661. widget=forms.Select(
  662. attrs={'filter-for': 'group', 'nullable': 'true'}
  663. )
  664. )
  665. group = ChainedModelChoiceField(
  666. queryset=VLANGroup.objects.all(),
  667. chains=(
  668. ('site', 'site'),
  669. ),
  670. required=False,
  671. label='Group',
  672. widget=APISelect(
  673. api_url='/api/ipam/vlan-groups/?site_id={{site}}',
  674. )
  675. )
  676. tags = TagField(required=False)
  677. class Meta:
  678. model = VLAN
  679. fields = ['site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags']
  680. help_texts = {
  681. 'site': "Leave blank if this VLAN spans multiple sites",
  682. 'group': "VLAN group (optional)",
  683. 'vid': "Configured VLAN ID",
  684. 'name': "Configured VLAN name",
  685. 'status': "Operational status of this VLAN",
  686. 'role': "The primary function of this VLAN",
  687. }
  688. class VLANCSVForm(forms.ModelForm):
  689. site = forms.ModelChoiceField(
  690. queryset=Site.objects.all(),
  691. required=False,
  692. to_field_name='name',
  693. help_text='Name of parent site',
  694. error_messages={
  695. 'invalid_choice': 'Site not found.',
  696. }
  697. )
  698. group_name = forms.CharField(
  699. help_text='Name of VLAN group',
  700. required=False
  701. )
  702. tenant = forms.ModelChoiceField(
  703. queryset=Tenant.objects.all(),
  704. to_field_name='name',
  705. required=False,
  706. help_text='Name of assigned tenant',
  707. error_messages={
  708. 'invalid_choice': 'Tenant not found.',
  709. }
  710. )
  711. status = CSVChoiceField(
  712. choices=VLAN_STATUS_CHOICES,
  713. help_text='Operational status'
  714. )
  715. role = forms.ModelChoiceField(
  716. queryset=Role.objects.all(),
  717. required=False,
  718. to_field_name='name',
  719. help_text='Functional role',
  720. error_messages={
  721. 'invalid_choice': 'Invalid role.',
  722. }
  723. )
  724. class Meta:
  725. model = VLAN
  726. fields = VLAN.csv_headers
  727. help_texts = {
  728. 'vid': 'Numeric VLAN ID (1-4095)',
  729. 'name': 'VLAN name',
  730. }
  731. def clean(self):
  732. super(VLANCSVForm, self).clean()
  733. site = self.cleaned_data.get('site')
  734. group_name = self.cleaned_data.get('group_name')
  735. # Validate VLAN group
  736. if group_name:
  737. try:
  738. self.instance.group = VLANGroup.objects.get(site=site, name=group_name)
  739. except VLANGroup.DoesNotExist:
  740. if site:
  741. raise forms.ValidationError("VLAN group {} not found for site {}".format(group_name, site))
  742. else:
  743. raise forms.ValidationError("Global VLAN group {} not found".format(group_name))
  744. class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  745. pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
  746. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
  747. group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
  748. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  749. status = forms.ChoiceField(choices=add_blank_choice(VLAN_STATUS_CHOICES), required=False)
  750. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
  751. description = forms.CharField(max_length=100, required=False)
  752. class Meta:
  753. nullable_fields = ['site', 'group', 'tenant', 'role', 'description']
  754. class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
  755. model = VLAN
  756. q = forms.CharField(required=False, label='Search')
  757. site = FilterChoiceField(
  758. queryset=Site.objects.annotate(filter_count=Count('vlans')),
  759. to_field_name='slug',
  760. null_label='-- Global --'
  761. )
  762. group_id = FilterChoiceField(
  763. queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')),
  764. label='VLAN group',
  765. null_label='-- None --'
  766. )
  767. tenant = FilterChoiceField(
  768. queryset=Tenant.objects.annotate(filter_count=Count('vlans')),
  769. to_field_name='slug',
  770. null_label='-- None --'
  771. )
  772. status = AnnotatedMultipleChoiceField(
  773. choices=VLAN_STATUS_CHOICES,
  774. annotate=VLAN.objects.all(),
  775. annotate_field='status',
  776. required=False
  777. )
  778. role = FilterChoiceField(
  779. queryset=Role.objects.annotate(filter_count=Count('vlans')),
  780. to_field_name='slug',
  781. null_label='-- None --'
  782. )
  783. #
  784. # Services
  785. #
  786. class ServiceForm(BootstrapMixin, CustomFieldForm):
  787. tags = TagField(required=False)
  788. class Meta:
  789. model = Service
  790. fields = ['name', 'protocol', 'port', 'ipaddresses', 'description', 'tags']
  791. help_texts = {
  792. 'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
  793. "reachable via all IPs assigned to the device.",
  794. }
  795. def __init__(self, *args, **kwargs):
  796. super(ServiceForm, self).__init__(*args, **kwargs)
  797. # Limit IP address choices to those assigned to interfaces of the parent device/VM
  798. if self.instance.device:
  799. vc_interface_ids = [i['id'] for i in self.instance.device.vc_interfaces.values('id')]
  800. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  801. interface_id__in=vc_interface_ids
  802. )
  803. elif self.instance.virtual_machine:
  804. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  805. interface__virtual_machine=self.instance.virtual_machine
  806. )
  807. else:
  808. self.fields['ipaddresses'].choices = []
  809. class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
  810. model = Service
  811. q = forms.CharField(
  812. required=False,
  813. label='Search'
  814. )
  815. protocol = forms.ChoiceField(
  816. choices=add_blank_choice(IP_PROTOCOL_CHOICES),
  817. required=False
  818. )
  819. port = forms.IntegerField(
  820. required=False
  821. )
  822. class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  823. pk = forms.ModelMultipleChoiceField(queryset=Service.objects.all(), widget=forms.MultipleHiddenInput)
  824. protocol = forms.ChoiceField(choices=add_blank_choice(IP_PROTOCOL_CHOICES), required=False)
  825. port = forms.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(65535)], required=False)
  826. description = forms.CharField(max_length=100, required=False)
  827. class Meta:
  828. nullable_fields = ['site', 'group', 'tenant', 'role', 'description']