Coverage for dcim/forms/model_forms.py: 63%

702 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.validators import EMPTY_VALUES 

4from django.utils.html import format_html 

5from django.utils.translation import gettext_lazy as _ 

6from timezone_field import TimeZoneFormField 

7 

8from dcim.choices import * 

9from dcim.constants import * 

10from dcim.forms.mixins import FrontPortFormMixin 

11from dcim.models import * 

12from extras.models import ConfigTemplate 

13from ipam.choices import VLANQinQRoleChoices 

14from ipam.models import ASN, VLAN, VRF, IPAddress, VLANGroup, VLANTranslationPolicy 

15from netbox.choices import WeightUnitChoices 

16from netbox.forms import NestedGroupModelForm, NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm 

17from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin 

18from tenancy.forms import TenancyForm 

19from users.models import User 

20from utilities.choices import Choice 

21from utilities.forms import add_blank_choice, get_field_value 

22from utilities.forms.fields import ( 

23 ChoiceField, 

24 DynamicModelChoiceField, 

25 DynamicModelMultipleChoiceField, 

26 JSONField, 

27 NumericArrayField, 

28 SlugField, 

29 TypedChoiceField, 

30) 

31from utilities.forms.rendering import FieldSet, InlineFields, M2MAddRemoveFields, TabbedGroups 

32from utilities.forms.widgets import ( 

33 APISelect, 

34 ClearableFileInput, 

35 ClearableSelect, 

36 DatePicker, 

37 HTMXSelect, 

38 NumberWithOptions, 

39 SelectWithPK, 

40) 

41from utilities.jsonschema import JSONSchemaProperty 

42from virtualization.models import Cluster, VMInterface 

43from wireless.choices import WirelessChannelChoices, WirelessRoleChoices 

44from wireless.models import WirelessLAN, WirelessLANGroup 

45 

46from .common import InterfaceCommonForm, ModuleCommonForm 

47 

48__all__ = ( 

49 'CableBundleForm', 

50 'CableForm', 

51 'ConsolePortForm', 

52 'ConsolePortTemplateForm', 

53 'ConsoleServerPortForm', 

54 'ConsoleServerPortTemplateForm', 

55 'CoolingFeedForm', 

56 'CoolingIntakeForm', 

57 'CoolingIntakeTemplateForm', 

58 'CoolingOutflowForm', 

59 'CoolingOutflowTemplateForm', 

60 'CoolingSourceForm', 

61 'DeviceBayForm', 

62 'DeviceBayTemplateForm', 

63 'DeviceForm', 

64 'DeviceRoleForm', 

65 'DeviceTypeForm', 

66 'DeviceVCMembershipForm', 

67 'FrontPortForm', 

68 'FrontPortTemplateForm', 

69 'InterfaceForm', 

70 'InterfaceTemplateForm', 

71 'InventoryItemForm', 

72 'InventoryItemRoleForm', 

73 'InventoryItemTemplateForm', 

74 'LocationForm', 

75 'MACAddressForm', 

76 'ManufacturerForm', 

77 'ModuleBayForm', 

78 'ModuleBayTemplateForm', 

79 'ModuleBayTypeForm', 

80 'ModuleForm', 

81 'ModuleTypeForm', 

82 'ModuleTypeProfileForm', 

83 'PlatformForm', 

84 'PopulateDeviceBayForm', 

85 'PowerFeedForm', 

86 'PowerOutletForm', 

87 'PowerOutletTemplateForm', 

88 'PowerPanelForm', 

89 'PowerPortForm', 

90 'PowerPortTemplateForm', 

91 'RackForm', 

92 'RackGroupForm', 

93 'RackReservationForm', 

94 'RackRoleForm', 

95 'RackTypeForm', 

96 'RearPortForm', 

97 'RearPortTemplateForm', 

98 'RegionForm', 

99 'SiteForm', 

100 'SiteGroupForm', 

101 'VCMemberSelectForm', 

102 'VirtualChassisForm', 

103 'VirtualDeviceContextForm' 

104) 

105 

106 

107class RegionForm(NestedGroupModelForm): 

108 parent = DynamicModelChoiceField( 

109 label=_('Parent'), 

110 queryset=Region.objects.all(), 

111 required=False 

112 ) 

113 

114 fieldsets = ( 

115 FieldSet('parent', 'name', 'slug', 'description', 'tags'), 

116 ) 

117 

118 class Meta: 

119 model = Region 

120 fields = ( 

121 'parent', 'name', 'slug', 'description', 'owner', 'tags', 'comments', 

122 ) 

123 

124 

125class SiteGroupForm(NestedGroupModelForm): 

126 parent = DynamicModelChoiceField( 

127 label=_('Parent'), 

128 queryset=SiteGroup.objects.all(), 

129 required=False 

130 ) 

131 

132 fieldsets = ( 

133 FieldSet('parent', 'name', 'slug', 'description', 'tags'), 

134 ) 

135 

136 class Meta: 

137 model = SiteGroup 

138 fields = ( 

139 'parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags', 

140 ) 

141 

142 

143class SiteForm(TenancyForm, PrimaryModelForm): 

144 status = ChoiceField( 

145 label=_('Status'), 

146 choices=SiteStatusChoices, 

147 initial=SiteStatusChoices.STATUS_ACTIVE, 

148 ) 

149 region = DynamicModelChoiceField( 

150 label=_('Region'), 

151 queryset=Region.objects.all(), 

152 required=False, 

153 quick_add=True 

154 ) 

155 group = DynamicModelChoiceField( 

156 label=_('Group'), 

157 queryset=SiteGroup.objects.all(), 

158 required=False, 

159 quick_add=True 

160 ) 

161 asns = DynamicModelMultipleChoiceField( 

162 queryset=ASN.objects.all(), 

163 label=_('ASNs'), 

164 required=False 

165 ) 

166 add_asns = DynamicModelMultipleChoiceField( 

167 queryset=ASN.objects.all(), 

168 label=_('Add ASNs'), 

169 required=False 

170 ) 

171 remove_asns = DynamicModelMultipleChoiceField( 

172 queryset=ASN.objects.all(), 

173 label=_('Remove ASNs'), 

174 required=False 

175 ) 

176 slug = SlugField() 

177 time_zone = TimeZoneFormField( 

178 label=_('Time zone'), 

179 choices=add_blank_choice(TimeZoneFormField().choices), 

180 required=False 

181 ) 

182 

183 fieldsets = ( 

184 FieldSet( 

185 'name', 'slug', 'status', 'region', 'group', 'facility', M2MAddRemoveFields('asns'), 'time_zone', 

186 'description', 'tags', 

187 name=_('Site') 

188 ), 

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

190 FieldSet('physical_address', 'shipping_address', 'latitude', 'longitude', name=_('Contact Info')), 

191 ) 

192 

193 class Meta: 

194 model = Site 

195 fields = ( 

196 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'time_zone', 

197 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'owner', 'comments', 'tags', 

198 ) 

199 widgets = { 

200 'physical_address': forms.Textarea( 

201 attrs={ 

202 'rows': 3, 

203 } 

204 ), 

205 'shipping_address': forms.Textarea( 

206 attrs={ 

207 'rows': 3, 

208 } 

209 ), 

210 } 

211 

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

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

214 if self.instance.pk and (count := self.instance.asns.count()) >= M2MAddRemoveFields.THRESHOLD: 

215 # Add/remove mode for large M2M sets 

216 self.fields.pop('asns') 

217 self.fields['add_asns'].widget.add_query_param('site_id__n', self.instance.pk) 

218 self.fields['remove_asns'].widget.add_query_param('site_id', self.instance.pk) 

219 self.fields['remove_asns'].help_text = _("{count} ASNs currently assigned").format(count=count) 

220 else: 

221 # Simple mode for new objects or small M2M sets 

222 self.fields.pop('add_asns') 

223 self.fields.pop('remove_asns') 

224 if self.instance.pk: 

225 self.initial['asns'] = list(self.instance.asns.values_list('pk', flat=True)) 

226 

227 

228class LocationForm(TenancyForm, NestedGroupModelForm): 

229 status = ChoiceField( 

230 label=_('Status'), 

231 choices=LocationStatusChoices, 

232 initial=LocationStatusChoices.STATUS_ACTIVE, 

233 ) 

234 site = DynamicModelChoiceField( 

235 label=_('Site'), 

236 queryset=Site.objects.all(), 

237 selector=True 

238 ) 

239 parent = DynamicModelChoiceField( 

240 label=_('Parent'), 

241 queryset=Location.objects.all(), 

242 required=False, 

243 query_params={ 

244 'site_id': '$site' 

245 } 

246 ) 

247 

248 fieldsets = ( 

249 FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')), 

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

251 ) 

252 

253 class Meta: 

254 model = Location 

255 fields = ( 

256 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'owner', 

257 'comments', 'tags', 

258 ) 

259 

260 

261class RackGroupForm(OrganizationalModelForm): 

262 fieldsets = ( 

263 FieldSet('name', 'slug', 'description', 'tags', name=_('Rack Group')), 

264 ) 

265 

266 class Meta: 

267 model = RackGroup 

268 fields = [ 

269 'name', 'slug', 'description', 'owner', 'comments', 'tags', 

270 ] 

271 

272 

273class RackRoleForm(OrganizationalModelForm): 

274 fieldsets = ( 

275 FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')), 

276 ) 

277 

278 class Meta: 

279 model = RackRole 

280 fields = [ 

281 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags', 

282 ] 

283 

284 

285class RackTypeForm(PrimaryModelForm): 

286 form_factor = ChoiceField( 

287 label=_('Form factor'), 

288 choices=RackFormFactorChoices, 

289 ) 

290 outer_unit = TypedChoiceField( 

291 label=_('Outer unit'), 

292 choices=add_blank_choice(RackDimensionUnitChoices), 

293 required=False, 

294 ) 

295 weight_unit = TypedChoiceField( 

296 label=_('Weight unit'), 

297 choices=add_blank_choice(WeightUnitChoices), 

298 required=False, 

299 ) 

300 manufacturer = DynamicModelChoiceField( 

301 label=_('Manufacturer'), 

302 queryset=Manufacturer.objects.all(), 

303 quick_add=True 

304 ) 

305 slug = SlugField( 

306 label=_('Slug'), 

307 slug_source='model' 

308 ) 

309 

310 fieldsets = ( 

311 FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')), 

312 FieldSet( 

313 'width', 'u_height', 

314 InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), 

315 InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), 

316 'mounting_depth', name=_('Dimensions') 

317 ), 

318 FieldSet('starting_unit', 'desc_units', name=_('Numbering')), 

319 FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')), 

320 ) 

321 

322 class Meta: 

323 model = RackType 

324 fields = [ 

325 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 

326 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 

327 'weight_unit', 'cooling_capability', 'cooling_capacity', 'description', 'owner', 'comments', 'tags', 

328 ] 

329 

330 

331class RackForm(TenancyForm, PrimaryModelForm): 

332 status = ChoiceField( 

333 label=_('Status'), 

334 choices=RackStatusChoices, 

335 initial=RackStatusChoices.STATUS_ACTIVE, 

336 ) 

337 form_factor = TypedChoiceField( 

338 label=_('Form factor'), 

339 choices=add_blank_choice(RackFormFactorChoices), 

340 required=False, 

341 ) 

342 outer_unit = TypedChoiceField( 

343 label=_('Outer unit'), 

344 choices=add_blank_choice(RackDimensionUnitChoices), 

345 required=False, 

346 ) 

347 airflow = TypedChoiceField( 

348 label=_('Airflow'), 

349 choices=add_blank_choice(RackAirflowChoices), 

350 required=False, 

351 ) 

352 weight_unit = TypedChoiceField( 

353 label=_('Weight unit'), 

354 choices=add_blank_choice(WeightUnitChoices), 

355 required=False, 

356 ) 

357 site = DynamicModelChoiceField( 

358 label=_('Site'), 

359 queryset=Site.objects.all(), 

360 selector=True 

361 ) 

362 location = DynamicModelChoiceField( 

363 label=_('Location'), 

364 queryset=Location.objects.all(), 

365 required=False, 

366 query_params={ 

367 'site_id': '$site' 

368 } 

369 ) 

370 group = DynamicModelChoiceField( 

371 label=_('Rack Group'), 

372 queryset=RackGroup.objects.all(), 

373 required=False 

374 ) 

375 role = DynamicModelChoiceField( 

376 label=_('Role'), 

377 queryset=RackRole.objects.all(), 

378 required=False 

379 ) 

380 rack_type = DynamicModelChoiceField( 

381 label=_('Rack Type'), 

382 queryset=RackType.objects.all(), 

383 required=False, 

384 selector=True, 

385 help_text=_("Select a pre-defined rack type, or set physical characteristics below."), 

386 ) 

387 

388 fieldsets = ( 

389 FieldSet( 

390 'site', 'location', 'group', 'name', 'status', 'role', 'rack_type', 'description', 'airflow', 'tags', 

391 name=_('Rack') 

392 ), 

393 FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')), 

394 FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')), 

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

396 ) 

397 

398 class Meta: 

399 model = Rack 

400 fields = [ 

401 'site', 'location', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 

402 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 

403 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'cooling_capability', 

404 'cooling_capacity', 'weight', 'max_weight', 'weight_unit', 'description', 'owner', 'comments', 'tags', 

405 ] 

406 

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

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

409 

410 # Mimic HTMXSelect() 

411 self.fields['rack_type'].widget.attrs.update({ 

412 'hx-get': '.', 

413 'hx-include': '#form_fields', 

414 'hx-target': '#form_fields', 

415 }) 

416 

417 # Omit RackType-defined fields if rack_type is set 

418 if get_field_value(self, 'rack_type'): 

419 for field_name in Rack.RACKTYPE_FIELDS: 

420 del self.fields[field_name] 

421 else: 

422 # The form_factor, width, and outer_* fields are deprecated on the Rack model and will be removed in 

423 # NetBox v5.0. Their values should instead be defined on an assigned rack type. (See #22593.) 

424 deprecation_warning = format_html( 

425 '<span class="text-warning"><i class="mdi mdi-alert"></i> {}</span>', 

426 _( 

427 'Deprecated and will be removed in NetBox v5.0. Assign a rack type to define this attribute ' 

428 'instead.' 

429 ) 

430 ) 

431 self.fields['form_factor'].help_text = deprecation_warning 

432 self.fields['width'].help_text = deprecation_warning 

433 

434 self.fieldsets = ( 

435 *self.fieldsets, 

436 FieldSet( 

437 'form_factor', 

438 'width', 

439 'starting_unit', 

440 'u_height', 

441 InlineFields( 

442 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 

443 label=_('Outer Dimensions'), 

444 help_text=deprecation_warning, 

445 ), 

446 InlineFields( 

447 'weight', 'max_weight', 'weight_unit', label=_('Weight'), 

448 ), 

449 'mounting_depth', 

450 'desc_units', 

451 name=_('Dimensions') 

452 ), 

453 ) 

454 

455 

456class RackReservationForm(TenancyForm, PrimaryModelForm): 

457 status = ChoiceField( 

458 label=_('Status'), 

459 choices=RackReservationStatusChoices, 

460 initial=RackReservationStatusChoices.STATUS_ACTIVE, 

461 ) 

462 rack = DynamicModelChoiceField( 

463 label=_('Rack'), 

464 queryset=Rack.objects.all(), 

465 selector=True 

466 ) 

467 units = NumericArrayField( 

468 label=_('Units'), 

469 base_field=forms.IntegerField(), 

470 help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.") 

471 ) 

472 user = forms.ModelChoiceField( 

473 label=_('User'), 

474 queryset=User.objects.order_by('username') 

475 ) 

476 

477 fieldsets = ( 

478 FieldSet('rack', 'units', 'status', 'user', 'description', 'tags', name=_('Reservation')), 

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

480 ) 

481 

482 class Meta: 

483 model = RackReservation 

484 fields = [ 

485 'rack', 'units', 'status', 'user', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', 

486 ] 

487 

488 

489class ManufacturerForm(OrganizationalModelForm): 

490 fieldsets = ( 

491 FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')), 

492 ) 

493 

494 class Meta: 

495 model = Manufacturer 

496 fields = [ 

497 'name', 'slug', 'description', 'owner', 'comments', 'tags', 

498 ] 

499 

500 

501class DeviceTypeForm(PrimaryModelForm): 

502 subdevice_role = TypedChoiceField( 

503 label=_('Parent/child status'), 

504 choices=add_blank_choice(SubdeviceRoleChoices), 

505 required=False, 

506 help_text=_( 

507 'Parent devices house child devices in device bays. Leave blank if this device type is neither a ' 

508 'parent nor a child.' 

509 ), 

510 ) 

511 airflow = TypedChoiceField( 

512 label=_('Airflow'), 

513 choices=add_blank_choice(DeviceAirflowChoices), 

514 required=False, 

515 ) 

516 weight_unit = TypedChoiceField( 

517 label=_('Weight unit'), 

518 choices=add_blank_choice(WeightUnitChoices), 

519 required=False, 

520 ) 

521 manufacturer = DynamicModelChoiceField( 

522 label=_('Manufacturer'), 

523 queryset=Manufacturer.objects.all(), 

524 quick_add=True 

525 ) 

526 default_platform = DynamicModelChoiceField( 

527 label=_('Default platform'), 

528 queryset=Platform.objects.all(), 

529 required=False, 

530 selector=True, 

531 query_params={ 

532 'manufacturer_id': ['$manufacturer', 'null'], 

533 } 

534 ) 

535 slug = SlugField( 

536 label=_('Slug'), 

537 slug_source='model' 

538 ) 

539 

540 fieldsets = ( 

541 FieldSet('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags', name=_('Device Type')), 

542 FieldSet( 

543 'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 

544 'weight', 'weight_unit', name=_('Chassis') 

545 ), 

546 FieldSet('cooling_method', 'airflow', name=_('Cooling')), 

547 FieldSet('end_of_life', name=_('Lifecycle')), 

548 FieldSet('front_image', 'rear_image', name=_('Images')), 

549 ) 

550 

551 class Meta: 

552 model = DeviceType 

553 fields = [ 

554 'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization', 

555 'is_full_depth', 'subdevice_role', 'airflow', 'cooling_method', 'weight', 'weight_unit', 'end_of_life', 

556 'front_image', 

557 'rear_image', 'description', 'owner', 'comments', 'tags', 

558 ] 

559 widgets = { 

560 'front_image': ClearableFileInput(attrs={ 

561 'accept': DEVICETYPE_IMAGE_FORMATS 

562 }), 

563 'rear_image': ClearableFileInput(attrs={ 

564 'accept': DEVICETYPE_IMAGE_FORMATS 

565 }), 

566 'end_of_life': DatePicker(), 

567 } 

568 

569 

570class ModuleBayTypeForm(PrimaryModelForm): 

571 manufacturer = DynamicModelChoiceField( 

572 label=_('Manufacturer'), 

573 queryset=Manufacturer.objects.all(), 

574 required=False, 

575 ) 

576 slug = SlugField( 

577 slug_source='name', 

578 ) 

579 

580 fieldsets = ( 

581 FieldSet('name', 'slug', 'manufacturer', 'color', 'description', 'tags', name=_('Module Bay Type')), 

582 ) 

583 

584 class Meta: 

585 model = ModuleBayType 

586 fields = [ 

587 'name', 'slug', 'manufacturer', 'color', 'description', 'owner', 'comments', 'tags', 

588 ] 

589 

590 

591class ModuleTypeProfileForm(PrimaryModelForm): 

592 schema = JSONField( 

593 label=_('Schema'), 

594 required=False, 

595 help_text=_("Enter a valid JSON schema to define supported attributes.") 

596 ) 

597 

598 fieldsets = ( 

599 FieldSet('name', 'description', 'schema', 'tags', name=_('Profile')), 

600 ) 

601 

602 class Meta: 

603 model = ModuleTypeProfile 

604 fields = [ 

605 'name', 'description', 'schema', 'owner', 'comments', 'tags', 

606 ] 

607 

608 

609class ModuleTypeForm(PrimaryModelForm): 

610 airflow = TypedChoiceField( 

611 label=_('Airflow'), 

612 choices=add_blank_choice(ModuleAirflowChoices), 

613 required=False, 

614 ) 

615 weight_unit = TypedChoiceField( 

616 label=_('Weight unit'), 

617 choices=add_blank_choice(WeightUnitChoices), 

618 required=False, 

619 ) 

620 profile = forms.ModelChoiceField( 

621 queryset=ModuleTypeProfile.objects.all(), 

622 label=_('Profile'), 

623 required=False, 

624 widget=HTMXSelect(hx_target_id='profile-attributes') 

625 ) 

626 manufacturer = DynamicModelChoiceField( 

627 label=_('Manufacturer'), 

628 queryset=Manufacturer.objects.all() 

629 ) 

630 module_bay_types = DynamicModelMultipleChoiceField( 

631 label=_('Module bay types'), 

632 queryset=ModuleBayType.objects.all(), 

633 required=False, 

634 ) 

635 

636 @property 

637 def fieldsets(self): 

638 return [ 

639 FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')), 

640 FieldSet('weight', 'weight_unit', name=_('Hardware')), 

641 FieldSet('cooling_method', 'airflow', name=_('Cooling')), 

642 FieldSet('module_bay_types', name=_('Bay Type Compatibility')), 

643 FieldSet('end_of_life', name=_('Lifecycle')), 

644 FieldSet('profile', *self.attr_fields, name=_('Profile & Attributes'), html_id='profile-attributes') 

645 ] 

646 

647 class Meta: 

648 model = ModuleType 

649 fields = [ 

650 'profile', 'manufacturer', 'model', 'part_number', 'description', 'cooling_method', 'airflow', 'weight', 

651 'weight_unit', 'module_bay_types', 'end_of_life', 'owner', 'comments', 'tags', 

652 ] 

653 widgets = { 

654 'end_of_life': DatePicker(), 

655 } 

656 

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

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

659 

660 # Track profile-specific attribute fields 

661 self.attr_fields = [] 

662 

663 # Retrieve assigned ModuleTypeProfile, if any 

664 if not (profile_id := get_field_value(self, 'profile')): 

665 return 

666 if not (profile := ModuleTypeProfile.objects.filter(pk=profile_id).first()): 

667 return 

668 

669 # Extend form with fields for profile attributes 

670 for attr, form_field in self._get_attr_form_fields(profile).items(): 

671 field_name = f'attr_{attr}' 

672 self.attr_fields.append(field_name) 

673 self.fields[field_name] = form_field 

674 if self.instance.attribute_data: 

675 self.fields[field_name].initial = self.instance.attribute_data.get(attr) 

676 

677 @staticmethod 

678 def _get_attr_form_fields(profile): 

679 """ 

680 Return a dictionary mapping of attribute names to form fields, suitable for extending 

681 the form per the selected ModuleTypeProfile. 

682 """ 

683 if not profile.schema: 

684 return {} 

685 

686 properties = profile.schema.get('properties', {}) 

687 required_fields = profile.schema.get('required', []) 

688 

689 attr_fields = {} 

690 for name, options in properties.items(): 

691 prop = JSONSchemaProperty(**options) 

692 attr_fields[name] = prop.to_form_field(name, required=name in required_fields) 

693 

694 return dict(sorted(attr_fields.items())) 

695 

696 def _post_clean(self): 

697 

698 # Compile attribute data from the individual form fields 

699 if self.cleaned_data.get('profile'): 

700 self.instance.attribute_data = { 

701 name[5:]: self.cleaned_data[name] # Remove the attr_ prefix 

702 for name in self.attr_fields 

703 if self.cleaned_data.get(name) not in EMPTY_VALUES 

704 } 

705 

706 return super()._post_clean() 

707 

708 

709class DeviceRoleForm(NestedGroupModelForm): 

710 config_template = DynamicModelChoiceField( 

711 label=_('Config template'), 

712 queryset=ConfigTemplate.objects.all(), 

713 required=False 

714 ) 

715 parent = DynamicModelChoiceField( 

716 label=_('Parent'), 

717 queryset=DeviceRole.objects.all(), 

718 required=False, 

719 ) 

720 

721 fieldsets = ( 

722 FieldSet( 

723 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 

724 'tags', name=_('Device Role') 

725 ), 

726 ) 

727 

728 class Meta: 

729 model = DeviceRole 

730 fields = [ 

731 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'owner', 'comments', 'tags', 

732 ] 

733 

734 

735class PlatformForm(NestedGroupModelForm): 

736 parent = DynamicModelChoiceField( 

737 label=_('Parent'), 

738 queryset=Platform.objects.all(), 

739 required=False, 

740 ) 

741 manufacturer = DynamicModelChoiceField( 

742 label=_('Manufacturer'), 

743 queryset=Manufacturer.objects.all(), 

744 required=False, 

745 quick_add=True 

746 ) 

747 config_template = DynamicModelChoiceField( 

748 label=_('Config template'), 

749 queryset=ConfigTemplate.objects.all(), 

750 required=False 

751 ) 

752 slug = SlugField( 

753 label=_('Slug'), 

754 max_length=64 

755 ) 

756 

757 fieldsets = ( 

758 FieldSet( 

759 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform'), 

760 ), 

761 ) 

762 

763 class Meta: 

764 model = Platform 

765 fields = [ 

766 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'owner', 'comments', 'tags', 

767 ] 

768 

769 

770class DeviceForm(TenancyForm, PrimaryModelForm): 

771 status = ChoiceField( 

772 label=_('Status'), 

773 choices=DeviceStatusChoices, 

774 initial=DeviceStatusChoices.STATUS_ACTIVE, 

775 ) 

776 airflow = TypedChoiceField( 

777 label=_('Airflow'), 

778 choices=add_blank_choice(DeviceAirflowChoices), 

779 required=False, 

780 ) 

781 site = DynamicModelChoiceField( 

782 label=_('Site'), 

783 queryset=Site.objects.all(), 

784 selector=True 

785 ) 

786 location = DynamicModelChoiceField( 

787 label=_('Location'), 

788 queryset=Location.objects.all(), 

789 required=False, 

790 query_params={ 

791 'site_id': '$site' 

792 }, 

793 initial_params={ 

794 'racks': '$rack' 

795 } 

796 ) 

797 rack = DynamicModelChoiceField( 

798 label=_('Rack'), 

799 queryset=Rack.objects.all(), 

800 required=False, 

801 query_params={ 

802 'site_id': '$site', 

803 'location_id': '$location', 

804 } 

805 ) 

806 position = forms.DecimalField( 

807 label=_('Position'), 

808 required=False, 

809 help_text=_("The lowest-numbered unit occupied by the device"), 

810 localize=True, 

811 widget=APISelect( 

812 api_url='/api/dcim/racks/{{rack}}/elevation/', 

813 attrs={ 

814 'ts-disabled-field': 'device', 

815 'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]' 

816 }, 

817 ) 

818 ) 

819 face = TypedChoiceField( 

820 label=_('Face'), 

821 choices=add_blank_choice(DeviceFaceChoices), 

822 required=False, 

823 widget=ClearableSelect( 

824 requires_fields=['rack'] 

825 ) 

826 ) 

827 device_type = DynamicModelChoiceField( 

828 label=_('Device type'), 

829 queryset=DeviceType.objects.all(), 

830 context={ 

831 'parent': 'manufacturer', 

832 }, 

833 selector=True 

834 ) 

835 role = DynamicModelChoiceField( 

836 label=_('Device role'), 

837 queryset=DeviceRole.objects.all(), 

838 quick_add=True 

839 ) 

840 platform = DynamicModelChoiceField( 

841 label=_('Platform'), 

842 queryset=Platform.objects.all(), 

843 required=False, 

844 selector=True, 

845 query_params={ 

846 'available_for_device_type': '$device_type', 

847 } 

848 ) 

849 cluster = DynamicModelChoiceField( 

850 label=_('Cluster'), 

851 queryset=Cluster.objects.all(), 

852 required=False, 

853 selector=True, 

854 query_params={ 

855 'site_id': ['$site', 'null'] 

856 }, 

857 ) 

858 local_context_data = JSONField( 

859 required=False, 

860 label='', 

861 widget=forms.Textarea(attrs={'aria-label': _('Local config context data')}) 

862 ) 

863 virtual_chassis = DynamicModelChoiceField( 

864 label=_('Virtual chassis'), 

865 queryset=VirtualChassis.objects.all(), 

866 required=False, 

867 context={ 

868 'parent': 'master', 

869 }, 

870 selector=True 

871 ) 

872 vc_position = forms.IntegerField( 

873 required=False, 

874 label=_('Position'), 

875 help_text=_("The position in the virtual chassis this device is identified by") 

876 ) 

877 vc_priority = forms.IntegerField( 

878 required=False, 

879 label=_('Priority'), 

880 help_text=_("The priority of the device in the virtual chassis") 

881 ) 

882 config_template = DynamicModelChoiceField( 

883 label=_('Config template'), 

884 queryset=ConfigTemplate.objects.all(), 

885 required=False 

886 ) 

887 

888 class Meta: 

889 model = Device 

890 fields = [ 

891 'name', 'role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face', 

892 'latitude', 'longitude', 'status', 'airflow', 'cooling_method', 'platform', 'primary_ip4', 'primary_ip6', 

893 'oob_ip', 'cluster', 

894 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 

895 'owner', 'comments', 'tags', 'local_context_data', 

896 ] 

897 

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

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

900 

901 if self.instance.pk: 

902 

903 # Compile list of choices for primary IPv4 and IPv6 addresses 

904 oob_ip_choices = [(None, '---------')] 

905 for family in [4, 6]: 

906 ip_choices = [(None, '---------')] 

907 

908 # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member 

909 interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True) 

910 

911 # Collect interface IPs 

912 interface_ips = IPAddress.objects.filter( 

913 address__family=family, 

914 assigned_object_type=ContentType.objects.get_for_model(Interface), 

915 assigned_object_id__in=interface_ids 

916 ).prefetch_related('assigned_object') 

917 if interface_ips: 

918 ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips] 

919 ip_choices.append(('Interface IPs', ip_list)) 

920 oob_ip_choices.extend(ip_list) 

921 # Collect NAT IPs 

922 nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter( 

923 address__family=family, 

924 nat_inside__assigned_object_type=ContentType.objects.get_for_model(Interface), 

925 nat_inside__assigned_object_id__in=interface_ids 

926 ).prefetch_related('assigned_object') 

927 if nat_ips: 

928 ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips] 

929 ip_choices.append(('NAT IPs', ip_list)) 

930 self.fields['primary_ip{}'.format(family)].choices = ip_choices 

931 self.fields['oob_ip'].choices = oob_ip_choices 

932 

933 # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device 

934 # can be flipped from one face to another. 

935 self.fields['position'].widget.add_query_param('exclude', self.instance.pk) 

936 

937 # Disable rack assignment if this is a child device installed in a parent device 

938 if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'): 

939 self.fields['site'].disabled = True 

940 self.fields['rack'].disabled = True 

941 self.initial['site'] = self.instance.parent_bay.device.site_id 

942 self.initial['rack'] = self.instance.parent_bay.device.rack_id 

943 

944 else: 

945 

946 # An object that doesn't exist yet can't have any IPs assigned to it 

947 self.fields['primary_ip4'].choices = [] 

948 self.fields['primary_ip4'].widget.attrs['readonly'] = True 

949 self.fields['primary_ip6'].choices = [] 

950 self.fields['primary_ip6'].widget.attrs['readonly'] = True 

951 self.fields['oob_ip'].choices = [] 

952 self.fields['oob_ip'].widget.attrs['readonly'] = True 

953 

954 # Rack position 

955 position = self.data.get('position') or self.initial.get('position') 

956 if position: 

957 self.fields['position'].widget.choices = [(position, f'U{position}')] 

958 

959 

960class ModuleForm(ModuleCommonForm, PrimaryModelForm): 

961 status = ChoiceField( 

962 label=_('Status'), 

963 choices=ModuleStatusChoices, 

964 initial=ModuleStatusChoices.STATUS_ACTIVE, 

965 ) 

966 device = DynamicModelChoiceField( 

967 label=_('Device'), 

968 queryset=Device.objects.all(), 

969 initial_params={ 

970 'modulebays': '$module_bay' 

971 } 

972 ) 

973 module_bay = DynamicModelChoiceField( 

974 label=_('Module bay'), 

975 queryset=ModuleBay.objects.all(), 

976 query_params={ 

977 'device_id': '$device', 

978 }, 

979 context={ 

980 'disabled': '_occupied', 

981 }, 

982 ) 

983 module_type = DynamicModelChoiceField( 

984 label=_('Module type'), 

985 queryset=ModuleType.objects.all(), 

986 context={ 

987 'parent': 'manufacturer', 

988 }, 

989 selector=True 

990 ) 

991 replicate_components = forms.BooleanField( 

992 label=_('Replicate components'), 

993 required=False, 

994 initial=True, 

995 help_text=_("Automatically populate components associated with this module type") 

996 ) 

997 adopt_components = forms.BooleanField( 

998 label=_('Adopt components'), 

999 required=False, 

1000 initial=False, 

1001 help_text=_("Adopt already existing components") 

1002 ) 

1003 

1004 fieldsets = ( 

1005 FieldSet('device', 'module_bay', 'module_type', 'status', 'description', 'tags', name=_('Module')), 

1006 FieldSet('serial', 'asset_tag', 'replicate_components', 'adopt_components', name=_('Hardware')), 

1007 ) 

1008 

1009 class Meta: 

1010 model = Module 

1011 fields = [ 

1012 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components', 

1013 'adopt_components', 'description', 'owner', 'comments', 

1014 ] 

1015 

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

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

1018 

1019 if self.instance.pk: 

1020 self.fields['replicate_components'].initial = False 

1021 self.fields['replicate_components'].disabled = True 

1022 self.fields['adopt_components'].initial = False 

1023 self.fields['adopt_components'].disabled = True 

1024 

1025 

1026def get_termination_type_choices(): 

1027 return add_blank_choice([ 

1028 Choice(f'{ct.app_label}.{ct.model}', ct.model_class()._meta.verbose_name.title()) 

1029 for ct in ContentType.objects.filter(CABLE_TERMINATION_MODELS) 

1030 ]) 

1031 

1032 

1033class CableBundleForm(PrimaryModelForm): 

1034 

1035 fieldsets = ( 

1036 FieldSet('name', 'description', 'tags', name=_('Cable Bundle')), 

1037 ) 

1038 

1039 class Meta: 

1040 model = CableBundle 

1041 fields = ['name', 'description', 'owner', 'comments', 'tags'] 

1042 

1043 

1044class CableForm(TenancyForm, PrimaryModelForm): 

1045 type = TypedChoiceField( 

1046 label=_('Type'), 

1047 choices=add_blank_choice(CableTypeChoices), 

1048 required=False, 

1049 ) 

1050 status = ChoiceField( 

1051 label=_('Status'), 

1052 choices=LinkStatusChoices, 

1053 initial=LinkStatusChoices.STATUS_CONNECTED, 

1054 ) 

1055 profile = ChoiceField( 

1056 label=_('Profile'), 

1057 choices=add_blank_choice(CableProfileChoices), 

1058 required=False, 

1059 ) 

1060 length_unit = TypedChoiceField( 

1061 label=_('Length unit'), 

1062 choices=add_blank_choice(CableLengthUnitChoices), 

1063 required=False, 

1064 ) 

1065 a_terminations_type = ChoiceField( 

1066 choices=get_termination_type_choices, 

1067 required=False, 

1068 widget=HTMXSelect(hx_target_id='cable-side-a'), 

1069 label=_('Type') 

1070 ) 

1071 b_terminations_type = ChoiceField( 

1072 choices=get_termination_type_choices, 

1073 required=False, 

1074 widget=HTMXSelect(hx_target_id='cable-side-b'), 

1075 label=_('Type') 

1076 ) 

1077 bundle = DynamicModelChoiceField( 

1078 queryset=CableBundle.objects.all(), 

1079 required=False, 

1080 label=_('Bundle'), 

1081 ) 

1082 

1083 class Meta: 

1084 model = Cable 

1085 fields = [ 

1086 'a_terminations_type', 'b_terminations_type', 'type', 'status', 'profile', 'tenant_group', 'tenant', 

1087 'bundle', 'label', 'color', 'length', 'length_unit', 'description', 'owner', 'comments', 'tags', 

1088 ] 

1089 

1090 

1091class PowerPanelForm(PrimaryModelForm): 

1092 site = DynamicModelChoiceField( 

1093 label=_('Site'), 

1094 queryset=Site.objects.all(), 

1095 selector=True 

1096 ) 

1097 location = DynamicModelChoiceField( 

1098 label=_('Location'), 

1099 queryset=Location.objects.all(), 

1100 required=False, 

1101 query_params={ 

1102 'site_id': '$site' 

1103 } 

1104 ) 

1105 

1106 fieldsets = ( 

1107 FieldSet('site', 'location', 'name', 'description', 'tags', name=_('Power Panel')), 

1108 ) 

1109 

1110 class Meta: 

1111 model = PowerPanel 

1112 fields = [ 

1113 'site', 'location', 'name', 'description', 'owner', 'comments', 'tags', 

1114 ] 

1115 

1116 

1117class PowerFeedForm(TenancyForm, PrimaryModelForm): 

1118 status = ChoiceField( 

1119 label=_('Status'), 

1120 choices=PowerFeedStatusChoices, 

1121 initial=PowerFeedStatusChoices.STATUS_ACTIVE, 

1122 ) 

1123 type = ChoiceField( 

1124 label=_('Type'), 

1125 choices=PowerFeedTypeChoices, 

1126 initial=PowerFeedTypeChoices.TYPE_PRIMARY, 

1127 ) 

1128 supply = ChoiceField( 

1129 label=_('Supply'), 

1130 choices=PowerFeedSupplyChoices, 

1131 initial=PowerFeedSupplyChoices.SUPPLY_AC, 

1132 ) 

1133 phase = ChoiceField( 

1134 label=_('Phase'), 

1135 choices=PowerFeedPhaseChoices, 

1136 initial=PowerFeedPhaseChoices.PHASE_SINGLE, 

1137 ) 

1138 power_panel = DynamicModelChoiceField( 

1139 label=_('Power panel'), 

1140 queryset=PowerPanel.objects.all(), 

1141 selector=True, 

1142 quick_add=True 

1143 ) 

1144 rack = DynamicModelChoiceField( 

1145 label=_('Rack'), 

1146 queryset=Rack.objects.all(), 

1147 required=False, 

1148 selector=True 

1149 ) 

1150 

1151 fieldsets = ( 

1152 FieldSet( 

1153 'power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags', 

1154 name=_('Power Feed') 

1155 ), 

1156 FieldSet('supply', 'voltage', 'amperage', 'phase', 'max_utilization', name=_('Characteristics')), 

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

1158 ) 

1159 

1160 class Meta: 

1161 model = PowerFeed 

1162 fields = [ 

1163 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 

1164 'max_utilization', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags' 

1165 ] 

1166 

1167 

1168# 

1169# Cooling 

1170# 

1171 

1172class CoolingSourceForm(PrimaryModelForm): 

1173 site = DynamicModelChoiceField( 

1174 label=_('Site'), 

1175 queryset=Site.objects.all(), 

1176 selector=True 

1177 ) 

1178 location = DynamicModelChoiceField( 

1179 label=_('Location'), 

1180 queryset=Location.objects.all(), 

1181 required=False, 

1182 query_params={ 

1183 'site_id': '$site' 

1184 } 

1185 ) 

1186 

1187 fieldsets = ( 

1188 FieldSet('site', 'location', 'name', 'type', 'status', 'description', 'tags', name=_('Cooling Source')), 

1189 FieldSet('fluid_type', 'cooling_capacity', name=_('Characteristics')), 

1190 ) 

1191 

1192 class Meta: 

1193 model = CoolingSource 

1194 fields = [ 

1195 'site', 'location', 'name', 'type', 'status', 'fluid_type', 'cooling_capacity', 'description', 'owner', 

1196 'comments', 'tags', 

1197 ] 

1198 

1199 

1200class CoolingFeedForm(TenancyForm, PrimaryModelForm): 

1201 cooling_source = DynamicModelChoiceField( 

1202 label=_('Cooling source'), 

1203 queryset=CoolingSource.objects.all(), 

1204 selector=True, 

1205 quick_add=True 

1206 ) 

1207 rack = DynamicModelChoiceField( 

1208 label=_('Rack'), 

1209 queryset=Rack.objects.all(), 

1210 required=False, 

1211 selector=True 

1212 ) 

1213 

1214 fieldsets = ( 

1215 FieldSet( 

1216 'cooling_source', 'rack', 'name', 'status', 'description', 'tags', 

1217 name=_('Cooling Feed') 

1218 ), 

1219 FieldSet( 

1220 'cooling_capacity', 

1221 InlineFields('max_flow', 'max_flow_unit', label=_('Max flow')), 

1222 name=_('Characteristics') 

1223 ), 

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

1225 ) 

1226 

1227 class Meta: 

1228 model = CoolingFeed 

1229 fields = [ 

1230 'cooling_source', 'rack', 'name', 'status', 'cooling_capacity', 

1231 'max_flow', 'max_flow_unit', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 

1232 'tags', 

1233 ] 

1234 

1235 

1236# 

1237# Virtual chassis 

1238# 

1239 

1240class VirtualChassisForm(PrimaryModelForm): 

1241 master = forms.ModelChoiceField( 

1242 label=_('Master'), 

1243 queryset=Device.objects.all(), 

1244 required=False, 

1245 widget=SelectWithPK(), 

1246 ) 

1247 

1248 class Meta: 

1249 model = VirtualChassis 

1250 fields = [ 

1251 'name', 'domain', 'master', 'description', 'owner', 'comments', 'tags', 

1252 ] 

1253 

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

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

1256 

1257 self.fields['master'].queryset = Device.objects.filter(virtual_chassis=self.instance) 

1258 

1259 

1260class DeviceVCMembershipForm(forms.ModelForm): 

1261 class Meta: 

1262 model = Device 

1263 fields = [ 

1264 'vc_position', 'vc_priority', 

1265 ] 

1266 labels = { 

1267 'vc_position': 'Position', 

1268 'vc_priority': 'Priority', 

1269 } 

1270 

1271 def __init__(self, validate_vc_position=False, *args, **kwargs): 

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

1273 

1274 # Require VC position (only required when the Device is a VirtualChassis member) 

1275 self.fields['vc_position'].required = True 

1276 

1277 # Add bootstrap classes to form elements. 

1278 self.fields['vc_position'].widget.attrs = {'class': 'form-control'} 

1279 self.fields['vc_priority'].widget.attrs = {'class': 'form-control'} 

1280 

1281 # Validation of vc_position is optional. This is only required when adding a new member to an existing 

1282 # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet. 

1283 self.validate_vc_position = validate_vc_position 

1284 

1285 def clean_vc_position(self): 

1286 vc_position = self.cleaned_data['vc_position'] 

1287 

1288 if self.validate_vc_position: 

1289 conflicting_members = Device.objects.filter( 

1290 virtual_chassis=self.instance.virtual_chassis, 

1291 vc_position=vc_position 

1292 ) 

1293 if conflicting_members.exists(): 

1294 raise forms.ValidationError( 

1295 'A virtual chassis member already exists in position {}.'.format(vc_position) 

1296 ) 

1297 

1298 return vc_position 

1299 

1300 

1301class VCMemberSelectForm(forms.Form): 

1302 device = DynamicModelChoiceField( 

1303 label=_('Device'), 

1304 queryset=Device.objects.all(), 

1305 query_params={ 

1306 'virtual_chassis_id': 'null', 

1307 }, 

1308 selector=True 

1309 ) 

1310 

1311 def clean_device(self): 

1312 device = self.cleaned_data['device'] 

1313 if device.virtual_chassis is not None: 

1314 raise forms.ValidationError( 

1315 f"Device {device} is already assigned to a virtual chassis." 

1316 ) 

1317 return device 

1318 

1319 

1320# 

1321# Device component templates 

1322# 

1323 

1324class ComponentTemplateForm(ChangelogMessageMixin, forms.ModelForm): 

1325 device_type = DynamicModelChoiceField( 

1326 label=_('Device type'), 

1327 queryset=DeviceType.objects.all(), 

1328 context={ 

1329 'parent': 'manufacturer', 

1330 } 

1331 ) 

1332 

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

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

1335 

1336 # Disable reassignment of DeviceType when editing an existing instance 

1337 if self.instance.pk: 

1338 self.fields['device_type'].disabled = True 

1339 

1340 

1341class ModularComponentTemplateForm(ComponentTemplateForm): 

1342 device_type = DynamicModelChoiceField( 

1343 label=_('Device type'), 

1344 queryset=DeviceType.objects.all(), 

1345 required=False, 

1346 context={ 

1347 'parent': 'manufacturer', 

1348 } 

1349 ) 

1350 module_type = DynamicModelChoiceField( 

1351 label=_('Module type'), 

1352 queryset=ModuleType.objects.all(), 

1353 required=False, 

1354 context={ 

1355 'parent': 'manufacturer', 

1356 } 

1357 ) 

1358 

1359 fieldsets = ( 

1360 FieldSet( 

1361 TabbedGroups( 

1362 FieldSet('device_type', name=_('Device Type')), 

1363 FieldSet('module_type', name=_('Module Type')), 

1364 ), 

1365 'name', 'label', 'type', 'description' 

1366 ), 

1367 ) 

1368 

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

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

1371 

1372 # Disable reassignment of ModuleType when editing an existing instance 

1373 if self.instance.pk: 

1374 self.fields['module_type'].disabled = True 

1375 

1376 # Components attached to a module need to present this standardized substitution help text. 

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

1378 "Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range are not " 

1379 "supported (example: <code>[ge,xe]-0/0/[0-9]</code>). The token <code>{module}</code>, if present, will be " 

1380 "automatically replaced with the position value when creating a new module. " 

1381 "The token <code>{vc_position}</code> will be replaced with the device's Virtual Chassis position " 

1382 "(use <code>{vc_position:1}</code> to specify a fallback (default is 0))" 

1383 ) 

1384 

1385 

1386class ConsolePortTemplateForm(ModularComponentTemplateForm): 

1387 type = TypedChoiceField( 

1388 label=_('Type'), 

1389 choices=add_blank_choice(ConsolePortTypeChoices), 

1390 required=False, 

1391 ) 

1392 

1393 class Meta: 

1394 model = ConsolePortTemplate 

1395 fields = [ 

1396 'device_type', 'module_type', 'name', 'label', 'type', 'description', 

1397 ] 

1398 

1399 

1400class ConsoleServerPortTemplateForm(ModularComponentTemplateForm): 

1401 type = TypedChoiceField( 

1402 label=_('Type'), 

1403 choices=add_blank_choice(ConsolePortTypeChoices), 

1404 required=False, 

1405 ) 

1406 

1407 class Meta: 

1408 model = ConsoleServerPortTemplate 

1409 fields = [ 

1410 'device_type', 'module_type', 'name', 'label', 'type', 'description', 

1411 ] 

1412 

1413 

1414class PowerPortTemplateForm(ModularComponentTemplateForm): 

1415 type = TypedChoiceField( 

1416 label=_('Type'), 

1417 choices=add_blank_choice(PowerPortTypeChoices), 

1418 required=False, 

1419 ) 

1420 fieldsets = ( 

1421 FieldSet( 

1422 TabbedGroups( 

1423 FieldSet('device_type', name=_('Device Type')), 

1424 FieldSet('module_type', name=_('Module Type')), 

1425 ), 

1426 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 

1427 ), 

1428 ) 

1429 

1430 class Meta: 

1431 model = PowerPortTemplate 

1432 fields = [ 

1433 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 

1434 ] 

1435 

1436 

1437class PowerOutletTemplateForm(ModularComponentTemplateForm): 

1438 type = TypedChoiceField( 

1439 label=_('Type'), 

1440 choices=add_blank_choice(PowerOutletTypeChoices), 

1441 required=False, 

1442 ) 

1443 feed_leg = TypedChoiceField( 

1444 label=_('Feed leg'), 

1445 choices=add_blank_choice(PowerOutletFeedLegChoices), 

1446 required=False, 

1447 help_text=_('Phase (for three-phase feeds)'), 

1448 ) 

1449 power_port = DynamicModelChoiceField( 

1450 label=_('Power port'), 

1451 queryset=PowerPortTemplate.objects.all(), 

1452 required=False, 

1453 query_params={ 

1454 'device_type_id': '$device_type', 

1455 } 

1456 ) 

1457 

1458 fieldsets = ( 

1459 FieldSet( 

1460 TabbedGroups( 

1461 FieldSet('device_type', name=_('Device Type')), 

1462 FieldSet('module_type', name=_('Module Type')), 

1463 ), 

1464 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description', 

1465 ), 

1466 ) 

1467 

1468 class Meta: 

1469 model = PowerOutletTemplate 

1470 fields = [ 

1471 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description', 

1472 ] 

1473 

1474 

1475class CoolingIntakeTemplateForm(ModularComponentTemplateForm): 

1476 fieldsets = ( 

1477 FieldSet( 

1478 TabbedGroups( 

1479 FieldSet('device_type', name=_('Device Type')), 

1480 FieldSet('module_type', name=_('Module Type')), 

1481 ), 

1482 'name', 'label', 'type', 

1483 InlineFields('diameter', 'diameter_unit', label=_('Diameter')), 

1484 InlineFields('max_flow', 'max_flow_unit', label=_('Max flow')), 

1485 'description', 

1486 ), 

1487 ) 

1488 

1489 class Meta: 

1490 model = CoolingIntakeTemplate 

1491 fields = [ 

1492 'device_type', 'module_type', 'name', 'label', 'type', 'diameter', 'diameter_unit', 

1493 'max_flow', 'max_flow_unit', 'description', 

1494 ] 

1495 

1496 

1497class CoolingOutflowTemplateForm(ModularComponentTemplateForm): 

1498 cooling_intake = DynamicModelChoiceField( 

1499 label=_('Cooling intake'), 

1500 queryset=CoolingIntakeTemplate.objects.all(), 

1501 required=False, 

1502 query_params={ 

1503 'device_type_id': '$device_type', 

1504 } 

1505 ) 

1506 

1507 fieldsets = ( 

1508 FieldSet( 

1509 TabbedGroups( 

1510 FieldSet('device_type', name=_('Device Type')), 

1511 FieldSet('module_type', name=_('Module Type')), 

1512 ), 

1513 'name', 'label', 'type', 

1514 InlineFields('diameter', 'diameter_unit', label=_('Diameter')), 

1515 'cooling_intake', 'description', 

1516 ), 

1517 ) 

1518 

1519 class Meta: 

1520 model = CoolingOutflowTemplate 

1521 fields = [ 

1522 'device_type', 'module_type', 'name', 'label', 'type', 'diameter', 'diameter_unit', 

1523 'cooling_intake', 'description', 

1524 ] 

1525 

1526 

1527class InterfaceTemplateForm(ModularComponentTemplateForm): 

1528 type = ChoiceField( 

1529 label=_('Type'), 

1530 choices=InterfaceTypeChoices, 

1531 ) 

1532 poe_mode = TypedChoiceField( 

1533 label=_('PoE mode'), 

1534 choices=add_blank_choice(InterfacePoEModeChoices), 

1535 required=False, 

1536 ) 

1537 poe_type = TypedChoiceField( 

1538 label=_('PoE type'), 

1539 choices=add_blank_choice(InterfacePoETypeChoices), 

1540 required=False, 

1541 ) 

1542 rf_role = TypedChoiceField( 

1543 label=_('Wireless role'), 

1544 choices=add_blank_choice(WirelessRoleChoices), 

1545 required=False, 

1546 ) 

1547 parent = DynamicModelChoiceField( 

1548 label=_('Parent'), 

1549 queryset=InterfaceTemplate.objects.all(), 

1550 required=False, 

1551 query_params={ 

1552 'device_type_id': '$device_type', 

1553 'module_type_id': '$module_type', 

1554 } 

1555 ) 

1556 bridge = DynamicModelChoiceField( 

1557 label=_('Bridge'), 

1558 queryset=InterfaceTemplate.objects.all(), 

1559 required=False, 

1560 query_params={ 

1561 'device_type_id': '$device_type', 

1562 'module_type_id': '$module_type', 

1563 } 

1564 ) 

1565 

1566 fieldsets = ( 

1567 FieldSet( 

1568 TabbedGroups( 

1569 FieldSet('device_type', name=_('Device Type')), 

1570 FieldSet('module_type', name=_('Module Type')), 

1571 ), 

1572 'name', 'label', 'type', 'channels', 'channel_id', 'enabled', 'mgmt_only', 'description', 'parent', 

1573 'bridge', 

1574 ), 

1575 FieldSet('poe_mode', 'poe_type', name=_('PoE')), 

1576 FieldSet('rf_role', name=_('Wireless')), 

1577 ) 

1578 

1579 class Meta: 

1580 model = InterfaceTemplate 

1581 fields = [ 

1582 'device_type', 'module_type', 'name', 'label', 'type', 'channels', 'channel_id', 'mgmt_only', 'enabled', 

1583 'description', 'poe_mode', 'poe_type', 'parent', 'bridge', 'rf_role', 

1584 ] 

1585 

1586 

1587class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm): 

1588 type = ChoiceField( 

1589 label=_('Type'), 

1590 choices=PortTypeChoices, 

1591 ) 

1592 fieldsets = ( 

1593 FieldSet( 

1594 TabbedGroups( 

1595 FieldSet('device_type', name=_('Device Type')), 

1596 FieldSet('module_type', name=_('Module Type')), 

1597 ), 

1598 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description', 

1599 ), 

1600 ) 

1601 

1602 port_mapping_model = PortTemplateMapping 

1603 rear_port_model = RearPortTemplate 

1604 

1605 class Meta: 

1606 model = FrontPortTemplate 

1607 fields = [ 

1608 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description', 

1609 ] 

1610 

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

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

1613 

1614 # Populate rear port choices based on parent DeviceType or ModuleType 

1615 if device_type_id := self.data.get('device_type') or self.initial.get('device_type'): 

1616 parent_filter = Q(device_type=device_type_id) 

1617 elif module_type_id := self.data.get('module_type') or self.initial.get('module_type'): 

1618 parent_filter = Q(module_type=module_type_id) 

1619 else: 

1620 return 

1621 self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance) 

1622 

1623 # Set initial rear port mappings 

1624 if self.instance.pk: 

1625 self.initial['rear_ports'] = [ 

1626 f'{mapping.rear_port_id}:{mapping.rear_port_position}' 

1627 for mapping in PortTemplateMapping.objects.filter(front_port_id=self.instance.pk) 

1628 ] 

1629 

1630 

1631class RearPortTemplateForm(ModularComponentTemplateForm): 

1632 type = ChoiceField( 

1633 label=_('Type'), 

1634 choices=PortTypeChoices, 

1635 ) 

1636 fieldsets = ( 

1637 FieldSet( 

1638 TabbedGroups( 

1639 FieldSet('device_type', name=_('Device Type')), 

1640 FieldSet('module_type', name=_('Module Type')), 

1641 ), 

1642 'name', 'label', 'type', 'color', 'positions', 'description', 

1643 ), 

1644 ) 

1645 

1646 class Meta: 

1647 model = RearPortTemplate 

1648 fields = [ 

1649 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description', 

1650 ] 

1651 

1652 

1653class ModuleBayTemplateForm(ModularComponentTemplateForm): 

1654 module_bay_types = DynamicModelMultipleChoiceField( 

1655 label=_('Module bay types'), 

1656 queryset=ModuleBayType.objects.all(), 

1657 required=False, 

1658 ) 

1659 

1660 fieldsets = ( 

1661 FieldSet( 

1662 TabbedGroups( 

1663 FieldSet('device_type', name=_('Device Type')), 

1664 FieldSet('module_type', name=_('Module Type')), 

1665 ), 

1666 'name', 'label', 'position', 'enabled', 'description', 'module_bay_types', 

1667 ), 

1668 ) 

1669 

1670 class Meta: 

1671 model = ModuleBayTemplate 

1672 fields = [ 

1673 'device_type', 'module_type', 'name', 'label', 'position', 'enabled', 'description', 'module_bay_types', 

1674 ] 

1675 

1676 

1677class DeviceBayTemplateForm(ComponentTemplateForm): 

1678 fieldsets = ( 

1679 FieldSet('device_type', 'name', 'label', 'enabled', 'description'), 

1680 ) 

1681 

1682 class Meta: 

1683 model = DeviceBayTemplate 

1684 fields = [ 

1685 'device_type', 'name', 'label', 'enabled', 'description', 

1686 ] 

1687 

1688 

1689class InventoryItemTemplateForm(ComponentTemplateForm): 

1690 parent = DynamicModelChoiceField( 

1691 label=_('Parent'), 

1692 queryset=InventoryItemTemplate.objects.all(), 

1693 required=False, 

1694 query_params={ 

1695 'device_type_id': '$device_type' 

1696 } 

1697 ) 

1698 role = DynamicModelChoiceField( 

1699 label=_('Role'), 

1700 queryset=InventoryItemRole.objects.all(), 

1701 required=False 

1702 ) 

1703 manufacturer = DynamicModelChoiceField( 

1704 label=_('Manufacturer'), 

1705 queryset=Manufacturer.objects.all(), 

1706 required=False 

1707 ) 

1708 

1709 # Assigned component selectors 

1710 consoleporttemplate = DynamicModelChoiceField( 

1711 queryset=ConsolePortTemplate.objects.all(), 

1712 required=False, 

1713 query_params={ 

1714 'device_type_id': '$device_type' 

1715 }, 

1716 label=_('Console port template') 

1717 ) 

1718 consoleserverporttemplate = DynamicModelChoiceField( 

1719 queryset=ConsoleServerPortTemplate.objects.all(), 

1720 required=False, 

1721 query_params={ 

1722 'device_type_id': '$device_type' 

1723 }, 

1724 label=_('Console server port template') 

1725 ) 

1726 frontporttemplate = DynamicModelChoiceField( 

1727 queryset=FrontPortTemplate.objects.all(), 

1728 required=False, 

1729 query_params={ 

1730 'device_type_id': '$device_type' 

1731 }, 

1732 label=_('Front port template') 

1733 ) 

1734 interfacetemplate = DynamicModelChoiceField( 

1735 queryset=InterfaceTemplate.objects.all(), 

1736 required=False, 

1737 query_params={ 

1738 'device_type_id': '$device_type' 

1739 }, 

1740 label=_('Interface template') 

1741 ) 

1742 poweroutlettemplate = DynamicModelChoiceField( 

1743 queryset=PowerOutletTemplate.objects.all(), 

1744 required=False, 

1745 query_params={ 

1746 'device_type_id': '$device_type' 

1747 }, 

1748 label=_('Power outlet template') 

1749 ) 

1750 powerporttemplate = DynamicModelChoiceField( 

1751 queryset=PowerPortTemplate.objects.all(), 

1752 required=False, 

1753 query_params={ 

1754 'device_type_id': '$device_type' 

1755 }, 

1756 label=_('Power port template') 

1757 ) 

1758 rearporttemplate = DynamicModelChoiceField( 

1759 queryset=RearPortTemplate.objects.all(), 

1760 required=False, 

1761 query_params={ 

1762 'device_type_id': '$device_type' 

1763 }, 

1764 label=_('Rear port template') 

1765 ) 

1766 

1767 fieldsets = ( 

1768 FieldSet( 

1769 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', 

1770 ), 

1771 FieldSet( 

1772 TabbedGroups( 

1773 FieldSet('interfacetemplate', name=_('Interface')), 

1774 FieldSet('consoleporttemplate', name=_('Console Port')), 

1775 FieldSet('consoleserverporttemplate', name=_('Console Server Port')), 

1776 FieldSet('frontporttemplate', name=_('Front Port')), 

1777 FieldSet('rearporttemplate', name=_('Rear Port')), 

1778 FieldSet('powerporttemplate', name=_('Power Port')), 

1779 FieldSet('poweroutlettemplate', name=_('Power Outlet')), 

1780 ), 

1781 name=_('Component Assignment') 

1782 ) 

1783 ) 

1784 

1785 class Meta: 

1786 model = InventoryItemTemplate 

1787 fields = [ 

1788 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', 

1789 ] 

1790 

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

1792 instance = kwargs.get('instance') 

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

1794 component_type = initial.get('component_type') 

1795 component_id = initial.get('component_id') 

1796 

1797 if instance: 

1798 # When editing set the initial value for component selection 

1799 for component_model in ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS): 

1800 if type(instance.component) is component_model.model_class(): 

1801 initial[component_model.model] = instance.component 

1802 break 

1803 elif component_type and component_id: 

1804 # When adding the InventoryItem from a component page 

1805 content_type = ContentType.objects.filter( 

1806 MODULAR_COMPONENT_TEMPLATE_MODELS 

1807 ).filter(pk=component_type).first() 

1808 if content_type: 

1809 if component := content_type.model_class().objects.filter(pk=component_id).first(): 

1810 initial[content_type.model] = component 

1811 

1812 kwargs['initial'] = initial 

1813 

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

1815 

1816 def clean(self): 

1817 super().clean() 

1818 

1819 # Handle object assignment 

1820 selected_objects = [ 

1821 field for field in ( 

1822 'consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate', 

1823 'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate' 

1824 ) if self.cleaned_data[field] 

1825 ] 

1826 if len(selected_objects) > 1: 

1827 raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component.")) 

1828 if selected_objects: 

1829 self.instance.component = self.cleaned_data[selected_objects[0]] 

1830 else: 

1831 self.instance.component = None 

1832 

1833 

1834# 

1835# Device components 

1836# 

1837 

1838class DeviceComponentForm(OwnerMixin, NetBoxModelForm): 

1839 device = DynamicModelChoiceField( 

1840 label=_('Device'), 

1841 queryset=Device.objects.all(), 

1842 selector=True 

1843 ) 

1844 

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

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

1847 

1848 # Disable reassignment of Device when editing an existing instance 

1849 if self.instance.pk: 

1850 self.fields['device'].disabled = True 

1851 

1852 

1853class ModularDeviceComponentForm(DeviceComponentForm): 

1854 module = DynamicModelChoiceField( 

1855 label=_('Module'), 

1856 queryset=Module.objects.all(), 

1857 required=False, 

1858 query_params={ 

1859 'device_id': '$device', 

1860 } 

1861 ) 

1862 

1863 

1864class ConsolePortForm(ModularDeviceComponentForm): 

1865 type = TypedChoiceField( 

1866 label=_('Type'), 

1867 choices=add_blank_choice(ConsolePortTypeChoices), 

1868 required=False, 

1869 help_text=_('Physical port type'), 

1870 ) 

1871 fieldsets = ( 

1872 FieldSet( 

1873 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', 

1874 ), 

1875 ) 

1876 

1877 class Meta: 

1878 model = ConsolePort 

1879 fields = [ 

1880 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags', 

1881 ] 

1882 

1883 

1884class ConsoleServerPortForm(ModularDeviceComponentForm): 

1885 type = TypedChoiceField( 

1886 label=_('Type'), 

1887 choices=add_blank_choice(ConsolePortTypeChoices), 

1888 required=False, 

1889 help_text=_('Physical port type'), 

1890 ) 

1891 fieldsets = ( 

1892 FieldSet( 

1893 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', 

1894 ), 

1895 ) 

1896 

1897 class Meta: 

1898 model = ConsoleServerPort 

1899 fields = [ 

1900 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags', 

1901 ] 

1902 

1903 

1904class PowerPortForm(ModularDeviceComponentForm): 

1905 type = TypedChoiceField( 

1906 label=_('Type'), 

1907 choices=add_blank_choice(PowerPortTypeChoices), 

1908 required=False, 

1909 help_text=_('Physical port type'), 

1910 ) 

1911 fieldsets = ( 

1912 FieldSet( 

1913 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 

1914 'description', 'tags', 

1915 ), 

1916 ) 

1917 

1918 class Meta: 

1919 model = PowerPort 

1920 fields = [ 

1921 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 

1922 'description', 'owner', 'tags', 

1923 ] 

1924 

1925 

1926class PowerOutletForm(ModularDeviceComponentForm): 

1927 type = TypedChoiceField( 

1928 label=_('Type'), 

1929 choices=add_blank_choice(PowerOutletTypeChoices), 

1930 required=False, 

1931 help_text=_('Physical port type'), 

1932 ) 

1933 status = ChoiceField( 

1934 label=_('Status'), 

1935 choices=PowerOutletStatusChoices, 

1936 initial=PowerOutletStatusChoices.STATUS_ENABLED, 

1937 ) 

1938 feed_leg = TypedChoiceField( 

1939 label=_('Feed leg'), 

1940 choices=add_blank_choice(PowerOutletFeedLegChoices), 

1941 required=False, 

1942 help_text=_('Phase (for three-phase feeds)'), 

1943 ) 

1944 power_port = DynamicModelChoiceField( 

1945 label=_('Power port'), 

1946 queryset=PowerPort.objects.all(), 

1947 required=False, 

1948 query_params={ 

1949 'device_id': '$device', 

1950 } 

1951 ) 

1952 

1953 fieldsets = ( 

1954 FieldSet( 

1955 'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected', 

1956 'description', 'tags', 

1957 ), 

1958 ) 

1959 

1960 class Meta: 

1961 model = PowerOutlet 

1962 fields = [ 

1963 'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected', 

1964 'description', 'owner', 'tags', 

1965 ] 

1966 

1967 

1968class CoolingIntakeForm(ModularDeviceComponentForm): 

1969 cooling_outflow = DynamicModelChoiceField( 

1970 label=_('Cooling outflow'), 

1971 queryset=CoolingOutflow.objects.all(), 

1972 required=False, 

1973 selector=True 

1974 ) 

1975 

1976 fieldsets = ( 

1977 FieldSet( 

1978 'device', 'module', 'name', 'label', 'type', 

1979 InlineFields('diameter', 'diameter_unit', label=_('Diameter')), 

1980 InlineFields('max_flow', 'max_flow_unit', label=_('Max flow')), 

1981 'cooling_outflow', 'description', 'tags', 

1982 ), 

1983 ) 

1984 

1985 class Meta: 

1986 model = CoolingIntake 

1987 fields = [ 

1988 'device', 'module', 'name', 'label', 'type', 'diameter', 'diameter_unit', 'max_flow', 

1989 'max_flow_unit', 'cooling_outflow', 'description', 'owner', 'tags', 

1990 ] 

1991 

1992 

1993class CoolingOutflowForm(ModularDeviceComponentForm): 

1994 cooling_intake = DynamicModelChoiceField( 

1995 label=_('Cooling intake'), 

1996 queryset=CoolingIntake.objects.all(), 

1997 required=False, 

1998 query_params={ 

1999 'device_id': '$device', 

2000 } 

2001 ) 

2002 

2003 fieldsets = ( 

2004 FieldSet( 

2005 'device', 'module', 'name', 'label', 'type', 

2006 InlineFields('diameter', 'diameter_unit', label=_('Diameter')), 

2007 'cooling_intake', 'description', 'tags', 

2008 ), 

2009 ) 

2010 

2011 class Meta: 

2012 model = CoolingOutflow 

2013 fields = [ 

2014 'device', 'module', 'name', 'label', 'type', 'diameter', 'diameter_unit', 'cooling_intake', 'description', 

2015 'owner', 'tags', 

2016 ] 

2017 

2018 

2019class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): 

2020 type = ChoiceField( 

2021 label=_('Type'), 

2022 choices=InterfaceTypeChoices, 

2023 ) 

2024 duplex = TypedChoiceField( 

2025 label=_('Duplex'), 

2026 choices=add_blank_choice(InterfaceDuplexChoices), 

2027 required=False, 

2028 ) 

2029 poe_mode = TypedChoiceField( 

2030 label=_('PoE mode'), 

2031 choices=add_blank_choice(InterfacePoEModeChoices), 

2032 required=False, 

2033 ) 

2034 poe_type = TypedChoiceField( 

2035 label=_('PoE type'), 

2036 choices=add_blank_choice(InterfacePoETypeChoices), 

2037 required=False, 

2038 ) 

2039 mode = TypedChoiceField( 

2040 label=_('802.1Q Mode'), 

2041 choices=add_blank_choice(InterfaceModeChoices), 

2042 required=False, 

2043 help_text=_('IEEE 802.1Q tagging strategy'), 

2044 widget=HTMXSelect(hx_target_id='dot1q-switching'), 

2045 ) 

2046 rf_role = TypedChoiceField( 

2047 label=_('Wireless role'), 

2048 choices=add_blank_choice(WirelessRoleChoices), 

2049 required=False, 

2050 ) 

2051 rf_channel = TypedChoiceField( 

2052 label=_('Wireless channel'), 

2053 choices=add_blank_choice(WirelessChannelChoices), 

2054 required=False, 

2055 ) 

2056 vdcs = DynamicModelMultipleChoiceField( 

2057 queryset=VirtualDeviceContext.objects.all(), 

2058 required=False, 

2059 label=_('Virtual device contexts'), 

2060 initial_params={ 

2061 'interfaces': '$parent', 

2062 }, 

2063 query_params={ 

2064 'device_id': '$device', 

2065 } 

2066 ) 

2067 parent = DynamicModelChoiceField( 

2068 queryset=Interface.objects.all(), 

2069 required=False, 

2070 label=_('Parent interface'), 

2071 query_params={ 

2072 'virtual_chassis_member_id': '$device', 

2073 } 

2074 ) 

2075 bridge = DynamicModelChoiceField( 

2076 queryset=Interface.objects.all(), 

2077 required=False, 

2078 label=_('Bridged interface'), 

2079 query_params={ 

2080 'virtual_chassis_member_id': '$device', 

2081 } 

2082 ) 

2083 lag = DynamicModelChoiceField( 

2084 queryset=Interface.objects.all(), 

2085 required=False, 

2086 label=_('LAG interface'), 

2087 query_params={ 

2088 'virtual_chassis_member_id': '$device', 

2089 'type': 'lag', 

2090 } 

2091 ) 

2092 wireless_lan_group = DynamicModelChoiceField( 

2093 queryset=WirelessLANGroup.objects.all(), 

2094 required=False, 

2095 label=_('Wireless LAN group') 

2096 ) 

2097 wireless_lans = DynamicModelMultipleChoiceField( 

2098 queryset=WirelessLAN.objects.all(), 

2099 required=False, 

2100 label=_('Wireless LANs'), 

2101 query_params={ 

2102 'group_id': '$wireless_lan_group', 

2103 } 

2104 ) 

2105 vlan_group = DynamicModelChoiceField( 

2106 queryset=VLANGroup.objects.all(), 

2107 required=False, 

2108 label=_('VLAN group'), 

2109 help_text=_("Filter VLANs available for assignment by group.") 

2110 ) 

2111 untagged_vlan = DynamicModelChoiceField( 

2112 queryset=VLAN.objects.all(), 

2113 required=False, 

2114 label=_('Untagged VLAN'), 

2115 query_params={ 

2116 'group_id': '$vlan_group', 

2117 'available_on_device': '$device', 

2118 } 

2119 ) 

2120 tagged_vlans = DynamicModelMultipleChoiceField( 

2121 queryset=VLAN.objects.all(), 

2122 required=False, 

2123 label=_('Tagged VLANs'), 

2124 query_params={ 

2125 'group_id': '$vlan_group', 

2126 'available_on_device': '$device', 

2127 } 

2128 ) 

2129 qinq_svlan = DynamicModelChoiceField( 

2130 queryset=VLAN.objects.all(), 

2131 required=False, 

2132 label=_('Q-in-Q Service VLAN'), 

2133 query_params={ 

2134 'group_id': '$vlan_group', 

2135 'available_on_device': '$device', 

2136 'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE, 

2137 } 

2138 ) 

2139 vrf = DynamicModelChoiceField( 

2140 queryset=VRF.objects.all(), 

2141 required=False, 

2142 label=_('VRF') 

2143 ) 

2144 wwn = forms.CharField( 

2145 empty_value=None, 

2146 required=False, 

2147 label=_('WWN') 

2148 ) 

2149 vlan_translation_policy = DynamicModelChoiceField( 

2150 queryset=VLANTranslationPolicy.objects.all(), 

2151 required=False, 

2152 label=_('VLAN Translation Policy') 

2153 ) 

2154 

2155 fieldsets = ( 

2156 FieldSet( 

2157 'device', 'module', 'name', 'label', 'type', 'channels', 'speed', 'duplex', 'description', 'tags', 

2158 name=_('Interface') 

2159 ), 

2160 FieldSet('vrf', 'mac_address', 'wwn', name=_('Addressing')), 

2161 FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')), 

2162 FieldSet('parent', 'channel_id', 'bridge', 'lag', name=_('Related Interfaces')), 

2163 FieldSet('poe_mode', 'poe_type', name=_('PoE')), 

2164 FieldSet( 

2165 'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 

2166 name=_('802.1Q Switching'), 

2167 html_id='dot1q-switching', 

2168 ), 

2169 FieldSet( 

2170 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans', 

2171 name=_('Wireless') 

2172 ), 

2173 ) 

2174 

2175 class Meta: 

2176 model = Interface 

2177 fields = [ 

2178 'device', 'module', 'vdcs', 'name', 'label', 'type', 'channels', 'channel_id', 'speed', 'duplex', 

2179 'enabled', 'parent', 'bridge', 'lag', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 

2180 'poe_mode', 'poe_type', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 

2181 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 

2182 'vrf', 'owner', 'tags', 

2183 ] 

2184 widgets = { 

2185 'speed': NumberWithOptions( 

2186 options=InterfaceSpeedChoices 

2187 ), 

2188 } 

2189 

2190 

2191class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm): 

2192 type = ChoiceField( 

2193 label=_('Type'), 

2194 choices=PortTypeChoices, 

2195 ) 

2196 fieldsets = ( 

2197 FieldSet( 

2198 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected', 

2199 'description', 'tags', 

2200 ), 

2201 ) 

2202 

2203 port_mapping_model = PortMapping 

2204 rear_port_model = RearPort 

2205 

2206 class Meta: 

2207 model = FrontPort 

2208 fields = [ 

2209 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', 

2210 'tags', 

2211 ] 

2212 

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

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

2215 

2216 # Populate rear port choices 

2217 if device_id := self.data.get('device') or self.initial.get('device'): 

2218 parent_filter = Q(device=device_id) 

2219 else: 

2220 return 

2221 self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance) 

2222 

2223 # Set initial rear port mappings 

2224 if self.instance.pk: 

2225 self.initial['rear_ports'] = [ 

2226 f'{mapping.rear_port_id}:{mapping.rear_port_position}' 

2227 for mapping in PortMapping.objects.filter(front_port_id=self.instance.pk) 

2228 ] 

2229 

2230 

2231class RearPortForm(ModularDeviceComponentForm): 

2232 type = ChoiceField( 

2233 label=_('Type'), 

2234 choices=PortTypeChoices, 

2235 ) 

2236 fieldsets = ( 

2237 FieldSet( 

2238 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags', 

2239 ), 

2240 ) 

2241 

2242 class Meta: 

2243 model = RearPort 

2244 fields = [ 

2245 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', 

2246 'tags', 

2247 ] 

2248 

2249 

2250class ModuleBayForm(ModularDeviceComponentForm): 

2251 module_bay_types = DynamicModelMultipleChoiceField( 

2252 label=_('Module bay types'), 

2253 queryset=ModuleBayType.objects.all(), 

2254 required=False, 

2255 ) 

2256 

2257 fieldsets = ( 

2258 FieldSet('device', 'module', 'name', 'label', 'position', 'enabled', 'description', 'module_bay_types', 'tags'), 

2259 ) 

2260 

2261 class Meta: 

2262 model = ModuleBay 

2263 fields = [ 

2264 'device', 'module', 'name', 'label', 'position', 'enabled', 'description', 'module_bay_types', 'owner', 

2265 'tags', 

2266 ] 

2267 

2268 

2269class DeviceBayForm(DeviceComponentForm): 

2270 fieldsets = ( 

2271 FieldSet('device', 'name', 'label', 'enabled', 'description', 'tags',), 

2272 ) 

2273 

2274 class Meta: 

2275 model = DeviceBay 

2276 fields = [ 

2277 'device', 'name', 'label', 'enabled', 'description', 'owner', 'tags', 

2278 ] 

2279 

2280 

2281class PopulateDeviceBayForm(forms.Form): 

2282 installed_device = forms.ModelChoiceField( 

2283 queryset=Device.objects.all(), 

2284 label=_('Child Device'), 

2285 help_text=_("Child devices must first be created and assigned to the site and rack of the parent device.") 

2286 ) 

2287 

2288 def __init__(self, device_bay, *args, **kwargs): 

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

2290 

2291 self.fields['installed_device'].queryset = Device.objects.filter( 

2292 site=device_bay.device.site, 

2293 rack=device_bay.device.rack, 

2294 parent_bay__isnull=True, 

2295 device_type__u_height=0, 

2296 device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD 

2297 ).exclude(pk=device_bay.device.pk) 

2298 

2299 

2300class InventoryItemForm(DeviceComponentForm): 

2301 status = ChoiceField( 

2302 label=_('Status'), 

2303 choices=InventoryItemStatusChoices, 

2304 initial=InventoryItemStatusChoices.STATUS_ACTIVE, 

2305 ) 

2306 parent = DynamicModelChoiceField( 

2307 label=_('Parent'), 

2308 queryset=InventoryItem.objects.all(), 

2309 required=False, 

2310 query_params={ 

2311 'device_id': '$device' 

2312 } 

2313 ) 

2314 role = DynamicModelChoiceField( 

2315 label=_('Role'), 

2316 queryset=InventoryItemRole.objects.all(), 

2317 required=False 

2318 ) 

2319 manufacturer = DynamicModelChoiceField( 

2320 label=_('Manufacturer'), 

2321 queryset=Manufacturer.objects.all(), 

2322 required=False 

2323 ) 

2324 

2325 # Assigned component selectors 

2326 consoleport = DynamicModelChoiceField( 

2327 queryset=ConsolePort.objects.all(), 

2328 required=False, 

2329 query_params={ 

2330 'device_id': '$device' 

2331 }, 

2332 label=_('Console port') 

2333 ) 

2334 consoleserverport = DynamicModelChoiceField( 

2335 queryset=ConsoleServerPort.objects.all(), 

2336 required=False, 

2337 query_params={ 

2338 'device_id': '$device' 

2339 }, 

2340 label=_('Console server port') 

2341 ) 

2342 frontport = DynamicModelChoiceField( 

2343 queryset=FrontPort.objects.all(), 

2344 required=False, 

2345 query_params={ 

2346 'device_id': '$device' 

2347 }, 

2348 label=_('Front port') 

2349 ) 

2350 interface = DynamicModelChoiceField( 

2351 queryset=Interface.objects.all(), 

2352 required=False, 

2353 query_params={ 

2354 'device_id': '$device' 

2355 }, 

2356 label=_('Interface') 

2357 ) 

2358 poweroutlet = DynamicModelChoiceField( 

2359 queryset=PowerOutlet.objects.all(), 

2360 required=False, 

2361 query_params={ 

2362 'device_id': '$device' 

2363 }, 

2364 label=_('Power outlet') 

2365 ) 

2366 powerport = DynamicModelChoiceField( 

2367 queryset=PowerPort.objects.all(), 

2368 required=False, 

2369 query_params={ 

2370 'device_id': '$device' 

2371 }, 

2372 label=_('Power port') 

2373 ) 

2374 rearport = DynamicModelChoiceField( 

2375 queryset=RearPort.objects.all(), 

2376 required=False, 

2377 query_params={ 

2378 'device_id': '$device' 

2379 }, 

2380 label=_('Rear port') 

2381 ) 

2382 

2383 fieldsets = ( 

2384 FieldSet( 

2385 'device', 'parent', 'name', 'label', 'status', 'role', 'description', 'tags', 

2386 name=_('Inventory Item') 

2387 ), 

2388 FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')), 

2389 FieldSet( 

2390 TabbedGroups( 

2391 FieldSet('interface', name=_('Interface')), 

2392 FieldSet('consoleport', name=_('Console Port')), 

2393 FieldSet('consoleserverport', name=_('Console Server Port')), 

2394 FieldSet('frontport', name=_('Front Port')), 

2395 FieldSet('rearport', name=_('Rear Port')), 

2396 FieldSet('powerport', name=_('Power Port')), 

2397 FieldSet('poweroutlet', name=_('Power Outlet')), 

2398 ), 

2399 name=_('Component Assignment') 

2400 ) 

2401 ) 

2402 

2403 class Meta: 

2404 model = InventoryItem 

2405 fields = [ 

2406 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 

2407 'status', 'description', 'owner', 'tags', 

2408 ] 

2409 

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

2411 instance = kwargs.get('instance') 

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

2413 component_type = initial.get('component_type') 

2414 component_id = initial.get('component_id') 

2415 

2416 if instance: 

2417 # When editing set the initial value for component selection 

2418 for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS): 

2419 if type(instance.component) is component_model.model_class(): 

2420 initial[component_model.model] = instance.component 

2421 break 

2422 elif component_type and component_id: 

2423 # When adding the InventoryItem from a component page 

2424 if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first(): 

2425 if component := content_type.model_class().objects.filter(pk=component_id).first(): 

2426 initial[content_type.model] = component 

2427 

2428 kwargs['initial'] = initial 

2429 

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

2431 

2432 # Specifically allow editing the device of IntentoryItems 

2433 if self.instance.pk: 

2434 self.fields['device'].disabled = False 

2435 

2436 def clean(self): 

2437 super().clean() 

2438 

2439 # Handle object assignment 

2440 selected_objects = [ 

2441 field for field in ( 

2442 'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport' 

2443 ) if self.cleaned_data[field] 

2444 ] 

2445 if len(selected_objects) > 1: 

2446 raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component.")) 

2447 if selected_objects: 

2448 self.instance.component = self.cleaned_data[selected_objects[0]] 

2449 else: 

2450 self.instance.component = None 

2451 

2452 

2453class InventoryItemRoleForm(OrganizationalModelForm): 

2454 fieldsets = ( 

2455 FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Inventory Item Role')), 

2456 ) 

2457 

2458 class Meta: 

2459 model = InventoryItemRole 

2460 fields = [ 

2461 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags', 

2462 ] 

2463 

2464 

2465class VirtualDeviceContextForm(TenancyForm, PrimaryModelForm): 

2466 status = ChoiceField( 

2467 label=_('Status'), 

2468 choices=VirtualDeviceContextStatusChoices, 

2469 ) 

2470 device = DynamicModelChoiceField( 

2471 label=_('Device'), 

2472 queryset=Device.objects.all(), 

2473 selector=True 

2474 ) 

2475 primary_ip4 = DynamicModelChoiceField( 

2476 queryset=IPAddress.objects.all(), 

2477 label=_('Primary IPv4'), 

2478 required=False, 

2479 query_params={ 

2480 'device_id': '$device', 

2481 'family': '4', 

2482 } 

2483 ) 

2484 primary_ip6 = DynamicModelChoiceField( 

2485 queryset=IPAddress.objects.all(), 

2486 label=_('Primary IPv6'), 

2487 required=False, 

2488 query_params={ 

2489 'device_id': '$device', 

2490 'family': '6', 

2491 } 

2492 ) 

2493 

2494 fieldsets = ( 

2495 FieldSet( 

2496 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags', 

2497 name=_('Virtual Device Context') 

2498 ), 

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

2500 ) 

2501 

2502 class Meta: 

2503 model = VirtualDeviceContext 

2504 fields = [ 

2505 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'owner', 

2506 'comments', 'tags' 

2507 ] 

2508 

2509 

2510# 

2511# Addressing 

2512# 

2513 

2514class MACAddressForm(PrimaryModelForm): 

2515 mac_address = forms.CharField( 

2516 required=True, 

2517 label=_('MAC address') 

2518 ) 

2519 interface = DynamicModelChoiceField( 

2520 label=_('Interface'), 

2521 queryset=Interface.objects.all(), 

2522 required=False, 

2523 selector=True, 

2524 context={ 

2525 'parent': 'device', 

2526 }, 

2527 ) 

2528 vminterface = DynamicModelChoiceField( 

2529 label=_('VM Interface'), 

2530 queryset=VMInterface.objects.all(), 

2531 required=False, 

2532 selector=True, 

2533 context={ 

2534 'parent': 'virtual_machine', 

2535 }, 

2536 ) 

2537 

2538 fieldsets = ( 

2539 FieldSet( 

2540 'mac_address', 'description', 'tags', 

2541 ), 

2542 FieldSet( 

2543 TabbedGroups( 

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

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

2546 ), 

2547 ), 

2548 ) 

2549 

2550 class Meta: 

2551 model = MACAddress 

2552 fields = [ 

2553 'mac_address', 'interface', 'vminterface', 'description', 'owner', 'comments', 'tags', 

2554 ] 

2555 

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

2557 

2558 # Initialize helper selectors 

2559 instance = kwargs.get('instance') 

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

2561 if instance: 

2562 if type(instance.assigned_object) is Interface: 

2563 initial['interface'] = instance.assigned_object 

2564 elif type(instance.assigned_object) is VMInterface: 

2565 initial['vminterface'] = instance.assigned_object 

2566 kwargs['initial'] = initial 

2567 

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

2569 

2570 if instance and instance.assigned_object and instance.assigned_object.primary_mac_address: 

2571 if instance.assigned_object.primary_mac_address.pk == instance.pk: 

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

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

2574 

2575 def clean(self): 

2576 super().clean() 

2577 

2578 # Handle object assignment 

2579 selected_objects = [ 

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

2581 ] 

2582 if len(selected_objects) > 1: 

2583 raise forms.ValidationError({ 

2584 selected_objects[1]: _("A MAC address can only be assigned to a single object.") 

2585 }) 

2586 if selected_objects: 

2587 self.instance.assigned_object = self.cleaned_data[selected_objects[0]] 

2588 else: 

2589 self.instance.assigned_object = None