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

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 

7 

8_UNSET = object() 

9 

10__all__ = ( 

11 '_UNSET', 

12 'MACAddressShortcutMixin', 

13) 

14 

15 

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 """ 

24 

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 

32 

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 

40 

41 if shortcut_value != primary_value: 

42 raise serializers.ValidationError( 

43 _("The provided 'mac_address' and 'primary_mac_address' values conflict.") 

44 ) 

45 

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

54 

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 

64 

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 

74 

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