Coverage for ipam/fields.py: 82%

95 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from django.core.exceptions import ValidationError 

2from django.core.validators import MaxValueValidator, MinValueValidator 

3from django.db import models 

4from django.utils.translation import gettext as _ 

5from netaddr import AddrFormatError, IPNetwork 

6 

7from . import lookups, validators 

8from .formfields import IPNetworkFormField 

9 

10__all__ = ( 

11 'ASNField', 

12 'IPAddressField', 

13 'IPNetworkField', 

14) 

15 

16# BGP ASN bounds 

17BGP_ASN_MIN = 1 

18BGP_ASN_MAX = 2**32 - 1 

19BGP_ASN_ASDOT_BASE = 2**16 

20 

21 

22class BaseIPField(models.Field): 

23 

24 def python_type(self): 

25 return IPNetwork 

26 

27 def from_db_value(self, value, expression, connection): 

28 return self.to_python(value) 

29 

30 def get_internal_type(self): 

31 return 'CharField' 

32 

33 def to_python(self, value): 

34 if not value: 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true

35 return value 

36 try: 

37 # Always return a netaddr.IPNetwork object. (netaddr.IPAddress does not provide a mask.) 

38 return IPNetwork(value) 

39 except AddrFormatError: 

40 raise ValidationError(_("Invalid IP address format: {address}").format(address=value)) 

41 except (TypeError, ValueError) as e: 

42 raise ValidationError(e) 

43 

44 def get_prep_value(self, value): 

45 # Membership check; `not value` incorrectly treats the valid zero addresses 

46 # 0.0.0.0 and :: as empty. netaddr objects compare unequal to all three 

47 # sentinels; raw int 0 stays "empty" for backward compatibility. 

48 if value in (None, '', 0): 

49 return None 

50 if isinstance(value, list): 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true

51 return [str(self.to_python(v)) for v in value] 

52 return str(self.to_python(value)) 

53 

54 def form_class(self): 

55 return IPNetworkFormField 

56 

57 def formfield(self, **kwargs): 

58 defaults = {'form_class': self.form_class()} 

59 defaults.update(kwargs) 

60 return super().formfield(**defaults) 

61 

62 

63class IPNetworkField(BaseIPField): 

64 """ 

65 IP prefix (network and mask) 

66 """ 

67 description = 'PostgreSQL CIDR field' 

68 default_validators = [validators.prefix_validator] 

69 

70 def db_type(self, connection): 

71 return 'cidr' 

72 

73 

74IPNetworkField.register_lookup(lookups.IExact) 

75IPNetworkField.register_lookup(lookups.EndsWith) 

76IPNetworkField.register_lookup(lookups.IEndsWith) 

77IPNetworkField.register_lookup(lookups.StartsWith) 

78IPNetworkField.register_lookup(lookups.IStartsWith) 

79IPNetworkField.register_lookup(lookups.Regex) 

80IPNetworkField.register_lookup(lookups.IRegex) 

81IPNetworkField.register_lookup(lookups.NetContained) 

82IPNetworkField.register_lookup(lookups.NetContainedOrEqual) 

83IPNetworkField.register_lookup(lookups.NetContains) 

84IPNetworkField.register_lookup(lookups.NetContainsOrEquals) 

85IPNetworkField.register_lookup(lookups.NetFamily) 

86IPNetworkField.register_lookup(lookups.NetMaskLength) 

87 

88 

89class IPAddressField(BaseIPField): 

90 """ 

91 IP address (host address and mask) 

92 """ 

93 description = 'PostgreSQL INET field' 

94 

95 def db_type(self, connection): 

96 return 'inet' 

97 

98 

99IPAddressField.register_lookup(lookups.IExact) 

100IPAddressField.register_lookup(lookups.EndsWith) 

101IPAddressField.register_lookup(lookups.IEndsWith) 

102IPAddressField.register_lookup(lookups.StartsWith) 

103IPAddressField.register_lookup(lookups.IStartsWith) 

104IPAddressField.register_lookup(lookups.Regex) 

105IPAddressField.register_lookup(lookups.IRegex) 

106IPAddressField.register_lookup(lookups.NetContained) 

107IPAddressField.register_lookup(lookups.NetContainedOrEqual) 

108IPAddressField.register_lookup(lookups.NetContains) 

109IPAddressField.register_lookup(lookups.NetContainsOrEquals) 

110IPAddressField.register_lookup(lookups.NetHost) 

111IPAddressField.register_lookup(lookups.NetIn) 

112IPAddressField.register_lookup(lookups.NetHostContained) 

113IPAddressField.register_lookup(lookups.NetHostBetween) 

114IPAddressField.register_lookup(lookups.NetFamily) 

115IPAddressField.register_lookup(lookups.NetMaskLength) 

116IPAddressField.register_lookup(lookups.Host) 

117IPAddressField.register_lookup(lookups.Inet) 

118 

119 

120class ASNField(models.BigIntegerField): 

121 description = '32-bit ASN field' 

122 default_validators = [ 

123 MinValueValidator(BGP_ASN_MIN), 

124 MaxValueValidator(BGP_ASN_MAX), 

125 ] 

126 

127 def formfield(self, **kwargs): 

128 defaults = { 

129 'min_value': BGP_ASN_MIN, 

130 'max_value': BGP_ASN_MAX, 

131 } 

132 defaults.update(**kwargs) 

133 return super().formfield(**defaults) 

134 

135 @staticmethod 

136 def to_asdot(value) -> str: 

137 """ 

138 Return ASDOT notation for AS numbers greater than 16 bits. 

139 """ 

140 if value is None: 

141 return '' 

142 

143 if value >= BGP_ASN_ASDOT_BASE: 

144 hi, lo = divmod(value, BGP_ASN_ASDOT_BASE) 

145 return f'{hi}.{lo}' 

146 return str(value)