Coverage for dcim/api/serializers_/mixins.py: 49%
52 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.core.exceptions import ValidationError as DjangoValidationError
2from django.db import transaction
3from django.utils.translation import gettext as _
4from netaddr import EUI, AddrFormatError
5from rest_framework import serializers
6from rest_framework.exceptions import PermissionDenied
8_UNSET = object()
10__all__ = (
11 '_UNSET',
12 'MACAddressShortcutMixin',
13)
16class MACAddressShortcutMixin:
17 """
18 Mixin for Interface and VMInterface serializers that adds a `mac_address` shortcut field for
19 creating/updating the primary MACAddress in a single request. The validated write is centralized
20 on the interface model (BaseInterface.set_primary_mac_address); this mixin is a thin adapter that
21 owns the permission check, the shortcut-vs-primary_mac_address conflict guard, and translating the
22 model's validation errors into API errors.
23 """
25 @staticmethod
26 def _validate_no_mac_conflict(data, mac_address):
27 # The mac_address shortcut and the primary_mac_address field both set the primary MAC. Reject
28 # only when both are supplied AND they disagree, so a read-modify-write round-trip (which echoes
29 # both readable fields with matching values) is accepted while a genuine conflict is rejected.
30 if mac_address is _UNSET or not isinstance(data, dict) or 'primary_mac_address' not in data: 30 ↛ 33line 30 didn't jump to line 33 because the condition on line 30 was always true
31 return
33 primary = data['primary_mac_address']
34 primary_value = primary.mac_address if primary is not None else None
35 try:
36 shortcut_value = EUI(mac_address, version=48) if mac_address is not None else None
37 except (AddrFormatError, ValueError, TypeError):
38 # Leave an invalid shortcut value to the format check in each serializer's validate().
39 return
41 if shortcut_value != primary_value:
42 raise serializers.ValidationError(
43 _("The provided 'mac_address' and 'primary_mac_address' values conflict.")
44 )
46 def _check_add_mac_permission(self, instance, mac_address):
47 # A submitted value that doesn't already exist on this interface will be created, which requires
48 # add_macaddress. An existing value is only reassigned, so it doesn't.
49 if instance is not None and instance.mac_addresses.filter(mac_address=mac_address).exists():
50 return
51 request = self.context.get('request')
52 if request and not request.user.has_perm('dcim.add_macaddress'):
53 raise PermissionDenied(_('You do not have permission to create MAC addresses.'))
55 def create(self, validated_data):
56 mac_address = validated_data.pop('mac_address', None)
57 if mac_address is not None: 57 ↛ 58line 57 didn't jump to line 58 because the condition on line 57 was never true
58 self._check_add_mac_permission(None, mac_address)
59 with transaction.atomic():
60 instance = super().create(validated_data)
61 if mac_address is not None: 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true
62 self._set_primary_mac(instance, mac_address)
63 return instance
65 def update(self, instance, validated_data):
66 mac_address = validated_data.pop('mac_address', _UNSET)
67 if mac_address not in (_UNSET, None): 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true
68 self._check_add_mac_permission(instance, mac_address)
69 with transaction.atomic():
70 instance = super().update(instance, validated_data)
71 if mac_address is not _UNSET: 71 ↛ 72line 71 didn't jump to line 72 because the condition on line 71 was never true
72 self._set_primary_mac(instance, mac_address)
73 return instance
75 def _set_primary_mac(self, instance, mac_address):
76 # Surface model/custom validation raised by the centralized operation as a DRF 400 rather than
77 # a 500 (it runs after DRF's own validation phase). The viewset's discard_events_on_rollback()
78 # clears any event queued for the rolled-back interface save.
79 try:
80 instance.set_primary_mac_address_from_value(mac_address)
81 except DjangoValidationError as e:
82 # Field-scoped errors (e.g. an interface-level check like qinq_svlan) keep their field key;
83 # non-field errors are attributed to the mac_address shortcut that triggered the operation.
84 if hasattr(e, 'error_dict'):
85 raise serializers.ValidationError(e.message_dict)
86 raise serializers.ValidationError({'mac_address': e.messages})