model_forms.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  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 BootstrapMixin, add_blank_choice
  14. from utilities.forms.fields import (
  15. CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
  16. SlugField,
  17. )
  18. from utilities.forms.widgets import DatePicker
  19. from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
  20. __all__ = (
  21. 'AggregateForm',
  22. 'ASNForm',
  23. 'ASNRangeForm',
  24. 'FHRPGroupForm',
  25. 'FHRPGroupAssignmentForm',
  26. 'IPAddressAssignForm',
  27. 'IPAddressBulkAddForm',
  28. 'IPAddressForm',
  29. 'IPRangeForm',
  30. 'L2VPNForm',
  31. 'L2VPNTerminationForm',
  32. 'PrefixForm',
  33. 'RIRForm',
  34. 'RoleForm',
  35. 'RouteTargetForm',
  36. 'ServiceForm',
  37. 'ServiceCreateForm',
  38. 'ServiceTemplateForm',
  39. 'VLANForm',
  40. 'VLANGroupForm',
  41. 'VRFForm',
  42. )
  43. class VRFForm(TenancyForm, NetBoxModelForm):
  44. import_targets = DynamicModelMultipleChoiceField(
  45. queryset=RouteTarget.objects.all(),
  46. required=False
  47. )
  48. export_targets = DynamicModelMultipleChoiceField(
  49. queryset=RouteTarget.objects.all(),
  50. required=False
  51. )
  52. comments = CommentField()
  53. fieldsets = (
  54. ('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')),
  55. ('Route Targets', ('import_targets', 'export_targets')),
  56. ('Tenancy', ('tenant_group', 'tenant')),
  57. )
  58. class Meta:
  59. model = VRF
  60. fields = [
  61. 'name', 'rd', 'enforce_unique', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'description',
  62. 'comments', 'tags',
  63. ]
  64. labels = {
  65. 'rd': "RD",
  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. widgets = {
  106. 'date_added': DatePicker(),
  107. }
  108. class ASNRangeForm(TenancyForm, NetBoxModelForm):
  109. rir = DynamicModelChoiceField(
  110. queryset=RIR.objects.all(),
  111. label=_('RIR'),
  112. )
  113. slug = SlugField()
  114. fieldsets = (
  115. ('ASN Range', ('name', 'slug', 'rir', 'start', 'end', 'description', 'tags')),
  116. ('Tenancy', ('tenant_group', 'tenant')),
  117. )
  118. class Meta:
  119. model = ASNRange
  120. fields = [
  121. 'name', 'slug', 'rir', 'start', 'end', 'tenant_group', 'tenant', 'description', 'tags'
  122. ]
  123. class ASNForm(TenancyForm, NetBoxModelForm):
  124. rir = DynamicModelChoiceField(
  125. queryset=RIR.objects.all(),
  126. label=_('RIR'),
  127. )
  128. sites = DynamicModelMultipleChoiceField(
  129. queryset=Site.objects.all(),
  130. label=_('Sites'),
  131. required=False
  132. )
  133. comments = CommentField()
  134. fieldsets = (
  135. ('ASN', ('asn', 'rir', 'sites', 'description', 'tags')),
  136. ('Tenancy', ('tenant_group', 'tenant')),
  137. )
  138. class Meta:
  139. model = ASN
  140. fields = [
  141. 'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
  142. ]
  143. widgets = {
  144. 'date_added': DatePicker(),
  145. }
  146. def __init__(self, data=None, instance=None, *args, **kwargs):
  147. super().__init__(data=data, instance=instance, *args, **kwargs)
  148. if self.instance and self.instance.pk is not None:
  149. self.fields['sites'].initial = self.instance.sites.all().values_list('id', flat=True)
  150. def save(self, *args, **kwargs):
  151. instance = super().save(*args, **kwargs)
  152. instance.sites.set(self.cleaned_data['sites'])
  153. return instance
  154. class RoleForm(NetBoxModelForm):
  155. slug = SlugField()
  156. fieldsets = (
  157. ('Role', (
  158. 'name', 'slug', 'weight', 'description', 'tags',
  159. )),
  160. )
  161. class Meta:
  162. model = Role
  163. fields = [
  164. 'name', 'slug', 'weight', 'description', 'tags',
  165. ]
  166. class PrefixForm(TenancyForm, NetBoxModelForm):
  167. vrf = DynamicModelChoiceField(
  168. queryset=VRF.objects.all(),
  169. required=False,
  170. label=_('VRF')
  171. )
  172. site = DynamicModelChoiceField(
  173. queryset=Site.objects.all(),
  174. required=False,
  175. selector=True,
  176. null_option='None'
  177. )
  178. vlan = DynamicModelChoiceField(
  179. queryset=VLAN.objects.all(),
  180. required=False,
  181. selector=True,
  182. label=_('VLAN'),
  183. )
  184. role = DynamicModelChoiceField(
  185. queryset=Role.objects.all(),
  186. required=False
  187. )
  188. comments = CommentField()
  189. fieldsets = (
  190. ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
  191. ('Site/VLAN Assignment', ('site', 'vlan')),
  192. ('Tenancy', ('tenant_group', 'tenant')),
  193. )
  194. class Meta:
  195. model = Prefix
  196. fields = [
  197. 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'tenant_group', 'tenant',
  198. 'description', 'comments', 'tags',
  199. ]
  200. class IPRangeForm(TenancyForm, NetBoxModelForm):
  201. vrf = DynamicModelChoiceField(
  202. queryset=VRF.objects.all(),
  203. required=False,
  204. label=_('VRF')
  205. )
  206. role = DynamicModelChoiceField(
  207. queryset=Role.objects.all(),
  208. required=False
  209. )
  210. comments = CommentField()
  211. fieldsets = (
  212. ('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags')),
  213. ('Tenancy', ('tenant_group', 'tenant')),
  214. )
  215. class Meta:
  216. model = IPRange
  217. fields = [
  218. 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_utilized',
  219. 'description', 'comments', 'tags',
  220. ]
  221. class IPAddressForm(TenancyForm, NetBoxModelForm):
  222. interface = DynamicModelChoiceField(
  223. queryset=Interface.objects.all(),
  224. required=False,
  225. selector=True,
  226. )
  227. vminterface = DynamicModelChoiceField(
  228. queryset=VMInterface.objects.all(),
  229. required=False,
  230. selector=True,
  231. label=_('Interface'),
  232. )
  233. fhrpgroup = DynamicModelChoiceField(
  234. queryset=FHRPGroup.objects.all(),
  235. required=False,
  236. selector=True,
  237. label=_('FHRP Group')
  238. )
  239. vrf = DynamicModelChoiceField(
  240. queryset=VRF.objects.all(),
  241. required=False,
  242. label=_('VRF')
  243. )
  244. nat_inside = DynamicModelChoiceField(
  245. queryset=IPAddress.objects.all(),
  246. required=False,
  247. selector=True,
  248. label=_('IP Address'),
  249. )
  250. primary_for_parent = forms.BooleanField(
  251. required=False,
  252. label=_('Make this the primary IP for the device/VM')
  253. )
  254. comments = CommentField()
  255. class Meta:
  256. model = IPAddress
  257. fields = [
  258. 'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_inside', 'tenant_group',
  259. 'tenant', 'description', 'comments', 'tags',
  260. ]
  261. def __init__(self, *args, **kwargs):
  262. # Initialize helper selectors
  263. instance = kwargs.get('instance')
  264. initial = kwargs.get('initial', {}).copy()
  265. if instance:
  266. if type(instance.assigned_object) is Interface:
  267. initial['interface'] = instance.assigned_object
  268. elif type(instance.assigned_object) is VMInterface:
  269. initial['vminterface'] = instance.assigned_object
  270. elif type(instance.assigned_object) is FHRPGroup:
  271. initial['fhrpgroup'] = instance.assigned_object
  272. kwargs['initial'] = initial
  273. super().__init__(*args, **kwargs)
  274. # Initialize primary_for_parent if IP address is already assigned
  275. if self.instance.pk and self.instance.assigned_object:
  276. parent = getattr(self.instance.assigned_object, 'parent_object', None)
  277. if parent and (
  278. self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
  279. self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
  280. ):
  281. self.initial['primary_for_parent'] = True
  282. # Disable object assignment fields if the IP address is designated as primary
  283. if self.initial.get('primary_for_parent'):
  284. self.fields['interface'].disabled = True
  285. self.fields['vminterface'].disabled = True
  286. self.fields['fhrpgroup'].disabled = True
  287. def clean(self):
  288. super().clean()
  289. # Handle object assignment
  290. selected_objects = [
  291. field for field in ('interface', 'vminterface', 'fhrpgroup') if self.cleaned_data[field]
  292. ]
  293. if len(selected_objects) > 1:
  294. raise forms.ValidationError({
  295. selected_objects[1]: "An IP address can only be assigned to a single object."
  296. })
  297. elif selected_objects:
  298. assigned_object = self.cleaned_data[selected_objects[0]]
  299. if self.instance.pk and self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object:
  300. raise ValidationError(
  301. "Cannot reassign IP address while it is designated as the primary IP for the parent object"
  302. )
  303. self.instance.assigned_object = assigned_object
  304. else:
  305. self.instance.assigned_object = None
  306. # Primary IP assignment is only available if an interface has been assigned.
  307. interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
  308. if self.cleaned_data.get('primary_for_parent') and not interface:
  309. self.add_error(
  310. 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
  311. )
  312. # Do not allow assigning a network ID or broadcast address to an interface.
  313. if interface and (address := self.cleaned_data.get('address')):
  314. if address.ip == address.network:
  315. msg = f"{address} is a network ID, which may not be assigned to an interface."
  316. if address.version == 4 and address.prefixlen not in (31, 32):
  317. raise ValidationError(msg)
  318. if address.version == 6 and address.prefixlen not in (127, 128):
  319. raise ValidationError(msg)
  320. if address.version == 4 and address.ip == address.broadcast and address.prefixlen not in (31, 32):
  321. msg = f"{address} is a broadcast address, which may not be assigned to an interface."
  322. raise ValidationError(msg)
  323. def save(self, *args, **kwargs):
  324. ipaddress = super().save(*args, **kwargs)
  325. # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
  326. interface = self.instance.assigned_object
  327. if type(interface) in (Interface, VMInterface):
  328. parent = interface.parent_object
  329. parent.snapshot()
  330. if self.cleaned_data['primary_for_parent']:
  331. if ipaddress.address.version == 4:
  332. parent.primary_ip4 = ipaddress
  333. else:
  334. parent.primary_ip6 = ipaddress
  335. parent.save()
  336. elif ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
  337. parent.primary_ip4 = None
  338. parent.save()
  339. elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
  340. parent.primary_ip6 = None
  341. parent.save()
  342. return ipaddress
  343. class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
  344. vrf = DynamicModelChoiceField(
  345. queryset=VRF.objects.all(),
  346. required=False,
  347. label=_('VRF')
  348. )
  349. class Meta:
  350. model = IPAddress
  351. fields = [
  352. 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
  353. ]
  354. class IPAddressAssignForm(BootstrapMixin, forms.Form):
  355. vrf_id = DynamicModelChoiceField(
  356. queryset=VRF.objects.all(),
  357. required=False,
  358. label=_('VRF')
  359. )
  360. q = forms.CharField(
  361. required=False,
  362. label=_('Search'),
  363. )
  364. class FHRPGroupForm(NetBoxModelForm):
  365. # Optionally create a new IPAddress along with the FHRPGroup
  366. ip_vrf = DynamicModelChoiceField(
  367. queryset=VRF.objects.all(),
  368. required=False,
  369. label=_('VRF')
  370. )
  371. ip_address = IPNetworkFormField(
  372. required=False,
  373. label=_('Address')
  374. )
  375. ip_status = forms.ChoiceField(
  376. choices=add_blank_choice(IPAddressStatusChoices),
  377. required=False,
  378. label=_('Status')
  379. )
  380. comments = CommentField()
  381. fieldsets = (
  382. ('FHRP Group', ('protocol', 'group_id', 'name', 'description', 'tags')),
  383. ('Authentication', ('auth_type', 'auth_key')),
  384. ('Virtual IP Address', ('ip_vrf', 'ip_address', 'ip_status'))
  385. )
  386. class Meta:
  387. model = FHRPGroup
  388. fields = (
  389. 'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'ip_vrf', 'ip_address', 'ip_status', 'description',
  390. 'comments', 'tags',
  391. )
  392. def save(self, *args, **kwargs):
  393. instance = super().save(*args, **kwargs)
  394. user = getattr(instance, '_user', None) # Set under FHRPGroupEditView.alter_object()
  395. # Check if we need to create a new IPAddress for the group
  396. if self.cleaned_data.get('ip_address'):
  397. ipaddress = IPAddress(
  398. vrf=self.cleaned_data['ip_vrf'],
  399. address=self.cleaned_data['ip_address'],
  400. status=self.cleaned_data['ip_status'],
  401. role=FHRP_PROTOCOL_ROLE_MAPPINGS.get(self.cleaned_data['protocol'], IPAddressRoleChoices.ROLE_VIP),
  402. assigned_object=instance
  403. )
  404. ipaddress.populate_custom_field_defaults()
  405. ipaddress.save()
  406. # Check that the new IPAddress conforms with any assigned object-level permissions
  407. if not IPAddress.objects.restrict(user, 'add').filter(pk=ipaddress.pk).first():
  408. raise PermissionsViolation()
  409. return instance
  410. def clean(self):
  411. super().clean()
  412. ip_vrf = self.cleaned_data.get('ip_vrf')
  413. ip_address = self.cleaned_data.get('ip_address')
  414. ip_status = self.cleaned_data.get('ip_status')
  415. if ip_address:
  416. ip_form = IPAddressForm({
  417. 'address': ip_address,
  418. 'vrf': ip_vrf,
  419. 'status': ip_status,
  420. })
  421. if not ip_form.is_valid():
  422. self.errors.update({
  423. f'ip_{field}': error for field, error in ip_form.errors.items()
  424. })
  425. class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm):
  426. group = DynamicModelChoiceField(
  427. queryset=FHRPGroup.objects.all()
  428. )
  429. class Meta:
  430. model = FHRPGroupAssignment
  431. fields = ('group', 'priority')
  432. def __init__(self, *args, **kwargs):
  433. super().__init__(*args, **kwargs)
  434. ipaddresses = self.instance.interface.ip_addresses.all()
  435. for ipaddress in ipaddresses:
  436. self.fields['group'].widget.add_query_param('related_ip', ipaddress.pk)
  437. class VLANGroupForm(NetBoxModelForm):
  438. scope_type = ContentTypeChoiceField(
  439. queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
  440. required=False
  441. )
  442. region = DynamicModelChoiceField(
  443. queryset=Region.objects.all(),
  444. required=False,
  445. initial_params={
  446. 'sites': '$site'
  447. }
  448. )
  449. sitegroup = DynamicModelChoiceField(
  450. queryset=SiteGroup.objects.all(),
  451. required=False,
  452. initial_params={
  453. 'sites': '$site'
  454. },
  455. label=_('Site group')
  456. )
  457. site = DynamicModelChoiceField(
  458. queryset=Site.objects.all(),
  459. required=False,
  460. initial_params={
  461. 'locations': '$location'
  462. },
  463. query_params={
  464. 'region_id': '$region',
  465. 'group_id': '$sitegroup',
  466. }
  467. )
  468. location = DynamicModelChoiceField(
  469. queryset=Location.objects.all(),
  470. required=False,
  471. initial_params={
  472. 'racks': '$rack'
  473. },
  474. query_params={
  475. 'site_id': '$site',
  476. }
  477. )
  478. rack = DynamicModelChoiceField(
  479. queryset=Rack.objects.all(),
  480. required=False,
  481. query_params={
  482. 'site_id': '$site',
  483. 'location_id': '$location',
  484. }
  485. )
  486. clustergroup = DynamicModelChoiceField(
  487. queryset=ClusterGroup.objects.all(),
  488. required=False,
  489. initial_params={
  490. 'clusters': '$cluster'
  491. },
  492. label=_('Cluster group')
  493. )
  494. cluster = DynamicModelChoiceField(
  495. queryset=Cluster.objects.all(),
  496. required=False,
  497. query_params={
  498. 'group_id': '$clustergroup',
  499. }
  500. )
  501. slug = SlugField()
  502. fieldsets = (
  503. ('VLAN Group', ('name', 'slug', 'description', 'tags')),
  504. ('Child VLANs', ('min_vid', 'max_vid')),
  505. ('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
  506. )
  507. class Meta:
  508. model = VLANGroup
  509. fields = [
  510. 'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
  511. 'clustergroup', 'cluster', 'min_vid', 'max_vid', 'tags',
  512. ]
  513. def __init__(self, *args, **kwargs):
  514. instance = kwargs.get('instance')
  515. initial = kwargs.get('initial', {})
  516. if instance is not None and instance.scope:
  517. initial[instance.scope_type.model] = instance.scope
  518. kwargs['initial'] = initial
  519. super().__init__(*args, **kwargs)
  520. def clean(self):
  521. super().clean()
  522. # Assign scope based on scope_type
  523. if self.cleaned_data.get('scope_type'):
  524. scope_field = self.cleaned_data['scope_type'].model
  525. self.instance.scope = self.cleaned_data.get(scope_field)
  526. else:
  527. self.instance.scope_id = None
  528. class VLANForm(TenancyForm, NetBoxModelForm):
  529. group = DynamicModelChoiceField(
  530. queryset=VLANGroup.objects.all(),
  531. required=False,
  532. selector=True,
  533. label=_('VLAN Group')
  534. )
  535. site = DynamicModelChoiceField(
  536. queryset=Site.objects.all(),
  537. required=False,
  538. null_option='None',
  539. selector=True
  540. )
  541. role = DynamicModelChoiceField(
  542. queryset=Role.objects.all(),
  543. required=False
  544. )
  545. comments = CommentField()
  546. class Meta:
  547. model = VLAN
  548. fields = [
  549. 'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'description', 'comments',
  550. 'tags',
  551. ]
  552. class ServiceTemplateForm(NetBoxModelForm):
  553. ports = NumericArrayField(
  554. base_field=forms.IntegerField(
  555. min_value=SERVICE_PORT_MIN,
  556. max_value=SERVICE_PORT_MAX
  557. ),
  558. help_text=_("Comma-separated list of one or more port numbers. A range may be specified using a hyphen.")
  559. )
  560. comments = CommentField()
  561. fieldsets = (
  562. ('Service Template', (
  563. 'name', 'protocol', 'ports', 'description', 'tags',
  564. )),
  565. )
  566. class Meta:
  567. model = ServiceTemplate
  568. fields = ('name', 'protocol', 'ports', 'description', 'comments', 'tags')
  569. class ServiceForm(NetBoxModelForm):
  570. device = DynamicModelChoiceField(
  571. queryset=Device.objects.all(),
  572. required=False,
  573. selector=True
  574. )
  575. virtual_machine = DynamicModelChoiceField(
  576. queryset=VirtualMachine.objects.all(),
  577. required=False,
  578. selector=True
  579. )
  580. ports = NumericArrayField(
  581. base_field=forms.IntegerField(
  582. min_value=SERVICE_PORT_MIN,
  583. max_value=SERVICE_PORT_MAX
  584. ),
  585. help_text=_("Comma-separated list of one or more port numbers. A range may be specified using a hyphen.")
  586. )
  587. ipaddresses = DynamicModelMultipleChoiceField(
  588. queryset=IPAddress.objects.all(),
  589. required=False,
  590. label=_('IP Addresses'),
  591. query_params={
  592. 'device_id': '$device',
  593. 'virtual_machine_id': '$virtual_machine',
  594. }
  595. )
  596. comments = CommentField()
  597. class Meta:
  598. model = Service
  599. fields = [
  600. 'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',
  601. ]
  602. class ServiceCreateForm(ServiceForm):
  603. service_template = DynamicModelChoiceField(
  604. queryset=ServiceTemplate.objects.all(),
  605. required=False
  606. )
  607. class Meta(ServiceForm.Meta):
  608. fields = [
  609. 'device', 'virtual_machine', 'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description',
  610. 'tags',
  611. ]
  612. def __init__(self, *args, **kwargs):
  613. super().__init__(*args, **kwargs)
  614. # Fields which may be populated from a ServiceTemplate are not required
  615. for field in ('name', 'protocol', 'ports'):
  616. self.fields[field].required = False
  617. del self.fields[field].widget.attrs['required']
  618. def clean(self):
  619. super().clean()
  620. if self.cleaned_data['service_template']:
  621. # Create a new Service from the specified template
  622. service_template = self.cleaned_data['service_template']
  623. self.cleaned_data['name'] = service_template.name
  624. self.cleaned_data['protocol'] = service_template.protocol
  625. self.cleaned_data['ports'] = service_template.ports
  626. if not self.cleaned_data['description']:
  627. self.cleaned_data['description'] = service_template.description
  628. elif not all(self.cleaned_data[f] for f in ('name', 'protocol', 'ports')):
  629. raise forms.ValidationError("Must specify name, protocol, and port(s) if not using a service template.")
  630. #
  631. # L2VPN
  632. #
  633. class L2VPNForm(TenancyForm, NetBoxModelForm):
  634. slug = SlugField()
  635. import_targets = DynamicModelMultipleChoiceField(
  636. queryset=RouteTarget.objects.all(),
  637. required=False
  638. )
  639. export_targets = DynamicModelMultipleChoiceField(
  640. queryset=RouteTarget.objects.all(),
  641. required=False
  642. )
  643. comments = CommentField()
  644. fieldsets = (
  645. ('L2VPN', ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
  646. ('Route Targets', ('import_targets', 'export_targets')),
  647. ('Tenancy', ('tenant_group', 'tenant')),
  648. )
  649. class Meta:
  650. model = L2VPN
  651. fields = (
  652. 'name', 'slug', 'type', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description',
  653. 'comments', 'tags'
  654. )
  655. class L2VPNTerminationForm(NetBoxModelForm):
  656. l2vpn = DynamicModelChoiceField(
  657. queryset=L2VPN.objects.all(),
  658. required=True,
  659. query_params={},
  660. label=_('L2VPN'),
  661. fetch_trigger='open'
  662. )
  663. vlan = DynamicModelChoiceField(
  664. queryset=VLAN.objects.all(),
  665. required=False,
  666. selector=True,
  667. label=_('VLAN')
  668. )
  669. interface = DynamicModelChoiceField(
  670. queryset=Interface.objects.all(),
  671. required=False,
  672. selector=True
  673. )
  674. vminterface = DynamicModelChoiceField(
  675. queryset=VMInterface.objects.all(),
  676. required=False,
  677. selector=True,
  678. label=_('Interface')
  679. )
  680. class Meta:
  681. model = L2VPNTermination
  682. fields = ('l2vpn', )
  683. def __init__(self, *args, **kwargs):
  684. instance = kwargs.get('instance')
  685. initial = kwargs.get('initial', {}).copy()
  686. if instance:
  687. if type(instance.assigned_object) is Interface:
  688. initial['interface'] = instance.assigned_object
  689. elif type(instance.assigned_object) is VLAN:
  690. initial['vlan'] = instance.assigned_object
  691. elif type(instance.assigned_object) is VMInterface:
  692. initial['vminterface'] = instance.assigned_object
  693. kwargs['initial'] = initial
  694. super().__init__(*args, **kwargs)
  695. def clean(self):
  696. super().clean()
  697. interface = self.cleaned_data.get('interface')
  698. vminterface = self.cleaned_data.get('vminterface')
  699. vlan = self.cleaned_data.get('vlan')
  700. if not (interface or vminterface or vlan):
  701. raise ValidationError('A termination must specify an interface or VLAN.')
  702. if len([x for x in (interface, vminterface, vlan) if x]) > 1:
  703. raise ValidationError('A termination can only have one terminating object (an interface or VLAN).')
  704. self.instance.assigned_object = interface or vminterface or vlan