Răsfoiți Sursa

Fix race condition in available-prefix/ip APIs

Implement advisory lock to prevent duplicate records being inserted
when making simultaneous calls. Fixes #2519
Matt Olenik 6 ani în urmă
părinte
comite
2e83ce76ed
4 a modificat fișierele cu 26 adăugiri și 0 ștergeri
  1. 4 0
      base_requirements.txt
  2. 10 0
      netbox/ipam/api/views.py
  3. 11 0
      netbox/utilities/constants.py
  4. 1 0
      requirements.txt

+ 4 - 0
base_requirements.txt

@@ -22,6 +22,10 @@ django-filter
 # https://github.com/django-mptt/django-mptt
 # https://github.com/django-mptt/django-mptt
 django-mptt
 django-mptt
 
 
+# Context managers for PostgreSQL advisory locks
+# https://github.com/Xof/django-pglocks
+django-pglocks
+
 # Prometheus metrics library for Django
 # Prometheus metrics library for Django
 # https://github.com/korfuri/django-prometheus
 # https://github.com/korfuri/django-prometheus
 django-prometheus
 django-prometheus

+ 10 - 0
netbox/ipam/api/views.py

@@ -1,6 +1,7 @@
 from django.conf import settings
 from django.conf import settings
 from django.db.models import Count
 from django.db.models import Count
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
+from django_pglocks import advisory_lock
 from rest_framework import status
 from rest_framework import status
 from rest_framework.decorators import action
 from rest_framework.decorators import action
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.exceptions import PermissionDenied
@@ -10,6 +11,7 @@ from extras.api.views import CustomFieldModelViewSet
 from ipam import filters
 from ipam import filters
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from utilities.api import FieldChoicesViewSet, ModelViewSet
 from utilities.api import FieldChoicesViewSet, ModelViewSet
+from utilities.constants import ADVISORY_LOCK_KEYS
 from utilities.utils import get_subquery
 from utilities.utils import get_subquery
 from . import serializers
 from . import serializers
 
 
@@ -86,9 +88,13 @@ class PrefixViewSet(CustomFieldModelViewSet):
     filterset_class = filters.PrefixFilterSet
     filterset_class = filters.PrefixFilterSet
 
 
     @action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
     @action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
+    @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
     def available_prefixes(self, request, pk=None):
     def available_prefixes(self, request, pk=None):
         """
         """
         A convenience method for returning available child prefixes within a parent.
         A convenience method for returning available child prefixes within a parent.
+
+        The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
+        invoked in parallel, which results in a race condition where multiple insertions can occur.
         """
         """
         prefix = get_object_or_404(Prefix, pk=pk)
         prefix = get_object_or_404(Prefix, pk=pk)
         available_prefixes = prefix.get_available_prefixes()
         available_prefixes = prefix.get_available_prefixes()
@@ -180,11 +186,15 @@ class PrefixViewSet(CustomFieldModelViewSet):
             return Response(serializer.data)
             return Response(serializer.data)
 
 
     @action(detail=True, url_path='available-ips', methods=['get', 'post'])
     @action(detail=True, url_path='available-ips', methods=['get', 'post'])
+    @advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
     def available_ips(self, request, pk=None):
     def available_ips(self, request, pk=None):
         """
         """
         A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
         A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
         returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed,
         returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed,
         however results will not be paginated.
         however results will not be paginated.
+
+        The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
+        invoked in parallel, which results in a race condition where multiple insertions can occur.
         """
         """
         prefix = get_object_or_404(Prefix, pk=pk)
         prefix = get_object_or_404(Prefix, pk=pk)
 
 

+ 11 - 0
netbox/utilities/constants.py

@@ -27,3 +27,14 @@ COLOR_CHOICES = (
     ('111111', 'Black'),
     ('111111', 'Black'),
     ('ffffff', 'White'),
     ('ffffff', 'White'),
 )
 )
+
+# Keys for PostgreSQL advisory locks. These are arbitrary bigints used by
+# the advisory_lock contextmanager. When a lock is acquired,
+# one of these keys will be used to identify said lock.
+#
+# When adding a new key, pick something arbitrary and unique so
+# that it is easily searchable in query logs.
+ADVISORY_LOCK_KEYS = {
+    'available-prefixes': 100100,
+    'available-ips': 100200,
+}

+ 1 - 0
requirements.txt

@@ -4,6 +4,7 @@ django-cors-headers==3.2.1
 django-debug-toolbar==2.1
 django-debug-toolbar==2.1
 django-filter==2.2.0
 django-filter==2.2.0
 django-mptt==0.9.1
 django-mptt==0.9.1
+django-pglocks==1.0.4
 django-prometheus==1.1.0
 django-prometheus==1.1.0
 django-rq==2.2.0
 django-rq==2.2.0
 django-tables2==2.2.1
 django-tables2==2.2.1