From 3c8edd4d88719b5a7dff73a7f51db28e24d46f4a Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Mon, 12 Feb 2018 11:35:14 +1300 Subject: [PATCH] Add support for Octavia resources in Heat Octavia is already an official LBaaS solution for Openstack (https://governance.openstack.org/tc/reference/projects/octavia.html) and will deprecate the neutron-lbaas extension starting from Queens release. For deployment support Octavia service for load balancing functionlity, Octavia related resources instead of LBaaS should be used in Heat template. Tested in my DevStack environment. Change-Id: Icc45e0a126c648fbcba4ebcd1bb258d60957f2d6 Closes-Bug: #1748577 --- magnum/common/exception.py | 4 + magnum/common/keystone.py | 29 ++++++ .../environments/with_master_lb_octavia.yaml | 14 +++ magnum/drivers/heat/template_def.py | 6 +- .../handlers/test_mesos_cluster_conductor.py | 78 +++++++++++++++- .../handlers/test_swarm_cluster_conductor.py | 92 ++++++++++++++++++- 6 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 magnum/drivers/common/templates/environments/with_master_lb_octavia.yaml diff --git a/magnum/common/exception.py b/magnum/common/exception.py index b99c20a17d..d75f981243 100755 --- a/magnum/common/exception.py +++ b/magnum/common/exception.py @@ -367,6 +367,10 @@ class RegionsListFailed(MagnumException): message = _("Failed to list regions.") +class ServicesListFailed(MagnumException): + message = _("Failed to list services.") + + class TrusteeOrTrustToClusterFailed(MagnumException): message = _("Failed to create trustee or trust for Cluster: " "%(cluster_uuid)s") diff --git a/magnum/common/keystone.py b/magnum/common/keystone.py index b22a8cd031..44bf38f827 100755 --- a/magnum/common/keystone.py +++ b/magnum/common/keystone.py @@ -291,3 +291,32 @@ class KeystoneClientV3(object): 'region_name_list': '/'.join( region_list + ['unspecified'])}) return region_name + + +def is_octavia_enabled(): + """Check if Octavia service is deployed in the cloud. + + Octavia is already an official LBaaS solution for Openstack + (https://governance.openstack.org/tc/reference/projects/octavia.html) and + will deprecate the neutron-lbaas extension starting from Queens release. + + We use Octavia instead of Neutron LBaaS API for load balancing + functionality for k8s cluster if Octavia service is deployed and enabled + in the cloud. + """ + # Put the import here to avoid circular importing. + from magnum.common import context + admin_context = context.make_admin_context() + keystone = KeystoneClientV3(admin_context) + + try: + octavia_svc = keystone.client.services.list(type='load-balancer') + except Exception: + LOG.exception('Failed to list services') + raise exception.ServicesListFailed() + + # Always assume there is only one load balancing service configured. + if octavia_svc and octavia_svc[0].enabled: + return True + + return False diff --git a/magnum/drivers/common/templates/environments/with_master_lb_octavia.yaml b/magnum/drivers/common/templates/environments/with_master_lb_octavia.yaml new file mode 100644 index 0000000000..77020de3ff --- /dev/null +++ b/magnum/drivers/common/templates/environments/with_master_lb_octavia.yaml @@ -0,0 +1,14 @@ +# Environment file to enable LBaaS in a cluster by mapping +# LBaaS-related resource types to the real Octavia resource types. +resource_registry: + "Magnum::ApiGatewaySwitcher": ../fragments/api_gateway_switcher_pool.yaml + + # Cluster template + "Magnum::Optional::Neutron::LBaaS::LoadBalancer": "OS::Octavia::LoadBalancer" + "Magnum::Optional::Neutron::LBaaS::Listener": "OS::Octavia::Listener" + "Magnum::Optional::Neutron::LBaaS::Pool": "OS::Octavia::Pool" + "Magnum::Optional::Neutron::LBaaS::HealthMonitor": "OS::Octavia::HealthMonitor" + "Magnum::Optional::Neutron::LBaaS::FloatingIP": "OS::Neutron::FloatingIP" + + # Master node template + "Magnum::Optional::Neutron::LBaaS::PoolMember": "OS::Octavia::PoolMember" diff --git a/magnum/drivers/heat/template_def.py b/magnum/drivers/heat/template_def.py index b1eae2bd89..0849c325b9 100755 --- a/magnum/drivers/heat/template_def.py +++ b/magnum/drivers/heat/template_def.py @@ -20,6 +20,7 @@ import six from magnum.common import clients from magnum.common import exception +from magnum.common import keystone from magnum.common import utils import magnum.conf @@ -328,7 +329,10 @@ class BaseTemplateDefinition(TemplateDefinition): def add_lb_env_file(env_files, cluster_template): if cluster_template.master_lb_enabled: - env_files.append(COMMON_ENV_PATH + 'with_master_lb.yaml') + if keystone.is_octavia_enabled(): + env_files.append(COMMON_ENV_PATH + 'with_master_lb_octavia.yaml') + else: + env_files.append(COMMON_ENV_PATH + 'with_master_lb.yaml') else: env_files.append(COMMON_ENV_PATH + 'no_master_lb.yaml') diff --git a/magnum/tests/unit/conductor/handlers/test_mesos_cluster_conductor.py b/magnum/tests/unit/conductor/handlers/test_mesos_cluster_conductor.py index 3389fe1189..750938f22a 100644 --- a/magnum/tests/unit/conductor/handlers/test_mesos_cluster_conductor.py +++ b/magnum/tests/unit/conductor/handlers/test_mesos_cluster_conductor.py @@ -207,8 +207,10 @@ class TestClusterConductorWithMesos(base.TestCase): @patch('magnum.objects.ClusterTemplate.get_by_uuid') @patch('magnum.drivers.common.driver.Driver.get_driver') - def test_extract_template_definition_with_lb( + @patch('magnum.common.keystone.KeystoneClientV3') + def test_extract_template_definition_with_lb_neutron( self, + mock_kc, mock_driver, mock_objects_cluster_template_get_by_uuid): self.cluster_template_dict['master_lb_enabled'] = True @@ -219,6 +221,8 @@ class TestClusterConductorWithMesos(base.TestCase): cluster = objects.Cluster(self.context, **self.cluster_dict) mock_driver.return_value = mesos_dr.Driver() + mock_kc.return_value.client.services.list.return_value = [] + (template_path, definition, env_files) = mock_driver()._extract_template_definition(self.context, @@ -266,8 +270,78 @@ class TestClusterConductorWithMesos(base.TestCase): @patch('magnum.objects.ClusterTemplate.get_by_uuid') @patch('magnum.drivers.common.driver.Driver.get_driver') + @patch('magnum.common.keystone.KeystoneClientV3') + def test_extract_template_definition_with_lb_octavia( + self, + mock_kc, + mock_driver, + mock_objects_cluster_template_get_by_uuid): + self.cluster_template_dict['master_lb_enabled'] = True + cluster_template = objects.ClusterTemplate( + self.context, **self.cluster_template_dict) + mock_objects_cluster_template_get_by_uuid.return_value = \ + cluster_template + cluster = objects.Cluster(self.context, **self.cluster_dict) + mock_driver.return_value = mesos_dr.Driver() + + class Service(object): + def __init__(self): + self.enabled = True + + mock_kc.return_value.client.services.list.return_value = [Service()] + + (template_path, + definition, + env_files) = mock_driver()._extract_template_definition(self.context, + cluster) + + expected = { + 'ssh_key_name': 'keypair_id', + 'external_network': 'external_network_id', + 'fixed_network': 'fixed_network', + 'fixed_subnet': 'fixed_subnet', + 'dns_nameserver': 'dns_nameserver', + 'server_image': 'image_id', + 'master_flavor': 'master_flavor_id', + 'slave_flavor': 'flavor_id', + 'number_of_slaves': 1, + 'number_of_masters': 1, + 'http_proxy': 'http_proxy', + 'https_proxy': 'https_proxy', + 'no_proxy': 'no_proxy', + 'cluster_name': 'cluster1', + 'trustee_domain_id': self.mock_keystone.trustee_domain_id, + 'trustee_username': 'fake_trustee', + 'trustee_password': 'fake_trustee_password', + 'trustee_user_id': '7b489f04-b458-4541-8179-6a48a553e656', + 'trust_id': '', + 'volume_driver': 'volume_driver', + 'auth_url': 'http://192.168.10.10:5000/v3', + 'region_name': self.mock_osc.cinder_region_name.return_value, + 'username': 'mesos_user', + 'tenant_name': 'admin', + 'domain_name': 'domainname', + 'rexray_preempt': 'False', + 'mesos_slave_executor_env_variables': '{}', + 'mesos_slave_isolation': 'docker/runtime,filesystem/linux', + 'mesos_slave_work_dir': '/tmp/mesos/slave', + 'mesos_slave_image_providers': 'docker', + 'verify_ca': True, + 'openstack_ca': '', + } + self.assertEqual(expected, definition) + self.assertEqual( + ['../../common/templates/environments/no_private_network.yaml', + '../../common/templates/environments/with_master_lb_octavia.yaml' + ], + env_files) + + @patch('magnum.objects.ClusterTemplate.get_by_uuid') + @patch('magnum.drivers.common.driver.Driver.get_driver') + @patch('magnum.common.keystone.KeystoneClientV3') def test_extract_template_definition_multi_master( self, + mock_kc, mock_driver, mock_objects_cluster_template_get_by_uuid): self.cluster_template_dict['master_lb_enabled'] = True @@ -279,6 +353,8 @@ class TestClusterConductorWithMesos(base.TestCase): cluster = objects.Cluster(self.context, **self.cluster_dict) mock_driver.return_value = mesos_dr.Driver() + mock_kc.return_value.client.services.list.return_value = [] + (template_path, definition, env_files) = mock_driver()._extract_template_definition(self.context, diff --git a/magnum/tests/unit/conductor/handlers/test_swarm_cluster_conductor.py b/magnum/tests/unit/conductor/handlers/test_swarm_cluster_conductor.py index 0fecf15850..4837984e08 100644 --- a/magnum/tests/unit/conductor/handlers/test_swarm_cluster_conductor.py +++ b/magnum/tests/unit/conductor/handlers/test_swarm_cluster_conductor.py @@ -330,8 +330,10 @@ class TestClusterConductorWithSwarm(base.TestCase): @patch('requests.get') @patch('magnum.objects.ClusterTemplate.get_by_uuid') @patch('magnum.drivers.common.driver.Driver.get_driver') - def test_extract_template_definition_with_lb( + @patch('magnum.common.keystone.KeystoneClientV3') + def test_extract_template_definition_with_lb_neutron( self, + mock_kc, mock_driver, mock_objects_cluster_template_get_by_uuid, mock_get): @@ -348,6 +350,8 @@ class TestClusterConductorWithSwarm(base.TestCase): mock_driver.return_value = swarm_dr.Driver() cluster = objects.Cluster(self.context, **self.cluster_dict) + mock_kc.return_value.client.services.list.return_value = [] + (template_path, definition, env_files) = mock_driver()._extract_template_definition(self.context, @@ -403,8 +407,92 @@ class TestClusterConductorWithSwarm(base.TestCase): @patch('requests.get') @patch('magnum.objects.ClusterTemplate.get_by_uuid') @patch('magnum.drivers.common.driver.Driver.get_driver') + @patch('magnum.common.keystone.KeystoneClientV3') + def test_extract_template_definition_with_lb_octavia( + self, + mock_kc, + mock_driver, + mock_objects_cluster_template_get_by_uuid, + mock_get): + self.cluster_template_dict['master_lb_enabled'] = True + cluster_template = objects.ClusterTemplate( + self.context, **self.cluster_template_dict) + mock_objects_cluster_template_get_by_uuid.return_value = \ + cluster_template + expected_result = str('{"action":"get","node":{"key":"test","value":' + '"1","modifiedIndex":10,"createdIndex":10}}') + mock_resp = mock.MagicMock() + mock_resp.text = expected_result + mock_get.return_value = mock_resp + mock_driver.return_value = swarm_dr.Driver() + cluster = objects.Cluster(self.context, **self.cluster_dict) + + class Service(object): + def __init__(self): + self.enabled = True + + mock_kc.return_value.client.services.list.return_value = [Service()] + + (template_path, + definition, + env_files) = mock_driver()._extract_template_definition(self.context, + cluster) + + expected = { + 'ssh_key_name': 'keypair_id', + 'external_network': 'external_network_id', + 'fixed_network': 'fixed_network', + 'fixed_subnet': 'fixed_subnet', + 'dns_nameserver': 'dns_nameserver', + 'server_image': 'image_id', + 'master_flavor': 'master_flavor_id', + 'node_flavor': 'flavor_id', + 'number_of_masters': 1, + 'number_of_nodes': 1, + 'docker_volume_size': 20, + 'docker_storage_driver': 'devicemapper', + 'discovery_url': 'https://discovery.test.io/123456789', + 'http_proxy': 'http_proxy', + 'https_proxy': 'https_proxy', + 'no_proxy': 'no_proxy', + 'cluster_uuid': '5d12f6fd-a196-4bf0-ae4c-1f639a523a52', + 'magnum_url': self.mock_osc.magnum_url.return_value, + 'tls_disabled': False, + 'registry_enabled': False, + 'network_driver': 'network_driver', + 'flannel_network_cidr': '10.101.0.0/16', + 'flannel_network_subnetlen': '26', + 'flannel_backend': 'vxlan', + 'trustee_domain_id': self.mock_keystone.trustee_domain_id, + 'trustee_username': 'fake_trustee', + 'trustee_password': 'fake_trustee_password', + 'trustee_user_id': '7b489f04-b458-4541-8179-6a48a553e656', + 'trust_id': 'bd11efc5-d4e2-4dac-bbce-25e348ddf7de', + 'auth_url': 'http://192.168.10.10:5000/v3', + 'swarm_version': 'fake-version', + 'swarm_strategy': u'spread', + 'volume_driver': 'rexray', + 'rexray_preempt': 'False', + 'docker_volume_type': 'lvmdriver-1', + 'verify_ca': True, + 'openstack_ca': '', + 'nodes_affinity_policy': 'soft-anti-affinity' + } + self.assertEqual(expected, definition) + self.assertEqual( + ['../../common/templates/environments/no_private_network.yaml', + '../../common/templates/environments/with_volume.yaml', + '../../common/templates/environments/with_master_lb_octavia.yaml' + ], + env_files) + + @patch('requests.get') + @patch('magnum.objects.ClusterTemplate.get_by_uuid') + @patch('magnum.drivers.common.driver.Driver.get_driver') + @patch('magnum.common.keystone.KeystoneClientV3') def test_extract_template_definition_multi_master( self, + mock_kc, mock_driver, mock_objects_cluster_template_get_by_uuid, mock_get): @@ -422,6 +510,8 @@ class TestClusterConductorWithSwarm(base.TestCase): mock_driver.return_value = swarm_dr.Driver() cluster = objects.Cluster(self.context, **self.cluster_dict) + mock_kc.return_value.client.services.list.return_value = [] + (template_path, definition, env_files) = mock_driver()._extract_template_definition(self.context,