Coverage for ipam/fields.py: 82%
95 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
2from django.core.validators import MaxValueValidator, MinValueValidator
3from django.db import models
4from django.utils.translation import gettext as _
5from netaddr import AddrFormatError, IPNetwork
7from . import lookups, validators
8from .formfields import IPNetworkFormField
10__all__ = (
11 'ASNField',
12 'IPAddressField',
13 'IPNetworkField',
14)
16# BGP ASN bounds
17BGP_ASN_MIN = 1
18BGP_ASN_MAX = 2**32 - 1
19BGP_ASN_ASDOT_BASE = 2**16
22class BaseIPField(models.Field):
24 def python_type(self):
25 return IPNetwork
27 def from_db_value(self, value, expression, connection):
28 return self.to_python(value)
30 def get_internal_type(self):
31 return 'CharField'
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)
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))
54 def form_class(self):
55 return IPNetworkFormField
57 def formfield(self, **kwargs):
58 defaults = {'form_class': self.form_class()}
59 defaults.update(kwargs)
60 return super().formfield(**defaults)
63class IPNetworkField(BaseIPField):
64 """
65 IP prefix (network and mask)
66 """
67 description = 'PostgreSQL CIDR field'
68 default_validators = [validators.prefix_validator]
70 def db_type(self, connection):
71 return 'cidr'
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)
89class IPAddressField(BaseIPField):
90 """
91 IP address (host address and mask)
92 """
93 description = 'PostgreSQL INET field'
95 def db_type(self, connection):
96 return 'inet'
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)
120class ASNField(models.BigIntegerField):
121 description = '32-bit ASN field'
122 default_validators = [
123 MinValueValidator(BGP_ASN_MIN),
124 MaxValueValidator(BGP_ASN_MAX),
125 ]
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)
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 ''
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)