mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-12 12:09:50 +02:00
Add an `enabled` boolean field to ModuleBay, ModuleBayTemplate, DeviceBay, and DeviceBayTemplate models. Disabled bays prevent component installation and display accordingly in the UI. Update serializers, filters, forms, and tables to support the new field. Fixes #20152
473 lines
18 KiB
Python
473 lines
18 KiB
Python
from django.contrib.contenttypes.models import ContentType
|
|
from django.utils.translation import gettext as _
|
|
from rest_framework import serializers
|
|
|
|
from dcim.choices import *
|
|
from dcim.constants import *
|
|
from dcim.models import (
|
|
ConsolePort,
|
|
ConsoleServerPort,
|
|
DeviceBay,
|
|
FrontPort,
|
|
Interface,
|
|
InventoryItem,
|
|
ModuleBay,
|
|
PortMapping,
|
|
PowerOutlet,
|
|
PowerPort,
|
|
RearPort,
|
|
VirtualDeviceContext,
|
|
)
|
|
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
|
|
from ipam.api.serializers_.vrfs import VRFSerializer
|
|
from ipam.models import VLAN
|
|
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
|
from netbox.api.gfk_fields import GFKSerializerField
|
|
from netbox.api.serializers import NetBoxModelSerializer
|
|
from users.api.serializers_.mixins import OwnerMixin
|
|
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
|
|
from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
|
|
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
|
|
from wireless.choices import *
|
|
from wireless.models import WirelessLAN
|
|
|
|
from .base import ConnectedEndpointsSerializer, PortSerializer
|
|
from .cables import CabledObjectSerializer
|
|
from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer
|
|
from .manufacturers import ManufacturerSerializer
|
|
from .nested import NestedInterfaceSerializer
|
|
from .roles import InventoryItemRoleSerializer
|
|
|
|
__all__ = (
|
|
'ConsolePortSerializer',
|
|
'ConsoleServerPortSerializer',
|
|
'DeviceBaySerializer',
|
|
'FrontPortSerializer',
|
|
'InterfaceSerializer',
|
|
'InventoryItemSerializer',
|
|
'ModuleBaySerializer',
|
|
'PowerOutletSerializer',
|
|
'PowerPortSerializer',
|
|
'RearPortSerializer',
|
|
)
|
|
|
|
|
|
class ConsoleServerPortSerializer(
|
|
OwnerMixin,
|
|
NetBoxModelSerializer,
|
|
CabledObjectSerializer,
|
|
ConnectedEndpointsSerializer
|
|
):
|
|
device = DeviceSerializer(nested=True)
|
|
module = ModuleSerializer(
|
|
nested=True,
|
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
type = ChoiceField(
|
|
choices=ConsolePortTypeChoices,
|
|
allow_blank=True,
|
|
required=False
|
|
)
|
|
speed = ChoiceField(
|
|
choices=ConsolePortSpeedChoices,
|
|
allow_null=True,
|
|
required=False
|
|
)
|
|
|
|
class Meta:
|
|
model = ConsoleServerPort
|
|
fields = [
|
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
|
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created',
|
|
'last_updated', '_occupied',
|
|
]
|
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
|
|
|
|
|
class ConsolePortSerializer(
|
|
OwnerMixin,
|
|
NetBoxModelSerializer,
|
|
CabledObjectSerializer,
|
|
ConnectedEndpointsSerializer
|
|
):
|
|
device = DeviceSerializer(nested=True)
|
|
module = ModuleSerializer(
|
|
nested=True,
|
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
type = ChoiceField(
|
|
choices=ConsolePortTypeChoices,
|
|
allow_blank=True,
|
|
required=False
|
|
)
|
|
speed = ChoiceField(
|
|
choices=ConsolePortSpeedChoices,
|
|
allow_null=True,
|
|
required=False
|
|
)
|
|
|
|
class Meta:
|
|
model = ConsolePort
|
|
fields = [
|
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
|
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created',
|
|
'last_updated', '_occupied',
|
|
]
|
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
|
|
|
|
|
class PowerPortSerializer(
|
|
OwnerMixin,
|
|
NetBoxModelSerializer,
|
|
CabledObjectSerializer,
|
|
ConnectedEndpointsSerializer
|
|
):
|
|
device = DeviceSerializer(nested=True)
|
|
module = ModuleSerializer(
|
|
nested=True,
|
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
type = ChoiceField(
|
|
choices=PowerPortTypeChoices,
|
|
allow_blank=True,
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
|
|
class Meta:
|
|
model = PowerPort
|
|
fields = [
|
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw',
|
|
'allocated_draw', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
|
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags',
|
|
'custom_fields', 'created', 'last_updated', '_occupied',
|
|
]
|
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
|
|
|
|
|
class PowerOutletSerializer(
|
|
OwnerMixin,
|
|
NetBoxModelSerializer,
|
|
CabledObjectSerializer,
|
|
ConnectedEndpointsSerializer
|
|
):
|
|
device = DeviceSerializer(nested=True)
|
|
module = ModuleSerializer(
|
|
nested=True,
|
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
type = ChoiceField(
|
|
choices=PowerOutletTypeChoices,
|
|
allow_blank=True,
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
power_port = PowerPortSerializer(
|
|
nested=True,
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
feed_leg = ChoiceField(
|
|
choices=PowerOutletFeedLegChoices,
|
|
allow_blank=True,
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
status = ChoiceField(choices=PowerOutletStatusChoices, required=False)
|
|
|
|
class Meta:
|
|
model = PowerOutlet
|
|
fields = [
|
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'status', 'color',
|
|
'power_port', 'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
|
'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
|
|
'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
|
]
|
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
|
|
|
|
|
class InterfaceSerializer(
|
|
OwnerMixin,
|
|
NetBoxModelSerializer,
|
|
CabledObjectSerializer,
|
|
ConnectedEndpointsSerializer
|
|
):
|
|
device = DeviceSerializer(nested=True)
|
|
vdcs = SerializedPKRelatedField(
|
|
queryset=VirtualDeviceContext.objects.all(),
|
|
serializer=VirtualDeviceContextSerializer,
|
|
nested=True,
|
|
required=False,
|
|
many=True
|
|
)
|
|
module = ModuleSerializer(
|
|
nested=True,
|
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
type = ChoiceField(choices=InterfaceTypeChoices)
|
|
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
|
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
|
bridge_interfaces = NestedInterfaceSerializer(many=True, read_only=True)
|
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
|
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
|
|
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
|
|
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
|
|
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
|
|
poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True)
|
|
poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True)
|
|
untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True)
|
|
tagged_vlans = SerializedPKRelatedField(
|
|
queryset=VLAN.objects.all(),
|
|
serializer=VLANSerializer,
|
|
nested=True,
|
|
required=False,
|
|
many=True
|
|
)
|
|
qinq_svlan = VLANSerializer(nested=True, required=False, allow_null=True)
|
|
vlan_translation_policy = VLANTranslationPolicySerializer(nested=True, required=False, allow_null=True)
|
|
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
|
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
|
|
wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True)
|
|
wireless_lans = SerializedPKRelatedField(
|
|
queryset=WirelessLAN.objects.all(),
|
|
serializer=WirelessLANSerializer,
|
|
nested=True,
|
|
required=False,
|
|
many=True
|
|
)
|
|
count_ipaddresses = serializers.IntegerField(read_only=True)
|
|
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
|
# Maintains backward compatibility with NetBox <v4.2
|
|
mac_address = serializers.CharField(allow_null=True, read_only=True)
|
|
primary_mac_address = MACAddressSerializer(nested=True, required=False, allow_null=True)
|
|
mac_addresses = MACAddressSerializer(many=True, nested=True, read_only=True, allow_null=True)
|
|
wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True)
|
|
|
|
class Meta:
|
|
model = Interface
|
|
fields = [
|
|
'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled',
|
|
'parent', 'bridge', 'bridge_interfaces', 'lag', 'mtu', 'mac_address', 'primary_mac_address',
|
|
'mac_addresses', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
|
|
'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan',
|
|
'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'mark_connected', 'cable', 'cable_end',
|
|
'wireless_link', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination',
|
|
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags',
|
|
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
|
]
|
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
|
|
|
def validate(self, data):
|
|
|
|
if not self.nested:
|
|
|
|
# Validate 802.1q mode and vlan(s)
|
|
mode = None
|
|
tagged_vlans = []
|
|
|
|
# Gather Information
|
|
if self.instance:
|
|
mode = data.get('mode') if 'mode' in data.keys() else self.instance.mode
|
|
untagged_vlan = data.get('untagged_vlan') if 'untagged_vlan' in data.keys() else \
|
|
self.instance.untagged_vlan
|
|
qinq_svlan = data.get('qinq_svlan') if 'qinq_svlan' in data.keys() else \
|
|
self.instance.qinq_svlan
|
|
tagged_vlans = data.get('tagged_vlans') if 'tagged_vlans' in data.keys() else \
|
|
self.instance.tagged_vlans.all()
|
|
else:
|
|
mode = data.get('mode', None)
|
|
untagged_vlan = data.get('untagged_vlan') if 'untagged_vlan' in data.keys() else None
|
|
qinq_svlan = data.get('qinq_svlan') if 'qinq_svlan' in data.keys() else None
|
|
tagged_vlans = data.get('tagged_vlans') if 'tagged_vlans' in data.keys() else None
|
|
|
|
errors = {}
|
|
|
|
# Non Q-in-Q mode with service vlan set
|
|
if mode != InterfaceModeChoices.MODE_Q_IN_Q and qinq_svlan:
|
|
errors.update({
|
|
'qinq_svlan': _("Interface mode does not support q-in-q service vlan")
|
|
})
|
|
# Routed mode
|
|
if not mode:
|
|
# Untagged vlan
|
|
if untagged_vlan:
|
|
errors.update({
|
|
'untagged_vlan': _("Interface mode does not support untagged vlan")
|
|
})
|
|
# Tagged vlan
|
|
if tagged_vlans:
|
|
errors.update({
|
|
'tagged_vlans': _("Interface mode does not support tagged vlans")
|
|
})
|
|
# Non-tagged mode
|
|
elif mode in (InterfaceModeChoices.MODE_TAGGED_ALL, InterfaceModeChoices.MODE_ACCESS) and tagged_vlans:
|
|
errors.update({
|
|
'tagged_vlans': _("Interface mode does not support tagged vlans")
|
|
})
|
|
|
|
if errors:
|
|
raise serializers.ValidationError(errors)
|
|
|
|
# Validate many-to-many VLAN assignments
|
|
device = self.instance.device if self.instance else data.get('device')
|
|
for vlan in data.get('tagged_vlans', []):
|
|
if vlan.site not in [device.site, None]:
|
|
raise serializers.ValidationError({
|
|
'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, "
|
|
f"or it must be global."
|
|
})
|
|
|
|
return super().validate(data)
|
|
|
|
|
|
class RearPortMappingSerializer(serializers.ModelSerializer):
|
|
position = serializers.IntegerField(
|
|
source='rear_port_position'
|
|
)
|
|
front_port = serializers.PrimaryKeyRelatedField(
|
|
queryset=FrontPort.objects.all(),
|
|
)
|
|
|
|
class Meta:
|
|
model = PortMapping
|
|
fields = ('position', 'front_port', 'front_port_position')
|
|
|
|
|
|
class RearPortSerializer(OwnerMixin, NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
|
|
device = DeviceSerializer(nested=True)
|
|
module = ModuleSerializer(
|
|
nested=True,
|
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
type = ChoiceField(choices=PortTypeChoices)
|
|
front_ports = RearPortMappingSerializer(
|
|
source='mappings',
|
|
many=True,
|
|
required=False,
|
|
)
|
|
|
|
class Meta:
|
|
model = RearPort
|
|
fields = [
|
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
|
|
'front_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
|
'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
|
]
|
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
|
|
|
|
|
class FrontPortMappingSerializer(serializers.ModelSerializer):
|
|
position = serializers.IntegerField(
|
|
source='front_port_position'
|
|
)
|
|
rear_port = serializers.PrimaryKeyRelatedField(
|
|
queryset=RearPort.objects.all(),
|
|
)
|
|
|
|
class Meta:
|
|
model = PortMapping
|
|
fields = ('position', 'rear_port', 'rear_port_position')
|
|
|
|
|
|
class FrontPortSerializer(OwnerMixin, NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
|
|
device = DeviceSerializer(nested=True)
|
|
module = ModuleSerializer(
|
|
nested=True,
|
|
fields=('id', 'url', 'display', 'device', 'module_bay'),
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
type = ChoiceField(choices=PortTypeChoices)
|
|
rear_ports = FrontPortMappingSerializer(
|
|
source='mappings',
|
|
many=True,
|
|
required=False,
|
|
)
|
|
|
|
class Meta:
|
|
model = FrontPort
|
|
fields = [
|
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
|
|
'rear_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
|
'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
|
]
|
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
|
|
|
|
|
class ModuleBaySerializer(OwnerMixin, NetBoxModelSerializer):
|
|
device = DeviceSerializer(nested=True)
|
|
module = ModuleSerializer(
|
|
nested=True,
|
|
fields=('id', 'url', 'display'),
|
|
required=False,
|
|
allow_null=True,
|
|
default=None
|
|
)
|
|
installed_module = ModuleSerializer(
|
|
nested=True,
|
|
fields=('id', 'url', 'display', 'serial', 'description'),
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
_occupied = serializers.BooleanField(required=False, read_only=True)
|
|
|
|
class Meta:
|
|
model = ModuleBay
|
|
fields = [
|
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'position', 'enabled',
|
|
'description', 'installed_module', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
|
]
|
|
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'enabled', 'description', '_occupied')
|
|
|
|
|
|
class DeviceBaySerializer(OwnerMixin, NetBoxModelSerializer):
|
|
device = DeviceSerializer(nested=True)
|
|
installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
|
|
_occupied = serializers.BooleanField(required=False, read_only=True)
|
|
|
|
class Meta:
|
|
model = DeviceBay
|
|
fields = [
|
|
'id', 'url', 'display_url', 'display', 'device', 'name', 'label', 'enabled', 'description',
|
|
'installed_device', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
|
]
|
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'enabled', 'description', '_occupied',)
|
|
|
|
|
|
class InventoryItemSerializer(OwnerMixin, NetBoxModelSerializer):
|
|
device = DeviceSerializer(nested=True)
|
|
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
|
role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
|
|
manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True, default=None)
|
|
component_type = ContentTypeField(
|
|
queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS),
|
|
required=False,
|
|
allow_null=True
|
|
)
|
|
component = GFKSerializerField(read_only=True)
|
|
_depth = serializers.IntegerField(source='level', read_only=True)
|
|
status = ChoiceField(choices=InventoryItemStatusChoices, required=False)
|
|
|
|
class Meta:
|
|
model = InventoryItem
|
|
fields = [
|
|
'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'status', 'role',
|
|
'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type',
|
|
'component_id', 'component', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
|
|
]
|
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth')
|