From 56bb1e134d670901bdfe0526495f5794a55f139a Mon Sep 17 00:00:00 2001 From: Gregory Thiemonge Date: Mon, 1 Jul 2019 16:57:14 +0200 Subject: [PATCH] Prevent UDP LBs to use different IP protocol versions in amphora driver The amphora doesn't support mixing IPv4 and IPv6 addresses for its members and its VIP when using UDP load balancers. This commit adds a validation step in the member_create and th e member_batch_update functions of the amphora driver to ensure that IP protocol versions are the same in a UDP load balancer. Story: 2005876 Task: 33689 Change-Id: If6fb3fde9b43ac82af46eaddc48ec7a3a5b95602 --- .../api/drivers/amphora_driver/v1/driver.py | 26 ++++ .../api/drivers/amphora_driver/v2/driver.py | 26 ++++ .../amphora_driver/v1/test_amphora_driver.py | 125 +++++++++++++++++- .../amphora_driver/v2/test_amphora_driver.py | 125 +++++++++++++++++- ...p-protocol-in-udp-lb-2813b545131097ec.yaml | 7 + 5 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/validate-same-ip-protocol-in-udp-lb-2813b545131097ec.yaml diff --git a/octavia/api/drivers/amphora_driver/v1/driver.py b/octavia/api/drivers/amphora_driver/v1/driver.py index 6eb43530b3..b572df576c 100644 --- a/octavia/api/drivers/amphora_driver/v1/driver.py +++ b/octavia/api/drivers/amphora_driver/v1/driver.py @@ -163,6 +163,11 @@ class AmphoraProviderDriver(driver_base.ProviderDriver): # Member def member_create(self, member): + pool_id = member.pool_id + db_pool = self.repositories.pool.get(db_apis.get_session(), + id=pool_id) + self._validate_members(db_pool, [member]) + payload = {consts.MEMBER_ID: member.member_id} self.client.cast({}, 'create_member', **payload) @@ -186,6 +191,9 @@ class AmphoraProviderDriver(driver_base.ProviderDriver): pool_id = members[0].pool_id # The DB should not have updated yet, so we can still use the pool db_pool = self.repositories.pool.get(db_apis.get_session(), id=pool_id) + + self._validate_members(db_pool, members) + old_members = db_pool.members old_member_ids = [m.id for m in old_members] @@ -218,6 +226,24 @@ class AmphoraProviderDriver(driver_base.ProviderDriver): 'updated_members': updated_members} self.client.cast({}, 'batch_update_members', **payload) + def _validate_members(self, db_pool, members): + if db_pool.protocol == consts.PROTOCOL_UDP: + # For UDP LBs, check that we are not mixing IPv4 and IPv6 + for member in members: + member_is_ipv6 = utils.is_ipv6(member.address) + + for listener in db_pool.listeners: + lb = listener.load_balancer + vip_is_ipv6 = utils.is_ipv6(lb.vip.ip_address) + + if member_is_ipv6 != vip_is_ipv6: + msg = ("This provider doesn't support mixing IPv4 and " + "IPv6 addresses for its VIP and members in UDP " + "load balancers.") + raise exceptions.UnsupportedOptionError( + user_fault_string=msg, + operator_fault_string=msg) + # Health Monitor def health_monitor_create(self, healthmonitor): payload = {consts.HEALTH_MONITOR_ID: healthmonitor.healthmonitor_id} diff --git a/octavia/api/drivers/amphora_driver/v2/driver.py b/octavia/api/drivers/amphora_driver/v2/driver.py index a1fdcd9d41..f09b999dde 100644 --- a/octavia/api/drivers/amphora_driver/v2/driver.py +++ b/octavia/api/drivers/amphora_driver/v2/driver.py @@ -162,6 +162,11 @@ class AmphoraProviderDriver(driver_base.ProviderDriver): # Member def member_create(self, member): + pool_id = member.pool_id + db_pool = self.repositories.pool.get(db_apis.get_session(), + id=pool_id) + self._validate_members(db_pool, [member]) + payload = {consts.MEMBER_ID: member.member_id} self.client.cast({}, 'create_member', **payload) @@ -185,6 +190,9 @@ class AmphoraProviderDriver(driver_base.ProviderDriver): pool_id = members[0].pool_id # The DB should not have updated yet, so we can still use the pool db_pool = self.repositories.pool.get(db_apis.get_session(), id=pool_id) + + self._validate_members(db_pool, members) + old_members = db_pool.members old_member_ids = [m.id for m in old_members] @@ -217,6 +225,24 @@ class AmphoraProviderDriver(driver_base.ProviderDriver): 'updated_members': updated_members} self.client.cast({}, 'batch_update_members', **payload) + def _validate_members(self, db_pool, members): + if db_pool.protocol == consts.PROTOCOL_UDP: + # For UDP LBs, check that we are not mixing IPv4 and IPv6 + for member in members: + member_is_ipv6 = utils.is_ipv6(member.address) + + for listener in db_pool.listeners: + lb = listener.load_balancer + vip_is_ipv6 = utils.is_ipv6(lb.vip.ip_address) + + if member_is_ipv6 != vip_is_ipv6: + msg = ("This provider doesn't support mixing IPv4 and " + "IPv6 addresses for its VIP and members in UDP " + "load balancers.") + raise exceptions.UnsupportedOptionError( + user_fault_string=msg, + operator_fault_string=msg) + # Health Monitor def health_monitor_create(self, healthmonitor): payload = {consts.HEALTH_MONITOR_ID: healthmonitor.healthmonitor_id} diff --git a/octavia/tests/unit/api/drivers/amphora_driver/v1/test_amphora_driver.py b/octavia/tests/unit/api/drivers/amphora_driver/v1/test_amphora_driver.py index 7bc0c7cf25..b93a79d318 100644 --- a/octavia/tests/unit/api/drivers/amphora_driver/v1/test_amphora_driver.py +++ b/octavia/tests/unit/api/drivers/amphora_driver/v1/test_amphora_driver.py @@ -206,14 +206,60 @@ class TestAmphoraDriver(base.TestRpc): mock_cast.assert_called_with({}, 'update_pool', **payload) # Member + @mock.patch('octavia.db.api.get_session') + @mock.patch('octavia.db.repositories.PoolRepository.get') @mock.patch('oslo_messaging.RPCClient.cast') - def test_member_create(self, mock_cast): + def test_member_create(self, mock_cast, mock_pool_get, mock_session): provider_member = driver_dm.Member( member_id=self.sample_data.member1_id) self.amp_driver.member_create(provider_member) payload = {consts.MEMBER_ID: self.sample_data.member1_id} mock_cast.assert_called_with({}, 'create_member', **payload) + @mock.patch('octavia.db.api.get_session') + @mock.patch('octavia.db.repositories.PoolRepository.get') + @mock.patch('oslo_messaging.RPCClient.cast') + def test_member_create_udp_ipv4(self, mock_cast, mock_pool_get, + mock_session): + mock_lb = mock.MagicMock() + mock_lb.vip = mock.MagicMock() + mock_lb.vip.ip_address = "192.0.1.1" + mock_listener = mock.MagicMock() + mock_listener.load_balancer = mock_lb + mock_pool = mock.MagicMock() + mock_pool.protocol = consts.PROTOCOL_UDP + mock_pool.listeners = [mock_listener] + mock_pool_get.return_value = mock_pool + + provider_member = driver_dm.Member( + member_id=self.sample_data.member1_id, + address="192.0.2.1") + self.amp_driver.member_create(provider_member) + payload = {consts.MEMBER_ID: self.sample_data.member1_id} + mock_cast.assert_called_with({}, 'create_member', **payload) + + @mock.patch('octavia.db.api.get_session') + @mock.patch('octavia.db.repositories.PoolRepository.get') + @mock.patch('oslo_messaging.RPCClient.cast') + def test_member_create_udp_ipv4_ipv6(self, mock_cast, mock_pool_get, + mock_session): + mock_lb = mock.MagicMock() + mock_lb.vip = mock.MagicMock() + mock_lb.vip.ip_address = "fe80::1" + mock_listener = mock.MagicMock() + mock_listener.load_balancer = mock_lb + mock_pool = mock.MagicMock() + mock_pool.protocol = consts.PROTOCOL_UDP + mock_pool.listeners = [mock_listener] + mock_pool_get.return_value = mock_pool + + provider_member = driver_dm.Member( + member_id=self.sample_data.member1_id, + address="192.0.2.1") + self.assertRaises(exceptions.UnsupportedOptionError, + self.amp_driver.member_create, + provider_member) + @mock.patch('oslo_messaging.RPCClient.cast') def test_member_delete(self, mock_cast): provider_member = driver_dm.Member( @@ -332,6 +378,83 @@ class TestAmphoraDriver(base.TestRpc): payload = {consts.HEALTH_MONITOR_ID: self.sample_data.hm1_id} mock_cast.assert_called_with({}, 'delete_health_monitor', **payload) + @mock.patch('octavia.db.api.get_session') + @mock.patch('octavia.db.repositories.PoolRepository.get') + @mock.patch('oslo_messaging.RPCClient.cast') + def test_member_batch_update_udp_ipv4(self, mock_cast, mock_pool_get, + mock_session): + + mock_lb = mock.MagicMock() + mock_lb.vip = mock.MagicMock() + mock_lb.vip.ip_address = "192.0.1.1" + mock_listener = mock.MagicMock() + mock_listener.load_balancer = mock_lb + mock_pool = mock.MagicMock() + mock_pool.protocol = consts.PROTOCOL_UDP + mock_pool.listeners = [mock_listener] + mock_pool.members = self.sample_data.db_pool1_members + mock_pool_get.return_value = mock_pool + + prov_mem_update = driver_dm.Member( + member_id=self.sample_data.member2_id, + pool_id=self.sample_data.pool1_id, admin_state_up=False, + address='192.0.2.17', monitor_address='192.0.2.77', + protocol_port=80, name='updated-member2') + prov_new_member = driver_dm.Member( + member_id=self.sample_data.member3_id, + pool_id=self.sample_data.pool1_id, + address='192.0.2.18', monitor_address='192.0.2.28', + protocol_port=80, name='member3') + prov_members = [prov_mem_update, prov_new_member] + + update_mem_dict = {'ip_address': '192.0.2.17', + 'name': 'updated-member2', + 'monitor_address': '192.0.2.77', + 'id': self.sample_data.member2_id, + 'enabled': False, + 'protocol_port': 80, + 'pool_id': self.sample_data.pool1_id} + + self.amp_driver.member_batch_update(prov_members) + + payload = {'old_member_ids': [self.sample_data.member1_id], + 'new_member_ids': [self.sample_data.member3_id], + 'updated_members': [update_mem_dict]} + mock_cast.assert_called_with({}, 'batch_update_members', **payload) + + @mock.patch('octavia.db.api.get_session') + @mock.patch('octavia.db.repositories.PoolRepository.get') + @mock.patch('oslo_messaging.RPCClient.cast') + def test_member_batch_update_udp_ipv4_ipv6(self, mock_cast, mock_pool_get, + mock_session): + + mock_lb = mock.MagicMock() + mock_lb.vip = mock.MagicMock() + mock_lb.vip.ip_address = "192.0.1.1" + mock_listener = mock.MagicMock() + mock_listener.load_balancer = mock_lb + mock_pool = mock.MagicMock() + mock_pool.protocol = consts.PROTOCOL_UDP + mock_pool.listeners = [mock_listener] + mock_pool.members = self.sample_data.db_pool1_members + mock_pool_get.return_value = mock_pool + + prov_mem_update = driver_dm.Member( + member_id=self.sample_data.member2_id, + pool_id=self.sample_data.pool1_id, admin_state_up=False, + address='fe80::1', monitor_address='fe80::2', + protocol_port=80, name='updated-member2') + prov_new_member = driver_dm.Member( + member_id=self.sample_data.member3_id, + pool_id=self.sample_data.pool1_id, + address='192.0.2.18', monitor_address='192.0.2.28', + protocol_port=80, name='member3') + prov_members = [prov_mem_update, prov_new_member] + + self.assertRaises(exceptions.UnsupportedOptionError, + self.amp_driver.member_batch_update, + prov_members) + @mock.patch('oslo_messaging.RPCClient.cast') def test_health_monitor_update(self, mock_cast): old_provider_hm = driver_dm.HealthMonitor( diff --git a/octavia/tests/unit/api/drivers/amphora_driver/v2/test_amphora_driver.py b/octavia/tests/unit/api/drivers/amphora_driver/v2/test_amphora_driver.py index 2d9f3357b3..15bbca1332 100644 --- a/octavia/tests/unit/api/drivers/amphora_driver/v2/test_amphora_driver.py +++ b/octavia/tests/unit/api/drivers/amphora_driver/v2/test_amphora_driver.py @@ -206,14 +206,60 @@ class TestAmphoraDriver(base.TestRpc): mock_cast.assert_called_with({}, 'update_pool', **payload) # Member + @mock.patch('octavia.db.api.get_session') + @mock.patch('octavia.db.repositories.PoolRepository.get') @mock.patch('oslo_messaging.RPCClient.cast') - def test_member_create(self, mock_cast): + def test_member_create(self, mock_cast, mock_pool_get, mock_session): provider_member = driver_dm.Member( member_id=self.sample_data.member1_id) self.amp_driver.member_create(provider_member) payload = {consts.MEMBER_ID: self.sample_data.member1_id} mock_cast.assert_called_with({}, 'create_member', **payload) + @mock.patch('octavia.db.api.get_session') + @mock.patch('octavia.db.repositories.PoolRepository.get') + @mock.patch('oslo_messaging.RPCClient.cast') + def test_member_create_udp_ipv4(self, mock_cast, mock_pool_get, + mock_session): + mock_lb = mock.MagicMock() + mock_lb.vip = mock.MagicMock() + mock_lb.vip.ip_address = "192.0.1.1" + mock_listener = mock.MagicMock() + mock_listener.load_balancer = mock_lb + mock_pool = mock.MagicMock() + mock_pool.protocol = consts.PROTOCOL_UDP + mock_pool.listeners = [mock_listener] + mock_pool_get.return_value = mock_pool + + provider_member = driver_dm.Member( + member_id=self.sample_data.member1_id, + address="192.0.2.1") + self.amp_driver.member_create(provider_member) + payload = {consts.MEMBER_ID: self.sample_data.member1_id} + mock_cast.assert_called_with({}, 'create_member', **payload) + + @mock.patch('octavia.db.api.get_session') + @mock.patch('octavia.db.repositories.PoolRepository.get') + @mock.patch('oslo_messaging.RPCClient.cast') + def test_member_create_udp_ipv4_ipv6(self, mock_cast, mock_pool_get, + mock_session): + mock_lb = mock.MagicMock() + mock_lb.vip = mock.MagicMock() + mock_lb.vip.ip_address = "fe80::1" + mock_listener = mock.MagicMock() + mock_listener.load_balancer = mock_lb + mock_pool = mock.MagicMock() + mock_pool.protocol = consts.PROTOCOL_UDP + mock_pool.listeners = [mock_listener] + mock_pool_get.return_value = mock_pool + + provider_member = driver_dm.Member( + member_id=self.sample_data.member1_id, + address="192.0.2.1") + self.assertRaises(exceptions.UnsupportedOptionError, + self.amp_driver.member_create, + provider_member) + @mock.patch('oslo_messaging.RPCClient.cast') def test_member_delete(self, mock_cast): provider_member = driver_dm.Member( @@ -332,6 +378,83 @@ class TestAmphoraDriver(base.TestRpc): payload = {consts.HEALTH_MONITOR_ID: self.sample_data.hm1_id} mock_cast.assert_called_with({}, 'delete_health_monitor', **payload) + @mock.patch('octavia.db.api.get_session') + @mock.patch('octavia.db.repositories.PoolRepository.get') + @mock.patch('oslo_messaging.RPCClient.cast') + def test_member_batch_update_udp_ipv4(self, mock_cast, mock_pool_get, + mock_session): + + mock_lb = mock.MagicMock() + mock_lb.vip = mock.MagicMock() + mock_lb.vip.ip_address = "192.0.1.1" + mock_listener = mock.MagicMock() + mock_listener.load_balancer = mock_lb + mock_pool = mock.MagicMock() + mock_pool.protocol = consts.PROTOCOL_UDP + mock_pool.listeners = [mock_listener] + mock_pool.members = self.sample_data.db_pool1_members + mock_pool_get.return_value = mock_pool + + prov_mem_update = driver_dm.Member( + member_id=self.sample_data.member2_id, + pool_id=self.sample_data.pool1_id, admin_state_up=False, + address='192.0.2.17', monitor_address='192.0.2.77', + protocol_port=80, name='updated-member2') + prov_new_member = driver_dm.Member( + member_id=self.sample_data.member3_id, + pool_id=self.sample_data.pool1_id, + address='192.0.2.18', monitor_address='192.0.2.28', + protocol_port=80, name='member3') + prov_members = [prov_mem_update, prov_new_member] + + update_mem_dict = {'ip_address': '192.0.2.17', + 'name': 'updated-member2', + 'monitor_address': '192.0.2.77', + 'id': self.sample_data.member2_id, + 'enabled': False, + 'protocol_port': 80, + 'pool_id': self.sample_data.pool1_id} + + self.amp_driver.member_batch_update(prov_members) + + payload = {'old_member_ids': [self.sample_data.member1_id], + 'new_member_ids': [self.sample_data.member3_id], + 'updated_members': [update_mem_dict]} + mock_cast.assert_called_with({}, 'batch_update_members', **payload) + + @mock.patch('octavia.db.api.get_session') + @mock.patch('octavia.db.repositories.PoolRepository.get') + @mock.patch('oslo_messaging.RPCClient.cast') + def test_member_batch_update_udp_ipv4_ipv6(self, mock_cast, mock_pool_get, + mock_session): + + mock_lb = mock.MagicMock() + mock_lb.vip = mock.MagicMock() + mock_lb.vip.ip_address = "192.0.1.1" + mock_listener = mock.MagicMock() + mock_listener.load_balancer = mock_lb + mock_pool = mock.MagicMock() + mock_pool.protocol = consts.PROTOCOL_UDP + mock_pool.listeners = [mock_listener] + mock_pool.members = self.sample_data.db_pool1_members + mock_pool_get.return_value = mock_pool + + prov_mem_update = driver_dm.Member( + member_id=self.sample_data.member2_id, + pool_id=self.sample_data.pool1_id, admin_state_up=False, + address='fe80::1', monitor_address='fe80::2', + protocol_port=80, name='updated-member2') + prov_new_member = driver_dm.Member( + member_id=self.sample_data.member3_id, + pool_id=self.sample_data.pool1_id, + address='192.0.2.18', monitor_address='192.0.2.28', + protocol_port=80, name='member3') + prov_members = [prov_mem_update, prov_new_member] + + self.assertRaises(exceptions.UnsupportedOptionError, + self.amp_driver.member_batch_update, + prov_members) + @mock.patch('oslo_messaging.RPCClient.cast') def test_health_monitor_update(self, mock_cast): old_provider_hm = driver_dm.HealthMonitor( diff --git a/releasenotes/notes/validate-same-ip-protocol-in-udp-lb-2813b545131097ec.yaml b/releasenotes/notes/validate-same-ip-protocol-in-udp-lb-2813b545131097ec.yaml new file mode 100644 index 0000000000..8d7ae40649 --- /dev/null +++ b/releasenotes/notes/validate-same-ip-protocol-in-udp-lb-2813b545131097ec.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Adding a member with different IP protocol version than the VIP IP protocol + version in a UDP load balancer caused a crash in the amphora. A validation + step in the amphora driver now prevents mixing IP protocol versions in UDP + load balancers.