Coverage for ipam/forms/model_forms.py: 47%

319 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from django import forms 

2from django.contrib.contenttypes.models import ContentType 

3from django.core.exceptions import ValidationError 

4from django.utils.safestring import mark_safe 

5from django.utils.translation import gettext_lazy as _ 

6 

7from dcim.forms.mixins import ScopedForm 

8from dcim.models import Device, Interface, Site, SiteGroup 

9from ipam.choices import * 

10from ipam.constants import * 

11from ipam.formfields import IPNetworkFormField 

12from ipam.forms.fields import PortMappingField 

13from ipam.models import * 

14from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm 

15from tenancy.forms import TenancyForm 

16from utilities.exceptions import PermissionsViolation 

17from utilities.forms import GenericObjectFormMixin, add_blank_choice 

18from utilities.forms.fields import ( 

19 ChoiceField, 

20 DynamicModelChoiceField, 

21 DynamicModelMultipleChoiceField, 

22 GenericObjectChoiceField, 

23 NumericRangeArrayField, 

24 TypedChoiceField, 

25) 

26from utilities.forms.rendering import FieldSet, ObjectAttribute, TabbedGroups 

27from utilities.forms.widgets import DatePicker 

28from virtualization.models import VirtualMachine, VMInterface 

29 

30__all__ = ( 

31 'ASNForm', 

32 'ASNRangeForm', 

33 'AggregateForm', 

34 'FHRPGroupAssignmentForm', 

35 'FHRPGroupForm', 

36 'IPAddressAssignForm', 

37 'IPAddressBulkAddForm', 

38 'IPAddressForm', 

39 'IPRangeForm', 

40 'PrefixBulkAddForm', 

41 'PrefixForm', 

42 'RIRForm', 

43 'RoleForm', 

44 'RouteTargetForm', 

45 'ServiceCreateForm', 

46 'ServiceForm', 

47 'ServiceTemplateForm', 

48 'VLANBulkAddForm', 

49 'VLANForm', 

50 'VLANGroupForm', 

51 'VLANTranslationPolicyForm', 

52 'VLANTranslationRuleForm', 

53 'VRFForm', 

54) 

55 

56 

57class VRFForm(TenancyForm, PrimaryModelForm): 

58 import_targets = DynamicModelMultipleChoiceField( 

59 label=_('Import targets'), 

60 queryset=RouteTarget.objects.all(), 

61 required=False 

62 ) 

63 export_targets = DynamicModelMultipleChoiceField( 

64 label=_('Export targets'), 

65 queryset=RouteTarget.objects.all(), 

66 required=False 

67 ) 

68 

69 fieldsets = ( 

70 FieldSet('name', 'rd', 'enforce_unique', 'description', 'tags', name=_('VRF')), 

71 FieldSet('import_targets', 'export_targets', name=_('Route Targets')), 

72 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

73 ) 

74 

75 class Meta: 

76 model = VRF 

77 fields = [ 

78 'name', 'rd', 'enforce_unique', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'description', 

79 'owner', 'comments', 'tags', 

80 ] 

81 labels = { 

82 'rd': "RD", 

83 } 

84 

85 

86class RouteTargetForm(TenancyForm, PrimaryModelForm): 

87 fieldsets = ( 

88 FieldSet('name', 'description', 'tags', name=_('Route Target')), 

89 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

90 ) 

91 

92 class Meta: 

93 model = RouteTarget 

94 fields = [ 

95 'name', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', 

96 ] 

97 

98 

99class RIRForm(OrganizationalModelForm): 

100 fieldsets = ( 

101 FieldSet('name', 'slug', 'is_private', 'description', 'tags', name=_('RIR')), 

102 ) 

103 

104 class Meta: 

105 model = RIR 

106 fields = [ 

107 'name', 'slug', 'is_private', 'description', 'owner', 'comments', 'tags', 

108 ] 

109 

110 

111class AggregateForm(TenancyForm, PrimaryModelForm): 

112 rir = DynamicModelChoiceField( 

113 queryset=RIR.objects.all(), 

114 label=_('RIR'), 

115 quick_add=True 

116 ) 

117 

118 fieldsets = ( 

119 FieldSet('prefix', 'rir', 'date_added', 'description', 'tags', name=_('Aggregate')), 

120 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

121 ) 

122 

123 class Meta: 

124 model = Aggregate 

125 fields = [ 

126 'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', 

127 ] 

128 widgets = { 

129 'date_added': DatePicker(), 

130 } 

131 

132 

133class ASNRangeForm(TenancyForm, OrganizationalModelForm): 

134 rir = DynamicModelChoiceField( 

135 queryset=RIR.objects.all(), 

136 label=_('RIR'), 

137 quick_add=True 

138 ) 

139 fieldsets = ( 

140 FieldSet('name', 'slug', 'rir', 'start', 'end', 'description', 'tags', name=_('ASN Range')), 

141 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

142 ) 

143 

144 class Meta: 

145 model = ASNRange 

146 fields = [ 

147 'name', 'slug', 'rir', 'start', 'end', 'tenant_group', 'tenant', 'owner', 'description', 'comments', 'tags' 

148 ] 

149 

150 

151class ASNForm(TenancyForm, PrimaryModelForm): 

152 rir = DynamicModelChoiceField( 

153 queryset=RIR.objects.all(), 

154 label=_('RIR'), 

155 quick_add=True 

156 ) 

157 role = DynamicModelChoiceField( 

158 queryset=Role.objects.all(), 

159 label=_('Role'), 

160 required=False, 

161 quick_add=True 

162 ) 

163 sites = DynamicModelMultipleChoiceField( 

164 queryset=Site.objects.all(), 

165 label=_('Sites'), 

166 required=False 

167 ) 

168 

169 fieldsets = ( 

170 FieldSet('asn', 'rir', 'role', 'sites', 'description', 'tags', name=_('ASN')), 

171 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

172 ) 

173 

174 class Meta: 

175 model = ASN 

176 fields = [ 

177 'asn', 'rir', 'role', 'sites', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags' 

178 ] 

179 widgets = { 

180 'date_added': DatePicker(), 

181 } 

182 

183 def __init__(self, data=None, instance=None, *args, **kwargs): 

184 super().__init__(data=data, instance=instance, *args, **kwargs) 

185 

186 if self.instance and self.instance.pk is not None: 

187 self.fields['sites'].initial = self.instance.sites.all().values_list('id', flat=True) 

188 

189 def save(self, *args, **kwargs): 

190 instance = super().save(*args, **kwargs) 

191 instance.sites.set(self.cleaned_data['sites']) 

192 return instance 

193 

194 

195class RoleForm(OrganizationalModelForm): 

196 fieldsets = ( 

197 FieldSet('name', 'slug', 'weight', 'description', 'tags', name=_('Role')), 

198 ) 

199 

200 class Meta: 

201 model = Role 

202 fields = [ 

203 'name', 'slug', 'weight', 'description', 'owner', 'comments', 'tags', 

204 ] 

205 

206 

207class PrefixForm(TenancyForm, ScopedForm, PrimaryModelForm): 

208 status = ChoiceField( 

209 label=_('Status'), 

210 choices=PrefixStatusChoices, 

211 initial=PrefixStatusChoices.STATUS_ACTIVE, 

212 help_text=_('Operational status of this prefix'), 

213 ) 

214 vrf = DynamicModelChoiceField( 

215 queryset=VRF.objects.all(), 

216 required=False, 

217 label=_('VRF') 

218 ) 

219 vlan = DynamicModelChoiceField( 

220 queryset=VLAN.objects.all(), 

221 required=False, 

222 selector=True, 

223 query_params={ 

224 'available_at_site': '$scope_object_id', 

225 }, 

226 label=_('VLAN'), 

227 ) 

228 role = DynamicModelChoiceField( 

229 label=_('Role'), 

230 queryset=Role.objects.all(), 

231 required=False, 

232 quick_add=True 

233 ) 

234 

235 fieldsets = ( 

236 FieldSet( 

237 'prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix') 

238 ), 

239 FieldSet('scope', name=_('Scope'), html_id='scope'), 

240 FieldSet('vlan', name=_('VLAN Assignment')), 

241 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

242 ) 

243 

244 class Meta: 

245 model = Prefix 

246 fields = [ 

247 'prefix', 'vrf', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'tenant_group', 

248 'tenant', 'description', 'owner', 'comments', 'tags', 

249 ] 

250 

251 def __init__(self, *args, **kwargs): 

252 super().__init__(*args, **kwargs) 

253 

254 # #18605: only filter the VLAN select list if the selected scope is a Site (or none is selected yet). 

255 # #22588: a Site Group scope filters VLANs by the group's member sites instead. 

256 if scope_field := self.fields.get('scope', None): 

257 selected_model = scope_field.selected_model 

258 if selected_model is SiteGroup: 

259 self.fields['vlan'].widget.dynamic_params.clear() 

260 self.fields['vlan'].widget.attrs.pop('data-dynamic-params', None) 

261 self.fields['vlan'].widget.add_query_params({ 

262 'available_at_site_group': '$scope_object_id', 

263 }) 

264 elif selected_model not in (None, Site): 

265 self.fields['vlan'].widget.attrs.pop('data-dynamic-params', None) 

266 

267 

268class PrefixBulkAddForm(PrefixForm): 

269 """ 

270 Subclass of PrefixForm for bulk creation. The prefix field is inherited 

271 but excluded from fieldsets — it is populated programmatically by BulkCreateView 

272 from the expanded pattern. 

273 """ 

274 

275 fieldsets = ( 

276 FieldSet( 

277 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix') 

278 ), 

279 FieldSet('scope', name=_('Scope'), html_id='scope'), 

280 FieldSet('vlan', name=_('VLAN Assignment')), 

281 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

282 ) 

283 

284 

285class IPRangeForm(TenancyForm, PrimaryModelForm): 

286 status = ChoiceField( 

287 label=_('Status'), 

288 choices=IPRangeStatusChoices, 

289 initial=IPRangeStatusChoices.STATUS_ACTIVE, 

290 help_text=_('Operational status of this range'), 

291 ) 

292 vrf = DynamicModelChoiceField( 

293 queryset=VRF.objects.all(), 

294 required=False, 

295 label=_('VRF') 

296 ) 

297 role = DynamicModelChoiceField( 

298 label=_('Role'), 

299 queryset=Role.objects.all(), 

300 required=False, 

301 quick_add=True 

302 ) 

303 

304 fieldsets = ( 

305 FieldSet( 

306 'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized', 'description', 

307 'tags', name=_('IP Range') 

308 ), 

309 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

310 ) 

311 

312 class Meta: 

313 model = IPRange 

314 fields = [ 

315 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated', 

316 'mark_utilized', 'description', 'owner', 'comments', 'tags', 

317 ] 

318 

319 

320class IPAddressForm(TenancyForm, PrimaryModelForm): 

321 status = ChoiceField( 

322 label=_('Status'), 

323 choices=IPAddressStatusChoices, 

324 initial=IPAddressStatusChoices.STATUS_ACTIVE, 

325 help_text=_('The operational status of this IP'), 

326 ) 

327 role = TypedChoiceField( 

328 label=_('Role'), 

329 choices=add_blank_choice(IPAddressRoleChoices), 

330 required=False, 

331 help_text=_('The functional role of this IP'), 

332 ) 

333 interface = DynamicModelChoiceField( 

334 queryset=Interface.objects.all(), 

335 required=False, 

336 context={ 

337 'parent': 'device', 

338 }, 

339 selector=True, 

340 label=_('Interface'), 

341 ) 

342 vminterface = DynamicModelChoiceField( 

343 queryset=VMInterface.objects.all(), 

344 required=False, 

345 context={ 

346 'parent': 'virtual_machine', 

347 }, 

348 selector=True, 

349 label=_('Interface'), 

350 ) 

351 fhrpgroup = DynamicModelChoiceField( 

352 queryset=FHRPGroup.objects.all(), 

353 required=False, 

354 selector=True, 

355 label=_('FHRP Group') 

356 ) 

357 vrf = DynamicModelChoiceField( 

358 queryset=VRF.objects.all(), 

359 required=False, 

360 label=_('VRF') 

361 ) 

362 nat_inside = DynamicModelChoiceField( 

363 queryset=IPAddress.objects.all(), 

364 required=False, 

365 selector=True, 

366 label=_('IP Address'), 

367 ) 

368 primary_for_parent = forms.BooleanField( 

369 required=False, 

370 label=_('Make this the primary IP for the device/VM') 

371 ) 

372 oob_for_parent = forms.BooleanField( 

373 required=False, 

374 label=_('Make this the out-of-band IP for the device') 

375 ) 

376 

377 fieldsets = ( 

378 FieldSet('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')), 

379 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

380 FieldSet( 

381 TabbedGroups( 

382 FieldSet('interface', name=_('Device')), 

383 FieldSet('vminterface', name=_('Virtual Machine')), 

384 FieldSet('fhrpgroup', name=_('FHRP Group')), 

385 ), 

386 'primary_for_parent', 'oob_for_parent', name=_('Assignment') 

387 ), 

388 FieldSet('nat_inside', name=_('NAT IP (Inside)')), 

389 ) 

390 

391 class Meta: 

392 model = IPAddress 

393 fields = [ 

394 'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', 'nat_inside', 

395 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', 

396 ] 

397 

398 def __init__(self, *args, **kwargs): 

399 

400 # Initialize helper selectors 

401 instance = kwargs.get('instance') 

402 initial = kwargs.get('initial', {}).copy() 

403 if instance: 

404 if type(instance.assigned_object) is Interface: 

405 initial['interface'] = instance.assigned_object 

406 elif type(instance.assigned_object) is VMInterface: 

407 initial['vminterface'] = instance.assigned_object 

408 elif type(instance.assigned_object) is FHRPGroup: 

409 initial['fhrpgroup'] = instance.assigned_object 

410 kwargs['initial'] = initial 

411 

412 super().__init__(*args, **kwargs) 

413 

414 # Initialize parent object & fields if IP address is already assigned 

415 if self.instance.pk and self.instance.assigned_object: 

416 parent = getattr(self.instance.assigned_object, 'parent_object', None) 

417 if parent and ( 

418 self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or 

419 self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk 

420 ): 

421 self.initial['primary_for_parent'] = True 

422 

423 if parent and getattr(parent, 'oob_ip_id', None) == self.instance.pk: 

424 self.initial['oob_for_parent'] = True 

425 

426 if type(instance.assigned_object) is Interface: 

427 self.fields['interface'].widget.add_query_params({ 

428 'device_id': instance.assigned_object.device.pk, 

429 }) 

430 elif type(instance.assigned_object) is VMInterface: 

431 self.fields['vminterface'].widget.add_query_params({ 

432 'virtual_machine_id': instance.assigned_object.virtual_machine.pk, 

433 }) 

434 

435 # Disable object assignment fields if the IP address is designated as primary or OOB 

436 if self.initial.get('primary_for_parent') or self.initial.get('oob_for_parent'): 

437 self.fields['interface'].disabled = True 

438 self.fields['vminterface'].disabled = True 

439 self.fields['fhrpgroup'].disabled = True 

440 

441 def clean(self): 

442 super().clean() 

443 

444 # Handle object assignment 

445 selected_objects = [ 

446 field for field in ('interface', 'vminterface', 'fhrpgroup') if self.cleaned_data[field] 

447 ] 

448 if len(selected_objects) > 1: 

449 raise forms.ValidationError({ 

450 selected_objects[1]: _("An IP address can only be assigned to a single object.") 

451 }) 

452 if selected_objects: 

453 assigned_object = self.cleaned_data[selected_objects[0]] 

454 if self.instance.pk and self.instance.assigned_object and assigned_object != self.instance.assigned_object: 

455 if self.cleaned_data['primary_for_parent']: 

456 raise ValidationError( 

457 _("Cannot reassign primary IP address for the parent device/VM") 

458 ) 

459 if self.cleaned_data['oob_for_parent']: 

460 raise ValidationError( 

461 _("Cannot reassign out-of-Band IP address for the parent device") 

462 ) 

463 self.instance.assigned_object = assigned_object 

464 else: 

465 self.instance.assigned_object = None 

466 

467 # Primary IP assignment is only available if an interface has been assigned. 

468 interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface') 

469 if self.cleaned_data.get('primary_for_parent') and not interface: 

470 self.add_error( 

471 'primary_for_parent', _("Only IP addresses assigned to an interface can be designated as primary IPs.") 

472 ) 

473 

474 # OOB IP assignment is only available if device interface has been assigned. 

475 interface = self.cleaned_data.get('interface') 

476 if self.cleaned_data.get('oob_for_parent') and not interface: 

477 self.add_error( 

478 'oob_for_parent', _( 

479 "Only IP addresses assigned to a device interface can be designated as the out-of-band IP for a " 

480 "device." 

481 ) 

482 ) 

483 

484 def save(self, *args, **kwargs): 

485 ipaddress = super().save(*args, **kwargs) 

486 

487 # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine. 

488 interface = self.instance.assigned_object 

489 if type(interface) in (Interface, VMInterface): 

490 parent = interface.parent_object 

491 parent.snapshot() 

492 if self.cleaned_data['primary_for_parent']: 

493 if ipaddress.address.version == 4: 

494 parent.primary_ip4 = ipaddress 

495 else: 

496 parent.primary_ip6 = ipaddress 

497 parent.save() 

498 elif ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress: 

499 parent.primary_ip4 = None 

500 parent.save() 

501 elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress: 

502 parent.primary_ip6 = None 

503 parent.save() 

504 

505 # Assign/clear this IPAddress as the OOB for the associated Device 

506 if type(interface) is Interface: 

507 parent = interface.parent_object 

508 parent.snapshot() 

509 if self.cleaned_data['oob_for_parent']: 

510 parent.oob_ip = ipaddress 

511 parent.save() 

512 elif parent.oob_ip == ipaddress: 

513 parent.oob_ip = None 

514 parent.save() 

515 

516 return ipaddress 

517 

518 

519class IPAddressBulkAddForm(TenancyForm, PrimaryModelForm): 

520 status = ChoiceField( 

521 label=_('Status'), 

522 choices=IPAddressStatusChoices, 

523 initial=IPAddressStatusChoices.STATUS_ACTIVE, 

524 help_text=_('The operational status of this IP'), 

525 ) 

526 role = TypedChoiceField( 

527 label=_('Role'), 

528 choices=add_blank_choice(IPAddressRoleChoices), 

529 required=False, 

530 help_text=_('The functional role of this IP'), 

531 ) 

532 vrf = DynamicModelChoiceField( 

533 queryset=VRF.objects.all(), 

534 required=False, 

535 label=_('VRF') 

536 ) 

537 

538 fieldsets = ( 

539 FieldSet('status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')), 

540 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

541 ) 

542 

543 class Meta: 

544 model = IPAddress 

545 fields = [ 

546 'address', 'vrf', 'status', 'role', 'dns_name', 'tenant_group', 'tenant', 'description', 'owner', 

547 'comments', 'tags', 

548 ] 

549 

550 

551class IPAddressAssignForm(forms.Form): 

552 vrf_id = DynamicModelChoiceField( 

553 queryset=VRF.objects.all(), 

554 required=False, 

555 label=_('VRF') 

556 ) 

557 q = forms.CharField( 

558 required=False, 

559 label=_('Search'), 

560 ) 

561 

562 

563class FHRPGroupForm(PrimaryModelForm): 

564 protocol = ChoiceField( 

565 label=_('Protocol'), 

566 choices=FHRPGroupProtocolChoices, 

567 ) 

568 auth_type = TypedChoiceField( 

569 label=_('Authentication type'), 

570 choices=add_blank_choice(FHRPGroupAuthTypeChoices), 

571 required=False, 

572 ) 

573 

574 # Optionally create a new IPAddress along with the FHRPGroup 

575 ip_vrf = DynamicModelChoiceField( 

576 queryset=VRF.objects.all(), 

577 required=False, 

578 label=_('VRF') 

579 ) 

580 ip_address = IPNetworkFormField( 

581 required=False, 

582 label=_('Address') 

583 ) 

584 ip_status = ChoiceField( 

585 choices=add_blank_choice(IPAddressStatusChoices), 

586 required=False, 

587 label=_('Status') 

588 ) 

589 

590 fieldsets = ( 

591 FieldSet('protocol', 'group_id', 'name', 'description', 'tags', name=_('FHRP Group')), 

592 FieldSet('auth_type', 'auth_key', name=_('Authentication')), 

593 FieldSet('ip_vrf', 'ip_address', 'ip_status', name=_('Virtual IP Address')) 

594 ) 

595 

596 class Meta: 

597 model = FHRPGroup 

598 fields = ( 

599 'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'ip_vrf', 'ip_address', 'ip_status', 'description', 

600 'owner', 'comments', 'tags', 

601 ) 

602 

603 def save(self, *args, **kwargs): 

604 instance = super().save(*args, **kwargs) 

605 user = getattr(instance, '_user', None) # Set under FHRPGroupEditView.alter_object() 

606 

607 # Check if we need to create a new IPAddress for the group 

608 if self.cleaned_data.get('ip_address'): 

609 ipaddress = IPAddress( 

610 vrf=self.cleaned_data['ip_vrf'], 

611 address=self.cleaned_data['ip_address'], 

612 status=self.cleaned_data['ip_status'], 

613 role=FHRP_PROTOCOL_ROLE_MAPPINGS.get(self.cleaned_data['protocol'], IPAddressRoleChoices.ROLE_VIP), 

614 assigned_object=instance 

615 ) 

616 ipaddress.save() 

617 

618 # Check that the new IPAddress conforms with any assigned object-level permissions 

619 if not IPAddress.objects.restrict(user, 'add').filter(pk=ipaddress.pk).first(): 

620 raise PermissionsViolation() 

621 

622 return instance 

623 

624 def clean(self): 

625 super().clean() 

626 

627 ip_vrf = self.cleaned_data.get('ip_vrf') 

628 ip_address = self.cleaned_data.get('ip_address') 

629 ip_status = self.cleaned_data.get('ip_status') 

630 

631 if ip_address: 

632 ip_form = IPAddressForm({ 

633 'address': ip_address, 

634 'vrf': ip_vrf, 

635 'status': ip_status, 

636 }) 

637 if not ip_form.is_valid(): 

638 self.errors.update({ 

639 f'ip_{field}': error for field, error in ip_form.errors.items() 

640 }) 

641 

642 

643class FHRPGroupAssignmentForm(forms.ModelForm): 

644 group = DynamicModelChoiceField( 

645 label=_('Group'), 

646 queryset=FHRPGroup.objects.all() 

647 ) 

648 

649 fieldsets = ( 

650 FieldSet(ObjectAttribute('interface'), 'group', 'priority'), 

651 ) 

652 

653 class Meta: 

654 model = FHRPGroupAssignment 

655 fields = ('group', 'priority') 

656 

657 def clean_group(self): 

658 group = self.cleaned_data['group'] 

659 

660 conflicting_assignments = FHRPGroupAssignment.objects.filter( 

661 interface_type=self.instance.interface_type, 

662 interface_id=self.instance.interface_id, 

663 group=group 

664 ) 

665 if self.instance.id: 

666 conflicting_assignments = conflicting_assignments.exclude(id=self.instance.id) 

667 

668 if conflicting_assignments.exists(): 

669 raise forms.ValidationError( 

670 _('Assignment already exists') 

671 ) 

672 

673 return group 

674 

675 

676class VLANGroupForm(GenericObjectFormMixin, TenancyForm, OrganizationalModelForm): 

677 vid_ranges = NumericRangeArrayField( 

678 label=_('VLAN IDs') 

679 ) 

680 scope = GenericObjectChoiceField( 

681 label=_('Scope'), 

682 content_type_queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), 

683 required=False, 

684 selector=True, 

685 hx_target_id='scope', 

686 ) 

687 

688 fieldsets = ( 

689 FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')), 

690 FieldSet('vid_ranges', name=_('Child VLANs')), 

691 FieldSet('scope', name=_('Scope'), html_id='scope'), 

692 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

693 ) 

694 

695 class Meta: 

696 model = VLANGroup 

697 fields = [ 

698 'name', 'slug', 'description', 'vid_ranges', 'tenant_group', 'tenant', 'owner', 'comments', 

699 'tags', 

700 ] 

701 

702 

703class VLANForm(TenancyForm, PrimaryModelForm): 

704 status = ChoiceField( 

705 label=_('Status'), 

706 choices=VLANStatusChoices, 

707 initial=VLANStatusChoices.STATUS_ACTIVE, 

708 help_text=_('Operational status of this VLAN'), 

709 ) 

710 qinq_role = TypedChoiceField( 

711 label=_('Q-in-Q role'), 

712 choices=add_blank_choice(VLANQinQRoleChoices), 

713 required=False, 

714 help_text=_('Customer/service VLAN designation (for Q-in-Q/IEEE 802.1ad)'), 

715 ) 

716 group = DynamicModelChoiceField( 

717 queryset=VLANGroup.objects.all(), 

718 required=False, 

719 selector=True, 

720 label=_('VLAN Group') 

721 ) 

722 site = DynamicModelChoiceField( 

723 label=_('Site'), 

724 queryset=Site.objects.all(), 

725 required=False, 

726 null_option='None', 

727 selector=True, 

728 help_text=mark_safe( 

729 '<span class="text-warning"><i class="mdi mdi-alert"></i> {text}</span>'.format( 

730 text=_( 

731 'The direct assignment of VLANs to a site is deprecated and will be removed in a future release. ' 

732 'Users are encouraged to utilize VLAN groups for this purpose.' 

733 ) 

734 ) 

735 ) 

736 ) 

737 role = DynamicModelChoiceField( 

738 label=_('Role'), 

739 queryset=Role.objects.all(), 

740 required=False, 

741 quick_add=True 

742 ) 

743 qinq_svlan = DynamicModelChoiceField( 

744 label=_('Q-in-Q SVLAN'), 

745 queryset=VLAN.objects.all(), 

746 required=False, 

747 query_params={ 

748 'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE, 

749 } 

750 ) 

751 

752 class Meta: 

753 model = VLAN 

754 fields = [ 

755 'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'qinq_role', 'qinq_svlan', 

756 'description', 'owner', 'comments', 'tags', 

757 ] 

758 

759 

760class VLANBulkAddForm(VLANForm): 

761 """ 

762 Subclass of VLANForm for bulk creation. 

763 

764 The VID field is inherited but excluded from the visible fieldsets, as it is 

765 populated programmatically by BulkCreateView from the expanded pattern. 

766 """ 

767 fieldsets = ( 

768 FieldSet('group', 'site', 'name', 'status', 'role', 'description', 'tags', name=_('VLAN')), 

769 FieldSet('qinq_role', 'qinq_svlan', name=_('Q-in-Q/802.1ad')), 

770 FieldSet('tenant_group', 'tenant', name=_('Tenancy')), 

771 ) 

772 

773 def __init__(self, *args, **kwargs): 

774 super().__init__(*args, **kwargs) 

775 self.fields['name'].help_text = _( 

776 'Use {vid} as a placeholder for the VLAN ID. Example: VLAN-{vid}.' 

777 ) 

778 

779 

780class VLANTranslationPolicyForm(PrimaryModelForm): 

781 

782 fieldsets = ( 

783 FieldSet('name', 'description', 'tags', name=_('VLAN Translation Policy')), 

784 ) 

785 

786 class Meta: 

787 model = VLANTranslationPolicy 

788 fields = [ 

789 'name', 'description', 'owner', 'tags', 

790 ] 

791 

792 

793class VLANTranslationRuleForm(NetBoxModelForm): 

794 policy = DynamicModelChoiceField( 

795 label=_('Policy'), 

796 queryset=VLANTranslationPolicy.objects.all(), 

797 selector=True 

798 ) 

799 

800 fieldsets = ( 

801 FieldSet('policy', 'local_vid', 'remote_vid', 'description', 'tags', name=_('VLAN Translation Rule')), 

802 ) 

803 

804 class Meta: 

805 model = VLANTranslationRule 

806 fields = [ 

807 'policy', 'local_vid', 'remote_vid', 'description', 'tags', 

808 ] 

809 

810 

811class ServicePortMappingsMixin(forms.Form): 

812 """ 

813 Adds a ``port_mappings`` field (protocol + ports rows) to a Service/ServiceTemplate form. The field 

814 maps directly to the model's ``port_mappings`` ArrayField, so no custom save handling is required. 

815 """ 

816 port_mappings = PortMappingField( 

817 label=_('Port Mappings'), 

818 help_text=_( 

819 "One protocol per row, each with one or more port numbers. A range may be specified using a " 

820 "hyphen (e.g. 80,443,8000-8010)." 

821 ), 

822 ) 

823 

824 

825class ServiceTemplateForm(ServicePortMappingsMixin, PrimaryModelForm): 

826 fieldsets = ( 

827 FieldSet('name', 'port_mappings', 'description', 'tags', name=_('Application Service Template')), 

828 ) 

829 

830 class Meta: 

831 model = ServiceTemplate 

832 fields = ('name', 'port_mappings', 'description', 'owner', 'comments', 'tags') 

833 

834 

835class ServiceForm(ServicePortMappingsMixin, GenericObjectFormMixin, PrimaryModelForm): 

836 parent = GenericObjectChoiceField( 

837 label=_('Parent'), 

838 content_type_queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS), 

839 required=True, 

840 selector=True, 

841 hx_target_id='service', 

842 ) 

843 ipaddresses = DynamicModelMultipleChoiceField( 

844 queryset=IPAddress.objects.all(), 

845 required=False, 

846 label=_('IP Addresses'), 

847 ) 

848 

849 fieldsets = ( 

850 FieldSet( 

851 'parent', 'name', 'port_mappings', 

852 'ipaddresses', 'description', 'tags', name=_('Application Service'), 

853 html_id='service', 

854 ), 

855 ) 

856 

857 class Meta: 

858 model = Service 

859 fields = [ 

860 'name', 'port_mappings', 'ipaddresses', 'description', 'owner', 'comments', 'tags', 

861 ] 

862 

863 def __init__(self, *args, **kwargs): 

864 super().__init__(*args, **kwargs) 

865 

866 # Filter the IP address selector to those belonging to the selected parent. The object subwidget is 

867 # named "parent_object_id", so the dynamic param references "$parent_object_id". 

868 parent_model = self.fields['parent'].selected_model 

869 if parent_model is Device: 

870 self.fields['ipaddresses'].widget.add_query_params({'device_id': '$parent_object_id'}) 

871 elif parent_model is VirtualMachine: 

872 self.fields['ipaddresses'].widget.add_query_params({'virtual_machine_id': '$parent_object_id'}) 

873 elif parent_model is FHRPGroup: 

874 self.fields['ipaddresses'].widget.add_query_params({'fhrpgroup_id': '$parent_object_id'}) 

875 

876 

877class ServiceCreateForm(ServiceForm): 

878 service_template = DynamicModelChoiceField( 

879 label=_('Application Service template'), 

880 queryset=ServiceTemplate.objects.all(), 

881 required=False 

882 ) 

883 

884 fieldsets = ( 

885 FieldSet( 

886 'parent', 

887 TabbedGroups( 

888 FieldSet('service_template', name=_('From Template')), 

889 FieldSet('name', 'port_mappings', name=_('Custom')), 

890 ), 

891 'ipaddresses', 'description', 'tags', name=_('Application Service'), 

892 html_id='service', 

893 ), 

894 ) 

895 

896 class Meta(ServiceForm.Meta): 

897 fields = [ 

898 'service_template', 'name', 'port_mappings', 'ipaddresses', 'description', 'owner', 

899 'comments', 'tags', 

900 ] 

901 

902 def __init__(self, *args, **kwargs): 

903 super().__init__(*args, **kwargs) 

904 

905 # Fields which may be populated from a ServiceTemplate are not required 

906 for field in ('name', 'port_mappings'): 

907 self.fields[field].required = False 

908 self.fields[field].widget.is_required = False 

909 

910 def clean(self): 

911 super().clean() 

912 if self.cleaned_data['service_template']: 

913 # Create a new Service from the specified template 

914 service_template = self.cleaned_data['service_template'] 

915 self.cleaned_data['name'] = service_template.name 

916 self.cleaned_data['port_mappings'] = list(service_template.port_mappings) 

917 if not self.cleaned_data['description']: 

918 self.cleaned_data['description'] = service_template.description 

919 elif not self.cleaned_data.get('name') or not self.cleaned_data.get('port_mappings'): 

920 raise forms.ValidationError( 

921 _("Must specify name and port mapping(s) if not using an application service template.") 

922 )