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

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 

5 

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 

22 

23from ...choices import * 

24from ...models import VirtualDisk, VirtualMachine, VirtualMachineType, VMInterface 

25from .clusters import ClusterSerializer 

26from .nested import NestedVMInterfaceSerializer 

27 

28__all__ = ( 

29 'VMInterfaceSerializer', 

30 'VirtualDiskSerializer', 

31 'VirtualMachineSerializer', 

32 'VirtualMachineTypeSerializer', 

33) 

34 

35 

36class VirtualMachineTypeSerializer(PrimaryModelSerializer): 

37 default_platform = PlatformSerializer(nested=True, required=False, allow_null=True) 

38 

39 # Counter fields 

40 virtual_machine_count = serializers.IntegerField(read_only=True) 

41 

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') 

50 

51 

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) 

82 

83 # Counter fields 

84 interface_count = serializers.IntegerField(read_only=True) 

85 virtual_disk_count = serializers.IntegerField(read_only=True) 

86 

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') 

97 

98 @extend_schema_field(serializers.JSONField(allow_null=True)) 

99 def get_config_context(self, obj): 

100 return obj.get_config_context() 

101 

102 

103# 

104# VM interfaces 

105# 

106 

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) 

131 

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') 

141 

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) 

149 

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 }) 

157 

158 # Validate many-to-many VLAN assignments 

159 virtual_machine = None 

160 tagged_vlans = [] 

161 

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() 

176 

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 }) 

184 

185 data = super().validate(data) 

186 

187 if mac_address is not _UNSET: 

188 data['mac_address'] = mac_address 

189 

190 return data 

191 

192 

193# 

194# Virtual Disk 

195# 

196 

197class VirtualDiskSerializer(OwnerMixin, NetBoxModelSerializer): 

198 virtual_machine = VirtualMachineSerializer(nested=True) 

199 

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')