Ver Fonte

Closes #20309: Add ASDOT notation support for ASN ranges (#21004)

* feat(ipam): Add ASDOT notation support for ASN ranges

Introduces ASDOT notation for ASN Ranges to improve readability of large
AS numbers. Adds `start_asdot` and `end_asdot` properties, columns, and
display logic for ASN ranges in the UI.

Fixes #20309

* Wrap "ASDOT" with parentheses in column header

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Martin Hauser há 1 mês atrás
pai
commit
f7219e0672

+ 14 - 0
netbox/ipam/fields.py

@@ -16,6 +16,7 @@ __all__ = (
 # BGP ASN bounds
 BGP_ASN_MIN = 1
 BGP_ASN_MAX = 2**32 - 1
+BGP_ASN_ASDOT_BASE = 2**16
 
 
 class BaseIPField(models.Field):
@@ -126,3 +127,16 @@ class ASNField(models.BigIntegerField):
         }
         defaults.update(**kwargs)
         return super().formfield(**defaults)
+
+    @staticmethod
+    def to_asdot(value) -> str:
+        """
+        Return ASDOT notation for AS numbers greater than 16 bits.
+        """
+        if value is None:
+            return ''
+
+        if value >= BGP_ASN_ASDOT_BASE:
+            hi, lo = divmod(value, BGP_ASN_ASDOT_BASE)
+            return f'{hi}.{lo}'
+        return str(value)

+ 45 - 14
netbox/ipam/models/asns.py

@@ -55,13 +55,6 @@ class ASNRange(OrganizationalModel):
     def __str__(self):
         return f'{self.name} ({self.range_as_string()})'
 
-    @property
-    def range(self):
-        return range(self.start, self.end + 1)
-
-    def range_as_string(self):
-        return f'{self.start}-{self.end}'
-
     def clean(self):
         super().clean()
 
@@ -72,7 +65,45 @@ class ASNRange(OrganizationalModel):
                 )
             )
 
+    @property
+    def range(self):
+        """
+        Return a range of integers representing the ASN range.
+        """
+        return range(self.start, self.end + 1)
+
+    @property
+    def start_asdot(self):
+        """
+        Return ASDOT notation for AS numbers greater than 16 bits.
+        """
+        return ASNField.to_asdot(self.start)
+
+    @property
+    def end_asdot(self):
+        """
+        Return ASDOT notation for AS numbers greater than 16 bits.
+        """
+        return ASNField.to_asdot(self.end)
+
+    def range_as_string(self):
+        """
+        Return a string representation of the ASN range.
+        """
+        return f'{self.start}-{self.end}'
+
+    def range_as_string_with_asdot(self):
+        """
+        Return a string representation of the ASN range, including ASDOT notation.
+        """
+        if self.end >= 65536:
+            return f'{self.range_as_string()} ({self.start_asdot}-{self.end_asdot})'
+        return self.range_as_string()
+
     def get_child_asns(self):
+        """
+        Return all child ASNs (ASNs within the range).
+        """
         return ASN.objects.filter(
             asn__gte=self.start,
             asn__lte=self.end
@@ -131,20 +162,20 @@ class ASN(ContactsMixin, PrimaryModel):
         """
         Return ASDOT notation for AS numbers greater than 16 bits.
         """
-        if self.asn > 65535:
-            return f'{self.asn // 65536}.{self.asn % 65536}'
-        return self.asn
+        return ASNField.to_asdot(self.asn)
 
     @property
     def asn_with_asdot(self):
         """
         Return both plain and ASDOT notation, where applicable.
         """
-        if self.asn > 65535:
-            return f'{self.asn} ({self.asn // 65536}.{self.asn % 65536})'
-        else:
-            return self.asn
+        if self.asn >= 65536:
+            return f'{self.asn} ({self.asn_asdot})'
+        return str(self.asn)
 
     @property
     def prefixed_name(self):
+        """
+        Return the ASN with ASDOT notation prefixed with "AS".
+        """
         return f'AS{self.asn_with_asdot}'

+ 12 - 2
netbox/ipam/tables/asn.py

@@ -20,6 +20,16 @@ class ASNRangeTable(TenancyColumnsMixin, NetBoxTable):
         verbose_name=_('RIR'),
         linkify=True
     )
+    start_asdot = tables.Column(
+        accessor=tables.A('start_asdot'),
+        order_by=tables.A('start'),
+        verbose_name=_('Start (ASDOT)')
+    )
+    end_asdot = tables.Column(
+        accessor=tables.A('end_asdot'),
+        order_by=tables.A('end'),
+        verbose_name=_('End (ASDOT)')
+    )
     tags = columns.TagColumn(
         url_name='ipam:asnrange_list'
     )
@@ -30,8 +40,8 @@ class ASNRangeTable(TenancyColumnsMixin, NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = ASNRange
         fields = (
-            'pk', 'name', 'slug', 'rir', 'start', 'end', 'asn_count', 'tenant', 'tenant_group', 'description', 'tags',
-            'created', 'last_updated', 'actions',
+            'pk', 'name', 'slug', 'rir', 'start', 'start_asdot', 'end', 'end_asdot', 'asn_count', 'tenant',
+            'tenant_group', 'description', 'tags', 'created', 'last_updated', 'actions',
         )
         default_columns = ('pk', 'name', 'rir', 'start', 'end', 'tenant', 'asn_count', 'description')
 

+ 1 - 1
netbox/templates/ipam/asnrange.html

@@ -23,7 +23,7 @@
           </tr>
           <tr>
             <th scope="row">{% trans "Range" %}</th>
-            <td>{{ object.range_as_string }}</td>
+            <td>{{ object.range_as_string_with_asdot }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "Tenant" %}</th>