forms.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299
  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. q = forms.CharField(
  379. required=False,
  380. label='Search'
  381. )
  382. within_include = forms.CharField(
  383. required=False,
  384. widget=forms.TextInput(
  385. attrs={
  386. 'placeholder': 'Prefix',
  387. }
  388. ),
  389. label='Search within'
  390. )
  391. family = forms.ChoiceField(
  392. required=False,
  393. choices=add_blank_choice(IPAddressFamilyChoices),
  394. label='Address family',
  395. widget=StaticSelect2()
  396. )
  397. mask_length = forms.ChoiceField(
  398. required=False,
  399. choices=PREFIX_MASK_LENGTH_CHOICES,
  400. label='Mask length',
  401. widget=StaticSelect2()
  402. )
  403. vrf_id = DynamicModelMultipleChoiceField(
  404. queryset=VRF.objects.all(),
  405. required=False,
  406. label='VRF',
  407. widget=APISelectMultiple(
  408. null_option=True,
  409. )
  410. )
  411. status = forms.MultipleChoiceField(
  412. choices=PrefixStatusChoices,
  413. required=False,
  414. widget=StaticSelect2Multiple()
  415. )
  416. region = DynamicModelMultipleChoiceField(
  417. queryset=Region.objects.all(),
  418. to_field_name='slug',
  419. required=False,
  420. widget=APISelectMultiple(
  421. value_field="slug",
  422. filter_for={
  423. 'site': 'region'
  424. }
  425. )
  426. )
  427. site = DynamicModelMultipleChoiceField(
  428. queryset=Site.objects.all(),
  429. to_field_name='slug',
  430. required=False,
  431. widget=APISelectMultiple(
  432. value_field="slug",
  433. null_option=True,
  434. )
  435. )
  436. role = DynamicModelMultipleChoiceField(
  437. queryset=Role.objects.all(),
  438. to_field_name='slug',
  439. required=False,
  440. widget=APISelectMultiple(
  441. value_field="slug",
  442. null_option=True,
  443. )
  444. )
  445. is_pool = forms.NullBooleanField(
  446. required=False,
  447. label='Is a pool',
  448. widget=StaticSelect2(
  449. choices=BOOLEAN_WITH_BLANK_CHOICES
  450. )
  451. )
  452. tag = TagFilterField(model)
  453. #
  454. # IP addresses
  455. #
  456. class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
  457. device = DynamicModelChoiceField(
  458. queryset=Device.objects.all(),
  459. required=False,
  460. widget=APISelect(
  461. filter_for={
  462. 'interface': 'device_id'
  463. }
  464. )
  465. )
  466. interface = DynamicModelChoiceField(
  467. queryset=Interface.objects.all(),
  468. required=False
  469. )
  470. virtual_machine = DynamicModelChoiceField(
  471. queryset=VirtualMachine.objects.all(),
  472. required=False,
  473. widget=APISelect(
  474. filter_for={
  475. 'vminterface': 'virtual_machine_id'
  476. }
  477. )
  478. )
  479. vminterface = DynamicModelChoiceField(
  480. queryset=VMInterface.objects.all(),
  481. required=False,
  482. label='Interface'
  483. )
  484. vrf = DynamicModelChoiceField(
  485. queryset=VRF.objects.all(),
  486. required=False,
  487. label='VRF'
  488. )
  489. nat_site = DynamicModelChoiceField(
  490. queryset=Site.objects.all(),
  491. required=False,
  492. label='Site',
  493. widget=APISelect(
  494. filter_for={
  495. 'nat_rack': 'site_id',
  496. 'nat_device': 'site_id'
  497. }
  498. )
  499. )
  500. nat_rack = DynamicModelChoiceField(
  501. queryset=Rack.objects.all(),
  502. required=False,
  503. label='Rack',
  504. widget=APISelect(
  505. display_field='display_name',
  506. filter_for={
  507. 'nat_device': 'rack_id'
  508. },
  509. attrs={
  510. 'nullable': 'true'
  511. }
  512. )
  513. )
  514. nat_device = DynamicModelChoiceField(
  515. queryset=Device.objects.all(),
  516. required=False,
  517. label='Device',
  518. widget=APISelect(
  519. display_field='display_name',
  520. filter_for={
  521. 'nat_inside': 'device_id'
  522. }
  523. )
  524. )
  525. nat_vrf = DynamicModelChoiceField(
  526. queryset=VRF.objects.all(),
  527. required=False,
  528. label='VRF',
  529. widget=APISelect(
  530. filter_for={
  531. 'nat_inside': 'vrf_id'
  532. }
  533. )
  534. )
  535. nat_inside = DynamicModelChoiceField(
  536. queryset=IPAddress.objects.all(),
  537. required=False,
  538. label='IP Address',
  539. widget=APISelect(
  540. display_field='address'
  541. )
  542. )
  543. primary_for_parent = forms.BooleanField(
  544. required=False,
  545. label='Make this the primary IP for the device/VM'
  546. )
  547. tags = DynamicModelMultipleChoiceField(
  548. queryset=Tag.objects.all(),
  549. required=False
  550. )
  551. class Meta:
  552. model = IPAddress
  553. fields = [
  554. 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack',
  555. 'nat_inside', 'tenant_group', 'tenant', 'tags',
  556. ]
  557. widgets = {
  558. 'status': StaticSelect2(),
  559. 'role': StaticSelect2(),
  560. }
  561. def __init__(self, *args, **kwargs):
  562. # Initialize helper selectors
  563. instance = kwargs.get('instance')
  564. initial = kwargs.get('initial', {}).copy()
  565. if instance:
  566. if type(instance.assigned_object) is Interface:
  567. initial['device'] = instance.assigned_object.device
  568. initial['interface'] = instance.assigned_object
  569. elif type(instance.assigned_object) is VMInterface:
  570. initial['virtual_machine'] = instance.assigned_object.virtual_machine
  571. initial['vminterface'] = instance.assigned_object
  572. if instance.nat_inside and instance.nat_inside.device is not None:
  573. initial['nat_site'] = instance.nat_inside.device.site
  574. initial['nat_rack'] = instance.nat_inside.device.rack
  575. initial['nat_device'] = instance.nat_inside.device
  576. kwargs['initial'] = initial
  577. super().__init__(*args, **kwargs)
  578. self.fields['vrf'].empty_label = 'Global'
  579. # Initialize primary_for_parent if IP address is already assigned
  580. if self.instance.pk and self.instance.assigned_object:
  581. parent = self.instance.assigned_object.parent
  582. if (
  583. self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
  584. self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
  585. ):
  586. self.initial['primary_for_parent'] = True
  587. def clean(self):
  588. super().clean()
  589. # Cannot select both a device interface and a VM interface
  590. if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'):
  591. raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface")
  592. # Primary IP assignment is only available if an interface has been assigned.
  593. interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
  594. if self.cleaned_data.get('primary_for_parent') and not interface:
  595. self.add_error(
  596. 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
  597. )
  598. def save(self, *args, **kwargs):
  599. # Set assigned object
  600. interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
  601. if interface:
  602. self.instance.assigned_object = interface
  603. ipaddress = super().save(*args, **kwargs)
  604. # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
  605. if interface and self.cleaned_data['primary_for_parent']:
  606. if ipaddress.address.version == 4:
  607. interface.parent.primary_ip4 = ipaddress
  608. else:
  609. interface.primary_ip6 = ipaddress
  610. interface.parent.save()
  611. elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress:
  612. interface.parent.primary_ip4 = None
  613. interface.parent.save()
  614. elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress:
  615. interface.parent.primary_ip4 = None
  616. interface.parent.save()
  617. return ipaddress
  618. class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
  619. pattern = ExpandableIPAddressField(
  620. label='Address pattern'
  621. )
  622. class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  623. vrf = DynamicModelChoiceField(
  624. queryset=VRF.objects.all(),
  625. required=False,
  626. label='VRF'
  627. )
  628. tags = DynamicModelMultipleChoiceField(
  629. queryset=Tag.objects.all(),
  630. required=False
  631. )
  632. class Meta:
  633. model = IPAddress
  634. fields = [
  635. 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
  636. ]
  637. widgets = {
  638. 'status': StaticSelect2(),
  639. 'role': StaticSelect2(),
  640. }
  641. def __init__(self, *args, **kwargs):
  642. super().__init__(*args, **kwargs)
  643. self.fields['vrf'].empty_label = 'Global'
  644. class IPAddressCSVForm(CustomFieldModelCSVForm):
  645. vrf = CSVModelChoiceField(
  646. queryset=VRF.objects.all(),
  647. to_field_name='name',
  648. required=False,
  649. help_text='Assigned VRF'
  650. )
  651. tenant = CSVModelChoiceField(
  652. queryset=Tenant.objects.all(),
  653. to_field_name='name',
  654. required=False,
  655. help_text='Assigned tenant'
  656. )
  657. status = CSVChoiceField(
  658. choices=IPAddressStatusChoices,
  659. help_text='Operational status'
  660. )
  661. role = CSVChoiceField(
  662. choices=IPAddressRoleChoices,
  663. required=False,
  664. help_text='Functional role'
  665. )
  666. device = CSVModelChoiceField(
  667. queryset=Device.objects.all(),
  668. required=False,
  669. to_field_name='name',
  670. help_text='Parent device of assigned interface (if any)'
  671. )
  672. virtual_machine = CSVModelChoiceField(
  673. queryset=VirtualMachine.objects.all(),
  674. required=False,
  675. to_field_name='name',
  676. help_text='Parent VM of assigned interface (if any)'
  677. )
  678. interface = CSVModelChoiceField(
  679. queryset=Interface.objects.none(), # Can also refer to VMInterface
  680. required=False,
  681. to_field_name='name',
  682. help_text='Assigned interface'
  683. )
  684. is_primary = forms.BooleanField(
  685. help_text='Make this the primary IP for the assigned device',
  686. required=False
  687. )
  688. class Meta:
  689. model = IPAddress
  690. fields = IPAddress.csv_headers
  691. def __init__(self, data=None, *args, **kwargs):
  692. super().__init__(data, *args, **kwargs)
  693. if data:
  694. # Limit interface queryset by assigned device
  695. if data.get('device'):
  696. self.fields['interface'].queryset = Interface.objects.filter(
  697. **{f"device__{self.fields['device'].to_field_name}": data['device']}
  698. )
  699. # Limit interface queryset by assigned device
  700. elif data.get('virtual_machine'):
  701. self.fields['interface'].queryset = VMInterface.objects.filter(
  702. **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
  703. )
  704. def clean(self):
  705. super().clean()
  706. device = self.cleaned_data.get('device')
  707. virtual_machine = self.cleaned_data.get('virtual_machine')
  708. is_primary = self.cleaned_data.get('is_primary')
  709. # Validate is_primary
  710. if is_primary and not device and not virtual_machine:
  711. raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP")
  712. def save(self, *args, **kwargs):
  713. # Set interface assignment
  714. if self.cleaned_data['interface']:
  715. self.instance.assigned_object = self.cleaned_data['interface']
  716. ipaddress = super().save(*args, **kwargs)
  717. # Set as primary for device/VM
  718. if self.cleaned_data['is_primary']:
  719. parent = self.cleaned_data['device'] or self.cleaned_data['virtual_machine']
  720. if self.instance.address.version == 4:
  721. parent.primary_ip4 = ipaddress
  722. elif self.instance.address.version == 6:
  723. parent.primary_ip6 = ipaddress
  724. parent.save()
  725. return ipaddress
  726. class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  727. pk = forms.ModelMultipleChoiceField(
  728. queryset=IPAddress.objects.all(),
  729. widget=forms.MultipleHiddenInput()
  730. )
  731. vrf = DynamicModelChoiceField(
  732. queryset=VRF.objects.all(),
  733. required=False,
  734. label='VRF'
  735. )
  736. mask_length = forms.IntegerField(
  737. min_value=IPADDRESS_MASK_LENGTH_MIN,
  738. max_value=IPADDRESS_MASK_LENGTH_MAX,
  739. required=False
  740. )
  741. tenant = DynamicModelChoiceField(
  742. queryset=Tenant.objects.all(),
  743. required=False
  744. )
  745. status = forms.ChoiceField(
  746. choices=add_blank_choice(IPAddressStatusChoices),
  747. required=False,
  748. widget=StaticSelect2()
  749. )
  750. role = forms.ChoiceField(
  751. choices=add_blank_choice(IPAddressRoleChoices),
  752. required=False,
  753. widget=StaticSelect2()
  754. )
  755. dns_name = forms.CharField(
  756. max_length=255,
  757. required=False
  758. )
  759. description = forms.CharField(
  760. max_length=100,
  761. required=False
  762. )
  763. class Meta:
  764. nullable_fields = [
  765. 'vrf', 'role', 'tenant', 'dns_name', 'description',
  766. ]
  767. class IPAddressAssignForm(BootstrapMixin, forms.Form):
  768. vrf_id = DynamicModelChoiceField(
  769. queryset=VRF.objects.all(),
  770. required=False,
  771. label='VRF',
  772. empty_label='Global'
  773. )
  774. q = forms.CharField(
  775. required=False,
  776. label='Search',
  777. )
  778. class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  779. model = IPAddress
  780. field_order = [
  781. 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'assigned_to_interface', 'tenant_group',
  782. 'tenant',
  783. ]
  784. q = forms.CharField(
  785. required=False,
  786. label='Search'
  787. )
  788. parent = forms.CharField(
  789. required=False,
  790. widget=forms.TextInput(
  791. attrs={
  792. 'placeholder': 'Prefix',
  793. }
  794. ),
  795. label='Parent Prefix'
  796. )
  797. family = forms.ChoiceField(
  798. required=False,
  799. choices=add_blank_choice(IPAddressFamilyChoices),
  800. label='Address family',
  801. widget=StaticSelect2()
  802. )
  803. mask_length = forms.ChoiceField(
  804. required=False,
  805. choices=IPADDRESS_MASK_LENGTH_CHOICES,
  806. label='Mask length',
  807. widget=StaticSelect2()
  808. )
  809. vrf_id = DynamicModelMultipleChoiceField(
  810. queryset=VRF.objects.all(),
  811. required=False,
  812. label='VRF',
  813. widget=APISelectMultiple(
  814. null_option=True,
  815. )
  816. )
  817. status = forms.MultipleChoiceField(
  818. choices=IPAddressStatusChoices,
  819. required=False,
  820. widget=StaticSelect2Multiple()
  821. )
  822. role = forms.MultipleChoiceField(
  823. choices=IPAddressRoleChoices,
  824. required=False,
  825. widget=StaticSelect2Multiple()
  826. )
  827. assigned_to_interface = forms.NullBooleanField(
  828. required=False,
  829. label='Assigned to an interface',
  830. widget=StaticSelect2(
  831. choices=BOOLEAN_WITH_BLANK_CHOICES
  832. )
  833. )
  834. tag = TagFilterField(model)
  835. #
  836. # VLAN groups
  837. #
  838. class VLANGroupForm(BootstrapMixin, forms.ModelForm):
  839. site = DynamicModelChoiceField(
  840. queryset=Site.objects.all(),
  841. required=False
  842. )
  843. slug = SlugField()
  844. class Meta:
  845. model = VLANGroup
  846. fields = [
  847. 'site', 'name', 'slug', 'description',
  848. ]
  849. class VLANGroupCSVForm(CSVModelForm):
  850. site = CSVModelChoiceField(
  851. queryset=Site.objects.all(),
  852. required=False,
  853. to_field_name='name',
  854. help_text='Assigned site'
  855. )
  856. slug = SlugField()
  857. class Meta:
  858. model = VLANGroup
  859. fields = VLANGroup.csv_headers
  860. class VLANGroupFilterForm(BootstrapMixin, forms.Form):
  861. region = DynamicModelMultipleChoiceField(
  862. queryset=Region.objects.all(),
  863. to_field_name='slug',
  864. required=False,
  865. widget=APISelectMultiple(
  866. value_field="slug",
  867. filter_for={
  868. 'site': 'region',
  869. }
  870. )
  871. )
  872. site = DynamicModelMultipleChoiceField(
  873. queryset=Site.objects.all(),
  874. to_field_name='slug',
  875. required=False,
  876. widget=APISelectMultiple(
  877. value_field="slug",
  878. null_option=True,
  879. )
  880. )
  881. #
  882. # VLANs
  883. #
  884. class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  885. site = DynamicModelChoiceField(
  886. queryset=Site.objects.all(),
  887. required=False,
  888. widget=APISelect(
  889. filter_for={
  890. 'group': 'site_id'
  891. },
  892. attrs={
  893. 'nullable': 'true',
  894. }
  895. )
  896. )
  897. group = DynamicModelChoiceField(
  898. queryset=VLANGroup.objects.all(),
  899. required=False
  900. )
  901. role = DynamicModelChoiceField(
  902. queryset=Role.objects.all(),
  903. required=False
  904. )
  905. tags = DynamicModelMultipleChoiceField(
  906. queryset=Tag.objects.all(),
  907. required=False
  908. )
  909. class Meta:
  910. model = VLAN
  911. fields = [
  912. 'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags',
  913. ]
  914. help_texts = {
  915. 'site': "Leave blank if this VLAN spans multiple sites",
  916. 'group': "VLAN group (optional)",
  917. 'vid': "Configured VLAN ID",
  918. 'name': "Configured VLAN name",
  919. 'status': "Operational status of this VLAN",
  920. 'role': "The primary function of this VLAN",
  921. }
  922. widgets = {
  923. 'status': StaticSelect2(),
  924. }
  925. class VLANCSVForm(CustomFieldModelCSVForm):
  926. site = CSVModelChoiceField(
  927. queryset=Site.objects.all(),
  928. required=False,
  929. to_field_name='name',
  930. help_text='Assigned site'
  931. )
  932. group = CSVModelChoiceField(
  933. queryset=VLANGroup.objects.all(),
  934. required=False,
  935. to_field_name='name',
  936. help_text='Assigned VLAN group'
  937. )
  938. tenant = CSVModelChoiceField(
  939. queryset=Tenant.objects.all(),
  940. to_field_name='name',
  941. required=False,
  942. help_text='Assigned tenant'
  943. )
  944. status = CSVChoiceField(
  945. choices=VLANStatusChoices,
  946. help_text='Operational status'
  947. )
  948. role = CSVModelChoiceField(
  949. queryset=Role.objects.all(),
  950. required=False,
  951. to_field_name='name',
  952. help_text='Functional role'
  953. )
  954. class Meta:
  955. model = VLAN
  956. fields = VLAN.csv_headers
  957. help_texts = {
  958. 'vid': 'Numeric VLAN ID (1-4095)',
  959. 'name': 'VLAN name',
  960. }
  961. def __init__(self, data=None, *args, **kwargs):
  962. super().__init__(data, *args, **kwargs)
  963. if data:
  964. # Limit vlan queryset by assigned group
  965. params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
  966. self.fields['group'].queryset = self.fields['group'].queryset.filter(**params)
  967. class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  968. pk = forms.ModelMultipleChoiceField(
  969. queryset=VLAN.objects.all(),
  970. widget=forms.MultipleHiddenInput()
  971. )
  972. site = DynamicModelChoiceField(
  973. queryset=Site.objects.all(),
  974. required=False,
  975. widget=APISelect(
  976. filter_for={
  977. 'group': 'site_id'
  978. }
  979. )
  980. )
  981. group = DynamicModelChoiceField(
  982. queryset=VLANGroup.objects.all(),
  983. required=False
  984. )
  985. tenant = DynamicModelChoiceField(
  986. queryset=Tenant.objects.all(),
  987. required=False
  988. )
  989. status = forms.ChoiceField(
  990. choices=add_blank_choice(VLANStatusChoices),
  991. required=False,
  992. widget=StaticSelect2()
  993. )
  994. role = DynamicModelChoiceField(
  995. queryset=Role.objects.all(),
  996. required=False
  997. )
  998. description = forms.CharField(
  999. max_length=100,
  1000. required=False
  1001. )
  1002. class Meta:
  1003. nullable_fields = [
  1004. 'site', 'group', 'tenant', 'role', 'description',
  1005. ]
  1006. class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  1007. model = VLAN
  1008. field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
  1009. q = forms.CharField(
  1010. required=False,
  1011. label='Search'
  1012. )
  1013. region = DynamicModelMultipleChoiceField(
  1014. queryset=Region.objects.all(),
  1015. to_field_name='slug',
  1016. required=False,
  1017. widget=APISelectMultiple(
  1018. value_field="slug",
  1019. filter_for={
  1020. 'site': 'region',
  1021. 'group_id': 'region'
  1022. }
  1023. )
  1024. )
  1025. site = DynamicModelMultipleChoiceField(
  1026. queryset=Site.objects.all(),
  1027. to_field_name='slug',
  1028. required=False,
  1029. widget=APISelectMultiple(
  1030. value_field="slug",
  1031. null_option=True,
  1032. )
  1033. )
  1034. group_id = DynamicModelMultipleChoiceField(
  1035. queryset=VLANGroup.objects.all(),
  1036. required=False,
  1037. label='VLAN group',
  1038. widget=APISelectMultiple(
  1039. null_option=True,
  1040. )
  1041. )
  1042. status = forms.MultipleChoiceField(
  1043. choices=VLANStatusChoices,
  1044. required=False,
  1045. widget=StaticSelect2Multiple()
  1046. )
  1047. role = DynamicModelMultipleChoiceField(
  1048. queryset=Role.objects.all(),
  1049. to_field_name='slug',
  1050. required=False,
  1051. widget=APISelectMultiple(
  1052. value_field="slug",
  1053. null_option=True,
  1054. )
  1055. )
  1056. tag = TagFilterField(model)
  1057. #
  1058. # Services
  1059. #
  1060. class ServiceForm(BootstrapMixin, CustomFieldModelForm):
  1061. port = forms.IntegerField(
  1062. min_value=SERVICE_PORT_MIN,
  1063. max_value=SERVICE_PORT_MAX
  1064. )
  1065. tags = DynamicModelMultipleChoiceField(
  1066. queryset=Tag.objects.all(),
  1067. required=False
  1068. )
  1069. class Meta:
  1070. model = Service
  1071. fields = [
  1072. 'name', 'protocol', 'port', 'ipaddresses', 'description', 'tags',
  1073. ]
  1074. help_texts = {
  1075. 'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
  1076. "reachable via all IPs assigned to the device.",
  1077. }
  1078. widgets = {
  1079. 'protocol': StaticSelect2(),
  1080. 'ipaddresses': StaticSelect2Multiple(),
  1081. }
  1082. def __init__(self, *args, **kwargs):
  1083. super().__init__(*args, **kwargs)
  1084. # Limit IP address choices to those assigned to interfaces of the parent device/VM
  1085. if self.instance.device:
  1086. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  1087. interface__in=self.instance.device.vc_interfaces.values_list('id', flat=True)
  1088. )
  1089. elif self.instance.virtual_machine:
  1090. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
  1091. vminterface__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True)
  1092. )
  1093. else:
  1094. self.fields['ipaddresses'].choices = []
  1095. class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
  1096. model = Service
  1097. q = forms.CharField(
  1098. required=False,
  1099. label='Search'
  1100. )
  1101. protocol = forms.ChoiceField(
  1102. choices=add_blank_choice(ServiceProtocolChoices),
  1103. required=False,
  1104. widget=StaticSelect2Multiple()
  1105. )
  1106. port = forms.IntegerField(
  1107. required=False,
  1108. )
  1109. tag = TagFilterField(model)
  1110. class ServiceCSVForm(CustomFieldModelCSVForm):
  1111. device = CSVModelChoiceField(
  1112. queryset=Device.objects.all(),
  1113. required=False,
  1114. to_field_name='name',
  1115. help_text='Required if not assigned to a VM'
  1116. )
  1117. virtual_machine = CSVModelChoiceField(
  1118. queryset=VirtualMachine.objects.all(),
  1119. required=False,
  1120. to_field_name='name',
  1121. help_text='Required if not assigned to a device'
  1122. )
  1123. protocol = CSVChoiceField(
  1124. choices=ServiceProtocolChoices,
  1125. help_text='IP protocol'
  1126. )
  1127. class Meta:
  1128. model = Service
  1129. fields = Service.csv_headers
  1130. class ServiceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  1131. pk = forms.ModelMultipleChoiceField(
  1132. queryset=Service.objects.all(),
  1133. widget=forms.MultipleHiddenInput()
  1134. )
  1135. protocol = forms.ChoiceField(
  1136. choices=add_blank_choice(ServiceProtocolChoices),
  1137. required=False,
  1138. widget=StaticSelect2()
  1139. )
  1140. port = forms.IntegerField(
  1141. validators=[
  1142. MinValueValidator(1),
  1143. MaxValueValidator(65535),
  1144. ],
  1145. required=False
  1146. )
  1147. description = forms.CharField(
  1148. max_length=100,
  1149. required=False
  1150. )
  1151. class Meta:
  1152. nullable_fields = [
  1153. 'description',
  1154. ]