Quellcode durchsuchen

Fixes #22802: Correct available-prefixes OpenAPI request schema (#23241)

Use a schema-only serializer inheriting from PrefixSerializer to document
the supported writable fields and required prefix_length. Exclude prefix
and vrf, which are assigned during allocation.

Keep shared field definitions in one place without changing runtime
validation or allocation.
Jason Novinger vor 2 Tagen
Ursprung
Commit
573f0e47b8
3 geänderte Dateien mit 57 neuen und 4 gelöschten Zeilen
  1. 21 0
      netbox/ipam/api/serializers_/ip.py
  2. 1 1
      netbox/ipam/api/views.py
  3. 35 3
      netbox/ipam/tests/test_api.py

+ 21 - 0
netbox/ipam/api/serializers_/ip.py

@@ -22,6 +22,7 @@ __all__ = (
     'AvailableIPRequestSerializer',
     'AvailableIPSerializer',
     'AvailablePrefixSerializer',
+    'CreateAvailablePrefixSerializer',
     'IPAddressSerializer',
     'IPRangeSerializer',
     'PrefixLengthSerializer',
@@ -102,6 +103,26 @@ class PrefixLengthSerializer(serializers.Serializer):
         return data
 
 
+class CreateAvailablePrefixSerializer(PrefixSerializer):
+    """
+    Request payload for creating prefixes from the available-prefixes endpoint. The parent prefix supplies both
+    the `prefix` value (via a requested `prefix_length`) and the `vrf`, so neither is accepted here. Field
+    definitions are inherited from PrefixSerializer; only the request-specific `prefix_length` and `Meta.fields`
+    differ.
+    """
+    prefix_length = serializers.IntegerField()
+
+    class Meta(PrefixSerializer.Meta):
+        fields = [
+            'prefix_length', 'scope_type', 'scope_id', 'tenant', 'vlan', 'status', 'role', 'is_pool',
+            'mark_utilized', 'description', 'owner', 'comments', 'tags', 'custom_fields',
+        ]
+
+    def validate(self, data):
+        # Bypass model validation since we don't have an allocated prefix yet
+        return data
+
+
 class AvailablePrefixSerializer(serializers.Serializer):
     """
     Representation of a prefix which does not exist in the database.

+ 1 - 1
netbox/ipam/api/views.py

@@ -400,7 +400,7 @@ class AvailablePrefixesView(AvailableObjectsView):
     @extend_schema(
         methods=["post"],
         responses={201: serializers.PrefixSerializer(many=True)},
-        request=serializers.PrefixLengthSerializer(many=True),
+        request=serializers.CreateAvailablePrefixSerializer(many=True),
     )
     def post(self, request, pk):
         return super().post(request, pk)

+ 35 - 3
netbox/ipam/tests/test_api.py

@@ -537,6 +537,8 @@ class PrefixTestCase(APIViewTestCases.APIViewTestCase):
         Test retrieval of the first available prefix within a parent prefix.
         """
         vrf = VRF.objects.create(name='VRF 1')
+        tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1')
+        role = Role.objects.create(name='Role 1', slug='role-1')
         prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=vrf, is_pool=True)
         url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
         self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
@@ -551,13 +553,22 @@ class PrefixTestCase(APIViewTestCases.APIViewTestCase):
         for i in range(4):
             data = {
                 'prefix_length': 30,
-                'description': 'Test Prefix {}'.format(i + 1)
+                'description': 'Test Prefix {}'.format(i + 1),
+                'status': 'reserved',
+                'is_pool': True,
+                'tenant': tenant.pk,
+                'role': role.pk,
             }
             response = self.client.post(url, data, format='json', **self.header)
             self.assertHttpStatus(response, status.HTTP_201_CREATED)
             self.assertEqual(response.data['prefix'], prefixes_to_be_created[i])
             self.assertEqual(response.data['vrf']['id'], vrf.pk)
             self.assertEqual(response.data['description'], data['description'])
+            # Verify that additional writable fields advertised by the schema are accepted and persisted
+            self.assertEqual(response.data['status']['value'], 'reserved')
+            self.assertEqual(response.data['is_pool'], True)
+            self.assertEqual(response.data['tenant']['id'], tenant.pk)
+            self.assertEqual(response.data['role']['id'], role.pk)
 
         # Try to create one more prefix
         response = self.client.post(url, {'prefix_length': 30}, format='json', **self.header)
@@ -569,19 +580,35 @@ class PrefixTestCase(APIViewTestCases.APIViewTestCase):
         self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
         self.assertIn('prefix_length', response.data[0])
 
+    def test_create_available_prefix_inherits_parent_vrf(self):
+        """
+        A caller-supplied VRF is ignored: a prefix created from the available-prefixes endpoint always inherits
+        the parent prefix's VRF. This is why `vrf` is not advertised as a writable field on the request.
+        """
+        parent_vrf = VRF.objects.create(name='VRF 1')
+        other_vrf = VRF.objects.create(name='VRF 2')
+        prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=parent_vrf, is_pool=True)
+        url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
+        self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
+
+        response = self.client.post(url, {'prefix_length': 30, 'vrf': other_vrf.pk}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_201_CREATED)
+        self.assertEqual(response.data['vrf']['id'], parent_vrf.pk)
+
     def test_create_multiple_available_prefixes(self):
         """
         Test the creation of available prefixes within a parent prefix.
         """
         vrf = VRF.objects.create(name='VRF 1')
+        tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1')
         prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=vrf, is_pool=True)
         url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
         self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
 
         # Try to create five /30s (only four are available)
         data = [
-            {'prefix_length': 30, 'description': 'Prefix 1'},
-            {'prefix_length': 30, 'description': 'Prefix 2'},
+            {'prefix_length': 30, 'description': 'Prefix 1', 'status': 'reserved', 'tenant': tenant.pk},
+            {'prefix_length': 30, 'description': 'Prefix 2', 'status': 'reserved', 'tenant': tenant.pk},
             {'prefix_length': 30, 'description': 'Prefix 3'},
             {'prefix_length': 30, 'description': 'Prefix 4'},
             {'prefix_length': 30, 'description': 'Prefix 5'},
@@ -599,6 +626,11 @@ class PrefixTestCase(APIViewTestCases.APIViewTestCase):
         response = self.client.post(url, data[:4], format='json', **self.header)
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
         self.assertEqual(len(response.data), 4)
+        # Verify that additional writable fields advertised by the schema round-trip in list mode
+        self.assertEqual(response.data[0]['status']['value'], 'reserved')
+        self.assertEqual(response.data[0]['tenant']['id'], tenant.pk)
+        self.assertEqual(response.data[1]['status']['value'], 'reserved')
+        self.assertEqual(response.data[1]['tenant']['id'], tenant.pk)
 
     def test_list_available_ips(self):
         """