forms.py 29 KB

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