Coverage for virtualization/api/serializers_/virtualmachines.py: 66%
108 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 django.utils.translation import gettext as _
2from drf_spectacular.utils import extend_schema_field
3from netaddr import EUI, AddrFormatError
4from rest_framework import serializers
6from dcim.api.serializers_.devices import DeviceSerializer, MACAddressSerializer
7from dcim.api.serializers_.mixins import _UNSET, MACAddressShortcutMixin
8from dcim.api.serializers_.platforms import PlatformSerializer
9from dcim.api.serializers_.roles import DeviceRoleSerializer
10from dcim.api.serializers_.sites import SiteSerializer
11from dcim.choices import InterfaceModeChoices
12from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
13from ipam.api.serializers_.ip import IPAddressSerializer
14from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
15from ipam.api.serializers_.vrfs import VRFSerializer
16from ipam.models import VLAN
17from netbox.api.fields import ChoiceField, SerializedPKRelatedField
18from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer
19from tenancy.api.serializers_.tenants import TenantSerializer
20from users.api.serializers_.mixins import OwnerMixin
21from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
23from ...choices import *
24from ...models import VirtualDisk, VirtualMachine, VirtualMachineType, VMInterface
25from .clusters import ClusterSerializer
26from .nested import NestedVMInterfaceSerializer
28__all__ = (
29 'VMInterfaceSerializer',
30 'VirtualDiskSerializer',
31 'VirtualMachineSerializer',
32 'VirtualMachineTypeSerializer',
33)
36class VirtualMachineTypeSerializer(PrimaryModelSerializer):
37 default_platform = PlatformSerializer(nested=True, required=False, allow_null=True)
39 # Counter fields
40 virtual_machine_count = serializers.IntegerField(read_only=True)
42 class Meta:
43 model = VirtualMachineType
44 fields = [
45 'id', 'url', 'display_url', 'display', 'name', 'slug', 'default_platform', 'default_vcpus',
46 'default_memory', 'description', 'owner', 'comments', 'tags',
47 'custom_fields', 'created', 'last_updated', 'virtual_machine_count',
48 ]
49 brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
52class VirtualMachineSerializer(PrimaryModelSerializer):
53 virtual_machine_type = VirtualMachineTypeSerializer(nested=True, required=False, allow_null=True, default=None)
54 status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
55 start_on_boot = ChoiceField(choices=VirtualMachineStartOnBootChoices, required=False)
56 site = SiteSerializer(nested=True, required=False, allow_null=True, default=None)
57 cluster = ClusterSerializer(nested=True, required=False, allow_null=True, default=None)
58 device = DeviceSerializer(nested=True, required=False, allow_null=True, default=None)
59 role = DeviceRoleSerializer(nested=True, required=False, allow_null=True)
60 tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
61 platform = PlatformSerializer(nested=True, required=False, allow_null=True)
62 primary_ip = IPAddressSerializer(
63 nested=True,
64 read_only=True,
65 allow_null=True,
66 fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
67 )
68 primary_ip4 = IPAddressSerializer(
69 nested=True,
70 required=False,
71 allow_null=True,
72 fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
73 )
74 primary_ip6 = IPAddressSerializer(
75 nested=True,
76 required=False,
77 allow_null=True,
78 fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
79 )
80 config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
81 config_context = serializers.SerializerMethodField(read_only=True)
83 # Counter fields
84 interface_count = serializers.IntegerField(read_only=True)
85 virtual_disk_count = serializers.IntegerField(read_only=True)
87 class Meta:
88 model = VirtualMachine
89 fields = [
90 'id', 'url', 'display_url', 'display', 'name', 'virtual_machine_type', 'role', 'status', 'start_on_boot',
91 'site', 'cluster', 'device', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory',
92 'disk', 'description', 'serial', 'tenant', 'owner', 'comments', 'tags', 'local_context_data',
93 'config_template', 'custom_fields', 'created', 'last_updated', 'interface_count', 'virtual_disk_count',
94 'config_context',
95 ]
96 brief_fields = ('id', 'url', 'display', 'name', 'description')
98 @extend_schema_field(serializers.JSONField(allow_null=True))
99 def get_config_context(self, obj):
100 return obj.get_config_context()
103#
104# VM interfaces
105#
107class VMInterfaceSerializer(MACAddressShortcutMixin, OwnerMixin, NetBoxModelSerializer):
108 virtual_machine = VirtualMachineSerializer(nested=True)
109 parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
110 bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
111 mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
112 untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True)
113 tagged_vlans = SerializedPKRelatedField(
114 queryset=VLAN.objects.all(),
115 serializer=VLANSerializer,
116 nested=True,
117 required=False,
118 many=True
119 )
120 qinq_svlan = VLANSerializer(nested=True, required=False, allow_null=True)
121 vlan_translation_policy = VLANTranslationPolicySerializer(nested=True, required=False, allow_null=True)
122 vrf = VRFSerializer(nested=True, required=False, allow_null=True)
123 l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
124 count_ipaddresses = serializers.IntegerField(read_only=True)
125 count_fhrp_groups = serializers.IntegerField(read_only=True)
126 # Maintains backward compatibility with NetBox <v4.2; also accepts a MAC string on write to
127 # create/update the primary MAC address in a single request.
128 mac_address = serializers.CharField(allow_null=True, required=False)
129 primary_mac_address = MACAddressSerializer(nested=True, required=False, allow_null=True)
130 mac_addresses = MACAddressSerializer(many=True, nested=True, read_only=True, allow_null=True)
132 class Meta:
133 model = VMInterface
134 fields = [
135 'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu',
136 'mac_address', 'primary_mac_address', 'mac_addresses', 'description', 'mode', 'untagged_vlan',
137 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'l2vpn_termination', 'owner', 'tags',
138 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups',
139 ]
140 brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
142 def validate(self, data):
143 # Pop mac_address before model validation — it's a cached_property, not a model field.
144 # data may be a VMInterface instance (not a dict) in some custom field code paths (#18887).
145 mac_address = _UNSET
146 if isinstance(data, dict):
147 mac_address = data.pop('mac_address', _UNSET)
148 self._validate_no_mac_conflict(data, mac_address)
150 if not self.nested and isinstance(data, dict) and mac_address not in (_UNSET, None):
151 try:
152 EUI(mac_address, version=48)
153 except (AddrFormatError, ValueError, TypeError):
154 raise serializers.ValidationError({
155 'mac_address': _('Enter a valid MAC address (e.g. 00:11:22:33:44:55).')
156 })
158 # Validate many-to-many VLAN assignments
159 virtual_machine = None
160 tagged_vlans = []
162 # #18887
163 # There seem to be multiple code paths coming through here. Previously, we might either get
164 # the VirtualMachine instance from self.instance or from incoming data. However, #18887
165 # illustrated that this is also being called when a custom field pointing to an object_type
166 # of VMInterface is on the right side of a custom-field assignment coming in from an API
167 # request. As such, we need to check a third way to access the VirtualMachine
168 # instance--where `data` is the VMInterface instance itself and we can get the associated
169 # VirtualMachine via attribute access.
170 if isinstance(data, dict):
171 virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine')
172 tagged_vlans = data.get('tagged_vlans', [])
173 elif isinstance(data, VMInterface):
174 virtual_machine = data.virtual_machine
175 tagged_vlans = data.tagged_vlans.all()
177 if virtual_machine:
178 for vlan in tagged_vlans:
179 if vlan.site not in [virtual_machine.site, None]:
180 raise serializers.ValidationError({
181 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual "
182 f"machine, or it must be global."
183 })
185 data = super().validate(data)
187 if mac_address is not _UNSET:
188 data['mac_address'] = mac_address
190 return data
193#
194# Virtual Disk
195#
197class VirtualDiskSerializer(OwnerMixin, NetBoxModelSerializer):
198 virtual_machine = VirtualMachineSerializer(nested=True)
200 class Meta:
201 model = VirtualDisk
202 fields = [
203 'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'description', 'size', 'owner', 'tags',
204 'custom_fields', 'created', 'last_updated',
205 ]
206 brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size')