1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #8550: Implement ASN ranges (#11835)

* Move ASN to a separate module

* Move ASNField from dcim to ipam

* Introduce ASNRange model

* Add relationship from ASN to ASNRange

* Add an available-asns API endpoint

* Add RIR assignment for ASNRange

* Add standard tests

* Move child ASNs to a tabbed view

* Remove FK on ASN to ASNRange

* Add tests for provisioning available ASNs

* Add docs for ASNRange
This commit is contained in:
Jeremy Stretch
2023-02-27 16:36:05 -05:00
committed by GitHub
parent e4e4d0c0ec
commit 7994073687
41 changed files with 1096 additions and 246 deletions

View File

@@ -7,6 +7,7 @@ from netbox.api.serializers import WritableNestedSerializer
__all__ = [
'NestedAggregateSerializer',
'NestedASNSerializer',
'NestedASNRangeSerializer',
'NestedFHRPGroupSerializer',
'NestedFHRPGroupAssignmentSerializer',
'NestedIPAddressSerializer',
@@ -25,6 +26,18 @@ __all__ = [
]
#
# ASN ranges
#
class NestedASNRangeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
class Meta:
model = models.ASNRange
fields = ['id', 'url', 'display', 'name']
#
# ASNs
#

View File

@@ -15,15 +15,31 @@ from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
from .nested_serializers import *
#
# ASN ranges
#
class ASNRangeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
rir = NestedRIRSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
asn_count = serializers.IntegerField(read_only=True)
class Meta:
model = ASNRange
fields = [
'id', 'url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'tags',
'custom_fields', 'created', 'last_updated', 'asn_count',
]
#
# ASNs
#
from .nested_serializers import NestedL2VPNSerializer
from ..models.l2vpn import L2VPNTermination, L2VPN
class ASNSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
rir = NestedRIRSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
site_count = serializers.IntegerField(read_only=True)
provider_count = serializers.IntegerField(read_only=True)
@@ -36,6 +52,22 @@ class ASNSerializer(NetBoxModelSerializer):
]
class AvailableASNSerializer(serializers.Serializer):
"""
Representation of an ASN which does not exist in the database.
"""
asn = serializers.IntegerField(read_only=True)
def to_representation(self, asn):
rir = NestedRIRSerializer(self.context['range'].rir, context={
'request': self.context['request']
}).data
return {
'rir': rir,
'asn': asn,
}
#
# VRFs
#

View File

@@ -7,50 +7,33 @@ from . import views
router = NetBoxRouter()
router.APIRootView = views.IPAMRootView
# ASNs
router.register('asns', views.ASNViewSet)
# VRFs
router.register('asn-ranges', views.ASNRangeViewSet)
router.register('vrfs', views.VRFViewSet)
# Route targets
router.register('route-targets', views.RouteTargetViewSet)
# RIRs
router.register('rirs', views.RIRViewSet)
# Aggregates
router.register('aggregates', views.AggregateViewSet)
# Prefixes
router.register('roles', views.RoleViewSet)
router.register('prefixes', views.PrefixViewSet)
# IP ranges
router.register('ip-ranges', views.IPRangeViewSet)
# IP addresses
router.register('ip-addresses', views.IPAddressViewSet)
# FHRP groups
router.register('fhrp-groups', views.FHRPGroupViewSet)
router.register('fhrp-group-assignments', views.FHRPGroupAssignmentViewSet)
# VLANs
router.register('vlan-groups', views.VLANGroupViewSet)
router.register('vlans', views.VLANViewSet)
# Services
router.register('service-templates', views.ServiceTemplateViewSet)
router.register('services', views.ServiceViewSet)
# L2VPN
router.register('l2vpns', views.L2VPNViewSet)
router.register('l2vpn-terminations', views.L2VPNTerminationViewSet)
app_name = 'ipam-api'
urlpatterns = [
path(
'asn-ranges/<int:pk>/available-asns/',
views.AvailableASNsView.as_view(),
name='asnrange-available-asns'
),
path(
'ip-ranges/<int:pk>/available-ips/',
views.IPRangeAvailableIPAddressesView.as_view(),

View File

@@ -33,6 +33,12 @@ class IPAMRootView(APIRootView):
# Viewsets
#
class ASNRangeViewSet(NetBoxModelViewSet):
queryset = ASNRange.objects.prefetch_related('tenant', 'rir').all()
serializer_class = serializers.ASNRangeSerializer
filterset_class = filtersets.ASNRangeFilterSet
class ASNViewSet(NetBoxModelViewSet):
queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(
site_count=count_related(Site, 'asns'),
@@ -201,6 +207,74 @@ def get_results_limit(request):
return limit
class AvailableASNsView(ObjectValidationMixin, APIView):
queryset = ASN.objects.all()
@swagger_auto_schema(responses={200: serializers.AvailableASNSerializer(many=True)})
def get(self, request, pk):
asnrange = get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk)
limit = get_results_limit(request)
available_asns = asnrange.get_available_asns()[:limit]
serializer = serializers.AvailableASNSerializer(available_asns, many=True, context={
'request': request,
'range': asnrange,
})
return Response(serializer.data)
@swagger_auto_schema(
request_body=serializers.AvailableASNSerializer,
responses={201: serializers.ASNSerializer(many=True)}
)
@advisory_lock(ADVISORY_LOCK_KEYS['available-asns'])
def post(self, request, pk):
self.queryset = self.queryset.restrict(request.user, 'add')
asnrange = get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk)
# Normalize to a list of objects
requested_asns = request.data if isinstance(request.data, list) else [request.data]
# Determine if the requested number of IPs is available
available_asns = asnrange.get_available_asns()
if len(available_asns) < len(requested_asns):
return Response(
{
"detail": f"An insufficient number of ASNs are available within {asnrange} "
f"({len(requested_asns)} requested, {len(available_asns)} available)"
},
status=status.HTTP_409_CONFLICT
)
# Assign ASNs from the list of available IPs and copy VRF assignment from the parent
for i, requested_asn in enumerate(requested_asns):
requested_asn.update({
'rir': asnrange.rir.pk,
'range': asnrange.pk,
'asn': available_asns[i],
})
# Initialize the serializer with a list or a single object depending on what was requested
context = {'request': request}
if isinstance(request.data, list):
serializer = serializers.ASNSerializer(data=requested_asns, many=True, context=context)
else:
serializer = serializers.ASNSerializer(data=requested_asns[0], context=context)
# Create the new IP address(es)
if serializer.is_valid():
try:
with transaction.atomic():
created = serializer.save()
self._validate_objects(created)
except ObjectDoesNotExist:
raise PermissionDenied()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class AvailablePrefixesView(ObjectValidationMixin, APIView):
queryset = Prefix.objects.all()