From 0ab265ff41d84f95c0a1b0c3f466da98c0c08102 Mon Sep 17 00:00:00 2001 From: Bertrand Lallau Date: Tue, 25 Aug 2015 11:59:21 +0200 Subject: [PATCH] Added support of multi-region environment Octavia does not support keystone regions and can choose an incorrect endpoint url when multiple regions provides same services. Change-Id: I5ea9de380419592920555a2c2fe7ac6f6935e700 Closes-Bug: #1487359 --- etc/octavia.conf | 6 +- octavia/common/clients.py | 69 +++++++++++++ octavia/common/config.py | 6 +- octavia/compute/drivers/nova_driver.py | 29 +----- .../controller/worker/tasks/compute_tasks.py | 2 +- .../controller/worker/tasks/network_tasks.py | 5 +- octavia/network/drivers/neutron/base.py | 11 +-- octavia/network/drivers/noop_driver/driver.py | 4 +- octavia/tests/unit/base.py | 5 +- octavia/tests/unit/common/test_clients.py | 96 +++++++++++++++++++ .../unit/compute/drivers/test_nova_driver.py | 55 ----------- .../neutron/test_allowed_address_pairs.py | 8 +- .../unit/network/drivers/neutron/test_base.py | 5 +- 13 files changed, 195 insertions(+), 106 deletions(-) create mode 100644 octavia/common/clients.py create mode 100644 octavia/tests/unit/common/test_clients.py diff --git a/etc/octavia.conf b/etc/octavia.conf index 0d5750c6b4..7a80f39dac 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -11,8 +11,10 @@ # # octavia_plugins = hot_plug_plugin -# nova_region_name = +# Region in Identity service catalog to use for communication with the OpenStack services. # +# os_region_name = + # Hostname to be used by the host machine for services running on it. # The default value is the hostname of the host machine. # host = @@ -143,4 +145,4 @@ # Cleanup interval for Deleted amphora # cleanup_interval = 30 # Amphora expiry age in seconds. Default is 1 week -# amphora_expiry_age = 604800 \ No newline at end of file +# amphora_expiry_age = 604800 diff --git a/octavia/common/clients.py b/octavia/common/clients.py new file mode 100644 index 0000000000..53a754b62f --- /dev/null +++ b/octavia/common/clients.py @@ -0,0 +1,69 @@ +# 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 neutronclient.neutron import client as neutron_client +from novaclient import client as nova_client +from oslo_log import log as logging +from oslo_utils import excutils + +from octavia.common import keystone +from octavia.i18n import _LE + +LOG = logging.getLogger(__name__) +NEUTRON_VERSION = '2.0' +NOVA_VERSION = '2' + + +class NovaAuth(object): + nova_client = None + + @classmethod + def get_nova_client(cls, region): + """Create nova client object. + + :param region: The region of the service + :return: a Nova Client object. + :raises Exception: if the client cannot be created + """ + if not cls.nova_client: + try: + cls.nova_client = nova_client.Client( + NOVA_VERSION, session=keystone.get_session(), + region_name=region + ) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception(_LE("Error creating Nova client.")) + return cls.nova_client + + +class NeutronAuth(object): + neutron_client = None + + @classmethod + def get_neutron_client(cls, region): + """Create neutron client object. + + :param region: The region of the service + :return: a Neutron Client object. + :raises Exception: if the client cannot be created + """ + if not cls.neutron_client: + try: + cls.neutron_client = neutron_client.Client( + NEUTRON_VERSION, session=keystone.get_session(), + region_name=region + ) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception(_LE("Error creating Neutron client.")) + return cls.neutron_client diff --git a/octavia/common/config.py b/octavia/common/config.py index 24ab15c55f..f42d9830de 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -72,9 +72,9 @@ core_opts = [ help=_('CA file for novaclient to verify server certificates')), cfg.BoolOpt('nova_api_insecure', default=False, help=_("If True, ignore any SSL validation issues")), - cfg.StrOpt('nova_region_name', - help=_('Name of nova region to use. Useful if keystone manages' - ' more than one region.')), + cfg.StrOpt('os_region_name', + help=_('Region in Identity service catalog to use for ' + 'communication with the OpenStack services.')), cfg.StrOpt('octavia_plugins', default='hot_plug_plugin', help=_('Name of the controller plugin to use')) diff --git a/octavia/compute/drivers/nova_driver.py b/octavia/compute/drivers/nova_driver.py index 13514d0cbc..d091a9c1d9 100644 --- a/octavia/compute/drivers/nova_driver.py +++ b/octavia/compute/drivers/nova_driver.py @@ -12,15 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import client as nova_client from oslo_config import cfg from oslo_log import log as logging -from oslo_utils import excutils +from octavia.common import clients from octavia.common import constants from octavia.common import data_models as models from octavia.common import exceptions -from octavia.common import keystone from octavia.compute import compute_base from octavia.i18n import _LE @@ -37,7 +35,7 @@ class VirtualMachineManager(compute_base.ComputeBase): def __init__(self, region=None): super(VirtualMachineManager, self).__init__() # Must initialize nova api - self._nova_client = NovaAuth.get_nova_client(region) + self._nova_client = clients.NovaAuth.get_nova_client(region) self.manager = self._nova_client.servers def build(self, name="amphora_name", amphora_flavor=None, image_id=None, @@ -144,26 +142,3 @@ class VirtualMachineManager(compute_base.ComputeBase): lb_network_ip=lb_network_ip ) return response - - -class NovaAuth(object): - _nova_client = None - - @classmethod - def get_nova_client(cls, region): - """Create nova client object. - - :param region: The region of the service - :return: a Nova Client object. - :raises Exception: if the client cannot be created - """ - if not cls._nova_client: - try: - cls._nova_client = nova_client.Client( - constants.NOVA_2, session=keystone.get_session(), - region_name=region - ) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.exception(_LE("Error creating Nova client.")) - return cls._nova_client diff --git a/octavia/controller/worker/tasks/compute_tasks.py b/octavia/controller/worker/tasks/compute_tasks.py index 61610cf059..2800ce08a7 100644 --- a/octavia/controller/worker/tasks/compute_tasks.py +++ b/octavia/controller/worker/tasks/compute_tasks.py @@ -39,7 +39,7 @@ class BaseComputeTask(task.Task): namespace='octavia.compute.drivers', name=CONF.controller_worker.compute_driver, invoke_on_load=True, - invoke_kwds={'region': CONF.nova_region_name} + invoke_kwds={'region': CONF.os_region_name} ).driver diff --git a/octavia/controller/worker/tasks/network_tasks.py b/octavia/controller/worker/tasks/network_tasks.py index b81721e0d3..5d529f667e 100644 --- a/octavia/controller/worker/tasks/network_tasks.py +++ b/octavia/controller/worker/tasks/network_tasks.py @@ -40,7 +40,8 @@ class BaseNetworkTask(task.Task): self.network_driver = stevedore_driver.DriverManager( namespace='octavia.network.drivers', name=CONF.controller_worker.network_driver, - invoke_on_load=True + invoke_on_load=True, + invoke_kwds={'region': CONF.os_region_name} ).driver @@ -341,4 +342,4 @@ class GetAmphoraeNetworkConfigs(BaseNetworkTask): ha_subnet=ha_subnet, ha_port=ha_port ) - return amp_net_configs \ No newline at end of file + return amp_net_configs diff --git a/octavia/network/drivers/neutron/base.py b/octavia/network/drivers/neutron/base.py index 2ec8210552..f9685e1849 100644 --- a/octavia/network/drivers/neutron/base.py +++ b/octavia/network/drivers/neutron/base.py @@ -13,11 +13,10 @@ # under the License. from neutronclient.common import exceptions as neutron_client_exceptions -from neutronclient.neutron import client as neutron_client from oslo_log import log as logging +from octavia.common import clients from octavia.common import data_models -from octavia.common import keystone from octavia.i18n import _LE, _LI from octavia.network import base from octavia.network import data_models as network_models @@ -25,16 +24,14 @@ from octavia.network.drivers.neutron import utils LOG = logging.getLogger(__name__) -NEUTRON_VERSION = '2.0' SEC_GRP_EXT_ALIAS = 'security-group' class BaseNeutronDriver(base.AbstractNetworkDriver): - def __init__(self): + def __init__(self, region=None): self.sec_grp_enabled = True - self.neutron_client = neutron_client.Client( - NEUTRON_VERSION, session=keystone.get_session()) + self.neutron_client = clients.NeutronAuth.get_neutron_client(region) extensions = self.neutron_client.list_extensions() self._extensions = extensions.get('extensions') self._check_sec_grps() @@ -172,4 +169,4 @@ class BaseNeutronDriver(base.AbstractNetworkDriver): '(port id: {port_id}.').format( port_id=port_id) LOG.exception(message) - raise base.NetworkException(message) \ No newline at end of file + raise base.NetworkException(message) diff --git a/octavia/network/drivers/noop_driver/driver.py b/octavia/network/drivers/noop_driver/driver.py index 79589b1aef..e8f9f27832 100644 --- a/octavia/network/drivers/noop_driver/driver.py +++ b/octavia/network/drivers/noop_driver/driver.py @@ -94,7 +94,7 @@ class NoopManager(object): class NoopNetworkDriver(driver_base.AbstractNetworkDriver): - def __init__(self): + def __init__(self, region=None): super(NoopNetworkDriver, self).__init__() self.driver = NoopManager() @@ -130,4 +130,4 @@ class NoopNetworkDriver(driver_base.AbstractNetworkDriver): self.driver.get_subnet(subnet_id) def get_port(self, port_id): - self.driver.get_port(port_id) \ No newline at end of file + self.driver.get_port(port_id) diff --git a/octavia/tests/unit/base.py b/octavia/tests/unit/base.py index 3fa2d14497..90618000e0 100644 --- a/octavia/tests/unit/base.py +++ b/octavia/tests/unit/base.py @@ -15,7 +15,7 @@ import mock import testtools -from octavia.compute.drivers import nova_driver +from octavia.common import clients class TestCase(testtools.TestCase): @@ -26,4 +26,5 @@ class TestCase(testtools.TestCase): self.addCleanup(self.clean_caches) def clean_caches(self): - nova_driver.NovaAuth._nova_client = None + clients.NovaAuth.nova_client = None + clients.NeutronAuth.neutron_client = None diff --git a/octavia/tests/unit/common/test_clients.py b/octavia/tests/unit/common/test_clients.py new file mode 100644 index 0000000000..f40c2a5522 --- /dev/null +++ b/octavia/tests/unit/common/test_clients.py @@ -0,0 +1,96 @@ +# 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 +import neutronclient.v2_0 +import novaclient.v2 +from oslo_config import cfg + +from octavia.common import clients +from octavia.common import keystone +from octavia.tests.unit import base + +CONF = cfg.CONF + + +class TestNovaAuth(base.TestCase): + + def setUp(self): + CONF.set_override(group='keystone_authtoken', name='auth_version', + override='2') + # Reset the session and client + clients.NovaAuth.nova_client = None + keystone._SESSION = None + + super(TestNovaAuth, self).setUp() + + def test_get_nova_client(self): + # There should be no existing client + self.assertIsNone( + clients.NovaAuth.nova_client + ) + + # Mock out the keystone session and get the client + keystone._SESSION = mock.MagicMock() + bc1 = clients.NovaAuth.get_nova_client(region=None) + + # Our returned client should also be the saved client + self.assertIsInstance( + clients.NovaAuth.nova_client, + novaclient.v2.client.Client + ) + self.assertIs( + clients.NovaAuth.nova_client, + bc1 + ) + + # Getting the session again should return the same object + bc2 = clients.NovaAuth.get_nova_client( + region="test-region") + self.assertIs(bc1, bc2) + + +class TestNeutronAuth(base.TestCase): + + def setUp(self): + CONF.set_override(group='keystone_authtoken', name='auth_version', + override='2') + # Reset the session and client + clients.NeutronAuth.neutron_client = None + keystone._SESSION = None + + super(TestNeutronAuth, self).setUp() + + def test_get_neutron_client(self): + # There should be no existing client + self.assertIsNone( + clients.NeutronAuth.neutron_client + ) + + # Mock out the keystone session and get the client + keystone._SESSION = mock.MagicMock() + bc1 = clients.NeutronAuth.get_neutron_client(region=None) + + # Our returned client should also be the saved client + self.assertIsInstance( + clients.NeutronAuth.neutron_client, + neutronclient.v2_0.client.Client + ) + self.assertIs( + clients.NeutronAuth.neutron_client, + bc1 + ) + + # Getting the session again should return the same object + bc2 = clients.NeutronAuth.get_neutron_client( + region="test-region") + self.assertIs(bc1, bc2) diff --git a/octavia/tests/unit/compute/drivers/test_nova_driver.py b/octavia/tests/unit/compute/drivers/test_nova_driver.py index fdbfe7743f..72e35f2b90 100644 --- a/octavia/tests/unit/compute/drivers/test_nova_driver.py +++ b/octavia/tests/unit/compute/drivers/test_nova_driver.py @@ -12,8 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneclient import session -import novaclient.v2 from oslo_config import cfg from oslo_utils import uuidutils import six @@ -21,7 +19,6 @@ import six from octavia.common import constants from octavia.common import data_models as models from octavia.common import exceptions -from octavia.common import keystone import octavia.compute.drivers.nova_driver as nova_common import octavia.tests.unit.base as base @@ -123,55 +120,3 @@ class TestNovaClient(base.TestCase): self.manager.manager.get.side_effect = Exception self.assertRaises(exceptions.ComputeGetException, self.manager.get_amphora, self.amphora.id) - - -class TestNovaAuth(base.TestCase): - - def setUp(self): - CONF.set_override(group='keystone_authtoken', name='auth_version', - override='2') - # Reset the session and client - nova_common.NovaAuth._nova_client = None - keystone._SESSION = None - - super(TestNovaAuth, self).setUp() - - def test_get_keystone_client(self): - # There should be no existing session - self.assertIsNone(keystone._SESSION) - - # Get us a session - ks1 = keystone.get_session() - - # Our returned session should also be the saved session - self.assertIsInstance(keystone._SESSION, session.Session) - self.assertIs(keystone._SESSION, ks1) - - # Getting the session again should return the same object - ks2 = keystone.get_session() - self.assertIs(ks1, ks2) - - def test_get_nova_client(self): - # There should be no existing client - self.assertIsNone( - nova_common.NovaAuth._nova_client - ) - - # Mock out the keystone session and get the client - keystone._SESSION = mock.MagicMock() - bc1 = nova_common.NovaAuth.get_nova_client(region=None) - - # Our returned client should also be the saved client - self.assertIsInstance( - nova_common.NovaAuth._nova_client, - novaclient.v2.client.Client - ) - self.assertIs( - nova_common.NovaAuth._nova_client, - bc1 - ) - - # Getting the session again should return the same object - bc2 = nova_common.NovaAuth.get_nova_client( - region="test-region") - self.assertIs(bc1, bc2) diff --git a/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py b/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py index 908d65c912..e516adc9a3 100644 --- a/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py +++ b/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py @@ -18,6 +18,7 @@ import mock from neutronclient.common import exceptions as neutron_exceptions from novaclient.client import exceptions as nova_exceptions +from octavia.common import clients from octavia.common import constants from octavia.common import data_models from octavia.network import base as network_base @@ -35,10 +36,11 @@ class TestAllowedAddressPairsDriver(base.TestCase): def setUp(self): super(TestAllowedAddressPairsDriver, self).setUp() - with mock.patch('neutronclient.neutron.client.Client', + with mock.patch('octavia.common.clients.neutron_client.Client', autospec=True) as neutron_client: - with mock.patch('novaclient.client.Client', autospec=True): - client = neutron_client(neutron_base.NEUTRON_VERSION) + with mock.patch('octavia.common.clients.nova_client.Client', + autospec=True): + client = neutron_client(clients.NEUTRON_VERSION) client.list_extensions.return_value = { 'extensions': [ {'alias': allowed_address_pairs.AAP_EXT_ALIAS}, diff --git a/octavia/tests/unit/network/drivers/neutron/test_base.py b/octavia/tests/unit/network/drivers/neutron/test_base.py index e5dca24212..796237a356 100644 --- a/octavia/tests/unit/network/drivers/neutron/test_base.py +++ b/octavia/tests/unit/network/drivers/neutron/test_base.py @@ -14,6 +14,7 @@ import mock +from octavia.common import clients from octavia.common import data_models from octavia.network import data_models as network_models from octavia.network.drivers.neutron import base as neutron_base @@ -37,9 +38,9 @@ class TestBaseNeutronNetworkDriver(base.TestCase): def setUp(self): super(TestBaseNeutronNetworkDriver, self).setUp() - with mock.patch('neutronclient.neutron.client.Client', + with mock.patch('octavia.common.clients.neutron_client.Client', autospec=True) as neutron_client: - client = neutron_client(neutron_base.NEUTRON_VERSION) + client = neutron_client(clients.NEUTRON_VERSION) client.list_extensions.return_value = { 'extensions': [ {'alias': neutron_base.SEC_GRP_EXT_ALIAS}