Browse Source

Merge "Add `additive_only` parameter to Batch Member call"

changes/44/681144/3
Zuul 1 week ago
parent
commit
510525a92b

+ 6
- 0
api-ref/source/parameters.yaml View File

@@ -76,6 +76,12 @@ path-provider:
76 76
 ###############################################################################
77 77
 # Query fields
78 78
 ###############################################################################
79
+additive-only:
80
+  description: |
81
+    If ``true`` no members will be deleted during the batch operation.
82
+  in: query
83
+  required: false
84
+  type: boolean
79 85
 cascade-delete:
80 86
   description: |
81 87
     If ``true`` will delete all child objects of the load balancer.

+ 8
- 0
api-ref/source/v2/member.inc View File

@@ -367,11 +367,18 @@ For example, assume a pool currently has two members. These members have the
367 367
 following address/port combinations: '192.0.2.15:80' and '192.0.2.16:80'.
368 368
 Now assume a PUT request is made that includes members with address/port
369 369
 combinations: '192.0.2.16:80' and '192.0.2.17:80'.
370
+
370 371
 The member '192.0.2.15:80' will be deleted, because it was not in the request.
372
+
371 373
 The member '192.0.2.16:80' will be updated to match the request data for that
372 374
 member, because it was matched.
375
+
373 376
 The member '192.0.2.17:80' will be created, because no such member existed.
374 377
 
378
+The optional parameter ``additive_only`` when defined as ``true`` will skip
379
+deletions for members missing from the provided list. If this were set in the
380
+above example, the member '192.0.2.15:80' would have remained in the pool.
381
+
375 382
 If the request is valid, the service returns the ``Accepted (202)``
376 383
 response code. To confirm the updates, check that the member provisioning
377 384
 statuses are ``ACTIVE`` for new or updated members, and that any unspecified
@@ -397,6 +404,7 @@ Request
397 404
 
398 405
 .. rest_parameters:: ../parameters.yaml
399 406
 
407
+   - additive_only: additive-only
400 408
    - admin_state_up: admin_state_up-default-optional
401 409
    - address: address
402 410
    - backup: backup-optional

+ 4
- 1
octavia/api/root_controller.py View File

@@ -79,6 +79,9 @@ class RootController(rest.RestController):
79 79
         self._add_a_version(versions, 'v2.9', 'v2', 'SUPPORTED',
80 80
                             '2019-03-04T00:00:00Z', host_url)
81 81
         # Healthmonitor host header
82
-        self._add_a_version(versions, 'v2.10', 'v2', 'CURRENT',
82
+        self._add_a_version(versions, 'v2.10', 'v2', 'SUPPORTED',
83 83
                             '2019-03-05T00:00:00Z', host_url)
84
+        # Additive batch member update
85
+        self._add_a_version(versions, 'v2.11', 'v2', 'CURRENT',
86
+                            '2019-06-24T00:00:00Z', host_url)
84 87
         return {'versions': versions}

+ 30
- 12
octavia/api/v2/controllers/member.py View File

@@ -16,6 +16,7 @@
16 16
 from oslo_db import exception as odb_exceptions
17 17
 from oslo_log import log as logging
18 18
 from oslo_utils import excutils
19
+from oslo_utils import strutils
19 20
 import pecan
20 21
 from wsme import types as wtypes
21 22
 from wsmeext import pecan as wsme_pecan
@@ -322,9 +323,10 @@ class MembersController(MemberController):
322 323
 
323 324
     @wsme_pecan.wsexpose(None, wtypes.text,
324 325
                          body=member_types.MembersRootPUT, status_code=202)
325
-    def put(self, members_):
326
+    def put(self, additive_only=False, members_=None):
326 327
         """Updates all members."""
327 328
         members = members_.members
329
+        additive_only = strutils.bool_from_string(additive_only)
328 330
         context = pecan.request.context.get('octavia_context')
329 331
 
330 332
         db_pool = self._get_db_pool(context.session, self.pool_id)
@@ -336,7 +338,9 @@ class MembersController(MemberController):
336 338
         # Check POST+PUT+DELETE since this operation is all of 'CUD'
337 339
         self._auth_validate_action(context, project_id, constants.RBAC_POST)
338 340
         self._auth_validate_action(context, project_id, constants.RBAC_PUT)
339
-        self._auth_validate_action(context, project_id, constants.RBAC_DELETE)
341
+        if not additive_only:
342
+            self._auth_validate_action(context, project_id,
343
+                                       constants.RBAC_DELETE)
340 344
 
341 345
         # Validate member subnets
342 346
         for member in members:
@@ -351,13 +355,6 @@ class MembersController(MemberController):
351 355
         with db_api.get_lock_session() as lock_session:
352 356
             self._test_lb_and_listener_and_pool_statuses(lock_session)
353 357
 
354
-            member_count_diff = len(members) - len(old_members)
355
-            if member_count_diff > 0 and self.repositories.check_quota_met(
356
-                    context.session, lock_session, data_models.Member,
357
-                    db_pool.project_id, count=member_count_diff):
358
-                raise exceptions.QuotaException(
359
-                    resource=data_models.Member._name())
360
-
361 358
             old_member_uniques = {
362 359
                 (m.ip_address, m.protocol_port): m.id for m in old_members}
363 360
             new_member_uniques = [
@@ -380,6 +377,16 @@ class MembersController(MemberController):
380 377
                 if (m.ip_address, m.protocol_port) not in new_member_uniques:
381 378
                     deleted_members.append(m)
382 379
 
380
+            if additive_only:
381
+                member_count_diff = len(new_members)
382
+            else:
383
+                member_count_diff = len(new_members) - len(deleted_members)
384
+            if member_count_diff > 0 and self.repositories.check_quota_met(
385
+                    context.session, lock_session, data_models.Member,
386
+                    db_pool.project_id, count=member_count_diff):
387
+                raise exceptions.QuotaException(
388
+                    resource=data_models.Member._name())
389
+
383 390
             provider_members = []
384 391
             # Create new members
385 392
             for m in new_members:
@@ -392,6 +399,7 @@ class MembersController(MemberController):
392 399
             # Update old members
393 400
             for m in updated_members:
394 401
                 m.provisioning_status = constants.PENDING_UPDATE
402
+                m.project_id = db_pool.project_id
395 403
                 db_member_dict = m.to_dict(render_unsets=False)
396 404
                 db_member_dict.pop('id')
397 405
                 self.repositories.member.update(
@@ -402,9 +410,19 @@ class MembersController(MemberController):
402 410
                     driver_utils.db_member_to_provider_member(m))
403 411
             # Delete old members
404 412
             for m in deleted_members:
405
-                self.repositories.member.update(
406
-                    lock_session, m.id,
407
-                    provisioning_status=constants.PENDING_DELETE)
413
+                if additive_only:
414
+                    # Members are appended to the dict and their status remains
415
+                    # unchanged, because they are logically "untouched".
416
+                    db_member_dict = m.to_dict(render_unsets=False)
417
+                    db_member_dict.pop('id')
418
+                    m.pool_id = self.pool_id
419
+                    provider_members.append(
420
+                        driver_utils.db_member_to_provider_member(m))
421
+                else:
422
+                    # Members are changed to PENDING_DELETE and not passed.
423
+                    self.repositories.member.update(
424
+                        lock_session, m.id,
425
+                        provisioning_status=constants.PENDING_DELETE)
408 426
 
409 427
             # Dispatch to the driver
410 428
             LOG.info("Sending Pool %s batch member update to provider %s",

+ 2
- 1
octavia/tests/functional/api/test_root_controller.py View File

@@ -43,7 +43,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
43 43
     def test_api_versions(self):
44 44
         versions = self._get_versions_with_config()
45 45
         version_ids = tuple(v.get('id') for v in versions)
46
-        self.assertEqual(11, len(version_ids))
46
+        self.assertEqual(12, len(version_ids))
47 47
         self.assertIn('v2.0', version_ids)
48 48
         self.assertIn('v2.1', version_ids)
49 49
         self.assertIn('v2.2', version_ids)
@@ -55,6 +55,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
55 55
         self.assertIn('v2.8', version_ids)
56 56
         self.assertIn('v2.9', version_ids)
57 57
         self.assertIn('v2.10', version_ids)
58
+        self.assertIn('v2.11', version_ids)
58 59
 
59 60
         # Each version should have a 'self' 'href' to the API version URL
60 61
         # [{u'rel': u'self', u'href': u'http://localhost/v2'}]

+ 71
- 8
octavia/tests/functional/api/v2/test_member.py View File

@@ -620,7 +620,6 @@ class TestMember(base.BaseAPITest):
620 620
             ('192.0.2.6', 80, 'PENDING_CREATE'),
621 621
         ]
622 622
 
623
-        member_ids = {}
624 623
         provider_creates = []
625 624
         provider_updates = []
626 625
         for rm in returned_members:
@@ -628,7 +627,6 @@ class TestMember(base.BaseAPITest):
628 627
                 (rm['address'],
629 628
                  rm['protocol_port'],
630 629
                  rm['provisioning_status']), expected_members)
631
-            member_ids[(rm['address'], rm['protocol_port'])] = rm['id']
632 630
 
633 631
             provider_dict = driver_utils.member_dict_to_provider_dict(rm)
634 632
             # Adjust for API response
@@ -672,7 +670,6 @@ class TestMember(base.BaseAPITest):
672 670
             ('192.0.2.6', 80, 'PENDING_CREATE', ['test_tag2']),
673 671
         ]
674 672
 
675
-        member_ids = {}
676 673
         provider_members = []
677 674
         for rm in returned_members:
678 675
             self.assertIn(
@@ -680,7 +677,6 @@ class TestMember(base.BaseAPITest):
680 677
                  rm['protocol_port'],
681 678
                  rm['provisioning_status'],
682 679
                  rm['tags']), expected_members)
683
-            member_ids[(rm['address'], rm['protocol_port'])] = rm['id']
684 680
 
685 681
             provider_dict = driver_utils.member_dict_to_provider_dict(rm)
686 682
             # Adjust for API response
@@ -723,6 +719,77 @@ class TestMember(base.BaseAPITest):
723 719
         err_msg = ("169.254.169.254 is not a valid option for member address")
724 720
         self.assertEqual(err_msg, response.get('faultstring'))
725 721
 
722
+    @mock.patch('octavia.api.drivers.driver_factory.get_driver')
723
+    @mock.patch('octavia.api.drivers.utils.call_provider')
724
+    def test_additive_only_batch_members(self, mock_provider, mock_get_driver):
725
+        mock_driver = mock.MagicMock()
726
+        mock_driver.name = 'noop_driver'
727
+        mock_get_driver.return_value = mock_driver
728
+
729
+        member1 = {'address': '192.0.2.1', 'protocol_port': 80}
730
+        member2 = {'address': '192.0.2.2', 'protocol_port': 80}
731
+        member3 = {'address': '192.0.2.3', 'protocol_port': 80}
732
+        member4 = {'address': '192.0.2.4', 'protocol_port': 80}
733
+        member5 = {'address': '192.0.2.5', 'protocol_port': 80}
734
+        member6 = {'address': '192.0.2.6', 'protocol_port': 80}
735
+        members = [member1, member2, member3, member4]
736
+        for m in members:
737
+            self.create_member(pool_id=self.pool_id, **m)
738
+            self.set_lb_status(self.lb_id)
739
+
740
+        # We are only concerned about the batch update, so clear out the
741
+        # create members calls above.
742
+        mock_provider.reset_mock()
743
+
744
+        req_dict = [member1, member2, member5, member6]
745
+        body = {self.root_tag_list: req_dict}
746
+        path = self.MEMBERS_PATH.format(pool_id=self.pool_id)
747
+        path = "{}?additive_only=True".format(path)
748
+        self.put(path, body, status=202)
749
+        returned_members = self.get(
750
+            self.MEMBERS_PATH.format(pool_id=self.pool_id)
751
+        ).json.get(self.root_tag_list)
752
+
753
+        # Members 1+2 should be updated, 3+4 left alone, and 5+6 created
754
+        expected_members = [
755
+            ('192.0.2.1', 80, 'PENDING_UPDATE'),
756
+            ('192.0.2.2', 80, 'PENDING_UPDATE'),
757
+            ('192.0.2.3', 80, 'ACTIVE'),
758
+            ('192.0.2.4', 80, 'ACTIVE'),
759
+            ('192.0.2.5', 80, 'PENDING_CREATE'),
760
+            ('192.0.2.6', 80, 'PENDING_CREATE'),
761
+        ]
762
+
763
+        provider_creates = []
764
+        provider_updates = []
765
+        provider_ignored = []
766
+        for rm in returned_members:
767
+            self.assertIn(
768
+                (rm['address'],
769
+                 rm['protocol_port'],
770
+                 rm['provisioning_status']), expected_members)
771
+
772
+            provider_dict = driver_utils.member_dict_to_provider_dict(rm)
773
+            # Adjust for API response
774
+            provider_dict['pool_id'] = self.pool_id
775
+            if rm['provisioning_status'] == 'PENDING_UPDATE':
776
+                del provider_dict['name']
777
+                del provider_dict['subnet_id']
778
+                provider_updates.append(driver_dm.Member(**provider_dict))
779
+            elif rm['provisioning_status'] == 'PENDING_CREATE':
780
+                provider_dict['name'] = None
781
+                provider_creates.append(driver_dm.Member(**provider_dict))
782
+            elif rm['provisioning_status'] == 'ACTIVE':
783
+                provider_dict['name'] = None
784
+                provider_ignored.append(driver_dm.Member(**provider_dict))
785
+        # Order matters here
786
+        provider_creates += provider_updates
787
+        provider_creates += provider_ignored
788
+
789
+        mock_provider.assert_called_once_with(u'noop_driver',
790
+                                              mock_driver.member_batch_update,
791
+                                              provider_creates)
792
+
726 793
     @mock.patch('octavia.api.drivers.driver_factory.get_driver')
727 794
     @mock.patch('octavia.api.drivers.utils.call_provider')
728 795
     def test_update_batch_members(self, mock_provider, mock_get_driver):
@@ -756,14 +823,12 @@ class TestMember(base.BaseAPITest):
756 823
             ('192.0.2.2', 80, 'PENDING_UPDATE'),
757 824
         ]
758 825
 
759
-        member_ids = {}
760 826
         provider_members = []
761 827
         for rm in returned_members:
762 828
             self.assertIn(
763 829
                 (rm['address'],
764 830
                  rm['protocol_port'],
765 831
                  rm['provisioning_status']), expected_members)
766
-            member_ids[(rm['address'], rm['protocol_port'])] = rm['id']
767 832
 
768 833
             provider_dict = driver_utils.member_dict_to_provider_dict(rm)
769 834
             # Adjust for API response
@@ -807,14 +872,12 @@ class TestMember(base.BaseAPITest):
807 872
             ('192.0.2.4', 80, 'PENDING_DELETE'),
808 873
         ]
809 874
 
810
-        member_ids = {}
811 875
         provider_members = []
812 876
         for rm in returned_members:
813 877
             self.assertIn(
814 878
                 (rm['address'],
815 879
                  rm['protocol_port'],
816 880
                  rm['provisioning_status']), expected_members)
817
-            member_ids[(rm['address'], rm['protocol_port'])] = rm['id']
818 881
 
819 882
         mock_provider.assert_called_once_with(u'noop_driver',
820 883
                                               mock_driver.member_batch_update,

+ 6
- 0
releasenotes/notes/make-batch-member-call-additive-4785163e625fed1a.yaml View File

@@ -0,0 +1,6 @@
1
+---
2
+features:
3
+  - |
4
+    The batch member update resource can now be used additively by passing the
5
+    query parameter ``additive_only=True``. Existing members can be updated and
6
+    new members will be created, but missing members will not be deleted.

Loading…
Cancel
Save