mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-21 11:08:44 -06:00
Enable specifying mask length when creating IP addresses via available-ips endpoint (#21193)
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
* Enable specifying mask length when creating IP addresses via available-ips endpoint Fixes #21144 Allow clients to specify an arbitrary mask length when creating IP addresses from a parent prefix or range using the 'next available' REST API endpoint. Changes: - Updated AvailableIPAddressesView to use PrefixLengthSerializer as write_serializer_class - Enhanced PrefixLengthSerializer to support both 'prefix' and 'parent' context keys - Added validation to ensure requested prefix_length >= parent mask_length - Updated prep_object_data to use requested prefix_length if provided, otherwise fall back to parent mask_length for backwards compatibility - Updated API schema documentation to reflect PrefixLengthSerializer usage This enables use cases like creating loopback IP addresses with /32 mask length from a parent prefix with a shorter mask length. * Refine available-ips prefix length handling Keep PrefixLengthSerializer strict for available-prefixes and introduce AvailableIPRequestSerializer for the available-ips endpoint, where prefix_length is optional and validated against the parent prefix/range. * Revert PrefixLengthSerializer to original strict state PrefixLengthSerializer should remain required and strict for the available-prefixes endpoint. The optional prefix_length functionality for available-ips is handled by AvailableIPRequestSerializer. * Add API test; misc cleanup --------- Co-authored-by: adionit7 <adionit7@users.noreply.github.com> Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
@@ -19,6 +19,7 @@ from ..field_serializers import IPAddressField, IPNetworkField
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateSerializer',
|
'AggregateSerializer',
|
||||||
'AvailableIPSerializer',
|
'AvailableIPSerializer',
|
||||||
|
'AvailableIPRequestSerializer',
|
||||||
'AvailablePrefixSerializer',
|
'AvailablePrefixSerializer',
|
||||||
'IPAddressSerializer',
|
'IPAddressSerializer',
|
||||||
'IPRangeSerializer',
|
'IPRangeSerializer',
|
||||||
@@ -147,6 +148,43 @@ class IPRangeSerializer(PrimaryModelSerializer):
|
|||||||
# IP addresses
|
# IP addresses
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class AvailableIPRequestSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
Request payload for creating IP addresses from the available-ips endpoint.
|
||||||
|
"""
|
||||||
|
prefix_length = serializers.IntegerField(required=False)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
data = super().to_internal_value(data)
|
||||||
|
|
||||||
|
prefix_length = data.get('prefix_length')
|
||||||
|
if prefix_length is None:
|
||||||
|
# No override requested; the parent prefix/range mask length will be used.
|
||||||
|
return data
|
||||||
|
|
||||||
|
parent = self.context.get('parent')
|
||||||
|
if parent is None:
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Validate the requested prefix length
|
||||||
|
if prefix_length < parent.mask_length:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'prefix_length': 'Prefix length must be greater than or equal to the parent mask length ({})'.format(
|
||||||
|
parent.mask_length
|
||||||
|
)
|
||||||
|
})
|
||||||
|
elif parent.family == 4 and prefix_length > 32:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'prefix_length': 'Invalid prefix length ({}) for IPv6'.format(prefix_length)
|
||||||
|
})
|
||||||
|
elif parent.family == 6 and prefix_length > 128:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'prefix_length': 'Invalid prefix length ({}) for IPv4'.format(prefix_length)
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class IPAddressSerializer(PrimaryModelSerializer):
|
class IPAddressSerializer(PrimaryModelSerializer):
|
||||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||||
address = IPAddressField()
|
address = IPAddressField()
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ class AvailablePrefixesView(AvailableObjectsView):
|
|||||||
class AvailableIPAddressesView(AvailableObjectsView):
|
class AvailableIPAddressesView(AvailableObjectsView):
|
||||||
queryset = IPAddress.objects.all()
|
queryset = IPAddress.objects.all()
|
||||||
read_serializer_class = serializers.AvailableIPSerializer
|
read_serializer_class = serializers.AvailableIPSerializer
|
||||||
write_serializer_class = serializers.AvailableIPSerializer
|
write_serializer_class = serializers.AvailableIPRequestSerializer
|
||||||
advisory_lock_key = 'available-ips'
|
advisory_lock_key = 'available-ips'
|
||||||
|
|
||||||
def get_available_objects(self, parent, limit=None):
|
def get_available_objects(self, parent, limit=None):
|
||||||
@@ -421,8 +421,9 @@ class AvailableIPAddressesView(AvailableObjectsView):
|
|||||||
def prep_object_data(self, requested_objects, available_objects, parent):
|
def prep_object_data(self, requested_objects, available_objects, parent):
|
||||||
available_ips = iter(available_objects)
|
available_ips = iter(available_objects)
|
||||||
for i, request_data in enumerate(requested_objects):
|
for i, request_data in enumerate(requested_objects):
|
||||||
|
prefix_length = request_data.pop('prefix_length', None) or parent.mask_length
|
||||||
request_data.update({
|
request_data.update({
|
||||||
'address': f'{next(available_ips)}/{parent.mask_length}',
|
'address': f'{next(available_ips)}/{prefix_length}',
|
||||||
'vrf': parent.vrf.pk if parent.vrf else None,
|
'vrf': parent.vrf.pk if parent.vrf else None,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -435,7 +436,7 @@ class AvailableIPAddressesView(AvailableObjectsView):
|
|||||||
@extend_schema(
|
@extend_schema(
|
||||||
methods=["post"],
|
methods=["post"],
|
||||||
responses={201: serializers.IPAddressSerializer(many=True)},
|
responses={201: serializers.IPAddressSerializer(many=True)},
|
||||||
request=serializers.IPAddressSerializer(many=True),
|
request=serializers.AvailableIPRequestSerializer(many=True),
|
||||||
)
|
)
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
return super().post(request, pk)
|
return super().post(request, pk)
|
||||||
|
|||||||
@@ -595,6 +595,31 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(len(response.data), 8)
|
self.assertEqual(len(response.data), 8)
|
||||||
|
|
||||||
|
def test_create_available_ip_with_mask(self):
|
||||||
|
"""
|
||||||
|
Test the creation of an available IP address with a specific prefix length.
|
||||||
|
"""
|
||||||
|
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/24'))
|
||||||
|
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
|
||||||
|
self.add_permissions('ipam.view_prefix', 'ipam.add_ipaddress')
|
||||||
|
|
||||||
|
# Create an available IP with a specific prefix length
|
||||||
|
data = {
|
||||||
|
'prefix_length': 32,
|
||||||
|
'description': 'Test IP 1',
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response.data['address'], '192.0.2.1/32')
|
||||||
|
self.assertEqual(response.data['description'], data['description'])
|
||||||
|
|
||||||
|
# Attempt to create an available IP with a prefix length less than its parent prefix
|
||||||
|
data = {
|
||||||
|
'prefix_length': 23, # Prefix is a /24
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@tag('regression')
|
@tag('regression')
|
||||||
def test_graphql_tenant_prefixes_contains_nested_skips_invalid(self):
|
def test_graphql_tenant_prefixes_contains_nested_skips_invalid(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user