diff --git a/doc/source/contributor/guides/providers.rst b/doc/source/contributor/guides/providers.rst index 0c4e07f44f..23822927f1 100644 --- a/doc/source/contributor/guides/providers.rst +++ b/doc/source/contributor/guides/providers.rst @@ -1703,33 +1703,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. @@ -1775,7 +1773,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.") @@ -1790,4 +1788,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 11758fb68b..b0837fafb9 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -204,12 +204,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' @@ -218,6 +220,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' @@ -231,8 +234,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 ^^^^^^^^^^^^^^^^^^^^