| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- import decimal
- from django.contrib.contenttypes.models import ContentType
- from django.utils.translation import gettext as _
- from drf_spectacular.utils import extend_schema_field
- from rest_framework import serializers
- from dcim.choices import *
- from dcim.constants import MACADDRESS_ASSIGNMENT_MODELS, MODULE_TOKEN
- from dcim.models import Device, DeviceBay, MACAddress, Module, VirtualDeviceContext
- from dcim.utils import get_module_bay_positions, resolve_module_placeholder
- from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
- from ipam.api.serializers_.ip import IPAddressSerializer
- from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
- from netbox.api.gfk_fields import GFKSerializerField
- from netbox.api.serializers import PrimaryModelSerializer
- from tenancy.api.serializers_.tenants import TenantSerializer
- from virtualization.api.serializers_.clusters import ClusterSerializer
- from .devicetypes import *
- from .nested import NestedDeviceBaySerializer, NestedDeviceSerializer, NestedModuleBaySerializer
- from .platforms import PlatformSerializer
- from .racks import RackSerializer
- from .roles import DeviceRoleSerializer
- from .sites import LocationSerializer, SiteSerializer
- from .virtualchassis import VirtualChassisSerializer
- __all__ = (
- 'DeviceSerializer',
- 'MACAddressSerializer',
- 'ModuleSerializer',
- 'VirtualDeviceContextSerializer',
- )
- class DeviceSerializer(PrimaryModelSerializer):
- device_type = DeviceTypeSerializer(nested=True)
- role = DeviceRoleSerializer(nested=True)
- tenant = TenantSerializer(
- nested=True,
- required=False,
- allow_null=True,
- default=None
- )
- platform = PlatformSerializer(nested=True, required=False, allow_null=True)
- site = SiteSerializer(nested=True)
- location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
- rack = RackSerializer(nested=True, required=False, allow_null=True, default=None)
- face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '')
- position = serializers.DecimalField(
- max_digits=4,
- decimal_places=1,
- allow_null=True,
- label=_('Position (U)'),
- min_value=decimal.Decimal(0.5),
- default=None
- )
- status = ChoiceField(choices=DeviceStatusChoices, required=False)
- airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
- primary_ip = IPAddressSerializer(
- nested=True,
- read_only=True,
- allow_null=True,
- fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
- )
- primary_ip4 = IPAddressSerializer(
- nested=True,
- required=False,
- allow_null=True,
- fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
- )
- primary_ip6 = IPAddressSerializer(
- nested=True,
- required=False,
- allow_null=True,
- fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
- )
- oob_ip = IPAddressSerializer(
- nested=True,
- required=False,
- allow_null=True,
- fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
- )
- parent_device = serializers.SerializerMethodField()
- cluster = ClusterSerializer(nested=True, required=False, allow_null=True)
- virtual_chassis = VirtualChassisSerializer(nested=True, required=False, allow_null=True, default=None)
- vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None)
- config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
- config_context = serializers.SerializerMethodField(read_only=True)
- # Counter fields
- console_port_count = serializers.IntegerField(read_only=True)
- console_server_port_count = serializers.IntegerField(read_only=True)
- power_port_count = serializers.IntegerField(read_only=True)
- power_outlet_count = serializers.IntegerField(read_only=True)
- interface_count = serializers.IntegerField(read_only=True)
- front_port_count = serializers.IntegerField(read_only=True)
- rear_port_count = serializers.IntegerField(read_only=True)
- device_bay_count = serializers.IntegerField(read_only=True)
- module_bay_count = serializers.IntegerField(read_only=True)
- inventory_item_count = serializers.IntegerField(read_only=True)
- class Meta:
- model = Device
- fields = [
- 'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
- 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
- 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
- 'vc_position', 'vc_priority', 'description', 'owner', 'comments', 'config_template', 'config_context',
- 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
- 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
- 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
- ]
- brief_fields = ('id', 'url', 'display', 'name', 'description')
- @extend_schema_field(NestedDeviceSerializer(allow_null=True))
- def get_parent_device(self, obj):
- try:
- device_bay = obj.parent_bay
- except DeviceBay.DoesNotExist:
- return None
- context = {'request': self.context['request']}
- data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
- data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
- return data
- @extend_schema_field(serializers.JSONField(allow_null=True))
- def get_config_context(self, obj):
- return obj.get_config_context()
- class VirtualDeviceContextSerializer(PrimaryModelSerializer):
- device = DeviceSerializer(nested=True)
- identifier = serializers.IntegerField(allow_null=True, max_value=32767, min_value=0, required=False, default=None)
- tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
- primary_ip = IPAddressSerializer(
- nested=True,
- read_only=True,
- allow_null=True,
- fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
- )
- primary_ip4 = IPAddressSerializer(
- nested=True,
- required=False,
- allow_null=True,
- fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
- )
- primary_ip6 = IPAddressSerializer(
- nested=True,
- required=False,
- allow_null=True,
- fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
- )
- status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
- # Related object counts
- interface_count = RelatedObjectCountField('interfaces')
- class Meta:
- model = VirtualDeviceContext
- fields = [
- 'id', 'url', 'display_url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip',
- 'primary_ip4', 'primary_ip6', 'status', 'description', 'owner', 'comments', 'tags', 'custom_fields',
- 'created', 'last_updated', 'interface_count',
- ]
- brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description')
- class ModuleSerializer(PrimaryModelSerializer):
- device = DeviceSerializer(nested=True)
- module_bay = NestedModuleBaySerializer()
- module_type = ModuleTypeSerializer(nested=True)
- status = ChoiceField(choices=ModuleStatusChoices, required=False)
- replicate_components = serializers.BooleanField(
- required=False,
- default=True,
- write_only=True,
- label=_('Replicate components'),
- help_text=_('Automatically populate components associated with this module type (default: true)')
- )
- adopt_components = serializers.BooleanField(
- required=False,
- default=False,
- write_only=True,
- label=_('Adopt components'),
- help_text=_('Adopt already existing components')
- )
- is_bay_compatible = serializers.BooleanField(read_only=True)
- class Meta:
- model = Module
- fields = [
- 'id', 'url', 'display_url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial',
- 'asset_tag', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
- 'replicate_components', 'adopt_components', 'is_bay_compatible',
- ]
- brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')
- def validate(self, data):
- # When used as a nested serializer (e.g. as the `module` field on device component
- # serializers), `data` is already a resolved Module instance — skip our custom logic.
- if self.nested:
- return super().validate(data)
- # Pop write-only transient fields before ValidatedModelSerializer tries to
- # construct a Module instance for full_clean(); restore them afterwards.
- replicate_components = data.pop('replicate_components', True)
- adopt_components = data.pop('adopt_components', False)
- if self.instance is not None:
- # Derive device from module_bay so full_clean() validates a consistent pair.
- if 'module_bay' in data and 'device' not in data:
- data['device'] = data['module_bay'].device
- move_requested = (
- ('module_bay' in data and data['module_bay'].pk != self.instance.module_bay_id) or
- ('device' in data and data['device'].pk != self.instance.device_id)
- )
- if move_requested and 'module_type' in data and data['module_type'].pk != self.instance.module_type_id:
- raise serializers.ValidationError({
- 'module_type': _(
- "Changing a module's type while moving it is not supported. Change the module "
- "type and move the module as separate operations."
- )
- })
- data = super().validate(data)
- # For updates these fields are not meaningful; omit them from validated_data so that
- # ModelSerializer.update() does not set unexpected attributes on the instance.
- if self.instance:
- return data
- # Always pass the flags to create() so it can set the correct private attributes.
- data['replicate_components'] = replicate_components
- data['adopt_components'] = adopt_components
- # Skip conflict checks when no component operations are requested.
- if not replicate_components and not adopt_components:
- return data
- device = data.get('device')
- module_type = data.get('module_type')
- module_bay = data.get('module_bay')
- # Required-field validation fires separately; skip here if any are missing.
- if not all([device, module_type, module_bay]):
- return data
- try:
- positions = get_module_bay_positions(module_bay)
- except ValueError as e:
- raise serializers.ValidationError({'module_bay': str(e)}) from e
- for templates_attr, component_attr in [
- ('consoleporttemplates', 'consoleports'),
- ('consoleserverporttemplates', 'consoleserverports'),
- ('interfacetemplates', 'interfaces'),
- ('powerporttemplates', 'powerports'),
- ('poweroutlettemplates', 'poweroutlets'),
- ('rearporttemplates', 'rearports'),
- ('frontporttemplates', 'frontports'),
- ]:
- installed_components = {
- component.name: component
- for component in getattr(device, component_attr).all()
- }
- for template in getattr(module_type, templates_attr).all():
- resolved_name = template.name
- if MODULE_TOKEN in template.name:
- if not module_bay.position:
- raise serializers.ValidationError(
- _("Cannot install module with placeholder values in a module bay with no position defined.")
- )
- try:
- resolved_name = resolve_module_placeholder(template.name, positions)
- except ValueError as e:
- raise serializers.ValidationError(str(e))
- existing_item = installed_components.get(resolved_name)
- if adopt_components and existing_item and existing_item.module:
- raise serializers.ValidationError(
- _("Cannot adopt {model} {name} as it already belongs to a module").format(
- model=template.component_model.__name__,
- name=resolved_name
- )
- )
- if not adopt_components and resolved_name in installed_components:
- raise serializers.ValidationError(
- _("A {model} named {name} already exists").format(
- model=template.component_model.__name__,
- name=resolved_name
- )
- )
- return data
- def create(self, validated_data):
- replicate_components = validated_data.pop('replicate_components', True)
- adopt_components = validated_data.pop('adopt_components', False)
- # Tags are handled after save; pop them here to pass to _save_tags()
- tags = validated_data.pop('tags', None)
- # _adopt_components and _disable_replication must be set on the instance before
- # save() is called, so we cannot delegate to super().create() here.
- instance = self.Meta.model(**validated_data)
- if adopt_components:
- instance._adopt_components = True
- if not replicate_components:
- instance._disable_replication = True
- instance.save()
- if tags is not None:
- self._save_tags(instance, tags)
- return instance
- class MACAddressSerializer(PrimaryModelSerializer):
- assigned_object_type = ContentTypeField(
- queryset=ContentType.objects.filter(MACADDRESS_ASSIGNMENT_MODELS),
- required=False,
- allow_null=True
- )
- assigned_object = GFKSerializerField(read_only=True)
- is_primary = serializers.BooleanField(read_only=True)
- class Meta:
- model = MACAddress
- fields = [
- 'id', 'url', 'display_url', 'display', 'mac_address', 'assigned_object_type', 'assigned_object_id',
- 'assigned_object', 'is_primary', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created',
- 'last_updated',
- ]
- brief_fields = ('id', 'url', 'display', 'mac_address', 'description')
|