model_forms.py 24 KB

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