From f83b2aebb181d702e452254e711f31e67a590b40 Mon Sep 17 00:00:00 2001 From: Tong Liu Date: Wed, 4 Oct 2017 15:13:28 -0700 Subject: [PATCH] NSXv3: Fix TERMINATED_HTTPS listener For TERMINATED_HTTPS listener protocol, we need to add server ssl profile binding to virtual server. The binding includes the following information: - client ssl profile id - default certificate id Also, this patch adds unit tests for https and terminated_https. Depends-on: I0429d1d7caf7995f044d4daaa46da13e506fddb2 Change-Id: I399ecb6b879cf4ebeb6ec0239096b648646c64e7 --- vmware_nsx/plugins/nsx_v3/plugin.py | 42 ++++++++++ vmware_nsx/services/lbaas/lb_const.py | 1 + .../services/lbaas/nsx_v3/listener_mgr.py | 77 ++++++++++++++++--- .../unit/services/lbaas/test_nsxv3_driver.py | 56 +++++++++++++- 4 files changed, 163 insertions(+), 13 deletions(-) diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index f1ce0f29a1..7589b8f886 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -124,6 +124,8 @@ NSX_V3_FW_DEFAULT_NS_GROUP = 'os_default_section_ns_group' NSX_V3_DEFAULT_SECTION = 'OS-Default-Section' NSX_V3_EXCLUDED_PORT_NSGROUP_NAME = 'neutron_excluded_port_nsgroup' NSX_V3_NON_VIF_PROFILE = 'nsx-default-switch-security-non-vif-profile' +NSX_V3_SERVER_SSL_PROFILE = 'nsx-default-server-ssl-profile' +NSX_V3_CLIENT_SSL_PROFILE = 'nsx-default-client-ssl-profile' def inject_headers(): @@ -359,6 +361,17 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, NSX_V3_NON_VIF_PROFILE)[0] self._no_switch_security = profile_client.build_switch_profile_ids( profile_client, no_switch_security_prof)[0] + self.server_ssl_profile = None + self.client_ssl_profile = None + # Only create LB profiles when nsxv3 version >= 2.1.0 + if self.nsxlib.feature_supported(nsxlib_consts.FEATURE_LOAD_BALANCER): + LOG.debug("Initializing NSX v3 Load Balancer default profiles") + try: + self._init_lb_profiles() + except Exception as e: + msg = (_("Unable to initialize NSX v3 lb profiles: " + "Reason: %(reason)s") % {'reason': str(e)}) + raise nsx_exc.NsxPluginException(err_msg=msg) def _translate_configured_names_to_uuids(self): # If using tags to find the objects, make sure tag scope is configured @@ -530,6 +543,35 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, profile_id=profile[0]['id']) if profile else None return self._mac_learning_profile + def _init_lb_profiles(self): + with locking.LockManager.get_lock('nsxv3_lb_profiles_init'): + lb_profiles = self._get_lb_profiles() + if not lb_profiles.get('client_ssl_profile'): + self.nsxlib.load_balancer.client_ssl_profile.create( + NSX_V3_CLIENT_SSL_PROFILE, + 'Neutron LB Client SSL Profile', + tags=self.nsxlib.build_v3_api_version_tag()) + if not lb_profiles.get('server_ssl_profile'): + self.nsxlib.load_balancer.server_ssl_profile.create( + NSX_V3_SERVER_SSL_PROFILE, + 'Neutron LB Server SSL Profile', + tags=self.nsxlib.build_v3_api_version_tag()) + + def _get_lb_profiles(self): + if not self.client_ssl_profile: + ssl_profile_client = self.nsxlib.load_balancer.client_ssl_profile + profile = ssl_profile_client.find_by_display_name( + NSX_V3_CLIENT_SSL_PROFILE) + self.client_ssl_profile = profile[0]['id'] if profile else None + if not self.server_ssl_profile: + ssl_profile_client = self.nsxlib.load_balancer.server_ssl_profile + profile = ssl_profile_client.find_by_display_name( + NSX_V3_SERVER_SSL_PROFILE) + self.server_ssl_profile = profile[0]['id'] if profile else None + + return {'client_ssl_profile': self.client_ssl_profile, + 'server_ssl_profile': self.server_ssl_profile} + def _get_port_security_profile_id(self): return self.nsxlib.switching_profile.build_switch_profile_ids( self.nsxlib.switching_profile, self._psec_profile)[0] diff --git a/vmware_nsx/services/lbaas/lb_const.py b/vmware_nsx/services/lbaas/lb_const.py index 465fb392de..2896d5e8df 100644 --- a/vmware_nsx/services/lbaas/lb_const.py +++ b/vmware_nsx/services/lbaas/lb_const.py @@ -97,6 +97,7 @@ LB_STATS_MAP = {'active_connections': 'current_sessions', 'total_connections': 'total_sessions'} LR_ROUTER_TYPE = 'os-neutron-router-id' LR_PORT_TYPE = 'os-neutron-rport-id' +LB_CERT_RESOURCE_TYPE = ['certificate_signed', 'certificate_self_signed'] DEFAULT_LB_SIZE = 'SMALL' LB_FLAVOR_SIZES = ['SMALL', 'MEDIUM', 'LARGE', 'small', 'medium', 'large'] LB_RULE_MATCH_TYPE = { diff --git a/vmware_nsx/services/lbaas/nsx_v3/listener_mgr.py b/vmware_nsx/services/lbaas/nsx_v3/listener_mgr.py index 9a399f29da..578af9dd8b 100644 --- a/vmware_nsx/services/lbaas/nsx_v3/listener_mgr.py +++ b/vmware_nsx/services/lbaas/nsx_v3/listener_mgr.py @@ -34,10 +34,51 @@ class EdgeListenerManager(base_mgr.Nsxv3LoadbalancerBaseManager): def __init__(self): super(EdgeListenerManager, self).__init__() + def _get_virtual_server_kwargs(self, context, listener, vs_name, tags, + app_profile_id, certificate=None): + kwargs = {'display_name': vs_name, + 'tags': tags, + 'enabled': listener.admin_state_up, + 'ip_address': listener.loadbalancer.vip_address, + 'port': listener.protocol_port, + 'application_profile_id': app_profile_id} + if listener.connection_limit != -1: + kwargs['max_concurrent_connections'] = \ + listener.connection_limit + if listener.default_pool_id: + pool_binding = nsx_db.get_nsx_lbaas_pool_binding( + context.session, listener.loadbalancer.id, + listener.default_pool_id) + if pool_binding: + kwargs['pool_id'] = pool_binding.get('lb_pool_id') + ssl_profile_binding = self._get_ssl_profile_binding( + tags, certificate=certificate) + if (listener.protocol == lb_const.LB_PROTOCOL_TERMINATED_HTTPS and + ssl_profile_binding): + kwargs.update(ssl_profile_binding) + return kwargs + + def _get_ssl_profile_binding(self, tags, certificate=None): + tm_client = self.core_plugin.nsxlib.trust_management + nsx_cert_id = None + ssl_profile_binding = None + if certificate: + nsx_cert_id = tm_client.create_cert( + certificate.get_certificate(), + private_key=certificate.get_private_key(), + passphrase=certificate.get_private_key_passphrase(), + tags=tags) + ssl_profile_binding = { + 'client_ssl_profile_binding': { + 'ssl_profile_id': self.core_plugin.client_ssl_profile, + 'default_certificate_id': nsx_cert_id + } + } + return ssl_profile_binding + @log_helpers.log_method_call def create(self, context, listener, certificate=None): lb_id = listener.loadbalancer_id - vip_address = listener.loadbalancer.vip_address load_balancer = self.core_plugin.nsxlib.load_balancer app_client = load_balancer.application_profile vs_client = load_balancer.virtual_server @@ -51,9 +92,12 @@ class EdgeListenerManager(base_mgr.Nsxv3LoadbalancerBaseManager): 'tag': listener.loadbalancer.name[:utils.MAX_TAG_LEN]}) tags.append({'scope': 'os-lbaas-lb-id', 'tag': lb_id}) - if listener.protocol == 'HTTP' or listener.protocol == 'HTTPS': + + if (listener.protocol == lb_const.LB_PROTOCOL_HTTP or + listener.protocol == lb_const.LB_PROTOCOL_TERMINATED_HTTPS): profile_type = lb_const.LB_HTTP_PROFILE - elif listener.protocol == 'TCP': + elif (listener.protocol == lb_const.LB_PROTOCOL_TCP or + listener.protocol == lb_const.LB_PROTOCOL_HTTPS): profile_type = lb_const.LB_TCP_PROFILE else: msg = (_('Cannot create listener %(listener)s with ' @@ -65,13 +109,9 @@ class EdgeListenerManager(base_mgr.Nsxv3LoadbalancerBaseManager): app_profile = app_client.create( display_name=vs_name, resource_type=profile_type, tags=tags) app_profile_id = app_profile['id'] - virtual_server = vs_client.create( - display_name=vs_name, - tags=tags, - enabled=listener.admin_state_up, - ip_address=vip_address, - port=listener.protocol_port, - application_profile_id=app_profile_id) + kwargs = self._get_virtual_server_kwargs( + context, listener, vs_name, tags, app_profile_id, certificate) + virtual_server = vs_client.create(**kwargs) except nsxlib_exc.ManagerError: self.lbv2_driver.listener.failed_completion(context, listener) msg = _('Failed to create virtual server at NSX backend') @@ -164,6 +204,23 @@ class EdgeListenerManager(base_mgr.Nsxv3LoadbalancerBaseManager): msg = (_('Failed to delete application profile: %(app)s') % {'app': app_profile_id}) raise n_exc.BadRequest(resource='lbaas-listener', msg=msg) + + # Delete imported NSX cert if there is any + cert_tags = [{'scope': lb_const.LB_LISTENER_TYPE, + 'tag': listener.id}] + results = self.core_plugin.nsxlib.search_by_tags( + tags=cert_tags) + # Only delete object related to certificate used by listener + for obj in results['results']: + if obj.get('resource_type') in lb_const.LB_CERT_RESOURCE_TYPE: + tm_client = self.core_plugin.nsxlib.trust_management + try: + tm_client.delete_cert(obj['id']) + except nsxlib_exc.ManagerError: + LOG.error("Exception thrown when trying to delete " + "certificate: %(cert)s", + {'cert': obj['id']}) + nsx_db.delete_nsx_lbaas_listener_binding( context.session, lb_id, listener.id) diff --git a/vmware_nsx/tests/unit/services/lbaas/test_nsxv3_driver.py b/vmware_nsx/tests/unit/services/lbaas/test_nsxv3_driver.py index 67b4224995..58d4b5b004 100644 --- a/vmware_nsx/tests/unit/services/lbaas/test_nsxv3_driver.py +++ b/vmware_nsx/tests/unit/services/lbaas/test_nsxv3_driver.py @@ -103,6 +103,8 @@ L7RULE_BINDING = {'loadbalancer_id': LB_ID, 'lb_vs_id': LB_VS_ID, 'lb_rule_id': LB_RULE_ID} +FAKE_CERT = {'id': 'cert-xyz'} + class BaseTestEdgeLbaasV2(base.BaseTestCase): def _tested_entity(self): @@ -127,6 +129,12 @@ class BaseTestEdgeLbaasV2(base.BaseTestCase): 'listener1', '', None, LB_ID, 'HTTP', protocol_port=80, loadbalancer=self.lb) + self.https_listener = lb_models.Listener( + LISTENER_ID, LB_TENANT_ID, 'listener1', '', None, LB_ID, + 'HTTPS', protocol_port=443, loadbalancer=self.lb) + self.terminated_https_listener = lb_models.Listener( + LISTENER_ID, LB_TENANT_ID, 'listener1', '', None, LB_ID, + 'TERMINATED_HTTPS', protocol_port=443, loadbalancer=self.lb) self.pool = lb_models.Pool(POOL_ID, LB_TENANT_ID, 'pool1', '', None, 'HTTP', 'ROUND_ROBIN', loadbalancer_id=LB_ID, @@ -182,6 +190,8 @@ class BaseTestEdgeLbaasV2(base.BaseTestCase): 'monitor').start() self.rule_client = mock.patch.object(load_balancer, 'rule').start() + self.tm_client = mock.patch.object(nsxlib, + 'trust_management').start() def _unpatch_lb_plugin(self, lb_plugin, manager): setattr(lb_plugin, manager, self.real_manager) @@ -256,7 +266,7 @@ class TestEdgeLbaasV2Listener(BaseTestEdgeLbaasV2): def _tested_entity(self): return 'listener' - def test_create(self): + def _create_listener(self, protocol='HTTP'): with mock.patch.object(self.app_client, 'create' ) as mock_create_app_profile, \ mock.patch.object(self.vs_client, 'create' @@ -270,8 +280,11 @@ class TestEdgeLbaasV2Listener(BaseTestEdgeLbaasV2): mock_create_app_profile.return_value = {'id': APP_PROFILE_ID} mock_create_virtual_server.return_value = {'id': LB_VS_ID} mock_get_lb_binding.return_value = LB_BINDING + listener = self.listener + if protocol == 'HTTPS': + listener = self.https_listener - self.edge_driver.listener.create(self.context, self.listener) + self.edge_driver.listener.create(self.context, listener) mock_add_virtual_server.assert_called_with(LB_SERVICE_ID, LB_VS_ID) @@ -282,7 +295,44 @@ class TestEdgeLbaasV2Listener(BaseTestEdgeLbaasV2): mock_successful_completion = ( self.lbv2_driver.listener.successful_completion) mock_successful_completion.assert_called_with(self.context, - self.listener) + listener) + + def test_create_http_listener(self): + self._create_listener() + + def test_create_https_listener(self): + self._create_listener(protocol='HTTPS') + + def test_create_terminated_https(self): + with mock.patch.object(self.tm_client, 'create_cert' + ) as mock_create_cert, \ + mock.patch.object(self.app_client, 'create' + ) as mock_create_app_profile, \ + mock.patch.object(self.vs_client, 'create' + ) as mock_create_virtual_server, \ + mock.patch.object(nsx_db, 'get_nsx_lbaas_loadbalancer_binding' + ) as mock_get_lb_binding, \ + mock.patch.object(self.service_client, 'add_virtual_server' + ) as mock_add_virtual_server, \ + mock.patch.object(nsx_db, 'add_nsx_lbaas_listener_binding' + ) as mock_add_listener_binding: + mock_create_cert.return_value = FAKE_CERT['id'] + mock_create_app_profile.return_value = {'id': APP_PROFILE_ID} + mock_create_virtual_server.return_value = {'id': LB_VS_ID} + mock_get_lb_binding.return_value = LB_BINDING + + self.edge_driver.listener.create(self.context, + self.terminated_https_listener) + mock_add_virtual_server.assert_called_with(LB_SERVICE_ID, + LB_VS_ID) + mock_add_listener_binding.assert_called_with( + self.context.session, LB_ID, LISTENER_ID, APP_PROFILE_ID, + LB_VS_ID) + + mock_successful_completion = ( + self.lbv2_driver.listener.successful_completion) + mock_successful_completion.assert_called_with( + self.context, self.terminated_https_listener) def test_update(self): new_listener = lb_models.Listener(LISTENER_ID, LB_TENANT_ID,