Coverage for dcim/tables/devices.py: 87%

396 statements  

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

1from urllib.parse import quote 

2 

3import django_tables2 as tables 

4from django.middleware.csrf import get_token 

5from django.urls import reverse 

6from django.utils.html import format_html 

7from django.utils.safestring import mark_safe 

8from django.utils.translation import gettext_lazy as _ 

9from django_tables2.utils import Accessor 

10 

11from dcim import models 

12from netbox.tables import NestedGroupModelTable, NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns 

13from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin 

14 

15from .template_code import * 

16 

17__all__ = ( 

18 'BaseInterfaceTable', 

19 'CableTerminationTable', 

20 'ConsolePortTable', 

21 'ConsoleServerPortTable', 

22 'DeviceBayTable', 

23 'DeviceConsolePortTable', 

24 'DeviceConsoleServerPortTable', 

25 'DeviceDeviceBayTable', 

26 'DeviceFrontPortTable', 

27 'DeviceInterfaceTable', 

28 'DeviceInventoryItemTable', 

29 'DeviceModuleBayTable', 

30 'DevicePowerOutletTable', 

31 'DevicePowerPortTable', 

32 'DeviceRearPortTable', 

33 'DeviceRoleTable', 

34 'DeviceTable', 

35 'FrontPortTable', 

36 'InterfaceLAGMemberTable', 

37 'InterfaceTable', 

38 'InventoryItemRoleTable', 

39 'InventoryItemTable', 

40 'MACAddressTable', 

41 'ModuleBayTable', 

42 'PlatformTable', 

43 'PowerOutletTable', 

44 'PowerPortTable', 

45 'RearPortTable', 

46 'VirtualChassisTable', 

47 'VirtualDeviceContextTable' 

48) 

49 

50MODULEBAY_STATUS = """ 

51{% badge record.installed_module.get_status_display bg_color=record.installed_module.get_status_color %} 

52""" 

53 

54MACADDRESS_LINK = """ 

55{% if record.pk %} 

56 <a href="{{ record.get_absolute_url }}" id="macaddress_{{ record.pk }}">{{ record.mac_address }}</a> 

57{% endif %} 

58""" 

59 

60MACADDRESS_COPY_BUTTON = """ 

61{% copy_content record.pk prefix="macaddress_" %} 

62""" 

63 

64 

65# 

66# Device roles 

67# 

68 

69class DeviceRoleTable(NestedGroupModelTable): 

70 device_count = columns.LinkedCountColumn( 

71 viewname='dcim:device_list', 

72 url_params={'role_id': 'pk'}, 

73 verbose_name=_('Devices') 

74 ) 

75 vm_count = columns.LinkedCountColumn( 

76 viewname='virtualization:virtualmachine_list', 

77 url_params={'role_id': 'pk'}, 

78 verbose_name=_('VMs') 

79 ) 

80 color = columns.ColorColumn() 

81 vm_role = columns.BooleanColumn( 

82 verbose_name=_('VM role'), 

83 false_mark=None 

84 ) 

85 config_template = tables.Column( 

86 linkify=True 

87 ) 

88 tags = columns.TagColumn( 

89 url_name='dcim:devicerole_list' 

90 ) 

91 

92 class Meta(NestedGroupModelTable.Meta): 

93 model = models.DeviceRole 

94 fields = ( 

95 'pk', 'id', 'name', 'parent', 'device_count', 'vm_count', 'color', 'vm_role', 'config_template', 

96 'description', 'slug', 'tags', 'actions', 'created', 'last_updated', 

97 ) 

98 default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description') 

99 

100 

101# 

102# Platforms 

103# 

104 

105class PlatformTable(NestedGroupModelTable): 

106 manufacturer = tables.Column( 

107 verbose_name=_('Manufacturer'), 

108 linkify=True 

109 ) 

110 config_template = tables.Column( 

111 verbose_name=_('Config Template'), 

112 linkify=True 

113 ) 

114 device_count = columns.LinkedCountColumn( 

115 viewname='dcim:device_list', 

116 url_params={'platform_id': 'pk'}, 

117 verbose_name=_('Devices') 

118 ) 

119 vm_count = columns.LinkedCountColumn( 

120 viewname='virtualization:virtualmachine_list', 

121 url_params={'platform_id': 'pk'}, 

122 verbose_name=_('VMs') 

123 ) 

124 tags = columns.TagColumn( 

125 url_name='dcim:platform_list' 

126 ) 

127 

128 class Meta(NestedGroupModelTable.Meta): 

129 model = models.Platform 

130 fields = ( 

131 'pk', 'id', 'name', 'parent', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 

132 'description', 'tags', 'actions', 'created', 'last_updated', 

133 ) 

134 default_columns = ( 

135 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'description', 

136 ) 

137 

138 

139# 

140# Devices 

141# 

142 

143class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable): 

144 name = tables.TemplateColumn( 

145 verbose_name=_('Name'), 

146 template_code=DEVICE_LINK, 

147 linkify=True, 

148 ) 

149 status = columns.ChoiceFieldColumn( 

150 verbose_name=_('Status'), 

151 ) 

152 region = tables.Column( 

153 verbose_name=_('Region'), 

154 accessor=Accessor('site__region'), 

155 linkify=True 

156 ) 

157 site_group = tables.Column( 

158 accessor=Accessor('site__group'), 

159 linkify=True, 

160 verbose_name=_('Site Group') 

161 ) 

162 site = tables.Column( 

163 verbose_name=_('Site'), 

164 linkify=True 

165 ) 

166 location = tables.Column( 

167 verbose_name=_('Location'), 

168 linkify=True 

169 ) 

170 rack = tables.Column( 

171 verbose_name=_('Rack'), 

172 linkify=True 

173 ) 

174 position = columns.TemplateColumn( 

175 verbose_name=_('Position'), 

176 template_code='{{ value|floatformat }}' 

177 ) 

178 role = columns.ColoredLabelColumn( 

179 verbose_name=_('Role') 

180 ) 

181 manufacturer = tables.Column( 

182 verbose_name=_('Manufacturer'), 

183 accessor=Accessor('device_type__manufacturer'), 

184 linkify=True 

185 ) 

186 device_type = tables.Column( 

187 linkify=True, 

188 verbose_name=_('Type') 

189 ) 

190 u_height = columns.TemplateColumn( 

191 accessor=tables.A('device_type__u_height'), 

192 verbose_name=_('U Height'), 

193 template_code='{{ value|floatformat }}' 

194 ) 

195 platform = tables.Column( 

196 linkify=True, 

197 verbose_name=_('Platform') 

198 ) 

199 primary_ip = tables.Column( 

200 linkify=True, 

201 order_by=('primary_ip4', 'primary_ip6'), 

202 verbose_name=_('IP Address') 

203 ) 

204 primary_ip4 = tables.Column( 

205 linkify=True, 

206 verbose_name=_('IPv4 Address') 

207 ) 

208 primary_ip6 = tables.Column( 

209 linkify=True, 

210 verbose_name=_('IPv6 Address') 

211 ) 

212 oob_ip = tables.Column( 

213 linkify=True, 

214 verbose_name='OOB IP' 

215 ) 

216 cluster = tables.Column( 

217 verbose_name=_('Cluster'), 

218 linkify=True 

219 ) 

220 virtual_chassis = tables.Column( 

221 verbose_name=_('Virtual Chassis'), 

222 linkify=True 

223 ) 

224 vc_position = tables.Column( 

225 verbose_name=_('VC Position') 

226 ) 

227 vc_priority = tables.Column( 

228 verbose_name=_('VC Priority') 

229 ) 

230 config_template = tables.Column( 

231 verbose_name=_('Config Template'), 

232 linkify=True 

233 ) 

234 parent_device = tables.Column( 

235 verbose_name=_('Parent Device'), 

236 linkify=True, 

237 accessor='parent_bay__device' 

238 ) 

239 device_bay_position = tables.Column( 

240 verbose_name=_('Position (Device Bay)'), 

241 accessor='parent_bay', 

242 linkify=True 

243 ) 

244 tags = columns.TagColumn( 

245 url_name='dcim:device_list' 

246 ) 

247 console_port_count = tables.Column( 

248 verbose_name=_('Console ports') 

249 ) 

250 console_server_port_count = tables.Column( 

251 verbose_name=_('Console server ports') 

252 ) 

253 power_port_count = tables.Column( 

254 verbose_name=_('Power ports') 

255 ) 

256 power_outlet_count = tables.Column( 

257 verbose_name=_('Power outlets') 

258 ) 

259 cooling_intake_count = tables.Column( 

260 verbose_name=_('Cooling intakes') 

261 ) 

262 cooling_outflow_count = tables.Column( 

263 verbose_name=_('Cooling outflows') 

264 ) 

265 interface_count = tables.Column( 

266 verbose_name=_('Interfaces') 

267 ) 

268 front_port_count = tables.Column( 

269 verbose_name=_('Front ports') 

270 ) 

271 rear_port_count = tables.Column( 

272 verbose_name=_('Rear ports') 

273 ) 

274 device_bay_count = tables.Column( 

275 verbose_name=_('Device bays') 

276 ) 

277 module_bay_count = tables.Column( 

278 verbose_name=_('Module bays') 

279 ) 

280 inventory_item_count = tables.Column( 

281 verbose_name=_('Inventory items') 

282 ) 

283 cooling_method = columns.ChoiceFieldColumn( 

284 verbose_name=_('Cooling Method'), 

285 ) 

286 

287 class Meta(PrimaryModelTable.Meta): 

288 model = models.Device 

289 fields = ( 

290 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'role', 'manufacturer', 'device_type', 

291 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device', 

292 'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'cooling_method', 

293 'primary_ip', 'primary_ip4', 

294 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 

295 'config_template', 'comments', 'contacts', 'tags', 'created', 'last_updated', 

296 ) 

297 default_columns = ( 

298 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'role', 'manufacturer', 'device_type', 

299 'primary_ip', 

300 ) 

301 

302 

303# 

304# Device components 

305# 

306 

307class DeviceComponentTable(NetBoxTable): 

308 device = tables.Column( 

309 verbose_name=_('Device'), 

310 linkify=True 

311 ) 

312 name = tables.Column( 

313 verbose_name=_('Name'), 

314 linkify=True, 

315 ) 

316 device_location = tables.Column( 

317 accessor=tables.A('device__location'), 

318 verbose_name=_('Device Location'), 

319 linkify=True, 

320 ) 

321 device_site = tables.Column( 

322 accessor=tables.A('device__site'), 

323 verbose_name=_('Device Site'), 

324 linkify=True, 

325 ) 

326 device_status = columns.ChoiceFieldColumn( 

327 accessor=tables.A('device__status'), 

328 verbose_name=_('Device Status'), 

329 color=lambda x: x.device.get_status_color(), 

330 ) 

331 

332 class Meta(NetBoxTable.Meta): 

333 order_by = ('device', 'name') 

334 

335 

336class ModularDeviceComponentTable(DeviceComponentTable): 

337 module_bay = tables.Column( 

338 verbose_name=_('Module Bay'), 

339 accessor=Accessor('module__module_bay'), 

340 linkify={ 

341 'viewname': 'dcim:device_modulebays', 

342 'args': [Accessor('device_id')], 

343 } 

344 ) 

345 module = tables.Column( 

346 verbose_name=_('Module'), 

347 linkify=True 

348 ) 

349 inventory_items = columns.ManyToManyColumn( 

350 linkify_item=True, 

351 verbose_name=_('Inventory Items'), 

352 ) 

353 

354 class Meta(NetBoxTable.Meta): 

355 pass 

356 

357 

358class CableTerminationTable(NetBoxTable): 

359 cable = tables.Column( 

360 verbose_name=_('Cable'), 

361 linkify=True 

362 ) 

363 cable_color = columns.ColorColumn( 

364 accessor='cable__color', 

365 orderable=False, 

366 verbose_name=_('Cable Color') 

367 ) 

368 link_peer = columns.TemplateColumn( 

369 accessor='link_peers', 

370 template_code=LINKTERMINATION, 

371 orderable=False, 

372 verbose_name=_('Link Peers') 

373 ) 

374 mark_connected = columns.BooleanColumn( 

375 verbose_name=_('Mark Connected'), 

376 false_mark=None 

377 ) 

378 

379 class Meta: 

380 row_attrs = { 

381 'data-name': lambda record: record.name, 

382 'data-mark-connected': lambda record: "true" if record.mark_connected else "false", 

383 'data-cable-status': lambda record: record.cable.status if record.cable else "", 

384 'data-type': lambda record: record.type 

385 } 

386 

387 def value_link_peer(self, value): 

388 return ', '.join([ 

389 f"{termination.parent_object} > {termination}" for termination in value 

390 ]) 

391 

392 

393class PathEndpointTable(CableTerminationTable): 

394 connection = columns.TemplateColumn( 

395 accessor='_path__destinations', 

396 template_code=LINKTERMINATION, 

397 verbose_name=_('Connection'), 

398 orderable=False 

399 ) 

400 

401 def value_connection(self, value): 

402 if value: 

403 connections = [] 

404 for termination in value: 

405 if hasattr(termination, 'parent_object'): 

406 connections.append(f'{termination.parent_object} > {termination}') 

407 else: 

408 connections.append(str(termination)) 

409 return ', '.join(connections) 

410 return None 

411 

412 

413class ConsolePortTable(ModularDeviceComponentTable, PathEndpointTable): 

414 device = tables.Column( 

415 verbose_name=_('Device'), 

416 linkify={ 

417 'viewname': 'dcim:device_consoleports', 

418 'args': [Accessor('device_id')], 

419 } 

420 ) 

421 tags = columns.TagColumn( 

422 url_name='dcim:consoleport_list' 

423 ) 

424 

425 class Meta(DeviceComponentTable.Meta): 

426 model = models.ConsolePort 

427 fields = ( 

428 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 

429 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'inventory_items', 'tags', 'created', 

430 'last_updated', 

431 ) 

432 default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description') 

433 

434 

435class DeviceConsolePortTable(ConsolePortTable): 

436 name = tables.TemplateColumn( 

437 verbose_name=_('Name'), 

438 template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', 

439 attrs={'td': {'class': 'text-nowrap'}} 

440 ) 

441 actions = columns.ActionsColumn( 

442 extra_buttons=CONSOLEPORT_BUTTONS 

443 ) 

444 

445 class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): 

446 model = models.ConsolePort 

447 fields = ( 

448 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected', 

449 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions' 

450 ) 

451 default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection') 

452 

453 

454class ConsoleServerPortTable(ModularDeviceComponentTable, PathEndpointTable): 

455 device = tables.Column( 

456 verbose_name=_('Device'), 

457 linkify={ 

458 'viewname': 'dcim:device_consoleserverports', 

459 'args': [Accessor('device_id')], 

460 } 

461 ) 

462 tags = columns.TagColumn( 

463 url_name='dcim:consoleserverport_list' 

464 ) 

465 

466 class Meta(DeviceComponentTable.Meta): 

467 model = models.ConsoleServerPort 

468 fields = ( 

469 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 

470 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'inventory_items', 'tags', 'created', 

471 'last_updated', 

472 ) 

473 default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description') 

474 

475 

476class DeviceConsoleServerPortTable(ConsoleServerPortTable): 

477 name = tables.TemplateColumn( 

478 verbose_name=_('Name'), 

479 template_code='<i class="mdi mdi-console-network-outline"></i> ' 

480 '<a href="{{ record.get_absolute_url }}">{{ value }}</a>', 

481 attrs={'td': {'class': 'text-nowrap'}} 

482 ) 

483 actions = columns.ActionsColumn( 

484 extra_buttons=CONSOLESERVERPORT_BUTTONS 

485 ) 

486 

487 class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): 

488 model = models.ConsoleServerPort 

489 fields = ( 

490 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected', 

491 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', 

492 ) 

493 default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection') 

494 

495 

496class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable): 

497 device = tables.Column( 

498 verbose_name=_('Device'), 

499 linkify={ 

500 'viewname': 'dcim:device_powerports', 

501 'args': [Accessor('device_id')], 

502 } 

503 ) 

504 maximum_draw = tables.Column( 

505 verbose_name=_('Maximum draw (W)') 

506 ) 

507 allocated_draw = tables.Column( 

508 verbose_name=_('Allocated draw (W)') 

509 ) 

510 tags = columns.TagColumn( 

511 url_name='dcim:powerport_list' 

512 ) 

513 

514 class Meta(DeviceComponentTable.Meta): 

515 model = models.PowerPort 

516 fields = ( 

517 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'mark_connected', 

518 'maximum_draw', 'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'inventory_items', 

519 'tags', 'created', 'last_updated', 

520 ) 

521 default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description') 

522 

523 

524class DevicePowerPortTable(PowerPortTable): 

525 name = tables.TemplateColumn( 

526 verbose_name=_('Name'), 

527 template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">' 

528 '{{ value }}</a>', 

529 attrs={'td': {'class': 'text-nowrap'}} 

530 ) 

531 actions = columns.ActionsColumn( 

532 extra_buttons=POWERPORT_BUTTONS 

533 ) 

534 

535 class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): 

536 model = models.PowerPort 

537 fields = ( 

538 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'maximum_draw', 'allocated_draw', 

539 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', 

540 ) 

541 default_columns = ( 

542 'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection', 

543 ) 

544 

545 

546class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable): 

547 device = tables.Column( 

548 verbose_name=_('Device'), 

549 linkify={ 

550 'viewname': 'dcim:device_poweroutlets', 

551 'args': [Accessor('device_id')], 

552 } 

553 ) 

554 power_port = tables.Column( 

555 verbose_name=_('Power Port'), 

556 linkify=True 

557 ) 

558 status = columns.ChoiceFieldColumn( 

559 verbose_name=_('Status'), 

560 ) 

561 color = columns.ColorColumn() 

562 tags = columns.TagColumn( 

563 url_name='dcim:poweroutlet_list' 

564 ) 

565 

566 class Meta(DeviceComponentTable.Meta): 

567 model = models.PowerOutlet 

568 fields = ( 

569 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port', 

570 'color', 'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'inventory_items', 

571 'tags', 'created', 'last_updated', 'status', 

572 ) 

573 default_columns = ( 

574 'pk', 'name', 'device', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'description', 

575 ) 

576 

577 

578class DevicePowerOutletTable(PowerOutletTable): 

579 name = tables.TemplateColumn( 

580 verbose_name=_('Name'), 

581 template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', 

582 attrs={'td': {'class': 'text-nowrap'}} 

583 ) 

584 actions = columns.ActionsColumn( 

585 extra_buttons=POWEROUTLET_BUTTONS 

586 ) 

587 

588 class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): 

589 model = models.PowerOutlet 

590 fields = ( 

591 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'power_port', 'feed_leg', 

592 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', 

593 'status', 

594 ) 

595 default_columns = ( 

596 'pk', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'description', 'cable', 

597 'connection', 

598 ) 

599 

600 

601class BaseInterfaceTable(NetBoxTable): 

602 name = tables.Column( 

603 verbose_name=_('Name'), 

604 linkify=True, 

605 order_by=('_name',) 

606 ) 

607 enabled = columns.BooleanColumn( 

608 verbose_name=_('Enabled'), 

609 ) 

610 ip_addresses = tables.TemplateColumn( 

611 template_code=INTERFACE_IPADDRESSES, 

612 orderable=False, 

613 verbose_name=_('IP Addresses') 

614 ) 

615 primary_mac_address = tables.Column( 

616 verbose_name=_('Primary MAC'), 

617 linkify=True 

618 ) 

619 mac_addresses = columns.ManyToManyColumn( 

620 orderable=False, 

621 linkify_item=True, 

622 verbose_name=_('MAC Addresses') 

623 ) 

624 fhrp_groups = tables.TemplateColumn( 

625 accessor=Accessor('fhrp_group_assignments'), 

626 template_code=INTERFACE_FHRPGROUPS, 

627 orderable=False, 

628 verbose_name=_('FHRP Groups') 

629 ) 

630 l2vpn = tables.Column( 

631 accessor=tables.A('l2vpn_termination__l2vpn'), 

632 linkify=True, 

633 orderable=False, 

634 verbose_name=_('L2VPN') 

635 ) 

636 tunnel = tables.Column( 

637 accessor=tables.A('tunnel_termination__tunnel'), 

638 linkify=True, 

639 orderable=False, 

640 verbose_name=_('Tunnel') 

641 ) 

642 untagged_vlan = tables.Column( 

643 verbose_name=_('Untagged VLAN'), 

644 linkify=True 

645 ) 

646 tagged_vlans = columns.TemplateColumn( 

647 template_code=INTERFACE_TAGGED_VLANS, 

648 orderable=False, 

649 verbose_name=_('Tagged VLANs') 

650 ) 

651 qinq_svlan = tables.Column( 

652 verbose_name=_('Q-in-Q SVLAN'), 

653 linkify=True 

654 ) 

655 

656 def value_ip_addresses(self, value): 

657 return ",".join([str(obj.address) for obj in value.all()]) 

658 

659 def value_tagged_vlans(self, value): 

660 return ",".join([str(obj) for obj in value.all()]) 

661 

662 

663class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpointTable): 

664 device = tables.Column( 

665 verbose_name=_('Device'), 

666 linkify={ 

667 'viewname': 'dcim:device_interfaces', 

668 'args': [Accessor('device_id')], 

669 } 

670 ) 

671 mgmt_only = columns.BooleanColumn( 

672 verbose_name=_('Management Only'), 

673 false_mark=None 

674 ) 

675 speed_formatted = columns.TemplateColumn( 

676 template_code='{% load helpers %}{{ value|humanize_speed }}', 

677 accessor=Accessor('speed'), 

678 verbose_name=_('Speed') 

679 ) 

680 wireless_link = tables.Column( 

681 verbose_name=_('Wireless link'), 

682 linkify=True 

683 ) 

684 wireless_lans = columns.TemplateColumn( 

685 template_code=INTERFACE_WIRELESS_LANS, 

686 orderable=False, 

687 verbose_name=_('Wireless LANs') 

688 ) 

689 vdcs = columns.ManyToManyColumn( 

690 linkify_item=True, 

691 verbose_name=_('VDCs') 

692 ) 

693 vrf = tables.Column( 

694 verbose_name=_('VRF'), 

695 linkify=True 

696 ) 

697 virtual_circuit_termination = tables.Column( 

698 verbose_name=_('Virtual Circuit'), 

699 linkify=True 

700 ) 

701 tags = columns.TagColumn( 

702 url_name='dcim:interface_list' 

703 ) 

704 

705 # Override PathEndpointTable.connection to accommodate virtual circuits 

706 connection = columns.TemplateColumn( 

707 accessor='_path__destinations', 

708 template_code=INTERFACE_LINKTERMINATION, 

709 verbose_name=_('Connection'), 

710 orderable=False 

711 ) 

712 

713 def value_connection(self, record, value): 

714 if record.is_virtual and hasattr(record, 'virtual_circuit_termination') and record.virtual_circuit_termination: 

715 connections = [ 

716 f"{t.interface.parent_object} > {t.interface} via {t.parent_object}" 

717 for t in record.connected_endpoints 

718 ] 

719 return ', '.join(connections) 

720 return super().value_connection(value) 

721 

722 class Meta(DeviceComponentTable.Meta): 

723 model = models.Interface 

724 fields = ( 

725 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'channels', 

726 'channel_id', 'mgmt_only', 'mtu', 'speed', 'speed_formatted', 'duplex', 'mode', 'mac_addresses', 

727 'primary_mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel', 'rf_channel_frequency', 

728 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 

729 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 

730 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'inventory_items', 'created', 'last_updated', 

731 'vlan_translation_policy', 

732 ) 

733 default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description') 

734 

735 

736class InterfaceLAGMemberTable(PathEndpointTable, NetBoxTable): 

737 parent = tables.Column( 

738 verbose_name=_('Parent'), 

739 accessor=Accessor('device'), 

740 linkify=True, 

741 ) 

742 name = tables.Column( 

743 verbose_name=_('Name'), 

744 linkify=True, 

745 order_by=('_name',), 

746 ) 

747 connection = columns.TemplateColumn( 

748 accessor='connected_endpoints', 

749 template_code=INTERFACE_LAG_MEMBERS_LINKTERMINATION, 

750 verbose_name=_('Peer'), 

751 orderable=False, 

752 ) 

753 tags = columns.TagColumn( 

754 url_name='dcim:interface_list' 

755 ) 

756 

757 class Meta(NetBoxTable.Meta): 

758 model = models.Interface 

759 fields = ('pk', 'parent', 'name', 'type', 'connection') 

760 default_columns = ('pk', 'parent', 'name', 'type', 'connection') 

761 

762 

763class DeviceInterfaceTable(InterfaceTable): 

764 name = tables.TemplateColumn( 

765 verbose_name=_('Name'), 

766 template_code='<i class="mdi mdi-{% if record.mgmt_only %}wrench{% elif record.is_lag %}reorder-horizontal' 

767 '{% elif record.is_virtual %}circle{% elif record.is_wireless %}wifi{% else %}ethernet' 

768 '{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', 

769 order_by=Accessor('_name'), 

770 attrs={'td': {'class': 'text-nowrap'}} 

771 ) 

772 parent = tables.Column( 

773 verbose_name=_('Parent'), 

774 linkify=True 

775 ) 

776 bridge = tables.Column( 

777 verbose_name=_('Bridge'), 

778 linkify=True 

779 ) 

780 lag = tables.Column( 

781 linkify=True, 

782 verbose_name=_('LAG') 

783 ) 

784 actions = columns.ActionsColumn( 

785 extra_buttons=INTERFACE_BUTTONS 

786 ) 

787 

788 class Meta(DeviceComponentTable.Meta): 

789 model = models.Interface 

790 fields = ( 

791 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag', 

792 'mgmt_only', 'mtu', 'mode', 'mac_addresses', 'primary_mac_address', 'wwn', 'rf_role', 'rf_channel', 

793 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 

794 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 

795 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 

796 'actions', 

797 ) 

798 default_columns = ( 

799 'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses', 

800 'cable', 'connection', 

801 ) 

802 row_attrs = { 

803 'data-name': lambda record: record.name, 

804 'data-enabled': lambda record: "enabled" if record.enabled else "disabled", 

805 'data-virtual': lambda record: "true" if record.is_virtual else "false", 

806 'data-mark-connected': lambda record: "true" if record.mark_connected else "false", 

807 'data-cable-status': lambda record: record.cable.status if record.cable else "", 

808 } 

809 

810 

811class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable): 

812 device = tables.Column( 

813 verbose_name=_('Device'), 

814 linkify={ 

815 'viewname': 'dcim:device_frontports', 

816 'args': [Accessor('device_id')], 

817 } 

818 ) 

819 color = columns.ColorColumn( 

820 verbose_name=_('Color'), 

821 ) 

822 mappings = columns.ManyToManyColumn( 

823 verbose_name=_('Mappings'), 

824 transform=lambda obj: f'{obj.rear_port}:{obj.rear_port_position}' 

825 ) 

826 tags = columns.TagColumn( 

827 url_name='dcim:frontport_list' 

828 ) 

829 

830 class Meta(DeviceComponentTable.Meta): 

831 model = models.FrontPort 

832 fields = ( 

833 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings', 

834 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created', 

835 'last_updated', 

836 ) 

837 default_columns = ( 

838 'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'mappings', 'description', 

839 ) 

840 

841 

842class DeviceFrontPortTable(FrontPortTable): 

843 name = tables.TemplateColumn( 

844 verbose_name=_('Name'), 

845 template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> ' 

846 '<a href="{{ record.get_absolute_url }}">{{ value }}</a>', 

847 attrs={'td': {'class': 'text-nowrap'}} 

848 ) 

849 actions = columns.ActionsColumn( 

850 extra_buttons=FRONTPORT_BUTTONS 

851 ) 

852 

853 class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): 

854 model = models.FrontPort 

855 fields = ( 

856 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings', 

857 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions', 

858 ) 

859 default_columns = ( 

860 'pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'cable', 'link_peer', 

861 ) 

862 

863 

864class RearPortTable(ModularDeviceComponentTable, CableTerminationTable): 

865 device = tables.Column( 

866 verbose_name=_('Device'), 

867 linkify={ 

868 'viewname': 'dcim:device_rearports', 

869 'args': [Accessor('device_id')], 

870 } 

871 ) 

872 color = columns.ColorColumn( 

873 verbose_name=_('Color'), 

874 ) 

875 mappings = columns.ManyToManyColumn( 

876 verbose_name=_('Mappings'), 

877 transform=lambda obj: f'{obj.front_port}:{obj.front_port_position}' 

878 ) 

879 tags = columns.TagColumn( 

880 url_name='dcim:rearport_list' 

881 ) 

882 

883 class Meta(DeviceComponentTable.Meta): 

884 model = models.RearPort 

885 fields = ( 

886 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings', 

887 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created', 

888 'last_updated', 

889 ) 

890 default_columns = ( 

891 'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'mappings', 'description', 

892 ) 

893 

894 

895class DeviceRearPortTable(RearPortTable): 

896 name = tables.TemplateColumn( 

897 verbose_name=_('Name'), 

898 template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> ' 

899 '<a href="{{ record.get_absolute_url }}">{{ value }}</a>', 

900 attrs={'td': {'class': 'text-nowrap'}} 

901 ) 

902 actions = columns.ActionsColumn( 

903 extra_buttons=REARPORT_BUTTONS 

904 ) 

905 

906 class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): 

907 model = models.RearPort 

908 fields = ( 

909 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings', 

910 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions', 

911 ) 

912 default_columns = ( 

913 'pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'cable', 'link_peer', 

914 ) 

915 

916 

917class DeviceBayTable(DeviceComponentTable): 

918 device = tables.Column( 

919 verbose_name=_('Device'), 

920 linkify={ 

921 'viewname': 'dcim:device_devicebays', 

922 'args': [Accessor('device_id')], 

923 } 

924 ) 

925 enabled = columns.BooleanColumn( 

926 verbose_name=_('Enabled'), 

927 ) 

928 status = tables.TemplateColumn( 

929 verbose_name=_('Status'), 

930 template_code=DEVICEBAY_STATUS, 

931 order_by=Accessor('installed_device__status') 

932 ) 

933 installed_device = tables.Column( 

934 verbose_name=_('Installed Device'), 

935 linkify=True 

936 ) 

937 installed_role = columns.ColoredLabelColumn( 

938 accessor=Accessor('installed_device__role'), 

939 verbose_name=_('Installed Role') 

940 ) 

941 installed_device_type = tables.Column( 

942 accessor=Accessor('installed_device__device_type'), 

943 linkify=True, 

944 verbose_name=_('Installed Type') 

945 ) 

946 installed_description = tables.Column( 

947 accessor=Accessor('installed_device__description'), 

948 verbose_name=_('Installed Description') 

949 ) 

950 installed_serial = tables.Column( 

951 accessor=Accessor('installed_device__serial'), 

952 verbose_name=_('Installed Serial') 

953 ) 

954 installed_asset_tag = tables.Column( 

955 accessor=Accessor('installed_device__asset_tag'), 

956 verbose_name=_('Installed Asset Tag') 

957 ) 

958 tags = columns.TagColumn( 

959 url_name='dcim:devicebay_list' 

960 ) 

961 

962 class Meta(DeviceComponentTable.Meta): 

963 model = models.DeviceBay 

964 fields = ( 

965 'pk', 'id', 'name', 'device', 'label', 'enabled', 'status', 'description', 'installed_device', 

966 'installed_role', 'installed_device_type', 'installed_description', 'installed_serial', 

967 'installed_asset_tag', 'tags', 'created', 'last_updated', 

968 ) 

969 

970 default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'status', 'installed_device', 'description') 

971 

972 

973class DeviceDeviceBayTable(DeviceBayTable): 

974 name = tables.TemplateColumn( 

975 verbose_name=_('Name'), 

976 template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}' 

977 '"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', 

978 attrs={'td': {'class': 'text-nowrap'}} 

979 ) 

980 enabled = columns.BooleanColumn( 

981 verbose_name=_('Enabled'), 

982 ) 

983 actions = columns.ActionsColumn( 

984 extra_buttons=DEVICEBAY_BUTTONS 

985 ) 

986 

987 class Meta(DeviceComponentTable.Meta): 

988 model = models.DeviceBay 

989 fields = ( 

990 'pk', 'id', 'name', 'label', 'enabled', 'status', 'installed_device', 'description', 'tags', 'actions', 

991 ) 

992 default_columns = ('pk', 'name', 'label', 'enabled', 'status', 'installed_device', 'description') 

993 

994 

995class ModuleBayTable(ModularDeviceComponentTable): 

996 device = tables.Column( 

997 verbose_name=_('Device'), 

998 linkify={ 

999 'viewname': 'dcim:device_modulebays', 

1000 'args': [Accessor('device_id')], 

1001 } 

1002 ) 

1003 enabled = columns.BooleanColumn( 

1004 verbose_name=_('Enabled'), 

1005 ) 

1006 parent = tables.Column( 

1007 linkify=True, 

1008 verbose_name=_('Parent'), 

1009 ) 

1010 installed_module = tables.Column( 

1011 linkify=True, 

1012 verbose_name=_('Installed Module') 

1013 ) 

1014 module_serial = tables.Column( 

1015 verbose_name=_('Module Serial'), 

1016 accessor=tables.A('installed_module__serial') 

1017 ) 

1018 module_asset_tag = tables.Column( 

1019 verbose_name=_('Module Asset Tag'), 

1020 accessor=tables.A('installed_module__asset_tag') 

1021 ) 

1022 tags = columns.TagColumn( 

1023 url_name='dcim:modulebay_list' 

1024 ) 

1025 module_status = columns.TemplateColumn( 

1026 accessor=tables.A('installed_module__status'), 

1027 template_code=MODULEBAY_STATUS, 

1028 verbose_name=_('Module Status') 

1029 ) 

1030 module_bay_types = columns.ManyToManyColumn( 

1031 verbose_name=_('Bay Types'), 

1032 linkify_item=True, 

1033 ) 

1034 

1035 class Meta(ModularDeviceComponentTable.Meta): 

1036 model = models.ModuleBay 

1037 fields = ( 

1038 'pk', 'id', 'name', 'device', 'enabled', 'parent', 'label', 'position', 'module_bay_types', 

1039 'installed_module', 'module_status', 'module_serial', 'module_asset_tag', 'description', 'tags', 

1040 ) 

1041 default_columns = ( 

1042 'pk', 'name', 'device', 'enabled', 'parent', 'label', 'installed_module', 'module_status', 'description', 

1043 ) 

1044 

1045 def render_parent_bay(self, value): 

1046 return value.name if value else '' 

1047 

1048 def render_installed_module(self, value): 

1049 return value.module_type if value else '' 

1050 

1051 

1052class DeviceModuleBayTable(ModuleBayTable): 

1053 name = columns.MPTTColumn( 

1054 verbose_name=_('Name'), 

1055 linkify=True, 

1056 ) 

1057 enabled = columns.BooleanColumn( 

1058 verbose_name=_('Enabled'), 

1059 ) 

1060 actions = columns.ActionsColumn( 

1061 extra_buttons=MODULEBAY_BUTTONS 

1062 ) 

1063 

1064 class Meta(ModuleBayTable.Meta): 

1065 model = models.ModuleBay 

1066 fields = ( 

1067 'pk', 'id', 'parent', 'name', 'label', 'enabled', 'position', 'installed_module', 'module_status', 

1068 'module_serial', 'module_asset_tag', 'description', 'tags', 'actions', 

1069 ) 

1070 default_columns = ('pk', 'name', 'label', 'enabled', 'installed_module', 'module_status', 'description') 

1071 

1072 

1073class InventoryItemTable(DeviceComponentTable): 

1074 device = tables.Column( 

1075 verbose_name=_('Device'), 

1076 linkify={ 

1077 'viewname': 'dcim:device_inventory', 

1078 'args': [Accessor('device_id')], 

1079 } 

1080 ) 

1081 role = columns.ColoredLabelColumn( 

1082 verbose_name=_('Role'), 

1083 ) 

1084 manufacturer = tables.Column( 

1085 verbose_name=_('Manufacturer'), 

1086 linkify=True 

1087 ) 

1088 component = tables.Column( 

1089 verbose_name=_('Component'), 

1090 orderable=False, 

1091 linkify=True 

1092 ) 

1093 discovered = columns.BooleanColumn( 

1094 verbose_name=_('Discovered'), 

1095 false_mark=None 

1096 ) 

1097 status = columns.ChoiceFieldColumn( 

1098 verbose_name=_('Status'), 

1099 ) 

1100 parent = tables.Column( 

1101 linkify=True, 

1102 verbose_name=_('Parent'), 

1103 ) 

1104 tags = columns.TagColumn( 

1105 url_name='dcim:inventoryitem_list' 

1106 ) 

1107 cable = None # Override DeviceComponentTable 

1108 

1109 class Meta(NetBoxTable.Meta): 

1110 model = models.InventoryItem 

1111 fields = ( 

1112 'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'status', 'role', 'manufacturer', 'part_id', 

1113 'serial', 'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated', 

1114 ) 

1115 default_columns = ( 

1116 'pk', 'name', 'device', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 

1117 ) 

1118 

1119 

1120class DeviceInventoryItemTable(InventoryItemTable): 

1121 name = tables.TemplateColumn( 

1122 verbose_name=_('Name'), 

1123 template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">' 

1124 '{{ value }}</a>', 

1125 attrs={'td': {'class': 'text-nowrap'}} 

1126 ) 

1127 

1128 class Meta(NetBoxTable.Meta): 

1129 model = models.InventoryItem 

1130 fields = ( 

1131 'pk', 'id', 'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 

1132 'component', 'description', 'discovered', 'tags', 'actions', 

1133 ) 

1134 default_columns = ( 

1135 'pk', 'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', 

1136 ) 

1137 

1138 

1139class InventoryItemRoleTable(OrganizationalModelTable): 

1140 name = tables.Column( 

1141 verbose_name=_('Name'), 

1142 linkify=True 

1143 ) 

1144 inventoryitem_count = columns.LinkedCountColumn( 

1145 viewname='dcim:inventoryitem_list', 

1146 url_params={'role_id': 'pk'}, 

1147 verbose_name=_('Items') 

1148 ) 

1149 color = columns.ColorColumn( 

1150 verbose_name=_('Color'), 

1151 ) 

1152 tags = columns.TagColumn( 

1153 url_name='dcim:inventoryitemrole_list' 

1154 ) 

1155 

1156 class Meta(OrganizationalModelTable.Meta): 

1157 model = models.InventoryItemRole 

1158 fields = ( 

1159 'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'comments', 'tags', 'actions', 

1160 ) 

1161 default_columns = ('pk', 'name', 'inventoryitem_count', 'color', 'description') 

1162 

1163 

1164# 

1165# Virtual chassis 

1166# 

1167 

1168class VirtualChassisTable(PrimaryModelTable): 

1169 name = tables.Column( 

1170 verbose_name=_('Name'), 

1171 linkify=True 

1172 ) 

1173 master = tables.Column( 

1174 verbose_name=_('Master'), 

1175 linkify=True 

1176 ) 

1177 member_count = columns.LinkedCountColumn( 

1178 viewname='dcim:device_list', 

1179 url_params={'virtual_chassis_id': 'pk'}, 

1180 verbose_name=_('Members') 

1181 ) 

1182 tags = columns.TagColumn( 

1183 url_name='dcim:virtualchassis_list' 

1184 ) 

1185 

1186 class Meta(PrimaryModelTable.Meta): 

1187 model = models.VirtualChassis 

1188 fields = ( 

1189 'pk', 'id', 'name', 'domain', 'master', 'member_count', 'description', 'comments', 'tags', 'created', 

1190 'last_updated', 

1191 ) 

1192 default_columns = ('pk', 'name', 'domain', 'master', 'member_count') 

1193 

1194 

1195class VirtualDeviceContextTable(TenancyColumnsMixin, PrimaryModelTable): 

1196 name = tables.Column( 

1197 verbose_name=_('Name'), 

1198 linkify=True 

1199 ) 

1200 device = tables.Column( 

1201 verbose_name=_('Device'), 

1202 order_by=('device__name',), 

1203 linkify=True 

1204 ) 

1205 status = columns.ChoiceFieldColumn( 

1206 verbose_name=_('Status'), 

1207 ) 

1208 primary_ip = tables.Column( 

1209 linkify=True, 

1210 order_by=('primary_ip4', 'primary_ip6'), 

1211 verbose_name=_('IP Address') 

1212 ) 

1213 primary_ip4 = tables.Column( 

1214 linkify=True, 

1215 verbose_name=_('IPv4 Address') 

1216 ) 

1217 primary_ip6 = tables.Column( 

1218 linkify=True, 

1219 verbose_name=_('IPv6 Address') 

1220 ) 

1221 interface_count = columns.LinkedCountColumn( 

1222 viewname='dcim:interface_list', 

1223 url_params={'vdc_id': 'pk'}, 

1224 verbose_name=_('Interfaces') 

1225 ) 

1226 tags = columns.TagColumn( 

1227 url_name='dcim:virtualdevicecontext_list' 

1228 ) 

1229 

1230 class Meta(PrimaryModelTable.Meta): 

1231 model = models.VirtualDeviceContext 

1232 fields = ( 

1233 'pk', 'id', 'name', 'status', 'identifier', 'tenant', 'tenant_group', 'primary_ip', 'primary_ip4', 

1234 'primary_ip6', 'comments', 'tags', 'interface_count', 'created', 'last_updated', 

1235 ) 

1236 default_columns = ( 

1237 'pk', 'name', 'identifier', 'status', 'tenant', 'primary_ip', 

1238 ) 

1239 

1240 

1241class MACAddressActionsColumn(columns.ActionsColumn): 

1242 actions = { 

1243 **columns.ActionsColumn.actions, 

1244 'set_primary': columns.ActionsItem('Set as primary', 'star-outline', None, 'warning'), 

1245 } 

1246 

1247 def render(self, record, table, **kwargs): 

1248 # Always exclude set_primary from the parent's action loop (which renders GET links). 

1249 # We inject a CSRF-protected POST form for set_primary in the dropdown below. 

1250 show_set_primary = not record.is_primary and record.assigned_object_id 

1251 original_actions = self.actions 

1252 self.actions = {k: v for k, v in original_actions.items() if k != 'set_primary'} 

1253 try: 

1254 html = super().render(record, table, **kwargs) 

1255 finally: 

1256 self.actions = original_actions 

1257 

1258 if show_set_primary and html: 

1259 request = getattr(table, 'context', {}).get('request') 

1260 if request: 

1261 url = reverse('dcim:macaddress_set_primary', kwargs={'pk': record.pk}) 

1262 # Return the user where they came from, the same way the parent's GET actions 

1263 # (edit/delete/changelog) do. In an embedded panel ObjectsTablePanel injects the parent 

1264 # object's URL as ?return_url=, so this lands on the interface; on the list view it falls 

1265 # back to the list path. 

1266 return_url = request.GET.get('return_url', request.get_full_path()) 

1267 url = f'{url}?return_url={quote(return_url)}' 

1268 # Embedded tables need their own form; list tables reuse the surrounding bulk form. 

1269 if getattr(table, 'embedded', False): 

1270 # No surrounding form: a self-contained POST form is valid and carries its own CSRF token. 

1271 action_li = format_html( 

1272 '<li><form method="post" action="{}">' 

1273 '<input type="hidden" name="csrfmiddlewaretoken" value="{}">' 

1274 '<button type="submit" class="dropdown-item">' 

1275 '<i class="mdi mdi-star-outline"></i> {}' 

1276 '</button></form></li>', 

1277 url, get_token(request), _('Set as primary'), 

1278 ) 

1279 else: 

1280 # Inside the bulk-edit <form>: a nested <form> is invalid HTML and gets dropped by the 

1281 # parser, so ride the surrounding form via formaction/formmethod instead (matching the 

1282 # DataSource sync button in core/tables/template_code.py). 

1283 action_li = format_html( 

1284 '<li><button type="submit" formaction="{}" formmethod="post" class="dropdown-item">' 

1285 '<i class="mdi mdi-star-outline"></i> {}' 

1286 '</button></li>', 

1287 url, _('Set as primary'), 

1288 ) 

1289 html_str = str(html) 

1290 if '</ul>' in html_str: 

1291 html = mark_safe(html_str.replace('</ul>', str(action_li) + '</ul>', 1)) 

1292 

1293 return html 

1294 

1295 

1296class MACAddressTable(PrimaryModelTable): 

1297 mac_address = tables.TemplateColumn( 

1298 template_code=MACADDRESS_LINK, 

1299 verbose_name=_('MAC Address') 

1300 ) 

1301 assigned_object = tables.Column( 

1302 linkify=True, 

1303 orderable=False, 

1304 verbose_name=_('Interface') 

1305 ) 

1306 assigned_object_parent = tables.Column( 

1307 accessor='assigned_object__parent_object', 

1308 linkify=True, 

1309 orderable=False, 

1310 verbose_name=_('Parent') 

1311 ) 

1312 is_primary = columns.BooleanColumn( 

1313 verbose_name=_('Primary'), 

1314 orderable=False, 

1315 ) 

1316 tags = columns.TagColumn( 

1317 url_name='dcim:macaddress_list' 

1318 ) 

1319 actions = MACAddressActionsColumn( 

1320 actions=('edit', 'delete', 'changelog', 'set_primary'), 

1321 extra_buttons=MACADDRESS_COPY_BUTTON 

1322 ) 

1323 

1324 class Meta(PrimaryModelTable.Meta): 

1325 model = models.MACAddress 

1326 fields = ( 

1327 'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'is_primary', 

1328 'comments', 'tags', 'created', 'last_updated', 

1329 ) 

1330 default_columns = ( 

1331 'pk', 'mac_address', 'is_primary', 'assigned_object_parent', 'assigned_object', 'description', 

1332 )