mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-20 18:48:45 -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__ = (
|
||||
'AggregateSerializer',
|
||||
'AvailableIPSerializer',
|
||||
'AvailableIPRequestSerializer',
|
||||
'AvailablePrefixSerializer',
|
||||
'IPAddressSerializer',
|
||||
'IPRangeSerializer',
|
||||
@@ -147,6 +148,43 @@ class IPRangeSerializer(PrimaryModelSerializer):
|
||||
# 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):
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
address = IPAddressField()
|
||||
|
||||
@@ -400,7 +400,7 @@ class AvailablePrefixesView(AvailableObjectsView):
|
||||
class AvailableIPAddressesView(AvailableObjectsView):
|
||||
queryset = IPAddress.objects.all()
|
||||
read_serializer_class = serializers.AvailableIPSerializer
|
||||
write_serializer_class = serializers.AvailableIPSerializer
|
||||
write_serializer_class = serializers.AvailableIPRequestSerializer
|
||||
advisory_lock_key = 'available-ips'
|
||||
|
||||
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):
|
||||
available_ips = iter(available_objects)
|
||||
for i, request_data in enumerate(requested_objects):
|
||||
prefix_length = request_data.pop('prefix_length', None) or parent.mask_length
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -435,7 +436,7 @@ class AvailableIPAddressesView(AvailableObjectsView):
|
||||
@extend_schema(
|
||||
methods=["post"],
|
||||
responses={201: serializers.IPAddressSerializer(many=True)},
|
||||
request=serializers.IPAddressSerializer(many=True),
|
||||
request=serializers.AvailableIPRequestSerializer(many=True),
|
||||
)
|
||||
def post(self, request, pk):
|
||||
return super().post(request, pk)
|
||||
|
||||
@@ -595,6 +595,31 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
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')
|
||||
def test_graphql_tenant_prefixes_contains_nested_skips_invalid(self):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user