From 4a9f83d039290a372a66d693e66a559fb98dce97 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Wed, 30 May 2018 19:14:15 -0700 Subject: [PATCH] Implement provider drivers - Driver Library This patch implements the provider driver support library. This library contains the callback methods that allow provider drivers to update status and statistics. This patch also clears some tech debt by correcting the IP addresses used in some test cases. Change-Id: I4e91e1b4f7ce611e603ea7aeb17f5c649cdb3c3d Story: 1655768 Task: 5165 --- doc/source/contributor/guides/providers.rst | 30 ++- octavia/api/drivers/driver_lib.py | 142 +++++++++++ octavia/api/drivers/exceptions.py | 57 +++++ octavia/common/constants.py | 6 + .../tests/functional/db/test_repositories.py | 51 ++-- .../tests/unit/api/drivers/test_driver_lib.py | 223 ++++++++++++++++++ .../tests/unit/api/drivers/test_exceptions.py | 31 +++ specs/version1.1/enable-provider-driver.rst | 28 +-- 8 files changed, 514 insertions(+), 54 deletions(-) create mode 100644 octavia/api/drivers/driver_lib.py create mode 100644 octavia/tests/unit/api/drivers/test_driver_lib.py diff --git a/doc/source/contributor/guides/providers.rst b/doc/source/contributor/guides/providers.rst index 218d638cbe..bb46de5725 100644 --- a/doc/source/contributor/guides/providers.rst +++ b/doc/source/contributor/guides/providers.rst @@ -1697,33 +1697,31 @@ The dictionary takes this form: Update Statistics API --------------------- -Provider drivers can update statistics for load balancers and listeners using -the following API. Similar to the status function above, a single dictionary -with multiple load balancer and/or listener statistics is used to update -statistics in a single call. If an existing load balancer or listener is not -included, the statistics those objects remain unchanged. +Provider drivers can update statistics for listeners using the following API. +Similar to the status function above, a single dictionary +with multiple listener statistics is used to update statistics in a single +call. If an existing listener is not included, the statistics that object +remain unchanged. -The general form of the input dictionary is a list of load balancer and -listener statistics: +The general form of the input dictionary is a list of listener statistics: .. code-block:: python - { "loadbalancers": [{"id": "123", + { "listeners": [{"id": "123", "active_connections": 12, "bytes_in": 238908, - "bytes_out": 290234}, + "bytes_out": 290234, "request_errors": 0, "total_connections": 3530},...] - "listeners": [] } .. code-block:: python - def update_loadbalancer_statistics(statistics): - """Update load balancer statistics. + def update_listener_statistics(statistics): + """Update listener statistics. - :param statistics (dict): Statistics for loadbalancers and listeners: - id (string): ID for load balancer or listener. + :param statistics (dict): Statistics for listeners: + id (string): ID of the listener. active_connections (int): Number of currently active connections. bytes_in (int): Total bytes received. bytes_out (int): Total bytes sent. @@ -1769,7 +1767,7 @@ references to the failed record if available. self.status_object_id = kwargs.pop('status_object_id', None) self.status_record = kwargs.pop('status_record', None) - super(UnsupportedOptionError, self).__init__(*args, **kwargs) + super(UpdateStatusError, self).__init__(*args, **kwargs) class UpdateStatisticsError(Exception): fault_string = _("The statistics update had an unknown error.") @@ -1784,4 +1782,4 @@ references to the failed record if available. self.stats_object_id = kwargs.pop('stats_object_id', None) self.stats_record = kwargs.pop('stats_record', None) - super(UnsupportedOptionError, self).__init__(*args, **kwargs) + super(UpdateStatisticsError, self).__init__(*args, **kwargs) diff --git a/octavia/api/drivers/driver_lib.py b/octavia/api/drivers/driver_lib.py new file mode 100644 index 0000000000..242b995a68 --- /dev/null +++ b/octavia/api/drivers/driver_lib.py @@ -0,0 +1,142 @@ +# Copyright 2018 Rackspace, US Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from octavia.api.drivers import exceptions as driver_exceptions +from octavia.common import constants as consts +from octavia.db import api as db_apis +from octavia.db import repositories as repo + + +class DriverLibrary(object): + + def __init__(self, **kwargs): + self.loadbalancer_repo = repo.LoadBalancerRepository() + self.listener_repo = repo.ListenerRepository() + self.pool_repo = repo.PoolRepository() + self.health_mon_repo = repo.HealthMonitorRepository() + self.member_repo = repo.MemberRepository() + self.l7policy_repo = repo.L7PolicyRepository() + self.l7rule_repo = repo.L7RuleRepository() + self.listener_stats_repo = repo.ListenerStatisticsRepository() + + self.db_session = db_apis.get_session() + super(DriverLibrary, self).__init__(**kwargs) + + def _process_status_update(self, repo, object_name, record, + delete_record=False): + # Zero it out so that if the ID is missing from a record we do not + # report the last LB as the failed record in the exception + record_id = None + try: + record_id = record['id'] + record_kwargs = {} + prov_status = record.get(consts.PROVISIONING_STATUS, None) + if prov_status: + if prov_status == consts.DELETED and delete_record: + repo.delete(self.db_session, id=record_id) + return + else: + record_kwargs[consts.PROVISIONING_STATUS] = prov_status + op_status = record.get(consts.OPERATING_STATUS, None) + if op_status: + record_kwargs[consts.OPERATING_STATUS] = op_status + if prov_status or op_status: + repo.update(self.db_session, record_id, **record_kwargs) + except Exception as e: + # We need to raise a failure here to notify the driver it is + # sending bad status data. + raise driver_exceptions.UpdateStatusError( + fault_string=str(e), status_object_id=record_id, + status_object=object_name) + + def update_loadbalancer_status(self, status): + """Update load balancer status. + + :param status: dictionary defining the provisioning status and + operating status for load balancer objects, including pools, + members, listeners, L7 policies, and L7 rules. + iod (string): ID for the object. + provisioning_status (string): Provisioning status for the object. + operating_status (string): Operating status for the object. + :type status: dict + :raises: UpdateStatusError + :returns: None + """ + members = status.pop(consts.MEMBERS, []) + for member in members: + self._process_status_update(self.member_repo, consts.MEMBERS, + member, delete_record=True) + + health_mons = status.pop(consts.HEALTHMONITORS, []) + for health_mon in health_mons: + self._process_status_update( + self.health_mon_repo, consts.HEALTHMONITORS, health_mon, + delete_record=True) + + pools = status.pop(consts.POOLS, []) + for pool in pools: + self._process_status_update(self.pool_repo, consts.POOLS, + pool, delete_record=True) + + l7rules = status.pop(consts.L7RULES, []) + for l7rule in l7rules: + self._process_status_update(self.l7rule_repo, consts.L7RULES, + l7rule, delete_record=True) + + l7policies = status.pop(consts.L7POLICIES, []) + for l7policy in l7policies: + self._process_status_update(self.l7policy_repo, consts.L7POLICIES, + l7policy, delete_record=True) + + listeners = status.pop(consts.LISTENERS, []) + for listener in listeners: + self._process_status_update(self.listener_repo, consts.LISTENERS, + listener, delete_record=True) + + lbs = status.pop(consts.LOADBALANCERS, []) + for lb in lbs: + self._process_status_update(self.loadbalancer_repo, + consts.LOADBALANCERS, lb) + + def update_listener_statistics(self, statistics): + """Update listener statistics. + + :param statistics: Statistics for listeners: + id (string): ID for listener. + active_connections (int): Number of currently active connections. + bytes_in (int): Total bytes received. + bytes_out (int): Total bytes sent. + request_errors (int): Total requests not fulfilled. + total_connections (int): The total connections handled. + :type statistics: dict + :raises: UpdateStatisticsError + :returns: None + """ + listener_stats = statistics.get('listeners', []) + for stat in listener_stats: + try: + listener_id = stat.pop('id') + except Exception as e: + raise driver_exceptions.UpdateStatisticsError( + fault_string=str(e), stats_object='listeners') + # Provider drivers other than the amphora driver do not have + # an amphora ID, use the listener ID again here to meet the + # constraint requirement. + try: + self.listener_stats_repo.replace(self.db_session, listener_id, + listener_id, **stat) + except Exception as e: + raise driver_exceptions.UpdateStatisticsError( + fault_string=str(e), stats_object='listeners', + stats_object_id=listener_id) diff --git a/octavia/api/drivers/exceptions.py b/octavia/api/drivers/exceptions.py index c04d71d72f..d2a6c45912 100644 --- a/octavia/api/drivers/exceptions.py +++ b/octavia/api/drivers/exceptions.py @@ -89,3 +89,60 @@ class UnsupportedOptionError(Exception): self.operator_fault_string = kwargs.pop('operator_fault_string', self.operator_fault_string) super(UnsupportedOptionError, self).__init__(*args, **kwargs) + + +class UpdateStatusError(Exception): + """Exception raised when a status update fails. + + Each exception will include a message field that describes the + error and references to the failed record if available. + :param fault_string: String describing the fault. + :type fault_string: string + :param status_object: The object the fault occurred on. + :type status_object: string + :param status_object_id: The ID of the object that failed status update. + :type status_object_id: string + :param status_record: The status update record that caused the fault. + :type status_record: string + """ + fault_string = _("The status update had an unknown error.") + status_object = None + status_object_id = None + status_record = None + + def __init__(self, *args, **kwargs): + self.fault_string = kwargs.pop('fault_string', self.fault_string) + self.status_object = kwargs.pop('status_object', None) + self.status_object_id = kwargs.pop('status_object_id', None) + self.status_record = kwargs.pop('status_record', None) + + super(UpdateStatusError, self).__init__(*args, **kwargs) + + +class UpdateStatisticsError(Exception): + """Exception raised when a statistics update fails. + + Each exception will include a message field that describes the + error and references to the failed record if available. + :param fault_string: String describing the fault. + :type fault_string: string + :param status_object: The object the fault occurred on. + :type status_object: string + :param status_object_id: The ID of the object that failed stats update. + :type status_object_id: string + :param status_record: The stats update record that caused the fault. + :type status_record: string + """ + fault_string = _("The statistics update had an unknown error.") + stats_object = None + stats_object_id = None + stats_record = None + + def __init__(self, *args, **kwargs): + self.fault_string = kwargs.pop('fault_string', + self.fault_string) + self.stats_object = kwargs.pop('stats_object', None) + self.stats_object_id = kwargs.pop('stats_object_id', None) + self.stats_record = kwargs.pop('stats_record', None) + + super(UpdateStatisticsError, self).__init__(*args, **kwargs) diff --git a/octavia/common/constants.py b/octavia/common/constants.py index c5c4f5a4c3..c8085ac239 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -203,12 +203,14 @@ LISTENER = 'listener' LISTENERS = 'listeners' LISTENER_ID = 'listener_id' LOADBALANCER = 'loadbalancer' +LOADBALANCERS = 'loadbalancers' LOADBALANCER_ID = 'loadbalancer_id' LOAD_BALANCER_ID = 'load_balancer_id' SERVER_GROUP_ID = 'server_group_id' ANTI_AFFINITY = 'anti-affinity' SOFT_ANTI_AFFINITY = 'soft-anti-affinity' MEMBER = 'member' +MEMBERS = 'members' MEMBER_ID = 'member_id' COMPUTE_ID = 'compute_id' COMPUTE_OBJ = 'compute_obj' @@ -217,6 +219,7 @@ AMPS_DATA = 'amps_data' NICS = 'nics' VIP = 'vip' POOL = 'pool' +POOLS = 'pools' POOL_CHILD_COUNT = 'pool_child_count' POOL_ID = 'pool_id' L7POLICY = 'l7policy' @@ -230,8 +233,11 @@ ADDED_PORTS = 'added_ports' PORTS = 'ports' MEMBER_PORTS = 'member_ports' LOADBALANCER_TOPOLOGY = 'topology' +HEALTHMONITORS = 'healthmonitors' HEALTH_MONITOR_ID = 'health_monitor_id' +L7POLICIES = 'l7policies' L7POLICY_ID = 'l7policy_id' +L7RULES = 'l7rules' L7RULE_ID = 'l7rule_id' LOAD_BALANCER_UPDATES = 'load_balancer_updates' LISTENER_UPDATES = 'listener_updates' diff --git a/octavia/tests/functional/db/test_repositories.py b/octavia/tests/functional/db/test_repositories.py index 301c98609c..38ea8fbf1c 100644 --- a/octavia/tests/functional/db/test_repositories.py +++ b/octavia/tests/functional/db/test_repositories.py @@ -35,7 +35,7 @@ CONF = cfg.CONF class BaseRepositoryTest(base.OctaviaDBTestBase): - FAKE_IP = "10.0.0.1" + FAKE_IP = "192.0.2.1" FAKE_UUID_1 = uuidutils.generate_uuid() FAKE_UUID_2 = uuidutils.generate_uuid() FAKE_UUID_3 = uuidutils.generate_uuid() @@ -131,7 +131,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'server_group_id': uuidutils.generate_uuid(), 'project_id': uuidutils.generate_uuid(), 'id': uuidutils.generate_uuid()} - vip = {'ip_address': '10.0.0.1', + vip = {'ip_address': '192.0.2.1', 'port_id': uuidutils.generate_uuid(), 'subnet_id': uuidutils.generate_uuid(), 'network_id': uuidutils.generate_uuid(), @@ -394,7 +394,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'operating_status': constants.ONLINE, 'id': uuidutils.generate_uuid()} l7policy['listener_id'] = listener.get('id') - vip = {'ip_address': '10.0.0.1', 'port_id': uuidutils.generate_uuid(), + vip = {'ip_address': '192.0.2.1', 'port_id': uuidutils.generate_uuid(), 'subnet_id': uuidutils.generate_uuid()} lb = {'name': 'lb1', 'description': 'desc1', 'enabled': True, 'topology': constants.TOPOLOGY_ACTIVE_STANDBY, @@ -419,7 +419,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): test below: `test_create_load_balancer_tree_quotas`. """ project_id = uuidutils.generate_uuid() - vip = {'ip_address': '10.0.0.1', 'port_id': uuidutils.generate_uuid(), + vip = {'ip_address': '192.0.2.1', 'port_id': uuidutils.generate_uuid(), 'subnet_id': uuidutils.generate_uuid()} lb = {'name': 'lb1', 'description': 'desc1', 'enabled': True, 'topology': constants.TOPOLOGY_ACTIVE_STANDBY, @@ -539,7 +539,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'operating_status': constants.ONLINE, 'id': uuidutils.generate_uuid()} l7policy['listener_id'] = listener.get('id') - vip = {'ip_address': '10.0.0.1', 'port_id': uuidutils.generate_uuid(), + vip = {'ip_address': '192.0.2.1', 'port_id': uuidutils.generate_uuid(), 'subnet_id': uuidutils.generate_uuid()} lb = {'name': 'lb1', 'description': 'desc1', 'enabled': True, 'topology': constants.TOPOLOGY_ACTIVE_STANDBY, @@ -1885,7 +1885,7 @@ class PoolRepositoryTest(BaseRepositoryTest): member = self.member_repo.create(self.session, id=self.FAKE_UUID_3, project_id=self.FAKE_UUID_2, pool_id=pool.id, - ip_address="10.0.0.1", + ip_address="192.0.2.1", protocol_port=80, enabled=True, provisioning_status=constants.ACTIVE, operating_status=constants.ONLINE, @@ -1939,7 +1939,7 @@ class PoolRepositoryTest(BaseRepositoryTest): member = self.member_repo.create(self.session, id=self.FAKE_UUID_3, project_id=self.FAKE_UUID_2, pool_id=pool.id, - ip_address="10.0.0.1", + ip_address="192.0.2.1", protocol_port=80, provisioning_status=constants.ACTIVE, operating_status=constants.ONLINE, @@ -1989,16 +1989,16 @@ class MemberRepositoryTest(BaseRepositoryTest): def test_get(self): member = self.create_member(self.FAKE_UUID_1, self.FAKE_UUID_2, - self.pool.id, "10.0.0.1") + self.pool.id, "192.0.2.1") new_member = self.member_repo.get(self.session, id=member.id) self.assertIsInstance(new_member, models.Member) self.assertEqual(member, new_member) def test_get_all(self): member_one = self.create_member(self.FAKE_UUID_1, self.FAKE_UUID_2, - self.pool.id, "10.0.0.1") + self.pool.id, "192.0.2.1") member_two = self.create_member(self.FAKE_UUID_3, self.FAKE_UUID_2, - self.pool.id, "10.0.0.2") + self.pool.id, "192.0.2.2") member_list, _ = self.member_repo.get_all(self.session, project_id=self.FAKE_UUID_2) self.assertIsInstance(member_list, list) @@ -2008,20 +2008,20 @@ class MemberRepositoryTest(BaseRepositoryTest): def test_create(self): member = self.create_member(self.FAKE_UUID_1, self.FAKE_UUID_2, - self.pool.id, ip_address="10.0.0.1") + self.pool.id, ip_address="192.0.2.1") new_member = self.member_repo.get(self.session, id=member.id) self.assertEqual(self.FAKE_UUID_1, new_member.id) self.assertEqual(self.FAKE_UUID_2, new_member.project_id) self.assertEqual(self.pool.id, new_member.pool_id) - self.assertEqual("10.0.0.1", new_member.ip_address) + self.assertEqual("192.0.2.1", new_member.ip_address) self.assertEqual(80, new_member.protocol_port) self.assertEqual(constants.ONLINE, new_member.operating_status) self.assertTrue(new_member.enabled) def test_update(self): - ip_address_change = "10.0.0.2" + ip_address_change = "192.0.2.2" member = self.create_member(self.FAKE_UUID_1, self.FAKE_UUID_2, - self.pool.id, "10.0.0.1") + self.pool.id, "192.0.2.1") self.member_repo.update(self.session, id=member.id, ip_address=ip_address_change) new_member = self.member_repo.get(self.session, id=member.id) @@ -2029,7 +2029,7 @@ class MemberRepositoryTest(BaseRepositoryTest): def test_delete(self): member = self.create_member(self.FAKE_UUID_1, self.FAKE_UUID_2, - self.pool.id, "10.0.0.1") + self.pool.id, "192.0.2.1") self.member_repo.delete(self.session, id=member.id) self.assertIsNone(self.member_repo.get(self.session, id=member.id)) new_pool = self.pool_repo.get(self.session, id=self.pool.id) @@ -2038,9 +2038,9 @@ class MemberRepositoryTest(BaseRepositoryTest): def test_update_pool_members(self): member1 = self.create_member(self.FAKE_UUID_1, self.FAKE_UUID_2, - self.pool.id, "10.0.0.1") + self.pool.id, "192.0.2.1") member2 = self.create_member(self.FAKE_UUID_3, self.FAKE_UUID_2, - self.pool.id, "10.0.0.2") + self.pool.id, "192.0.2.2") self.member_repo.update_pool_members( self.session, pool_id=self.pool.id, @@ -2481,6 +2481,13 @@ class HealthMonitorRepositoryTest(BaseRepositoryTest): lb_algorithm=constants.LB_ALGORITHM_ROUND_ROBIN, provisioning_status=constants.ACTIVE, operating_status=constants.ONLINE, enabled=True) + self.pool2 = self.pool_repo.create( + self.session, id=self.FAKE_UUID_2, project_id=self.FAKE_UUID_2, + name="pool2_test", description="pool2_description", + protocol=constants.PROTOCOL_HTTP, + lb_algorithm=constants.LB_ALGORITHM_ROUND_ROBIN, + provisioning_status=constants.ACTIVE, + operating_status=constants.ONLINE, enabled=True) def create_health_monitor(self, hm_id, pool_id): health_monitor = self.hm_repo.create( @@ -2626,7 +2633,7 @@ class LoadBalancerRepositoryTest(BaseRepositoryTest): def test_delete_with_vip(self): lb = self.create_loadbalancer(self.FAKE_UUID_1) vip = self.vip_repo.create(self.session, load_balancer_id=lb.id, - ip_address="10.0.0.1") + ip_address="192.0.2.1") new_lb = self.lb_repo.get(self.session, id=lb.id) self.assertIsNotNone(new_lb) self.assertIsNotNone(new_lb.vip) @@ -2689,7 +2696,7 @@ class LoadBalancerRepositoryTest(BaseRepositoryTest): lb_network_ip=self.FAKE_IP, status=constants.ACTIVE) vip = self.vip_repo.create(self.session, load_balancer_id=lb.id, - ip_address="10.0.0.1") + ip_address="192.0.2.1") listener = self.listener_repo.create( self.session, id=self.FAKE_UUID_1, project_id=self.FAKE_UUID_2, name="listener_name", description="listener_description", @@ -2851,7 +2858,7 @@ class VipRepositoryTest(BaseRepositoryTest): def create_vip(self, lb_id): vip = self.vip_repo.create(self.session, load_balancer_id=lb_id, - ip_address="10.0.0.1") + ip_address="192.0.2.1") return vip def test_get(self): @@ -2864,10 +2871,10 @@ class VipRepositoryTest(BaseRepositoryTest): def test_create(self): vip = self.create_vip(self.lb.id) self.assertEqual(self.lb.id, vip.load_balancer_id) - self.assertEqual("10.0.0.1", vip.ip_address) + self.assertEqual("192.0.2.1", vip.ip_address) def test_update(self): - address_change = "10.0.0.2" + address_change = "192.0.2.2" vip = self.create_vip(self.lb.id) self.vip_repo.update(self.session, vip.load_balancer_id, ip_address=address_change) diff --git a/octavia/tests/unit/api/drivers/test_driver_lib.py b/octavia/tests/unit/api/drivers/test_driver_lib.py new file mode 100644 index 0000000000..ee3797864b --- /dev/null +++ b/octavia/tests/unit/api/drivers/test_driver_lib.py @@ -0,0 +1,223 @@ +# Copyright 2018 Rackspace, US Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from mock import call + +from octavia.api.drivers import driver_lib +from octavia.api.drivers import exceptions as driver_exceptions +from octavia.common import constants +from octavia.tests.unit import base + + +class TestDriverLib(base.TestCase): + @mock.patch('octavia.db.repositories.L7RuleRepository') + @mock.patch('octavia.db.repositories.L7PolicyRepository') + @mock.patch('octavia.db.repositories.HealthMonitorRepository') + @mock.patch('octavia.db.repositories.MemberRepository') + @mock.patch('octavia.db.repositories.PoolRepository') + @mock.patch('octavia.db.repositories.ListenerRepository') + @mock.patch('octavia.db.repositories.LoadBalancerRepository') + @mock.patch('octavia.db.api.get_session') + def setUp(self, mock_get_session, mock_lb_repo, mock_list_repo, + mock_pool_repo, mock_member_repo, mock_health_repo, + mock_l7p_repo, mock_l7r_repo): + super(TestDriverLib, self).setUp() + self.mock_session = "FAKE_DB_SESSION" + mock_get_session.return_value = self.mock_session + lb_mock = mock.MagicMock() + mock_lb_repo.return_value = lb_mock + self.mock_lb_repo = lb_mock + list_mock = mock.MagicMock() + mock_list_repo.return_value = list_mock + self.mock_list_repo = list_mock + pool_mock = mock.MagicMock() + mock_pool_repo.return_value = pool_mock + self.mock_pool_repo = pool_mock + member_mock = mock.MagicMock() + mock_member_repo.return_value = member_mock + self.mock_member_repo = member_mock + health_mock = mock.MagicMock() + mock_health_repo.return_value = health_mock + self.mock_health_repo = health_mock + l7p_mock = mock.MagicMock() + mock_l7p_repo.return_value = l7p_mock + self.mock_l7p_repo = l7p_mock + l7r_mock = mock.MagicMock() + mock_l7r_repo.return_value = l7r_mock + self.mock_l7r_repo = l7r_mock + self.driver_lib = driver_lib.DriverLibrary() + listener_stats_list = [{"id": 1, "active_connections": 10, + "bytes_in": 20, + "bytes_out": 30, + "request_errors": 40, + "total_connections": 50}, + {"id": 2, "active_connections": 60, + "bytes_in": 70, + "bytes_out": 80, + "request_errors": 90, + "total_connections": 100}] + self.listener_stats_dict = {"listeners": listener_stats_list} + + def test_process_status_update(self): + mock_repo = mock.MagicMock() + list_dict = {"id": 2, constants.PROVISIONING_STATUS: constants.ACTIVE, + constants.OPERATING_STATUS: constants.ONLINE} + list_prov_dict = {"id": 2, + constants.PROVISIONING_STATUS: constants.ACTIVE} + list_oper_dict = {"id": 2, + constants.OPERATING_STATUS: constants.ONLINE} + list_deleted_dict = { + "id": 2, constants.PROVISIONING_STATUS: constants.DELETED, + constants.OPERATING_STATUS: constants.ONLINE} + + # Test with full record + self.driver_lib._process_status_update(mock_repo, 'FakeName', + list_dict) + mock_repo.update.assert_called_once_with( + self.mock_session, 2, provisioning_status=constants.ACTIVE, + operating_status=constants.ONLINE) + mock_repo.delete.assert_not_called() + + # Test with only provisioning status record + mock_repo.reset_mock() + self.driver_lib._process_status_update(mock_repo, 'FakeName', + list_prov_dict) + mock_repo.update.assert_called_once_with( + self.mock_session, 2, provisioning_status=constants.ACTIVE) + mock_repo.delete.assert_not_called() + + # Test with only operating status record + mock_repo.reset_mock() + self.driver_lib._process_status_update(mock_repo, 'FakeName', + list_oper_dict) + mock_repo.update.assert_called_once_with( + self.mock_session, 2, operating_status=constants.ONLINE) + mock_repo.delete.assert_not_called() + + # Test with deleted but delete_record False + mock_repo.reset_mock() + self.driver_lib._process_status_update(mock_repo, 'FakeName', + list_deleted_dict) + mock_repo.update.assert_called_once_with( + self.mock_session, 2, provisioning_status=constants.DELETED, + operating_status=constants.ONLINE) + mock_repo.delete.assert_not_called() + + # Test with an empty update + mock_repo.reset_mock() + self.driver_lib._process_status_update(mock_repo, 'FakeName', + {"id": 2}) + mock_repo.update.assert_not_called() + mock_repo.delete.assert_not_called() + + # Test with deleted and delete_record True + mock_repo.reset_mock() + self.driver_lib._process_status_update( + mock_repo, 'FakeName', list_deleted_dict, delete_record=True) + mock_repo.delete.assert_called_once_with(self.mock_session, id=2) + mock_repo.update.assert_not_called() + + # Test with an exception + mock_repo.reset_mock() + mock_repo.update.side_effect = Exception('boom') + self.assertRaises(driver_exceptions.UpdateStatusError, + self.driver_lib._process_status_update, + mock_repo, 'FakeName', list_dict) + + # Test with no ID record + mock_repo.reset_mock() + self.assertRaises(driver_exceptions.UpdateStatusError, + self.driver_lib._process_status_update, + mock_repo, 'FakeName', {"fake": "data"}) + + @mock.patch( + 'octavia.api.drivers.driver_lib.DriverLibrary._process_status_update') + def test_update_loadbalancer_status(self, mock_status_update): + lb_dict = {"id": 1, constants.PROVISIONING_STATUS: constants.ACTIVE, + constants.OPERATING_STATUS: constants.ONLINE} + list_dict = {"id": 2, constants.PROVISIONING_STATUS: constants.ACTIVE, + constants.OPERATING_STATUS: constants.ONLINE} + pool_dict = {"id": 3, constants.PROVISIONING_STATUS: constants.ACTIVE, + constants.OPERATING_STATUS: constants.ONLINE} + member_dict = {"id": 4, + constants.PROVISIONING_STATUS: constants.ACTIVE, + constants.OPERATING_STATUS: constants.ONLINE} + hm_dict = {"id": 5, constants.PROVISIONING_STATUS: constants.ACTIVE, + constants.OPERATING_STATUS: constants.ONLINE} + l7p_dict = {"id": 6, constants.PROVISIONING_STATUS: constants.ACTIVE, + constants.OPERATING_STATUS: constants.ONLINE} + l7r_dict = {"id": 7, constants.PROVISIONING_STATUS: constants.ACTIVE, + constants.OPERATING_STATUS: constants.ONLINE} + status_dict = {constants.LOADBALANCERS: [lb_dict], + constants.LISTENERS: [list_dict], + constants.POOLS: [pool_dict], + constants.MEMBERS: [member_dict], + constants.HEALTHMONITORS: [hm_dict], + constants.L7POLICIES: [l7p_dict], + constants.L7RULES: [l7r_dict]} + + self.driver_lib.update_loadbalancer_status(status_dict) + + calls = [call(self.mock_member_repo, constants.MEMBERS, member_dict, + delete_record=True), + call(self.mock_health_repo, constants.HEALTHMONITORS, + hm_dict, delete_record=True), + call(self.mock_pool_repo, constants.POOLS, pool_dict, + delete_record=True), + call(self.mock_l7r_repo, constants.L7RULES, l7r_dict, + delete_record=True), + call(self.mock_l7p_repo, constants.L7POLICIES, l7p_dict, + delete_record=True), + call(self.mock_list_repo, constants.LISTENERS, list_dict, + delete_record=True), + call(self.mock_lb_repo, constants.LOADBALANCERS, + lb_dict)] + mock_status_update.assert_has_calls(calls) + + mock_status_update.reset_mock() + self.driver_lib.update_loadbalancer_status({}) + mock_status_update.assert_not_called() + + @mock.patch('octavia.db.repositories.ListenerStatisticsRepository.replace') + def test_update_listener_statistics(self, mock_replace): + self.driver_lib.update_listener_statistics(self.listener_stats_dict) + calls = [call(self.mock_session, 1, 1, active_connections=10, + bytes_in=20, bytes_out=30, request_errors=40, + total_connections=50), + call(self.mock_session, 2, 2, active_connections=60, + bytes_in=70, bytes_out=80, request_errors=90, + total_connections=100)] + mock_replace.assert_has_calls(calls) + + mock_replace.reset_mock() + self.driver_lib.update_listener_statistics({}) + mock_replace.assert_not_called() + + # Test missing ID + bad_id_dict = {"listeners": [{"notID": "one"}]} + self.assertRaises(driver_exceptions.UpdateStatisticsError, + self.driver_lib.update_listener_statistics, + bad_id_dict) + + # Coverage doesn't like this test as part of the above test + # So, broke it out in it's own test + @mock.patch('octavia.db.repositories.ListenerStatisticsRepository.replace') + def test_update_listener_statistics_exception(self, mock_replace): + + # Test stats exception + mock_replace.side_effect = Exception('boom') + self.assertRaises(driver_exceptions.UpdateStatisticsError, + self.driver_lib.update_listener_statistics, + self.listener_stats_dict) diff --git a/octavia/tests/unit/api/drivers/test_exceptions.py b/octavia/tests/unit/api/drivers/test_exceptions.py index 239918d34e..75f5878c38 100644 --- a/octavia/tests/unit/api/drivers/test_exceptions.py +++ b/octavia/tests/unit/api/drivers/test_exceptions.py @@ -22,6 +22,9 @@ class TestProviderExceptions(base.TestCase): super(TestProviderExceptions, self).setUp() self.user_fault_string = 'Bad driver' self.operator_fault_string = 'Fix bad driver.' + self.fault_object = 'MCP' + self.fault_object_id = '-1' + self.fault_record = 'skip' def test_DriverError(self): driver_error = exceptions.DriverError( @@ -55,3 +58,31 @@ class TestProviderExceptions(base.TestCase): self.assertEqual(self.operator_fault_string, unsupported_option_error.operator_fault_string) self.assertIsInstance(unsupported_option_error, Exception) + + def test_UpdateStatusError(self): + update_status_error = exceptions.UpdateStatusError( + fault_string=self.user_fault_string, + status_object=self.fault_object, + status_object_id=self.fault_object_id, + status_record=self.fault_record) + + self.assertEqual(self.user_fault_string, + update_status_error.fault_string) + self.assertEqual(self.fault_object, update_status_error.status_object) + self.assertEqual(self.fault_object_id, + update_status_error.status_object_id) + self.assertEqual(self.fault_record, update_status_error.status_record) + + def test_UpdateStatisticsError(self): + update_stats_error = exceptions.UpdateStatisticsError( + fault_string=self.user_fault_string, + stats_object=self.fault_object, + stats_object_id=self.fault_object_id, + stats_record=self.fault_record) + + self.assertEqual(self.user_fault_string, + update_stats_error.fault_string) + self.assertEqual(self.fault_object, update_stats_error.stats_object) + self.assertEqual(self.fault_object_id, + update_stats_error.stats_object_id) + self.assertEqual(self.fault_record, update_stats_error.stats_record) diff --git a/specs/version1.1/enable-provider-driver.rst b/specs/version1.1/enable-provider-driver.rst index cb6af55a25..d66e1189fc 100644 --- a/specs/version1.1/enable-provider-driver.rst +++ b/specs/version1.1/enable-provider-driver.rst @@ -1634,38 +1634,35 @@ The dictionary takes this form: :raises: UpdateStatusError :returns: None """ - raise NotImplementedError() Update statistics API ^^^^^^^^^^^^^^^^^^^^^ -Provider drivers can update statistics for load balancers and listeners using -the following API. Similar to the status function above, a single dictionary -with multiple load balancer and/or listener statistics is used to update -statistics in a single call. If an existing load balancer or listener is not -included, the statistics those objects remain unchanged. +Provider drivers can update statistics for listeners using the following API. +Similar to the status function above, a single dictionary +with multiple listener statistics is used to update statistics in a single +call. If an existing listener is not included, the statistics for that object +will remain unchanged. -The general form of the input dictionary is a list of load balancer and -listener statistics: +The general form of the input dictionary is a list of listener statistics: .. code-block:: python - { "loadbalancers": [{"id": "123", + { "listeners": [{"id": "123", "active_connections": 12, "bytes_in": 238908, - "bytes_out": 290234}, + "bytes_out": 290234, "request_errors": 0, "total_connections": 3530},...] - "listeners": [] } .. code-block:: python - def update_loadbalancer_statistics(statistics): - """Update load balancer statistics. + def update_listener_statistics(statistics): + """Update listener statistics. - :param statistics (dict): Statistics for loadbalancers and listeners: - id (string): ID for load balancer or listener. + :param statistics (dict): Statistics for listeners: + id (string): ID of the listener. active_connections (int): Number of currently active connections. bytes_in (int): Total bytes received. bytes_out (int): Total bytes sent. @@ -1674,7 +1671,6 @@ listener statistics: :raises: UpdateStatisticsError :returns: None """ - raise NotImplementedError() Get Resource Support ^^^^^^^^^^^^^^^^^^^^