Coverage for ipam/models/asns.py: 64%

71 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.db import models 

3from django.utils.translation import gettext_lazy as _ 

4 

5from ipam.fields import ASNField 

6from ipam.querysets import ASNRangeQuerySet 

7from netbox.models import OrganizationalModel, PrimaryModel 

8from netbox.models.features import ContactsMixin 

9 

10__all__ = ( 

11 'ASN', 

12 'ASNRange', 

13) 

14 

15 

16class ASNRange(OrganizationalModel): 

17 name = models.CharField( 

18 verbose_name=_('name'), 

19 max_length=100, 

20 unique=True, 

21 db_collation="natural_sort" 

22 ) 

23 slug = models.SlugField( 

24 verbose_name=_('slug'), 

25 max_length=100, 

26 unique=True 

27 ) 

28 rir = models.ForeignKey( 

29 to='ipam.RIR', 

30 on_delete=models.PROTECT, 

31 related_name='asn_ranges', 

32 verbose_name=_('RIR') 

33 ) 

34 start = ASNField( 

35 verbose_name=_('start'), 

36 ) 

37 end = ASNField( 

38 verbose_name=_('end'), 

39 ) 

40 tenant = models.ForeignKey( 

41 to='tenancy.Tenant', 

42 on_delete=models.PROTECT, 

43 related_name='asn_ranges', 

44 blank=True, 

45 null=True 

46 ) 

47 

48 objects = ASNRangeQuerySet.as_manager() 

49 

50 class Meta: 

51 ordering = ('name',) 

52 verbose_name = _('ASN range') 

53 verbose_name_plural = _('ASN ranges') 

54 

55 def __str__(self): 

56 return f'{self.name} ({self.range_as_string()})' 

57 

58 def clean(self): 

59 super().clean() 

60 

61 if self.end <= self.start: 

62 raise ValidationError( 

63 _("Starting ASN ({start}) must be lower than ending ASN ({end}).").format( 

64 start=self.start, end=self.end 

65 ) 

66 ) 

67 

68 @property 

69 def range(self): 

70 """ 

71 Return a range of integers representing the ASN range. 

72 """ 

73 return range(self.start, self.end + 1) 

74 

75 @property 

76 def start_asdot(self): 

77 """ 

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

79 """ 

80 return ASNField.to_asdot(self.start) 

81 

82 @property 

83 def end_asdot(self): 

84 """ 

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

86 """ 

87 return ASNField.to_asdot(self.end) 

88 

89 def range_as_string(self): 

90 """ 

91 Return a string representation of the ASN range. 

92 """ 

93 return f'{self.start}-{self.end}' 

94 

95 def range_as_string_with_asdot(self): 

96 """ 

97 Return a string representation of the ASN range, including ASDOT notation. 

98 """ 

99 if self.end >= 65536: 

100 return f'{self.range_as_string()} ({self.start_asdot}-{self.end_asdot})' 

101 return self.range_as_string() 

102 

103 def get_child_asns(self): 

104 """ 

105 Return all child ASNs (ASNs within the range). 

106 """ 

107 return ASN.objects.filter( 

108 asn__gte=self.start, 

109 asn__lte=self.end 

110 ) 

111 

112 def get_available_asns(self): 

113 """ 

114 Return all available ASNs within this range. 

115 """ 

116 range = set(self.range) 

117 existing_asns = set(self.get_child_asns().values_list('asn', flat=True)) 

118 available_asns = sorted(range - existing_asns) 

119 

120 return available_asns 

121 

122 

123class ASN(ContactsMixin, PrimaryModel): 

124 """ 

125 An autonomous system (AS) number is typically used to represent an independent routing domain. A site can have 

126 one or more ASNs assigned to it. 

127 """ 

128 rir = models.ForeignKey( 

129 to='ipam.RIR', 

130 on_delete=models.PROTECT, 

131 related_name='asns', 

132 verbose_name=_('RIR'), 

133 help_text=_("Regional Internet Registry responsible for this AS number space") 

134 ) 

135 asn = ASNField( 

136 unique=True, 

137 verbose_name=_('ASN'), 

138 help_text=_('16- or 32-bit autonomous system number') 

139 ) 

140 role = models.ForeignKey( 

141 to='ipam.Role', 

142 on_delete=models.SET_NULL, 

143 related_name='asns', 

144 blank=True, 

145 null=True, 

146 verbose_name=_('role'), 

147 help_text=_("The primary function of this ASN") 

148 ) 

149 tenant = models.ForeignKey( 

150 to='tenancy.Tenant', 

151 on_delete=models.PROTECT, 

152 related_name='asns', 

153 blank=True, 

154 null=True 

155 ) 

156 

157 prerequisite_models = ( 

158 'ipam.RIR', 

159 ) 

160 

161 class Meta: 

162 ordering = ['asn'] 

163 verbose_name = _('ASN') 

164 verbose_name_plural = _('ASNs') 

165 

166 def __str__(self): 

167 return f'AS{self.asn_with_asdot}' 

168 

169 @property 

170 def asn_asdot(self): 

171 """ 

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

173 """ 

174 return ASNField.to_asdot(self.asn) 

175 

176 @property 

177 def asn_with_asdot(self): 

178 """ 

179 Return both plain and ASDOT notation, where applicable. 

180 """ 

181 if self.asn >= 65536: 

182 return f'{self.asn} ({self.asn_asdot})' 

183 return str(self.asn) 

184 

185 @property 

186 def prefixed_name(self): 

187 """ 

188 Return the ASN with ASDOT notation prefixed with "AS". 

189 """ 

190 return f'AS{self.asn_with_asdot}'