mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
* 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:
@@ -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
|
||||
#
|
||||
|
@@ -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
|
||||
#
|
||||
|
@@ -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(),
|
||||
|
@@ -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()
|
||||
|
||||
|
Reference in New Issue
Block a user