forms.py 34 KB

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