diff --git a/octavia/api/v2/controllers/member.py b/octavia/api/v2/controllers/member.py index 80fab04ede..b82bebfa0a 100644 --- a/octavia/api/v2/controllers/member.py +++ b/octavia/api/v2/controllers/member.py @@ -31,6 +31,7 @@ from octavia.common import data_models from octavia.common import exceptions from octavia.common import validate from octavia.db import prepare as db_prepare +from octavia.i18n import _ LOG = logging.getLogger(__name__) @@ -366,12 +367,21 @@ class MembersController(MemberController): # Find members that are brand new or updated new_members = [] updated_members = [] + updated_member_uniques = set() for m in members: - if (m.address, m.protocol_port) not in old_member_uniques: + key = (m.address, m.protocol_port) + if key not in old_member_uniques: validate.ip_not_reserved(m.address) new_members.append(m) else: - m.id = old_member_uniques[(m.address, m.protocol_port)] + m.id = old_member_uniques[key] + if key in updated_member_uniques: + LOG.error("Member %s is updated multiple times in " + "the same batch request.", m.id) + raise exceptions.ValidationException( + detail=_("Member must be updated only once in the " + "same request.")) + updated_member_uniques.add(key) updated_members.append(m) # Find members that are deleted diff --git a/octavia/tests/functional/api/v2/test_member.py b/octavia/tests/functional/api/v2/test_member.py index 2442c4d8f3..0411ac1aa5 100644 --- a/octavia/tests/functional/api/v2/test_member.py +++ b/octavia/tests/functional/api/v2/test_member.py @@ -913,6 +913,38 @@ class TestMember(base.BaseAPITest): m_subnet_exists.assert_called_once_with( member1['subnet_id'], context=mock.ANY) + @mock.patch('octavia.api.drivers.driver_factory.get_driver') + @mock.patch('octavia.api.drivers.utils.call_provider') + def test_update_members_member_duplicate( + self, mock_provider, mock_get_driver): + mock_driver = mock.MagicMock() + mock_driver.name = 'noop_driver' + mock_get_driver.return_value = mock_driver + subnet_id = uuidutils.generate_uuid() + + member1 = {'address': '192.0.2.1', 'protocol_port': 80, + 'project_id': self.project_id, 'subnet_id': subnet_id} + + req_dict = [member1] + body = {self.root_tag_list: req_dict} + path = self.MEMBERS_PATH.format(pool_id=self.pool_id) + self.put(path, body, status=202) + + self.set_lb_status(self.lb_id) + + # Same member (same address and protocol_port) updated twice in the + # same PUT request + member1 = {'address': '192.0.2.1', 'protocol_port': 80, + 'project_id': self.project_id, 'subnet_id': subnet_id, + 'name': 'member1'} + member2 = {'address': '192.0.2.1', 'protocol_port': 80, + 'project_id': self.project_id, 'subnet_id': subnet_id, + 'name': 'member2'} + + req_dict = [member1, member2] + body = {self.root_tag_list: req_dict} + self.put(path, body, status=400) + @mock.patch('octavia.api.drivers.driver_factory.get_driver') @mock.patch('octavia.api.drivers.utils.call_provider') def test_update_members_subnet_not_found( diff --git a/releasenotes/notes/fix-error-with-duplicate-members-in-batch-update-610ffbbf949927d0.yaml b/releasenotes/notes/fix-error-with-duplicate-members-in-batch-update-610ffbbf949927d0.yaml new file mode 100644 index 0000000000..a79300c24f --- /dev/null +++ b/releasenotes/notes/fix-error-with-duplicate-members-in-batch-update-610ffbbf949927d0.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Added a validation step in the batch member API request that checks if a + member is included multiple times in the list of updated members, this + additional check prevents the load balancer from being stuck in + PENDING_UPDATE. Duplicate members in the batch member flow triggered an + exception in Taskflow. + The API now returns 400 (ValidationException) if a member is already + present in the body of the request.