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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from urllib.parse import quote
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
11from dcim import models
12from netbox.tables import NestedGroupModelTable, NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
13from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
15from .template_code import *
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)
50MODULEBAY_STATUS = """
51{% badge record.installed_module.get_status_display bg_color=record.installed_module.get_status_color %}
52"""
54MACADDRESS_LINK = """
55{% if record.pk %}
56 <a href="{{ record.get_absolute_url }}" id="macaddress_{{ record.pk }}">{{ record.mac_address }}</a>
57{% endif %}
58"""
60MACADDRESS_COPY_BUTTON = """
61{% copy_content record.pk prefix="macaddress_" %}
62"""
65#
66# Device roles
67#
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 )
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')
101#
102# Platforms
103#
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 )
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 )
139#
140# Devices
141#
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 )
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 )
303#
304# Device components
305#
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 )
332 class Meta(NetBoxTable.Meta):
333 order_by = ('device', 'name')
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 )
354 class Meta(NetBoxTable.Meta):
355 pass
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 )
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 }
387 def value_link_peer(self, value):
388 return ', '.join([
389 f"{termination.parent_object} > {termination}" for termination in value
390 ])
393class PathEndpointTable(CableTerminationTable):
394 connection = columns.TemplateColumn(
395 accessor='_path__destinations',
396 template_code=LINKTERMINATION,
397 verbose_name=_('Connection'),
398 orderable=False
399 )
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
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 )
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')
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 )
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')
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 )
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')
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 )
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')
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 )
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')
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 )
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 )
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 )
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 )
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 )
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 )
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 )
656 def value_ip_addresses(self, value):
657 return ",".join([str(obj.address) for obj in value.all()])
659 def value_tagged_vlans(self, value):
660 return ",".join([str(obj) for obj in value.all()])
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 )
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 )
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)
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')
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 )
757 class Meta(NetBoxTable.Meta):
758 model = models.Interface
759 fields = ('pk', 'parent', 'name', 'type', 'connection')
760 default_columns = ('pk', 'parent', 'name', 'type', 'connection')
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 )
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 }
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
970 default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'status', 'installed_device', 'description')
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 )
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')
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 )
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 )
1045 def render_parent_bay(self, value):
1046 return value.name if value else ''
1048 def render_installed_module(self, value):
1049 return value.module_type if value else ''
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 )
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')
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
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 )
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 )
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 )
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 )
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')
1164#
1165# Virtual chassis
1166#
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 )
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')
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 )
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 )
1241class MACAddressActionsColumn(columns.ActionsColumn):
1242 actions = {
1243 **columns.ActionsColumn.actions,
1244 'set_primary': columns.ActionsItem('Set as primary', 'star-outline', None, 'warning'),
1245 }
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
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))
1293 return html
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 )
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 )