forms.py 36 KB

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