forms.py 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450
  1. from django import forms
  2. from dcim.models import Device, Interface, Rack, Region, Site
  3. from extras.forms import (
  4. AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
  5. )
  6. from extras.models import Tag
  7. from tenancy.forms import TenancyFilterForm, TenancyForm
  8. from tenancy.models import Tenant
  9. from utilities.forms import (
  10. add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, CSVModelChoiceField, DatePicker,
  11. DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, NumericArrayField,
  12. ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
  13. )
  14. from virtualization.models import Cluster, VirtualMachine, VMInterface
  15. from .choices import *
  16. from .constants import *
  17. from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
  18. PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([
  19. (i, i) for i in range(PREFIX_LENGTH_MIN, PREFIX_LENGTH_MAX + 1)
  20. ])
  21. IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
  22. (i, i) for i in range(IPADDRESS_MASK_LENGTH_MIN, IPADDRESS_MASK_LENGTH_MAX + 1)
  23. ])
  24. #
  25. # VRFs
  26. #
  27. class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  28. import_targets = DynamicModelMultipleChoiceField(
  29. queryset=RouteTarget.objects.all(),
  30. required=False
  31. )
  32. export_targets = DynamicModelMultipleChoiceField(
  33. queryset=RouteTarget.objects.all(),
  34. required=False
  35. )
  36. tags = DynamicModelMultipleChoiceField(
  37. queryset=Tag.objects.all(),
  38. required=False
  39. )
  40. class Meta:
  41. model = VRF
  42. fields = [
  43. 'name', 'rd', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tenant_group', 'tenant',
  44. 'tags',
  45. ]
  46. fieldsets = (
  47. ('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')),
  48. ('Route Targets', ('import_targets', 'export_targets')),
  49. ('Tenancy', ('tenant_group', 'tenant')),
  50. )
  51. labels = {
  52. 'rd': "RD",
  53. }
  54. help_texts = {
  55. 'rd': "Route distinguisher in any format",
  56. }
  57. class VRFCSVForm(CustomFieldModelCSVForm):
  58. tenant = CSVModelChoiceField(
  59. queryset=Tenant.objects.all(),
  60. required=False,
  61. to_field_name='name',
  62. help_text='Assigned tenant'
  63. )
  64. class Meta:
  65. model = VRF
  66. fields = VRF.csv_headers
  67. class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  68. pk = forms.ModelMultipleChoiceField(
  69. queryset=VRF.objects.all(),
  70. widget=forms.MultipleHiddenInput()
  71. )
  72. tenant = DynamicModelChoiceField(
  73. queryset=Tenant.objects.all(),
  74. required=False
  75. )
  76. enforce_unique = forms.NullBooleanField(
  77. required=False,
  78. widget=BulkEditNullBooleanSelect(),
  79. label='Enforce unique space'
  80. )
  81. description = forms.CharField(
  82. max_length=100,
  83. required=False
  84. )
  85. class Meta:
  86. nullable_fields = [
  87. 'tenant', 'description',
  88. ]
  89. class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  90. model = VRF
  91. field_order = ['q', 'import_target', 'export_target', 'tenant_group', 'tenant']
  92. q = forms.CharField(
  93. required=False,
  94. label='Search'
  95. )
  96. import_target = DynamicModelMultipleChoiceField(
  97. queryset=RouteTarget.objects.all(),
  98. to_field_name='name',
  99. required=False
  100. )
  101. export_target = DynamicModelMultipleChoiceField(
  102. queryset=RouteTarget.objects.all(),
  103. to_field_name='name',
  104. required=False
  105. )
  106. tag = TagFilterField(model)
  107. #
  108. # Route targets
  109. #
  110. class RouteTargetForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  111. tags = DynamicModelMultipleChoiceField(
  112. queryset=Tag.objects.all(),
  113. required=False
  114. )
  115. class Meta:
  116. model = RouteTarget
  117. fields = [
  118. 'name', 'description', 'tenant_group', 'tenant', 'tags',
  119. ]
  120. class RouteTargetCSVForm(CustomFieldModelCSVForm):
  121. tenant = CSVModelChoiceField(
  122. queryset=Tenant.objects.all(),
  123. required=False,
  124. to_field_name='name',
  125. help_text='Assigned tenant'
  126. )
  127. class Meta:
  128. model = RouteTarget
  129. fields = RouteTarget.csv_headers
  130. class RouteTargetBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  131. pk = forms.ModelMultipleChoiceField(
  132. queryset=RouteTarget.objects.all(),
  133. widget=forms.MultipleHiddenInput()
  134. )
  135. tenant = DynamicModelChoiceField(
  136. queryset=Tenant.objects.all(),
  137. required=False
  138. )
  139. description = forms.CharField(
  140. max_length=200,
  141. required=False
  142. )
  143. class Meta:
  144. nullable_fields = [
  145. 'tenant', 'description',
  146. ]
  147. class RouteTargetFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  148. model = RouteTarget
  149. field_order = ['q', 'name', 'tenant_group', 'tenant', 'importing_vrfs', 'exporting_vrfs']
  150. q = forms.CharField(
  151. required=False,
  152. label='Search'
  153. )
  154. importing_vrf_id = DynamicModelMultipleChoiceField(
  155. queryset=VRF.objects.all(),
  156. required=False,
  157. label='Imported by VRF'
  158. )
  159. exporting_vrf_id = DynamicModelMultipleChoiceField(
  160. queryset=VRF.objects.all(),
  161. required=False,
  162. label='Exported by VRF'
  163. )
  164. tag = TagFilterField(model)
  165. #
  166. # RIRs
  167. #
  168. class RIRForm(BootstrapMixin, CustomFieldModelForm):
  169. slug = SlugField()
  170. class Meta:
  171. model = RIR
  172. fields = [
  173. 'name', 'slug', 'is_private', 'description',
  174. ]
  175. class RIRCSVForm(CustomFieldModelCSVForm):
  176. slug = SlugField()
  177. class Meta:
  178. model = RIR
  179. fields = RIR.csv_headers
  180. help_texts = {
  181. 'name': 'RIR name',
  182. }
  183. class RIRFilterForm(BootstrapMixin, forms.Form):
  184. is_private = forms.NullBooleanField(
  185. required=False,
  186. label='Private',
  187. widget=StaticSelect2(
  188. choices=BOOLEAN_WITH_BLANK_CHOICES
  189. )
  190. )
  191. #
  192. # Aggregates
  193. #
  194. class AggregateForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  195. rir = DynamicModelChoiceField(
  196. queryset=RIR.objects.all(),
  197. label='RIR'
  198. )
  199. tags = DynamicModelMultipleChoiceField(
  200. queryset=Tag.objects.all(),
  201. required=False
  202. )
  203. class Meta:
  204. model = Aggregate
  205. fields = [
  206. 'prefix', 'rir', 'date_added', 'description', 'tenant_group', 'tenant', 'tags',
  207. ]
  208. fieldsets = (
  209. ('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')),
  210. ('Tenancy', ('tenant_group', 'tenant')),
  211. )
  212. help_texts = {
  213. 'prefix': "IPv4 or IPv6 network",
  214. 'rir': "Regional Internet Registry responsible for this prefix",
  215. }
  216. widgets = {
  217. 'date_added': DatePicker(),
  218. }
  219. class AggregateCSVForm(CustomFieldModelCSVForm):
  220. rir = CSVModelChoiceField(
  221. queryset=RIR.objects.all(),
  222. to_field_name='name',
  223. help_text='Assigned RIR'
  224. )
  225. tenant = CSVModelChoiceField(
  226. queryset=Tenant.objects.all(),
  227. required=False,
  228. to_field_name='name',
  229. help_text='Assigned tenant'
  230. )
  231. class Meta:
  232. model = Aggregate
  233. fields = Aggregate.csv_headers
  234. class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  235. pk = forms.ModelMultipleChoiceField(
  236. queryset=Aggregate.objects.all(),
  237. widget=forms.MultipleHiddenInput()
  238. )
  239. rir = DynamicModelChoiceField(
  240. queryset=RIR.objects.all(),
  241. required=False,
  242. label='RIR'
  243. )
  244. tenant = DynamicModelChoiceField(
  245. queryset=Tenant.objects.all(),
  246. required=False
  247. )
  248. date_added = forms.DateField(
  249. required=False
  250. )
  251. description = forms.CharField(
  252. max_length=100,
  253. required=False
  254. )
  255. class Meta:
  256. nullable_fields = [
  257. 'date_added', 'description',
  258. ]
  259. widgets = {
  260. 'date_added': DatePicker(),
  261. }
  262. class AggregateFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  263. model = Aggregate
  264. q = forms.CharField(
  265. required=False,
  266. label='Search'
  267. )
  268. family = forms.ChoiceField(
  269. required=False,
  270. choices=add_blank_choice(IPAddressFamilyChoices),
  271. label='Address family',
  272. widget=StaticSelect2()
  273. )
  274. rir = DynamicModelMultipleChoiceField(
  275. queryset=RIR.objects.all(),
  276. to_field_name='slug',
  277. required=False,
  278. label='RIR'
  279. )
  280. tag = TagFilterField(model)
  281. #
  282. # Roles
  283. #
  284. class RoleForm(BootstrapMixin, CustomFieldModelForm):
  285. slug = SlugField()
  286. class Meta:
  287. model = Role
  288. fields = [
  289. 'name', 'slug', 'weight', 'description',
  290. ]
  291. class RoleCSVForm(CustomFieldModelCSVForm):
  292. slug = SlugField()
  293. class Meta:
  294. model = Role
  295. fields = Role.csv_headers
  296. #
  297. # Prefixes
  298. #
  299. class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  300. vrf = DynamicModelChoiceField(
  301. queryset=VRF.objects.all(),
  302. required=False,
  303. label='VRF',
  304. display_field='display_name'
  305. )
  306. region = DynamicModelChoiceField(
  307. queryset=Region.objects.all(),
  308. required=False,
  309. initial_params={
  310. 'sites': '$site'
  311. }
  312. )
  313. site = DynamicModelChoiceField(
  314. queryset=Site.objects.all(),
  315. required=False,
  316. null_option='None',
  317. query_params={
  318. 'region_id': '$region'
  319. }
  320. )
  321. vlan_group = DynamicModelChoiceField(
  322. queryset=VLANGroup.objects.all(),
  323. required=False,
  324. label='VLAN group',
  325. null_option='None',
  326. query_params={
  327. 'site_id': '$site'
  328. },
  329. initial_params={
  330. 'vlans': '$vlan'
  331. }
  332. )
  333. vlan = DynamicModelChoiceField(
  334. queryset=VLAN.objects.all(),
  335. required=False,
  336. label='VLAN',
  337. display_field='display_name',
  338. query_params={
  339. 'site_id': '$site',
  340. 'group_id': '$vlan_group',
  341. }
  342. )
  343. role = DynamicModelChoiceField(
  344. queryset=Role.objects.all(),
  345. required=False
  346. )
  347. tags = DynamicModelMultipleChoiceField(
  348. queryset=Tag.objects.all(),
  349. required=False
  350. )
  351. class Meta:
  352. model = Prefix
  353. fields = [
  354. 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant',
  355. 'tags',
  356. ]
  357. fieldsets = (
  358. ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'description', 'tags')),
  359. ('Site/VLAN Assignment', ('region', 'site', 'vlan_group', 'vlan')),
  360. ('Tenancy', ('tenant_group', 'tenant')),
  361. )
  362. widgets = {
  363. 'status': StaticSelect2(),
  364. }
  365. def __init__(self, *args, **kwargs):
  366. super().__init__(*args, **kwargs)
  367. self.fields['vrf'].empty_label = 'Global'
  368. class PrefixCSVForm(CustomFieldModelCSVForm):
  369. vrf = CSVModelChoiceField(
  370. queryset=VRF.objects.all(),
  371. to_field_name='name',
  372. required=False,
  373. help_text='Assigned VRF'
  374. )
  375. tenant = CSVModelChoiceField(
  376. queryset=Tenant.objects.all(),
  377. required=False,
  378. to_field_name='name',
  379. help_text='Assigned tenant'
  380. )
  381. site = CSVModelChoiceField(
  382. queryset=Site.objects.all(),
  383. required=False,
  384. to_field_name='name',
  385. help_text='Assigned site'
  386. )
  387. vlan_group = CSVModelChoiceField(
  388. queryset=VLANGroup.objects.all(),
  389. required=False,
  390. to_field_name='name',
  391. help_text="VLAN's group (if any)"
  392. )
  393. vlan = CSVModelChoiceField(
  394. queryset=VLAN.objects.all(),
  395. required=False,
  396. to_field_name='vid',
  397. help_text="Assigned VLAN"
  398. )
  399. status = CSVChoiceField(
  400. choices=PrefixStatusChoices,
  401. help_text='Operational status'
  402. )
  403. role = CSVModelChoiceField(
  404. queryset=Role.objects.all(),
  405. required=False,
  406. to_field_name='name',
  407. help_text='Functional role'
  408. )
  409. class Meta:
  410. model = Prefix
  411. fields = Prefix.csv_headers
  412. def __init__(self, data=None, *args, **kwargs):
  413. super().__init__(data, *args, **kwargs)
  414. if data:
  415. # Limit vlan queryset by assigned site and group
  416. params = {
  417. f"site__{self.fields['site'].to_field_name}": data.get('site'),
  418. f"group__{self.fields['vlan_group'].to_field_name}": data.get('vlan_group'),
  419. }
  420. self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params)
  421. class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  422. pk = forms.ModelMultipleChoiceField(
  423. queryset=Prefix.objects.all(),
  424. widget=forms.MultipleHiddenInput()
  425. )
  426. region = DynamicModelChoiceField(
  427. queryset=Region.objects.all(),
  428. required=False,
  429. to_field_name='slug'
  430. )
  431. site = DynamicModelChoiceField(
  432. queryset=Site.objects.all(),
  433. required=False,
  434. query_params={
  435. 'region': '$region'
  436. }
  437. )
  438. vrf = DynamicModelChoiceField(
  439. queryset=VRF.objects.all(),
  440. required=False,
  441. label='VRF',
  442. display_field='display_name'
  443. )
  444. prefix_length = forms.IntegerField(
  445. min_value=PREFIX_LENGTH_MIN,
  446. max_value=PREFIX_LENGTH_MAX,
  447. required=False
  448. )
  449. tenant = DynamicModelChoiceField(
  450. queryset=Tenant.objects.all(),
  451. required=False
  452. )
  453. status = forms.ChoiceField(
  454. choices=add_blank_choice(PrefixStatusChoices),
  455. required=False,
  456. widget=StaticSelect2()
  457. )
  458. role = DynamicModelChoiceField(
  459. queryset=Role.objects.all(),
  460. required=False
  461. )
  462. is_pool = forms.NullBooleanField(
  463. required=False,
  464. widget=BulkEditNullBooleanSelect(),
  465. label='Is a pool'
  466. )
  467. description = forms.CharField(
  468. max_length=100,
  469. required=False
  470. )
  471. class Meta:
  472. nullable_fields = [
  473. 'site', 'vrf', 'tenant', 'role', 'description',
  474. ]
  475. class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  476. model = Prefix
  477. field_order = [
  478. 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'region', 'site',
  479. 'role', 'tenant_group', 'tenant', 'is_pool', 'expand',
  480. ]
  481. mask_length__lte = forms.IntegerField(
  482. widget=forms.HiddenInput()
  483. )
  484. q = forms.CharField(
  485. required=False,
  486. label='Search'
  487. )
  488. within_include = forms.CharField(
  489. required=False,
  490. widget=forms.TextInput(
  491. attrs={
  492. 'placeholder': 'Prefix',
  493. }
  494. ),
  495. label='Search within'
  496. )
  497. family = forms.ChoiceField(
  498. required=False,
  499. choices=add_blank_choice(IPAddressFamilyChoices),
  500. label='Address family',
  501. widget=StaticSelect2()
  502. )
  503. mask_length = forms.ChoiceField(
  504. required=False,
  505. choices=PREFIX_MASK_LENGTH_CHOICES,
  506. label='Mask length',
  507. widget=StaticSelect2()
  508. )
  509. vrf_id = DynamicModelMultipleChoiceField(
  510. queryset=VRF.objects.all(),
  511. required=False,
  512. label='Assigned VRF',
  513. null_option='Global'
  514. )
  515. present_in_vrf_id = DynamicModelChoiceField(
  516. queryset=VRF.objects.all(),
  517. required=False,
  518. label='Present in VRF'
  519. )
  520. status = forms.MultipleChoiceField(
  521. choices=PrefixStatusChoices,
  522. required=False,
  523. widget=StaticSelect2Multiple()
  524. )
  525. region = DynamicModelMultipleChoiceField(
  526. queryset=Region.objects.all(),
  527. to_field_name='slug',
  528. required=False
  529. )
  530. site = DynamicModelMultipleChoiceField(
  531. queryset=Site.objects.all(),
  532. to_field_name='slug',
  533. required=False,
  534. null_option='None',
  535. query_params={
  536. 'region': '$region'
  537. }
  538. )
  539. role = DynamicModelMultipleChoiceField(
  540. queryset=Role.objects.all(),
  541. to_field_name='slug',
  542. required=False,
  543. null_option='None'
  544. )
  545. is_pool = forms.NullBooleanField(
  546. required=False,
  547. label='Is a pool',
  548. widget=StaticSelect2(
  549. choices=BOOLEAN_WITH_BLANK_CHOICES
  550. )
  551. )
  552. tag = TagFilterField(model)
  553. #
  554. # IP addresses
  555. #
  556. class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
  557. device = DynamicModelChoiceField(
  558. queryset=Device.objects.all(),
  559. required=False,
  560. display_field='display_name',
  561. initial_params={
  562. 'interfaces': '$interface'
  563. }
  564. )
  565. interface = DynamicModelChoiceField(
  566. queryset=Interface.objects.all(),
  567. required=False,
  568. query_params={
  569. 'device_id': '$device'
  570. }
  571. )
  572. virtual_machine = DynamicModelChoiceField(
  573. queryset=VirtualMachine.objects.all(),
  574. required=False,
  575. initial_params={
  576. 'interfaces': '$vminterface'
  577. }
  578. )
  579. vminterface = DynamicModelChoiceField(
  580. queryset=VMInterface.objects.all(),
  581. required=False,
  582. label='Interface',
  583. query_params={
  584. 'virtual_machine_id': '$virtual_machine'
  585. }
  586. )
  587. vrf = DynamicModelChoiceField(
  588. queryset=VRF.objects.all(),
  589. required=False,
  590. label='VRF',
  591. display_field='display_name'
  592. )
  593. nat_region = DynamicModelChoiceField(
  594. queryset=Region.objects.all(),
  595. required=False,
  596. label='Region',
  597. initial_params={
  598. 'sites': '$nat_site'
  599. }
  600. )
  601. nat_site = DynamicModelChoiceField(
  602. queryset=Site.objects.all(),
  603. required=False,
  604. label='Site',
  605. query_params={
  606. 'region_id': '$nat_region'
  607. }
  608. )
  609. nat_rack = DynamicModelChoiceField(
  610. queryset=Rack.objects.all(),
  611. required=False,
  612. label='Rack',
  613. display_field='display_name',
  614. null_option='None',
  615. query_params={
  616. 'site_id': '$site'
  617. }
  618. )
  619. nat_device = DynamicModelChoiceField(
  620. queryset=Device.objects.all(),
  621. required=False,
  622. label='Device',
  623. display_field='display_name',
  624. query_params={
  625. 'site_id': '$site',
  626. 'rack_id': '$nat_rack',
  627. }
  628. )
  629. nat_cluster = DynamicModelChoiceField(
  630. queryset=Cluster.objects.all(),
  631. required=False,
  632. label='Cluster'
  633. )
  634. nat_virtual_machine = DynamicModelChoiceField(
  635. queryset=VirtualMachine.objects.all(),
  636. required=False,
  637. label='Virtual Machine',
  638. query_params={
  639. 'cluster_id': '$nat_cluster',
  640. }
  641. )
  642. nat_vrf = DynamicModelChoiceField(
  643. queryset=VRF.objects.all(),
  644. required=False,
  645. label='VRF',
  646. display_field='display_name'
  647. )
  648. nat_inside = DynamicModelChoiceField(
  649. queryset=IPAddress.objects.all(),
  650. required=False,
  651. label='IP Address',
  652. display_field='address',
  653. query_params={
  654. 'device_id': '$nat_device',
  655. 'virtual_machine_id': '$nat_virtual_machine',
  656. 'vrf_id': '$nat_vrf',
  657. }
  658. )
  659. primary_for_parent = forms.BooleanField(
  660. required=False,
  661. label='Make this the primary IP for the device/VM'
  662. )
  663. tags = DynamicModelMultipleChoiceField(
  664. queryset=Tag.objects.all(),
  665. required=False
  666. )
  667. class Meta:
  668. model = IPAddress
  669. fields = [
  670. 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack',
  671. 'nat_device', 'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant',
  672. 'tags',
  673. ]
  674. widgets = {
  675. 'status': StaticSelect2(),
  676. 'role': StaticSelect2(),
  677. }
  678. def __init__(self, *args, **kwargs):
  679. # Initialize helper selectors
  680. instance = kwargs.get('instance')
  681. initial = kwargs.get('initial', {}).copy()
  682. if instance:
  683. if type(instance.assigned_object) is Interface:
  684. initial['interface'] = instance.assigned_object
  685. elif type(instance.assigned_object) is VMInterface:
  686. initial['vminterface'] = instance.assigned_object
  687. if instance.nat_inside:
  688. nat_inside_parent = instance.nat_inside.assigned_object
  689. if type(nat_inside_parent) is Interface:
  690. initial['nat_site'] = nat_inside_parent.device.site.pk
  691. if nat_inside_parent.device.rack:
  692. initial['nat_rack'] = nat_inside_parent.device.rack.pk
  693. initial['nat_device'] = nat_inside_parent.device.pk
  694. elif type(nat_inside_parent) is VMInterface:
  695. initial['nat_cluster'] = nat_inside_parent.virtual_machine.cluster.pk
  696. initial['nat_virtual_machine'] = nat_inside_parent.virtual_machine.pk
  697. kwargs['initial'] = initial
  698. super().__init__(*args, **kwargs)
  699. self.fields['vrf'].empty_label = 'Global'
  700. # Initialize primary_for_parent if IP address is already assigned
  701. if self.instance.pk and self.instance.assigned_object:
  702. parent = self.instance.assigned_object.parent
  703. if (
  704. self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
  705. self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
  706. ):
  707. self.initial['primary_for_parent'] = True
  708. def clean(self):
  709. super().clean()
  710. # Cannot select both a device interface and a VM interface
  711. if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'):
  712. raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface")
  713. self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
  714. # Primary IP assignment is only available if an interface has been assigned.
  715. interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
  716. if self.cleaned_data.get('primary_for_parent') and not interface:
  717. self.add_error(
  718. 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
  719. )
  720. def save(self, *args, **kwargs):
  721. ipaddress = super().save(*args, **kwargs)
  722. # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
  723. interface = self.instance.assigned_object
  724. if interface and self.cleaned_data['primary_for_parent']:
  725. if ipaddress.address.version == 4:
  726. interface.parent.primary_ip4 = ipaddress
  727. else:
  728. interface.parent.primary_ip6 = ipaddress
  729. interface.parent.save()
  730. elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress:
  731. interface.parent.primary_ip4 = None
  732. interface.parent.save()
  733. elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress:
  734. interface.parent.primary_ip6 = None
  735. interface.parent.save()
  736. return ipaddress
  737. class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
  738. pattern = ExpandableIPAddressField(
  739. label='Address pattern'
  740. )
  741. class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  742. vrf = DynamicModelChoiceField(
  743. queryset=VRF.objects.all(),
  744. required=False,
  745. label='VRF',
  746. display_field='display_name'
  747. )
  748. tags = DynamicModelMultipleChoiceField(
  749. queryset=Tag.objects.all(),
  750. required=False
  751. )
  752. class Meta:
  753. model = IPAddress
  754. fields = [
  755. 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
  756. ]
  757. widgets = {
  758. 'status': StaticSelect2(),
  759. 'role': StaticSelect2(),
  760. }
  761. def __init__(self, *args, **kwargs):
  762. super().__init__(*args, **kwargs)
  763. self.fields['vrf'].empty_label = 'Global'
  764. class IPAddressCSVForm(CustomFieldModelCSVForm):
  765. vrf = CSVModelChoiceField(
  766. queryset=VRF.objects.all(),
  767. to_field_name='name',
  768. required=False,
  769. help_text='Assigned VRF'
  770. )
  771. tenant = CSVModelChoiceField(
  772. queryset=Tenant.objects.all(),
  773. to_field_name='name',
  774. required=False,
  775. help_text='Assigned tenant'
  776. )
  777. status = CSVChoiceField(
  778. choices=IPAddressStatusChoices,
  779. required=False,
  780. help_text='Operational status'
  781. )
  782. role = CSVChoiceField(
  783. choices=IPAddressRoleChoices,
  784. required=False,
  785. help_text='Functional role'
  786. )
  787. device = CSVModelChoiceField(
  788. queryset=Device.objects.all(),
  789. required=False,
  790. to_field_name='name',
  791. help_text='Parent device of assigned interface (if any)'
  792. )
  793. virtual_machine = CSVModelChoiceField(
  794. queryset=VirtualMachine.objects.all(),
  795. required=False,
  796. to_field_name='name',
  797. help_text='Parent VM of assigned interface (if any)'
  798. )
  799. interface = CSVModelChoiceField(
  800. queryset=Interface.objects.none(), # Can also refer to VMInterface
  801. required=False,
  802. to_field_name='name',
  803. help_text='Assigned interface'
  804. )
  805. is_primary = forms.BooleanField(
  806. help_text='Make this the primary IP for the assigned device',
  807. required=False
  808. )
  809. class Meta:
  810. model = IPAddress
  811. fields = [
  812. 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
  813. 'dns_name', 'description',
  814. ]
  815. def __init__(self, data=None, *args, **kwargs):
  816. super().__init__(data, *args, **kwargs)
  817. if data:
  818. # Limit interface queryset by assigned device
  819. if data.get('device'):
  820. self.fields['interface'].queryset = Interface.objects.filter(
  821. **{f"device__{self.fields['device'].to_field_name}": data['device']}
  822. )
  823. # Limit interface queryset by assigned device
  824. elif data.get('virtual_machine'):
  825. self.fields['interface'].queryset = VMInterface.objects.filter(
  826. **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
  827. )
  828. def clean(self):
  829. super().clean()
  830. device = self.cleaned_data.get('device')
  831. virtual_machine = self.cleaned_data.get('virtual_machine')
  832. is_primary = self.cleaned_data.get('is_primary')
  833. # Validate is_primary
  834. if is_primary and not device and not virtual_machine:
  835. raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP")
  836. def save(self, *args, **kwargs):
  837. # Set interface assignment
  838. if self.cleaned_data['interface']:
  839. self.instance.assigned_object = self.cleaned_data['interface']
  840. ipaddress = super().save(*args, **kwargs)
  841. # Set as primary for device/VM
  842. if self.cleaned_data['is_primary']:
  843. parent = self.cleaned_data['device'] or self.cleaned_data['virtual_machine']
  844. if self.instance.address.version == 4:
  845. parent.primary_ip4 = ipaddress
  846. elif self.instance.address.version == 6:
  847. parent.primary_ip6 = ipaddress
  848. parent.save()
  849. return ipaddress
  850. class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  851. pk = forms.ModelMultipleChoiceField(
  852. queryset=IPAddress.objects.all(),
  853. widget=forms.MultipleHiddenInput()
  854. )
  855. vrf = DynamicModelChoiceField(
  856. queryset=VRF.objects.all(),
  857. required=False,
  858. label='VRF',
  859. display_field='display_name'
  860. )
  861. mask_length = forms.IntegerField(
  862. min_value=IPADDRESS_MASK_LENGTH_MIN,
  863. max_value=IPADDRESS_MASK_LENGTH_MAX,
  864. required=False
  865. )
  866. tenant = DynamicModelChoiceField(
  867. queryset=Tenant.objects.all(),
  868. required=False
  869. )
  870. status = forms.ChoiceField(
  871. choices=add_blank_choice(IPAddressStatusChoices),
  872. required=False,
  873. widget=StaticSelect2()
  874. )
  875. role = forms.ChoiceField(
  876. choices=add_blank_choice(IPAddressRoleChoices),
  877. required=False,
  878. widget=StaticSelect2()
  879. )
  880. dns_name = forms.CharField(
  881. max_length=255,
  882. required=False
  883. )
  884. description = forms.CharField(
  885. max_length=100,
  886. required=False
  887. )
  888. class Meta:
  889. nullable_fields = [
  890. 'vrf', 'role', 'tenant', 'dns_name', 'description',
  891. ]
  892. class IPAddressAssignForm(BootstrapMixin, forms.Form):
  893. vrf_id = DynamicModelChoiceField(
  894. queryset=VRF.objects.all(),
  895. required=False,
  896. label='VRF',
  897. empty_label='Global'
  898. )
  899. q = forms.CharField(
  900. required=False,
  901. label='Search',
  902. )
  903. class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  904. model = IPAddress
  905. field_order = [
  906. 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'role',
  907. 'assigned_to_interface', 'tenant_group', 'tenant',
  908. ]
  909. q = forms.CharField(
  910. required=False,
  911. label='Search'
  912. )
  913. parent = forms.CharField(
  914. required=False,
  915. widget=forms.TextInput(
  916. attrs={
  917. 'placeholder': 'Prefix',
  918. }
  919. ),
  920. label='Parent Prefix'
  921. )
  922. family = forms.ChoiceField(
  923. required=False,
  924. choices=add_blank_choice(IPAddressFamilyChoices),
  925. label='Address family',
  926. widget=StaticSelect2()
  927. )
  928. mask_length = forms.ChoiceField(
  929. required=False,
  930. choices=IPADDRESS_MASK_LENGTH_CHOICES,
  931. label='Mask length',
  932. widget=StaticSelect2()
  933. )
  934. vrf_id = DynamicModelMultipleChoiceField(
  935. queryset=VRF.objects.all(),
  936. required=False,
  937. label='Assigned VRF',
  938. null_option='Global'
  939. )
  940. present_in_vrf_id = DynamicModelChoiceField(
  941. queryset=VRF.objects.all(),
  942. required=False,
  943. label='Present in VRF'
  944. )
  945. status = forms.MultipleChoiceField(
  946. choices=IPAddressStatusChoices,
  947. required=False,
  948. widget=StaticSelect2Multiple()
  949. )
  950. role = forms.MultipleChoiceField(
  951. choices=IPAddressRoleChoices,
  952. required=False,
  953. widget=StaticSelect2Multiple()
  954. )
  955. assigned_to_interface = forms.NullBooleanField(
  956. required=False,
  957. label='Assigned to an interface',
  958. widget=StaticSelect2(
  959. choices=BOOLEAN_WITH_BLANK_CHOICES
  960. )
  961. )
  962. tag = TagFilterField(model)
  963. #
  964. # VLAN groups
  965. #
  966. class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
  967. region = DynamicModelChoiceField(
  968. queryset=Region.objects.all(),
  969. required=False,
  970. initial_params={
  971. 'sites': '$site'
  972. }
  973. )
  974. site = DynamicModelChoiceField(
  975. queryset=Site.objects.all(),
  976. required=False,
  977. query_params={
  978. 'region_id': '$region'
  979. }
  980. )
  981. slug = SlugField()
  982. class Meta:
  983. model = VLANGroup
  984. fields = [
  985. 'region', 'site', 'name', 'slug', 'description',
  986. ]
  987. class VLANGroupCSVForm(CustomFieldModelCSVForm):
  988. site = CSVModelChoiceField(
  989. queryset=Site.objects.all(),
  990. required=False,
  991. to_field_name='name',
  992. help_text='Assigned site'
  993. )
  994. slug = SlugField()
  995. class Meta:
  996. model = VLANGroup
  997. fields = VLANGroup.csv_headers
  998. class VLANGroupFilterForm(BootstrapMixin, forms.Form):
  999. region = DynamicModelMultipleChoiceField(
  1000. queryset=Region.objects.all(),
  1001. to_field_name='slug',
  1002. required=False
  1003. )
  1004. site = DynamicModelMultipleChoiceField(
  1005. queryset=Site.objects.all(),
  1006. to_field_name='slug',
  1007. required=False,
  1008. null_option='None',
  1009. query_params={
  1010. 'region': '$region'
  1011. }
  1012. )
  1013. #
  1014. # VLANs
  1015. #
  1016. class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  1017. region = DynamicModelChoiceField(
  1018. queryset=Region.objects.all(),
  1019. required=False,
  1020. initial_params={
  1021. 'sites': '$site'
  1022. }
  1023. )
  1024. site = DynamicModelChoiceField(
  1025. queryset=Site.objects.all(),
  1026. required=False,
  1027. null_option='None',
  1028. query_params={
  1029. 'region_id': '$region'
  1030. }
  1031. )
  1032. group = DynamicModelChoiceField(
  1033. queryset=VLANGroup.objects.all(),
  1034. required=False,
  1035. query_params={
  1036. 'site_id': '$site'
  1037. }
  1038. )
  1039. role = DynamicModelChoiceField(
  1040. queryset=Role.objects.all(),
  1041. required=False
  1042. )
  1043. tags = DynamicModelMultipleChoiceField(
  1044. queryset=Tag.objects.all(),
  1045. required=False
  1046. )
  1047. class Meta:
  1048. model = VLAN
  1049. fields = [
  1050. 'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags',
  1051. ]
  1052. fieldsets = (
  1053. ('VLAN', ('vid', 'name', 'status', 'role', 'description', 'tags')),
  1054. ('Assignment', ('region', 'site', 'group')),
  1055. ('Tenancy', ('tenant_group', 'tenant')),
  1056. )
  1057. help_texts = {
  1058. 'site': "Leave blank if this VLAN spans multiple sites",
  1059. 'group': "VLAN group (optional)",
  1060. 'vid': "Configured VLAN ID",
  1061. 'name': "Configured VLAN name",
  1062. 'status': "Operational status of this VLAN",
  1063. 'role': "The primary function of this VLAN",
  1064. }
  1065. widgets = {
  1066. 'status': StaticSelect2(),
  1067. }
  1068. class VLANCSVForm(CustomFieldModelCSVForm):
  1069. site = CSVModelChoiceField(
  1070. queryset=Site.objects.all(),
  1071. required=False,
  1072. to_field_name='name',
  1073. help_text='Assigned site'
  1074. )
  1075. group = CSVModelChoiceField(
  1076. queryset=VLANGroup.objects.all(),
  1077. required=False,
  1078. to_field_name='name',
  1079. help_text='Assigned VLAN group'
  1080. )
  1081. tenant = CSVModelChoiceField(
  1082. queryset=Tenant.objects.all(),
  1083. to_field_name='name',
  1084. required=False,
  1085. help_text='Assigned tenant'
  1086. )
  1087. status = CSVChoiceField(
  1088. choices=VLANStatusChoices,
  1089. help_text='Operational status'
  1090. )
  1091. role = CSVModelChoiceField(
  1092. queryset=Role.objects.all(),
  1093. required=False,
  1094. to_field_name='name',
  1095. help_text='Functional role'
  1096. )
  1097. class Meta:
  1098. model = VLAN
  1099. fields = VLAN.csv_headers
  1100. help_texts = {
  1101. 'vid': 'Numeric VLAN ID (1-4095)',
  1102. 'name': 'VLAN name',
  1103. }
  1104. def __init__(self, data=None, *args, **kwargs):
  1105. super().__init__(data, *args, **kwargs)
  1106. if data:
  1107. # Limit vlan queryset by assigned group
  1108. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  1109. self.fields['group'].queryset = self.fields['group'].queryset.filter(**params)
  1110. class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  1111. pk = forms.ModelMultipleChoiceField(
  1112. queryset=VLAN.objects.all(),
  1113. widget=forms.MultipleHiddenInput()
  1114. )
  1115. region = DynamicModelChoiceField(
  1116. queryset=Region.objects.all(),
  1117. required=False,
  1118. to_field_name='slug'
  1119. )
  1120. site = DynamicModelChoiceField(
  1121. queryset=Site.objects.all(),
  1122. required=False,
  1123. query_params={
  1124. 'region': '$region'
  1125. }
  1126. )
  1127. group = DynamicModelChoiceField(
  1128. queryset=VLANGroup.objects.all(),
  1129. required=False,
  1130. query_params={
  1131. 'site_id': '$site'
  1132. }
  1133. )
  1134. tenant = DynamicModelChoiceField(
  1135. queryset=Tenant.objects.all(),
  1136. required=False
  1137. )
  1138. status = forms.ChoiceField(
  1139. choices=add_blank_choice(VLANStatusChoices),
  1140. required=False,
  1141. widget=StaticSelect2()
  1142. )
  1143. role = DynamicModelChoiceField(
  1144. queryset=Role.objects.all(),
  1145. required=False
  1146. )
  1147. description = forms.CharField(
  1148. max_length=100,
  1149. required=False
  1150. )
  1151. class Meta:
  1152. nullable_fields = [
  1153. 'site', 'group', 'tenant', 'role', 'description',
  1154. ]
  1155. class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  1156. model = VLAN
  1157. field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
  1158. q = forms.CharField(
  1159. required=False,
  1160. label='Search'
  1161. )
  1162. region = DynamicModelMultipleChoiceField(
  1163. queryset=Region.objects.all(),
  1164. to_field_name='slug',
  1165. required=False
  1166. )
  1167. site = DynamicModelMultipleChoiceField(
  1168. queryset=Site.objects.all(),
  1169. to_field_name='slug',
  1170. required=False,
  1171. null_option='None',
  1172. query_params={
  1173. 'region': '$region'
  1174. }
  1175. )
  1176. group_id = DynamicModelMultipleChoiceField(
  1177. queryset=VLANGroup.objects.all(),
  1178. required=False,
  1179. label='VLAN group',
  1180. null_option='None',
  1181. query_params={
  1182. 'region': '$region'
  1183. }
  1184. )
  1185. status = forms.MultipleChoiceField(
  1186. choices=VLANStatusChoices,
  1187. required=False,
  1188. widget=StaticSelect2Multiple()
  1189. )
  1190. role = DynamicModelMultipleChoiceField(
  1191. queryset=Role.objects.all(),
  1192. to_field_name='slug',
  1193. required=False,
  1194. null_option='None'
  1195. )
  1196. tag = TagFilterField(model)
  1197. #
  1198. # Services
  1199. #
  1200. class ServiceForm(BootstrapMixin, CustomFieldModelForm):
  1201. ports = NumericArrayField(
  1202. base_field=forms.IntegerField(
  1203. min_value=SERVICE_PORT_MIN,
  1204. max_value=SERVICE_PORT_MAX
  1205. ),
  1206. help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
  1207. )
  1208. tags = DynamicModelMultipleChoiceField(
  1209. queryset=Tag.objects.all(),
  1210. required=False
  1211. )
  1212. class Meta:
  1213. model = Service
  1214. fields = [
  1215. 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'tags',
  1216. ]
  1217. help_texts = {
  1218. 'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
  1219. "reachable via all IPs assigned to the device.",
  1220. }
  1221. widgets = {
  1222. 'protocol': StaticSelect2(),
  1223. 'ipaddresses': StaticSelect2Multiple(),
  1224. }
  1225. def __init__(self, *args, **kwargs):
  1226. super().__init__(*args, **kwargs)
  1227. # Limit IP address choices to those assigned to interfaces of the parent device/VM
  1228. if self.instance.device:
  1229. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  1230. interface__in=self.instance.device.vc_interfaces.values_list('id', flat=True)
  1231. )
  1232. elif self.instance.virtual_machine:
  1233. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  1234. vminterface__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True)
  1235. )
  1236. else:
  1237. self.fields['ipaddresses'].choices = []
  1238. class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
  1239. model = Service
  1240. q = forms.CharField(
  1241. required=False,
  1242. label='Search'
  1243. )
  1244. protocol = forms.ChoiceField(
  1245. choices=add_blank_choice(ServiceProtocolChoices),
  1246. required=False,
  1247. widget=StaticSelect2Multiple()
  1248. )
  1249. port = forms.IntegerField(
  1250. required=False,
  1251. )
  1252. tag = TagFilterField(model)
  1253. class ServiceCSVForm(CustomFieldModelCSVForm):
  1254. device = CSVModelChoiceField(
  1255. queryset=Device.objects.all(),
  1256. required=False,
  1257. to_field_name='name',
  1258. help_text='Required if not assigned to a VM'
  1259. )
  1260. virtual_machine = CSVModelChoiceField(
  1261. queryset=VirtualMachine.objects.all(),
  1262. required=False,
  1263. to_field_name='name',
  1264. help_text='Required if not assigned to a device'
  1265. )
  1266. protocol = CSVChoiceField(
  1267. choices=ServiceProtocolChoices,
  1268. help_text='IP protocol'
  1269. )
  1270. class Meta:
  1271. model = Service
  1272. fields = Service.csv_headers
  1273. class ServiceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  1274. pk = forms.ModelMultipleChoiceField(
  1275. queryset=Service.objects.all(),
  1276. widget=forms.MultipleHiddenInput()
  1277. )
  1278. protocol = forms.ChoiceField(
  1279. choices=add_blank_choice(ServiceProtocolChoices),
  1280. required=False,
  1281. widget=StaticSelect2()
  1282. )
  1283. ports = NumericArrayField(
  1284. base_field=forms.IntegerField(
  1285. min_value=SERVICE_PORT_MIN,
  1286. max_value=SERVICE_PORT_MAX
  1287. ),
  1288. required=False
  1289. )
  1290. description = forms.CharField(
  1291. max_length=100,
  1292. required=False
  1293. )
  1294. class Meta:
  1295. nullable_fields = [
  1296. 'description',
  1297. ]