model_forms.py 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021
  1. from django import forms
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.core.exceptions import ValidationError
  4. from django.utils.translation import gettext as _
  5. from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
  6. from ipam.choices import *
  7. from ipam.constants import *
  8. from ipam.formfields import IPNetworkFormField
  9. from ipam.models import *
  10. from netbox.forms import NetBoxModelForm
  11. from tenancy.forms import TenancyForm
  12. from utilities.exceptions import PermissionsViolation
  13. from utilities.forms import (
  14. add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField,
  15. DynamicModelMultipleChoiceField, NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple,
  16. )
  17. from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
  18. __all__ = (
  19. 'AggregateForm',
  20. 'ASNForm',
  21. 'FHRPGroupForm',
  22. 'FHRPGroupAssignmentForm',
  23. 'IPAddressAssignForm',
  24. 'IPAddressBulkAddForm',
  25. 'IPAddressForm',
  26. 'IPRangeForm',
  27. 'L2VPNForm',
  28. 'L2VPNTerminationForm',
  29. 'PrefixForm',
  30. 'RIRForm',
  31. 'RoleForm',
  32. 'RouteTargetForm',
  33. 'ServiceForm',
  34. 'ServiceCreateForm',
  35. 'ServiceTemplateForm',
  36. 'VLANForm',
  37. 'VLANGroupForm',
  38. 'VRFForm',
  39. )
  40. class VRFForm(TenancyForm, NetBoxModelForm):
  41. import_targets = DynamicModelMultipleChoiceField(
  42. queryset=RouteTarget.objects.all(),
  43. required=False
  44. )
  45. export_targets = DynamicModelMultipleChoiceField(
  46. queryset=RouteTarget.objects.all(),
  47. required=False
  48. )
  49. comments = CommentField()
  50. fieldsets = (
  51. ('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')),
  52. ('Route Targets', ('import_targets', 'export_targets')),
  53. ('Tenancy', ('tenant_group', 'tenant')),
  54. )
  55. class Meta:
  56. model = VRF
  57. fields = [
  58. 'name', 'rd', 'enforce_unique', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'description',
  59. 'comments', 'tags',
  60. ]
  61. labels = {
  62. 'rd': "RD",
  63. }
  64. help_texts = {
  65. 'rd': _("Route distinguisher in any format"),
  66. }
  67. class RouteTargetForm(TenancyForm, NetBoxModelForm):
  68. fieldsets = (
  69. ('Route Target', ('name', 'description', 'tags')),
  70. ('Tenancy', ('tenant_group', 'tenant')),
  71. )
  72. comments = CommentField()
  73. class Meta:
  74. model = RouteTarget
  75. fields = [
  76. 'name', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
  77. ]
  78. class RIRForm(NetBoxModelForm):
  79. slug = SlugField()
  80. fieldsets = (
  81. ('RIR', (
  82. 'name', 'slug', 'is_private', 'description', 'tags',
  83. )),
  84. )
  85. class Meta:
  86. model = RIR
  87. fields = [
  88. 'name', 'slug', 'is_private', 'description', 'tags',
  89. ]
  90. class AggregateForm(TenancyForm, NetBoxModelForm):
  91. rir = DynamicModelChoiceField(
  92. queryset=RIR.objects.all(),
  93. label=_('RIR')
  94. )
  95. comments = CommentField()
  96. fieldsets = (
  97. ('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')),
  98. ('Tenancy', ('tenant_group', 'tenant')),
  99. )
  100. class Meta:
  101. model = Aggregate
  102. fields = [
  103. 'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
  104. ]
  105. help_texts = {
  106. 'prefix': _("IPv4 or IPv6 network"),
  107. 'rir': _("Regional Internet Registry responsible for this prefix"),
  108. }
  109. widgets = {
  110. 'date_added': DatePicker(),
  111. }
  112. class ASNForm(TenancyForm, NetBoxModelForm):
  113. rir = DynamicModelChoiceField(
  114. queryset=RIR.objects.all(),
  115. label=_('RIR'),
  116. )
  117. sites = DynamicModelMultipleChoiceField(
  118. queryset=Site.objects.all(),
  119. label=_('Sites'),
  120. required=False
  121. )
  122. comments = CommentField()
  123. fieldsets = (
  124. ('ASN', ('asn', 'rir', 'sites', 'description', 'tags')),
  125. ('Tenancy', ('tenant_group', 'tenant')),
  126. )
  127. class Meta:
  128. model = ASN
  129. fields = [
  130. 'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
  131. ]
  132. help_texts = {
  133. 'asn': _("AS number"),
  134. 'rir': _("Regional Internet Registry responsible for this prefix"),
  135. }
  136. widgets = {
  137. 'date_added': DatePicker(),
  138. }
  139. def __init__(self, data=None, instance=None, *args, **kwargs):
  140. super().__init__(data=data, instance=instance, *args, **kwargs)
  141. if self.instance and self.instance.pk is not None:
  142. self.fields['sites'].initial = self.instance.sites.all().values_list('id', flat=True)
  143. def save(self, *args, **kwargs):
  144. instance = super().save(*args, **kwargs)
  145. instance.sites.set(self.cleaned_data['sites'])
  146. return instance
  147. class RoleForm(NetBoxModelForm):
  148. slug = SlugField()
  149. fieldsets = (
  150. ('Role', (
  151. 'name', 'slug', 'weight', 'description', 'tags',
  152. )),
  153. )
  154. class Meta:
  155. model = Role
  156. fields = [
  157. 'name', 'slug', 'weight', 'description', 'tags',
  158. ]
  159. class PrefixForm(TenancyForm, NetBoxModelForm):
  160. vrf = DynamicModelChoiceField(
  161. queryset=VRF.objects.all(),
  162. required=False,
  163. label=_('VRF')
  164. )
  165. region = DynamicModelChoiceField(
  166. queryset=Region.objects.all(),
  167. required=False,
  168. initial_params={
  169. 'sites': '$site'
  170. }
  171. )
  172. site_group = DynamicModelChoiceField(
  173. queryset=SiteGroup.objects.all(),
  174. required=False,
  175. initial_params={
  176. 'sites': '$site'
  177. }
  178. )
  179. site = DynamicModelChoiceField(
  180. queryset=Site.objects.all(),
  181. required=False,
  182. null_option='None',
  183. query_params={
  184. 'region_id': '$region',
  185. 'group_id': '$site_group',
  186. }
  187. )
  188. vlan_group = DynamicModelChoiceField(
  189. queryset=VLANGroup.objects.all(),
  190. required=False,
  191. label=_('VLAN group'),
  192. null_option='None',
  193. query_params={
  194. 'site': '$site'
  195. },
  196. initial_params={
  197. 'vlans': '$vlan'
  198. }
  199. )
  200. vlan = DynamicModelChoiceField(
  201. queryset=VLAN.objects.all(),
  202. required=False,
  203. label=_('VLAN'),
  204. query_params={
  205. 'site_id': '$site',
  206. 'group_id': '$vlan_group',
  207. }
  208. )
  209. role = DynamicModelChoiceField(
  210. queryset=Role.objects.all(),
  211. required=False
  212. )
  213. comments = CommentField()
  214. fieldsets = (
  215. ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
  216. ('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')),
  217. ('Tenancy', ('tenant_group', 'tenant')),
  218. )
  219. class Meta:
  220. model = Prefix
  221. fields = [
  222. 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'tenant_group', 'tenant',
  223. 'description', 'comments', 'tags',
  224. ]
  225. widgets = {
  226. 'status': StaticSelect(),
  227. }
  228. class IPRangeForm(TenancyForm, NetBoxModelForm):
  229. vrf = DynamicModelChoiceField(
  230. queryset=VRF.objects.all(),
  231. required=False,
  232. label=_('VRF')
  233. )
  234. role = DynamicModelChoiceField(
  235. queryset=Role.objects.all(),
  236. required=False
  237. )
  238. comments = CommentField()
  239. fieldsets = (
  240. ('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'description', 'tags')),
  241. ('Tenancy', ('tenant_group', 'tenant')),
  242. )
  243. class Meta:
  244. model = IPRange
  245. fields = [
  246. 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'description',
  247. 'comments', 'tags',
  248. ]
  249. widgets = {
  250. 'status': StaticSelect(),
  251. }
  252. class IPAddressForm(TenancyForm, NetBoxModelForm):
  253. device = DynamicModelChoiceField(
  254. queryset=Device.objects.all(),
  255. required=False,
  256. initial_params={
  257. 'interfaces': '$interface'
  258. }
  259. )
  260. interface = DynamicModelChoiceField(
  261. queryset=Interface.objects.all(),
  262. required=False,
  263. query_params={
  264. 'device_id': '$device'
  265. }
  266. )
  267. virtual_machine = DynamicModelChoiceField(
  268. queryset=VirtualMachine.objects.all(),
  269. required=False,
  270. initial_params={
  271. 'interfaces': '$vminterface'
  272. }
  273. )
  274. vminterface = DynamicModelChoiceField(
  275. queryset=VMInterface.objects.all(),
  276. required=False,
  277. label=_('Interface'),
  278. query_params={
  279. 'virtual_machine_id': '$virtual_machine'
  280. }
  281. )
  282. fhrpgroup = DynamicModelChoiceField(
  283. queryset=FHRPGroup.objects.all(),
  284. required=False,
  285. label=_('FHRP Group')
  286. )
  287. vrf = DynamicModelChoiceField(
  288. queryset=VRF.objects.all(),
  289. required=False,
  290. label=_('VRF')
  291. )
  292. nat_region = DynamicModelChoiceField(
  293. queryset=Region.objects.all(),
  294. required=False,
  295. label=_('Region'),
  296. initial_params={
  297. 'sites': '$nat_site'
  298. }
  299. )
  300. nat_site_group = DynamicModelChoiceField(
  301. queryset=SiteGroup.objects.all(),
  302. required=False,
  303. label=_('Site group'),
  304. initial_params={
  305. 'sites': '$nat_site'
  306. }
  307. )
  308. nat_site = DynamicModelChoiceField(
  309. queryset=Site.objects.all(),
  310. required=False,
  311. label=_('Site'),
  312. query_params={
  313. 'region_id': '$nat_region',
  314. 'group_id': '$nat_site_group',
  315. }
  316. )
  317. nat_rack = DynamicModelChoiceField(
  318. queryset=Rack.objects.all(),
  319. required=False,
  320. label=_('Rack'),
  321. null_option='None',
  322. query_params={
  323. 'site_id': '$site'
  324. }
  325. )
  326. nat_device = DynamicModelChoiceField(
  327. queryset=Device.objects.all(),
  328. required=False,
  329. label=_('Device'),
  330. query_params={
  331. 'site_id': '$site',
  332. 'rack_id': '$nat_rack',
  333. }
  334. )
  335. nat_cluster = DynamicModelChoiceField(
  336. queryset=Cluster.objects.all(),
  337. required=False,
  338. label=_('Cluster')
  339. )
  340. nat_virtual_machine = DynamicModelChoiceField(
  341. queryset=VirtualMachine.objects.all(),
  342. required=False,
  343. label=_('Virtual Machine'),
  344. query_params={
  345. 'cluster_id': '$nat_cluster',
  346. }
  347. )
  348. nat_vrf = DynamicModelChoiceField(
  349. queryset=VRF.objects.all(),
  350. required=False,
  351. label=_('VRF')
  352. )
  353. nat_inside = DynamicModelChoiceField(
  354. queryset=IPAddress.objects.all(),
  355. required=False,
  356. label=_('IP Address'),
  357. query_params={
  358. 'device_id': '$nat_device',
  359. 'virtual_machine_id': '$nat_virtual_machine',
  360. 'vrf_id': '$nat_vrf',
  361. }
  362. )
  363. primary_for_parent = forms.BooleanField(
  364. required=False,
  365. label=_('Make this the primary IP for the device/VM')
  366. )
  367. comments = CommentField()
  368. class Meta:
  369. model = IPAddress
  370. fields = [
  371. 'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_site', 'nat_rack', 'nat_device',
  372. 'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', 'description',
  373. 'comments', 'tags',
  374. ]
  375. widgets = {
  376. 'status': StaticSelect(),
  377. 'role': StaticSelect(),
  378. }
  379. def __init__(self, *args, **kwargs):
  380. # Initialize helper selectors
  381. instance = kwargs.get('instance')
  382. initial = kwargs.get('initial', {}).copy()
  383. if instance:
  384. if type(instance.assigned_object) is Interface:
  385. initial['interface'] = instance.assigned_object
  386. elif type(instance.assigned_object) is VMInterface:
  387. initial['vminterface'] = instance.assigned_object
  388. elif type(instance.assigned_object) is FHRPGroup:
  389. initial['fhrpgroup'] = instance.assigned_object
  390. if instance.nat_inside:
  391. nat_inside_parent = instance.nat_inside.assigned_object
  392. if type(nat_inside_parent) is Interface:
  393. initial['nat_site'] = nat_inside_parent.device.site.pk
  394. if nat_inside_parent.device.rack:
  395. initial['nat_rack'] = nat_inside_parent.device.rack.pk
  396. initial['nat_device'] = nat_inside_parent.device.pk
  397. elif type(nat_inside_parent) is VMInterface:
  398. initial['nat_cluster'] = nat_inside_parent.virtual_machine.cluster.pk
  399. initial['nat_virtual_machine'] = nat_inside_parent.virtual_machine.pk
  400. kwargs['initial'] = initial
  401. super().__init__(*args, **kwargs)
  402. # Initialize primary_for_parent if IP address is already assigned
  403. if self.instance.pk and self.instance.assigned_object:
  404. parent = getattr(self.instance.assigned_object, 'parent_object', None)
  405. if parent and (
  406. self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
  407. self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
  408. ):
  409. self.initial['primary_for_parent'] = True
  410. def clean(self):
  411. super().clean()
  412. # Handle object assignment
  413. selected_objects = [
  414. field for field in ('interface', 'vminterface', 'fhrpgroup') if self.cleaned_data[field]
  415. ]
  416. if len(selected_objects) > 1:
  417. raise forms.ValidationError({
  418. selected_objects[1]: "An IP address can only be assigned to a single object."
  419. })
  420. elif selected_objects:
  421. self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
  422. else:
  423. self.instance.assigned_object = None
  424. # Primary IP assignment is only available if an interface has been assigned.
  425. interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
  426. if self.cleaned_data.get('primary_for_parent') and not interface:
  427. self.add_error(
  428. 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
  429. )
  430. def save(self, *args, **kwargs):
  431. ipaddress = super().save(*args, **kwargs)
  432. # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
  433. interface = self.instance.assigned_object
  434. if type(interface) in (Interface, VMInterface):
  435. parent = interface.parent_object
  436. if self.cleaned_data['primary_for_parent']:
  437. if ipaddress.address.version == 4:
  438. parent.primary_ip4 = ipaddress
  439. else:
  440. parent.primary_ip6 = ipaddress
  441. parent.save()
  442. elif ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
  443. parent.primary_ip4 = None
  444. parent.save()
  445. elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
  446. parent.primary_ip6 = None
  447. parent.save()
  448. return ipaddress
  449. class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
  450. vrf = DynamicModelChoiceField(
  451. queryset=VRF.objects.all(),
  452. required=False,
  453. label=_('VRF')
  454. )
  455. class Meta:
  456. model = IPAddress
  457. fields = [
  458. 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
  459. ]
  460. widgets = {
  461. 'status': StaticSelect(),
  462. 'role': StaticSelect(),
  463. }
  464. class IPAddressAssignForm(BootstrapMixin, forms.Form):
  465. vrf_id = DynamicModelChoiceField(
  466. queryset=VRF.objects.all(),
  467. required=False,
  468. label=_('VRF')
  469. )
  470. q = forms.CharField(
  471. required=False,
  472. label=_('Search'),
  473. )
  474. class FHRPGroupForm(NetBoxModelForm):
  475. # Optionally create a new IPAddress along with the FHRPGroup
  476. ip_vrf = DynamicModelChoiceField(
  477. queryset=VRF.objects.all(),
  478. required=False,
  479. label=_('VRF')
  480. )
  481. ip_address = IPNetworkFormField(
  482. required=False,
  483. label=_('Address')
  484. )
  485. ip_status = forms.ChoiceField(
  486. choices=add_blank_choice(IPAddressStatusChoices),
  487. required=False,
  488. label=_('Status')
  489. )
  490. comments = CommentField()
  491. fieldsets = (
  492. ('FHRP Group', ('protocol', 'group_id', 'name', 'description', 'tags')),
  493. ('Authentication', ('auth_type', 'auth_key')),
  494. ('Virtual IP Address', ('ip_vrf', 'ip_address', 'ip_status'))
  495. )
  496. class Meta:
  497. model = FHRPGroup
  498. fields = (
  499. 'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'ip_vrf', 'ip_address', 'ip_status', 'description',
  500. 'comments', 'tags',
  501. )
  502. widgets = {
  503. 'protocol': StaticSelect(),
  504. 'auth_type': StaticSelect(),
  505. 'ip_status': StaticSelect(),
  506. }
  507. def save(self, *args, **kwargs):
  508. instance = super().save(*args, **kwargs)
  509. user = getattr(instance, '_user', None) # Set under FHRPGroupEditView.alter_object()
  510. # Check if we need to create a new IPAddress for the group
  511. if self.cleaned_data.get('ip_address'):
  512. ipaddress = IPAddress(
  513. vrf=self.cleaned_data['ip_vrf'],
  514. address=self.cleaned_data['ip_address'],
  515. status=self.cleaned_data['ip_status'],
  516. role=FHRP_PROTOCOL_ROLE_MAPPINGS.get(self.cleaned_data['protocol'], IPAddressRoleChoices.ROLE_VIP),
  517. assigned_object=instance
  518. )
  519. ipaddress.save()
  520. # Check that the new IPAddress conforms with any assigned object-level permissions
  521. if not IPAddress.objects.restrict(user, 'add').filter(pk=ipaddress.pk).first():
  522. raise PermissionsViolation()
  523. return instance
  524. def clean(self):
  525. super().clean()
  526. ip_vrf = self.cleaned_data.get('ip_vrf')
  527. ip_address = self.cleaned_data.get('ip_address')
  528. ip_status = self.cleaned_data.get('ip_status')
  529. if ip_address:
  530. ip_form = IPAddressForm({
  531. 'address': ip_address,
  532. 'vrf': ip_vrf,
  533. 'status': ip_status,
  534. })
  535. if not ip_form.is_valid():
  536. self.errors.update({
  537. f'ip_{field}': error for field, error in ip_form.errors.items()
  538. })
  539. class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm):
  540. group = DynamicModelChoiceField(
  541. queryset=FHRPGroup.objects.all()
  542. )
  543. class Meta:
  544. model = FHRPGroupAssignment
  545. fields = ('group', 'priority')
  546. def __init__(self, *args, **kwargs):
  547. super().__init__(*args, **kwargs)
  548. ipaddresses = self.instance.interface.ip_addresses.all()
  549. for ipaddress in ipaddresses:
  550. self.fields['group'].widget.add_query_param('related_ip', ipaddress.pk)
  551. class VLANGroupForm(NetBoxModelForm):
  552. scope_type = ContentTypeChoiceField(
  553. queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
  554. required=False
  555. )
  556. region = DynamicModelChoiceField(
  557. queryset=Region.objects.all(),
  558. required=False,
  559. initial_params={
  560. 'sites': '$site'
  561. }
  562. )
  563. sitegroup = DynamicModelChoiceField(
  564. queryset=SiteGroup.objects.all(),
  565. required=False,
  566. initial_params={
  567. 'sites': '$site'
  568. },
  569. label=_('Site group')
  570. )
  571. site = DynamicModelChoiceField(
  572. queryset=Site.objects.all(),
  573. required=False,
  574. initial_params={
  575. 'locations': '$location'
  576. },
  577. query_params={
  578. 'region_id': '$region',
  579. 'group_id': '$sitegroup',
  580. }
  581. )
  582. location = DynamicModelChoiceField(
  583. queryset=Location.objects.all(),
  584. required=False,
  585. initial_params={
  586. 'racks': '$rack'
  587. },
  588. query_params={
  589. 'site_id': '$site',
  590. }
  591. )
  592. rack = DynamicModelChoiceField(
  593. queryset=Rack.objects.all(),
  594. required=False,
  595. query_params={
  596. 'site_id': '$site',
  597. 'location_id': '$location',
  598. }
  599. )
  600. clustergroup = DynamicModelChoiceField(
  601. queryset=ClusterGroup.objects.all(),
  602. required=False,
  603. initial_params={
  604. 'clusters': '$cluster'
  605. },
  606. label=_('Cluster group')
  607. )
  608. cluster = DynamicModelChoiceField(
  609. queryset=Cluster.objects.all(),
  610. required=False,
  611. query_params={
  612. 'group_id': '$clustergroup',
  613. }
  614. )
  615. slug = SlugField()
  616. fieldsets = (
  617. ('VLAN Group', ('name', 'slug', 'description', 'tags')),
  618. ('Child VLANs', ('min_vid', 'max_vid')),
  619. ('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
  620. )
  621. class Meta:
  622. model = VLANGroup
  623. fields = [
  624. 'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
  625. 'clustergroup', 'cluster', 'min_vid', 'max_vid', 'tags',
  626. ]
  627. widgets = {
  628. 'scope_type': StaticSelect,
  629. }
  630. def __init__(self, *args, **kwargs):
  631. instance = kwargs.get('instance')
  632. initial = kwargs.get('initial', {})
  633. if instance is not None and instance.scope:
  634. initial[instance.scope_type.model] = instance.scope
  635. kwargs['initial'] = initial
  636. super().__init__(*args, **kwargs)
  637. def clean(self):
  638. super().clean()
  639. # Assign scope based on scope_type
  640. if self.cleaned_data.get('scope_type'):
  641. scope_field = self.cleaned_data['scope_type'].model
  642. self.instance.scope = self.cleaned_data.get(scope_field)
  643. else:
  644. self.instance.scope_id = None
  645. class VLANForm(TenancyForm, NetBoxModelForm):
  646. # VLANGroup assignment fields
  647. scope_type = forms.ChoiceField(
  648. choices=(
  649. ('', ''),
  650. ('dcim.region', 'Region'),
  651. ('dcim.sitegroup', 'Site group'),
  652. ('dcim.site', 'Site'),
  653. ('dcim.location', 'Location'),
  654. ('dcim.rack', 'Rack'),
  655. ('virtualization.clustergroup', 'Cluster group'),
  656. ('virtualization.cluster', 'Cluster'),
  657. ),
  658. required=False,
  659. widget=StaticSelect,
  660. label=_('Group scope')
  661. )
  662. group = DynamicModelChoiceField(
  663. queryset=VLANGroup.objects.all(),
  664. required=False,
  665. query_params={
  666. 'scope_type': '$scope_type',
  667. },
  668. label=_('VLAN Group')
  669. )
  670. # Site assignment fields
  671. region = DynamicModelChoiceField(
  672. queryset=Region.objects.all(),
  673. required=False,
  674. initial_params={
  675. 'sites': '$site'
  676. },
  677. label=_('Region')
  678. )
  679. sitegroup = DynamicModelChoiceField(
  680. queryset=SiteGroup.objects.all(),
  681. required=False,
  682. initial_params={
  683. 'sites': '$site'
  684. },
  685. label=_('Site group')
  686. )
  687. site = DynamicModelChoiceField(
  688. queryset=Site.objects.all(),
  689. required=False,
  690. null_option='None',
  691. query_params={
  692. 'region_id': '$region',
  693. 'group_id': '$sitegroup',
  694. }
  695. )
  696. # Other fields
  697. role = DynamicModelChoiceField(
  698. queryset=Role.objects.all(),
  699. required=False
  700. )
  701. comments = CommentField()
  702. class Meta:
  703. model = VLAN
  704. fields = [
  705. 'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'description', 'comments',
  706. 'tags',
  707. ]
  708. help_texts = {
  709. 'site': _("Leave blank if this VLAN spans multiple sites"),
  710. 'group': _("VLAN group (optional)"),
  711. 'vid': _("Configured VLAN ID"),
  712. 'name': _("Configured VLAN name"),
  713. 'status': _("Operational status of this VLAN"),
  714. 'role': _("The primary function of this VLAN"),
  715. }
  716. widgets = {
  717. 'status': StaticSelect(),
  718. }
  719. class ServiceTemplateForm(NetBoxModelForm):
  720. ports = NumericArrayField(
  721. base_field=forms.IntegerField(
  722. min_value=SERVICE_PORT_MIN,
  723. max_value=SERVICE_PORT_MAX
  724. ),
  725. help_text=_("Comma-separated list of one or more port numbers. A range may be specified using a hyphen.")
  726. )
  727. comments = CommentField()
  728. fieldsets = (
  729. ('Service Template', (
  730. 'name', 'protocol', 'ports', 'description', 'tags',
  731. )),
  732. )
  733. class Meta:
  734. model = ServiceTemplate
  735. fields = ('name', 'protocol', 'ports', 'description', 'comments', 'tags')
  736. widgets = {
  737. 'protocol': StaticSelect(),
  738. }
  739. class ServiceForm(NetBoxModelForm):
  740. device = DynamicModelChoiceField(
  741. queryset=Device.objects.all(),
  742. required=False
  743. )
  744. virtual_machine = DynamicModelChoiceField(
  745. queryset=VirtualMachine.objects.all(),
  746. required=False
  747. )
  748. ports = NumericArrayField(
  749. base_field=forms.IntegerField(
  750. min_value=SERVICE_PORT_MIN,
  751. max_value=SERVICE_PORT_MAX
  752. ),
  753. help_text=_("Comma-separated list of one or more port numbers. A range may be specified using a hyphen.")
  754. )
  755. ipaddresses = DynamicModelMultipleChoiceField(
  756. queryset=IPAddress.objects.all(),
  757. required=False,
  758. label=_('IP Addresses'),
  759. query_params={
  760. 'device_id': '$device',
  761. 'virtual_machine_id': '$virtual_machine',
  762. }
  763. )
  764. comments = CommentField()
  765. class Meta:
  766. model = Service
  767. fields = [
  768. 'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',
  769. ]
  770. help_texts = {
  771. 'ipaddresses': _("IP address assignment is optional. If no IPs are selected, the service is assumed to be "
  772. "reachable via all IPs assigned to the device."),
  773. }
  774. widgets = {
  775. 'protocol': StaticSelect(),
  776. 'ipaddresses': StaticSelectMultiple(),
  777. }
  778. class ServiceCreateForm(ServiceForm):
  779. service_template = DynamicModelChoiceField(
  780. queryset=ServiceTemplate.objects.all(),
  781. required=False
  782. )
  783. class Meta(ServiceForm.Meta):
  784. fields = [
  785. 'device', 'virtual_machine', 'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description',
  786. 'tags',
  787. ]
  788. def __init__(self, *args, **kwargs):
  789. super().__init__(*args, **kwargs)
  790. # Fields which may be populated from a ServiceTemplate are not required
  791. for field in ('name', 'protocol', 'ports'):
  792. self.fields[field].required = False
  793. del self.fields[field].widget.attrs['required']
  794. def clean(self):
  795. super().clean()
  796. if self.cleaned_data['service_template']:
  797. # Create a new Service from the specified template
  798. service_template = self.cleaned_data['service_template']
  799. self.cleaned_data['name'] = service_template.name
  800. self.cleaned_data['protocol'] = service_template.protocol
  801. self.cleaned_data['ports'] = service_template.ports
  802. if not self.cleaned_data['description']:
  803. self.cleaned_data['description'] = service_template.description
  804. elif not all(self.cleaned_data[f] for f in ('name', 'protocol', 'ports')):
  805. raise forms.ValidationError("Must specify name, protocol, and port(s) if not using a service template.")
  806. #
  807. # L2VPN
  808. #
  809. class L2VPNForm(TenancyForm, NetBoxModelForm):
  810. slug = SlugField()
  811. import_targets = DynamicModelMultipleChoiceField(
  812. queryset=RouteTarget.objects.all(),
  813. required=False
  814. )
  815. export_targets = DynamicModelMultipleChoiceField(
  816. queryset=RouteTarget.objects.all(),
  817. required=False
  818. )
  819. comments = CommentField()
  820. fieldsets = (
  821. ('L2VPN', ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
  822. ('Route Targets', ('import_targets', 'export_targets')),
  823. ('Tenancy', ('tenant_group', 'tenant')),
  824. )
  825. class Meta:
  826. model = L2VPN
  827. fields = (
  828. 'name', 'slug', 'type', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description',
  829. 'comments', 'tags'
  830. )
  831. widgets = {
  832. 'type': StaticSelect(),
  833. }
  834. class L2VPNTerminationForm(NetBoxModelForm):
  835. l2vpn = DynamicModelChoiceField(
  836. queryset=L2VPN.objects.all(),
  837. required=True,
  838. query_params={},
  839. label=_('L2VPN'),
  840. fetch_trigger='open'
  841. )
  842. device_vlan = DynamicModelChoiceField(
  843. queryset=Device.objects.all(),
  844. label=_("Available on Device"),
  845. required=False,
  846. query_params={}
  847. )
  848. vlan = DynamicModelChoiceField(
  849. queryset=VLAN.objects.all(),
  850. required=False,
  851. query_params={
  852. 'available_on_device': '$device_vlan'
  853. },
  854. label=_('VLAN')
  855. )
  856. device = DynamicModelChoiceField(
  857. queryset=Device.objects.all(),
  858. required=False,
  859. query_params={}
  860. )
  861. interface = DynamicModelChoiceField(
  862. queryset=Interface.objects.all(),
  863. required=False,
  864. query_params={
  865. 'device_id': '$device'
  866. }
  867. )
  868. virtual_machine = DynamicModelChoiceField(
  869. queryset=VirtualMachine.objects.all(),
  870. required=False,
  871. query_params={}
  872. )
  873. vminterface = DynamicModelChoiceField(
  874. queryset=VMInterface.objects.all(),
  875. required=False,
  876. query_params={
  877. 'virtual_machine_id': '$virtual_machine'
  878. },
  879. label=_('Interface')
  880. )
  881. class Meta:
  882. model = L2VPNTermination
  883. fields = ('l2vpn', )
  884. def __init__(self, *args, **kwargs):
  885. instance = kwargs.get('instance')
  886. initial = kwargs.get('initial', {}).copy()
  887. if instance:
  888. if type(instance.assigned_object) is Interface:
  889. initial['device'] = instance.assigned_object.parent
  890. initial['interface'] = instance.assigned_object
  891. elif type(instance.assigned_object) is VLAN:
  892. initial['vlan'] = instance.assigned_object
  893. elif type(instance.assigned_object) is VMInterface:
  894. initial['vminterface'] = instance.assigned_object
  895. kwargs['initial'] = initial
  896. super().__init__(*args, **kwargs)
  897. def clean(self):
  898. super().clean()
  899. interface = self.cleaned_data.get('interface')
  900. vminterface = self.cleaned_data.get('vminterface')
  901. vlan = self.cleaned_data.get('vlan')
  902. if not (interface or vminterface or vlan):
  903. raise ValidationError('A termination must specify an interface or VLAN.')
  904. if len([x for x in (interface, vminterface, vlan) if x]) > 1:
  905. raise ValidationError('A termination can only have one terminating object (an interface or VLAN).')
  906. self.instance.assigned_object = interface or vminterface or vlan