From 2256459aa1bcc46a25b8be2d320d44a852edca15 Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Tue, 3 Jan 2017 13:23:10 +0200 Subject: [PATCH] NSX-v| LBAAS L7 support Supporting L7 policies and rules in LBAAS-v2 Including a new db table nsxv_lbaas_l7policy_bindings for mapping between the lbaas policy ID and the nsx application rules. Depends-on: I3b14d107dbe0a72a6e24239f06bd6c3ac597cfbb Change-Id: Ic760be8956cea00b972b5f11f6acff294630892d --- .../notes/nsxv-lbaas-l7-704f748300d1a399.yaml | 5 + .../alembic_migrations/versions/EXPAND_HEAD | 2 +- .../expand/01a33f93f5fd_nsxv_lbv2_l7pol.py | 45 +++ vmware_nsx/db/nsxv_db.py | 28 ++ vmware_nsx/db/nsxv_models.py | 14 + .../services/lbaas/nsx_v/lbaas_const.py | 16 + .../nsx_v/v2/edge_loadbalancer_driver_v2.py | 4 + .../services/lbaas/nsx_v/v2/l7policy_mgr.py | 284 ++++++++++++++++++ .../services/lbaas/nsx_v/v2/l7rule_mgr.py | 67 +++++ .../services/lbaas/nsx_v/v2/listener_mgr.py | 22 +- .../services/lbaas/nsx_v/v2/member_mgr.py | 17 +- .../services/lbaas/nsx_v/v2/pool_mgr.py | 9 +- .../nsx_v/test_edge_loadbalancer_driver_v2.py | 258 ++++++++++++++++ .../tests/unit/nsx_v/vshield/fake_vcns.py | 15 + 14 files changed, 774 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/nsxv-lbaas-l7-704f748300d1a399.yaml create mode 100644 vmware_nsx/db/migration/alembic_migrations/versions/ocata/expand/01a33f93f5fd_nsxv_lbv2_l7pol.py create mode 100644 vmware_nsx/services/lbaas/nsx_v/v2/l7policy_mgr.py create mode 100644 vmware_nsx/services/lbaas/nsx_v/v2/l7rule_mgr.py diff --git a/releasenotes/notes/nsxv-lbaas-l7-704f748300d1a399.yaml b/releasenotes/notes/nsxv-lbaas-l7-704f748300d1a399.yaml new file mode 100644 index 0000000000..6803062964 --- /dev/null +++ b/releasenotes/notes/nsxv-lbaas-l7-704f748300d1a399.yaml @@ -0,0 +1,5 @@ +--- +prelude: > + The NSX-V lbaas plugin now supports L7 rules & policies. +features: + - The NSX-V lbaas plugin now supports L7 rules & policies. diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD index 76017b3931..444a940514 100644 --- a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -dd9fe5a3a526 +01a33f93f5fd diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/ocata/expand/01a33f93f5fd_nsxv_lbv2_l7pol.py b/vmware_nsx/db/migration/alembic_migrations/versions/ocata/expand/01a33f93f5fd_nsxv_lbv2_l7pol.py new file mode 100644 index 0000000000..3aaa3f767c --- /dev/null +++ b/vmware_nsx/db/migration/alembic_migrations/versions/ocata/expand/01a33f93f5fd_nsxv_lbv2_l7pol.py @@ -0,0 +1,45 @@ +# Copyright 2017 VMware, 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. + +"""nsxv_lbv2_l7policy + +Revision ID: 01a33f93f5fd +Revises: dd9fe5a3a526 +Create Date: 2017-01-04 10:10:59.990122 + +""" + +# revision identifiers, used by Alembic. +revision = '01a33f93f5fd' +down_revision = 'dd9fe5a3a526' +from alembic import op +import sqlalchemy as sa + +from neutron.db import migration + + +def upgrade(): + if migration.schema_has_table('lbaas_l7policies'): + op.create_table( + 'nsxv_lbaas_l7policy_bindings', + sa.Column('policy_id', sa.String(length=36), nullable=False), + sa.Column('edge_id', sa.String(length=36), nullable=False), + sa.Column('edge_app_rule_id', + sa.String(length=36), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('policy_id'), + sa.ForeignKeyConstraint(['policy_id'], + ['lbaas_l7policies.id'], + ondelete='CASCADE')) diff --git a/vmware_nsx/db/nsxv_db.py b/vmware_nsx/db/nsxv_db.py index b193d9d47b..8ace69b40e 100644 --- a/vmware_nsx/db/nsxv_db.py +++ b/vmware_nsx/db/nsxv_db.py @@ -779,6 +779,34 @@ def del_nsxv_lbaas_certificate_binding(session, cert_id, edge_id): edge_id=edge_id).delete()) +def add_nsxv_lbaas_l7policy_binding(session, policy_id, edge_id, + edge_app_rule_id): + with session.begin(subtransactions=True): + binding = nsxv_models.NsxvLbaasL7PolicyBinding( + policy_id=policy_id, + edge_id=edge_id, + edge_app_rule_id=edge_app_rule_id) + session.add(binding) + return binding + + +def get_nsxv_lbaas_l7policy_binding(session, policy_id): + try: + return session.query( + nsxv_models.NsxvLbaasL7PolicyBinding).filter_by( + policy_id=policy_id).one() + except exc.NoResultFound: + return + + +def del_nsxv_lbaas_l7policy_binding(session, policy_id): + try: + return (session.query(nsxv_models.NsxvLbaasL7PolicyBinding). + filter_by(policy_id=policy_id).delete()) + except exc.NoResultFound: + return + + def add_nsxv_subnet_ext_attributes(session, subnet_id, dns_search_domain=None, dhcp_mtu=None): diff --git a/vmware_nsx/db/nsxv_models.py b/vmware_nsx/db/nsxv_models.py index ac5aafa68e..16123ed698 100644 --- a/vmware_nsx/db/nsxv_models.py +++ b/vmware_nsx/db/nsxv_models.py @@ -331,6 +331,20 @@ class NsxvLbaasCertificateBinding(model_base.BASEV2, models.TimestampMixin): edge_cert_id = sa.Column(sa.String(36), nullable=False) +class NsxvLbaasL7PolicyBinding(model_base.BASEV2, models.TimestampMixin): + """Mapping between NSX Edge and LBaaSv2 L7 policy """ + + __tablename__ = 'nsxv_lbaas_l7policy_bindings' + + policy_id = sa.Column(sa.String(36), + sa.ForeignKey('lbaas_l7policies.id', + name='fk_lbaas_l7policies_id', + ondelete="CASCADE"), + primary_key=True) + edge_id = sa.Column(sa.String(36), nullable=False) + edge_app_rule_id = sa.Column(sa.String(36), nullable=False) + + class NsxvSubnetExtAttributes(model_base.BASEV2, models.TimestampMixin): """Subnet attributes managed by NSX plugin extensions.""" diff --git a/vmware_nsx/services/lbaas/nsx_v/lbaas_const.py b/vmware_nsx/services/lbaas/nsx_v/lbaas_const.py index 8a4518b712..4835e3f88c 100644 --- a/vmware_nsx/services/lbaas/nsx_v/lbaas_const.py +++ b/vmware_nsx/services/lbaas/nsx_v/lbaas_const.py @@ -56,3 +56,19 @@ SESSION_PERSISTENCE_METHOD_MAP = { SESSION_PERSISTENCE_COOKIE_MAP = { LB_SESSION_PERSISTENCE_APP_COOKIE: 'app', LB_SESSION_PERSISTENCE_HTTP_COOKIE: 'insert'} + +L7_POLICY_ACTION_REJECT = 'REJECT' +L7_POLICY_ACTION_REDIRECT_TO_POOL = 'REDIRECT_TO_POOL' +L7_POLICY_ACTION_REDIRECT_TO_URL = 'REDIRECT_TO_URL' + +L7_RULE_TYPE_HOST_NAME = 'HOST_NAME' +L7_RULE_TYPE_PATH = 'PATH' +L7_RULE_TYPE_FILE_TYPE = 'FILE_TYPE' +L7_RULE_TYPE_HEADER = 'HEADER' +L7_RULE_TYPE_COOKIE = 'COOKIE' + +L7_RULE_COMPARE_TYPE_REGEX = 'REGEX' +L7_RULE_COMPARE_TYPE_STARTS_WITH = 'STARTS_WITH' +L7_RULE_COMPARE_TYPE_ENDS_WITH = 'ENDS_WITH' +L7_RULE_COMPARE_TYPE_CONTAINS = 'CONTAINS' +L7_RULE_COMPARE_TYPE_EQUAL_TO = 'EQUAL_TO' diff --git a/vmware_nsx/services/lbaas/nsx_v/v2/edge_loadbalancer_driver_v2.py b/vmware_nsx/services/lbaas/nsx_v/v2/edge_loadbalancer_driver_v2.py index 713834debc..0465f83837 100644 --- a/vmware_nsx/services/lbaas/nsx_v/v2/edge_loadbalancer_driver_v2.py +++ b/vmware_nsx/services/lbaas/nsx_v/v2/edge_loadbalancer_driver_v2.py @@ -17,6 +17,8 @@ from oslo_log import helpers as log_helpers from vmware_nsx.services.lbaas.nsx_v.v2 import healthmon_mgr as hm_mgr +from vmware_nsx.services.lbaas.nsx_v.v2 import l7policy_mgr +from vmware_nsx.services.lbaas.nsx_v.v2 import l7rule_mgr from vmware_nsx.services.lbaas.nsx_v.v2 import listener_mgr from vmware_nsx.services.lbaas.nsx_v.v2 import loadbalancer_mgr as lb_mgr from vmware_nsx.services.lbaas.nsx_v.v2 import member_mgr @@ -32,3 +34,5 @@ class EdgeLoadbalancerDriverV2(object): self.pool = pool_mgr.EdgePoolManager(self) self.member = member_mgr.EdgeMemberManager(self) self.healthmonitor = hm_mgr.EdgeHealthMonitorManager(self) + self.l7policy = l7policy_mgr.EdgeL7PolicyManager(self) + self.l7rule = l7rule_mgr.EdgeL7RuleManager(self) diff --git a/vmware_nsx/services/lbaas/nsx_v/v2/l7policy_mgr.py b/vmware_nsx/services/lbaas/nsx_v/v2/l7policy_mgr.py new file mode 100644 index 0000000000..4c3aadff76 --- /dev/null +++ b/vmware_nsx/services/lbaas/nsx_v/v2/l7policy_mgr.py @@ -0,0 +1,284 @@ +# Copyright 2017 VMware, Inc. +# All Rights Reserved +# +# 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 oslo_log import helpers as log_helpers +from oslo_log import log as logging +from oslo_utils import excutils + +from neutron_lib import constants +from neutron_lib import exceptions as n_exc + +from vmware_nsx._i18n import _, _LE, _LW +from vmware_nsx.common import locking +from vmware_nsx.db import nsxv_db +from vmware_nsx.services.lbaas.nsx_v import lbaas_common as lb_common +from vmware_nsx.services.lbaas.nsx_v import lbaas_const as lb_const +from vmware_nsx.services.lbaas.nsx_v.v2 import base_mgr + +LOG = logging.getLogger(__name__) + +type_by_compare_type = { + lb_const.L7_RULE_COMPARE_TYPE_EQUAL_TO: '', + lb_const.L7_RULE_COMPARE_TYPE_REGEX: '_reg', + lb_const.L7_RULE_COMPARE_TYPE_STARTS_WITH: '_beg', + lb_const.L7_RULE_COMPARE_TYPE_ENDS_WITH: '_end', + lb_const.L7_RULE_COMPARE_TYPE_CONTAINS: '_sub' +} + + +def policy_to_application_rule(policy): + condition = '' + rule_lines = [] + for rule in policy.rules: + if rule.provisioning_status == constants.PENDING_DELETE: + # skip this rule as it is being deleted + continue + + type_by_comp = type_by_compare_type.get(rule.compare_type) + if type_by_comp is None: + type_by_comp = '' + LOG.warnning(_LW('Unsupported compare type %(type)s is used in ' + 'policy %(id)s'), {'type': rule.compare_type, + 'id': policy.id}) + + if rule.type == lb_const.L7_RULE_TYPE_COOKIE: + # Example: acl hdr_sub(cookie) SEEN=1 + hdr_type = 'hdr' + type_by_comp + rule_line = ('acl %(rule_id)s %(hdr_type)s(cookie) ' + '%(key)s=%(val)s' % {'rule_id': rule.id, + 'hdr_type': hdr_type, + 'key': rule.key, + 'val': rule.value}) + elif rule.type == lb_const.L7_RULE_TYPE_HEADER: + # Example: acl hdr(user-agent) -i test + hdr_type = 'hdr' + type_by_comp + rule_line = ('acl %(rule_id)s %(hdr_type)s(%(key)s) ' + '-i %(val)s' % {'rule_id': rule.id, + 'hdr_type': hdr_type, + 'key': rule.key, + 'val': rule.value}) + elif rule.type == lb_const.L7_RULE_TYPE_HOST_NAME: + # Example: acl hdr_beg(host) -i abcd + hdr_type = 'hdr' + type_by_comp + # -i for case insensitive host name + rule_line = ('acl %(rule_id)s %(hdr_type)s(host) ' + '-i %(val)s' % {'rule_id': rule.id, + 'hdr_type': hdr_type, + 'val': rule.value}) + elif rule.type == lb_const.L7_RULE_TYPE_PATH: + # Example: acl path_beg -i /images + # Regardless of the compare type, always look at the beginning of + # the path. + # -i for case insensitive path + rule_line = ('acl %(rule_id)s path_beg ' + '-i %(val)s' % {'rule_id': rule.id, + 'val': rule.value}) + elif rule.type == lb_const.L7_RULE_TYPE_FILE_TYPE: + # Example: acl path_sub -i .jpg + # Regardless of the compare type, always check contained in path. + # -i for case insensitive file type + val = rule.value + if not val.startswith('.'): + val = '.' + val + rule_line = ('acl %(rule_id)s path_sub ' + '-i %(val)s' % {'rule_id': rule.id, + 'val': val}) + else: + msg = _('Unsupported L7rule type %s') % rule.type + raise n_exc.BadRequest(resource='edge-lbaas', msg=msg) + + rule_lines.append(rule_line) + invert_sign = '!' if rule.invert else '' + condition = condition + invert_sign + rule.id + ' ' + + if rule_lines: + # concatenate all the rules with new lines + all_rules = '\n'.join(rule_lines + ['']) + # remove he last space from the condition + condition = condition[:-1] + else: + all_rules = '' + condition = 'TRUE' + + # prepare the action + if policy.action == lb_const.L7_POLICY_ACTION_REJECT: + action = 'tcp-request content reject' + elif policy.action == lb_const.L7_POLICY_ACTION_REDIRECT_TO_POOL: + action = 'use_backend pool_%s' % policy.redirect_pool_id + elif policy.action == lb_const.L7_POLICY_ACTION_REDIRECT_TO_URL: + action = 'redirect location %s' % policy.redirect_url + else: + msg = _('Unsupported L7policy action %s') % policy.action + raise n_exc.BadRequest(resource='edge-lbaas', msg=msg) + + # Build the final script + script = all_rules + '%(action)s if %(cond)s' % { + 'action': action, 'cond': condition} + app_rule = {'name': 'pol_' + policy.id, 'script': script} + return app_rule + + +def policy_to_edge_and_rule_id(context, policy_id): + # get the nsx application rule id and edge id + binding = nsxv_db.get_nsxv_lbaas_l7policy_binding( + context.session, policy_id) + if not binding: + msg = _('No suitable Edge found for policy %s') % policy_id + raise n_exc.BadRequest(resource='edge-lbaas', msg=msg) + return binding['edge_id'], binding['edge_app_rule_id'] + + +class EdgeL7PolicyManager(base_mgr.EdgeLoadbalancerBaseManager): + @log_helpers.log_method_call + def __init__(self, vcns_driver): + super(EdgeL7PolicyManager, self).__init__(vcns_driver) + + def _add_app_rule_to_virtual_server(self, edge_id, vse_id, app_rule_id, + policy_position): + """Add the new nsx application rule to the virtual server""" + # Get the current virtual server configuration + vse = self.vcns.get_vip(edge_id, vse_id)[1] + if 'applicationRuleId' not in vse: + vse['applicationRuleId'] = [] + + # Add the policy (=application rule) in the correct position + # (position begins at 1) + if len(vse['applicationRuleId']) < policy_position: + vse['applicationRuleId'].append(app_rule_id) + else: + vse['applicationRuleId'].insert(policy_position - 1, app_rule_id) + + # update the backend with the new configuration + self.vcns.update_vip(edge_id, vse_id, vse) + + def _del_app_rule_from_virtual_server(self, edge_id, vse_id, app_rule_id): + """Delete nsx application rule from the virtual server""" + # Get the current virtual server configuration + vse = self.vcns.get_vip(edge_id, vse_id)[1] + if 'applicationRuleId' not in vse: + vse['applicationRuleId'] = [] + + # Remove the rule from the list + if (app_rule_id in vse['applicationRuleId']): + vse['applicationRuleId'].remove(app_rule_id) + + # update the backend with the new configuration + self.vcns.update_vip(edge_id, vse_id, vse) + + @log_helpers.log_method_call + def create(self, context, pol): + # find out the edge to be updated, by the listener of this policy + lb_id = pol.listener.loadbalancer_id + lb_binding = nsxv_db.get_nsxv_lbaas_loadbalancer_binding( + context.session, lb_id) + if not lb_binding: + msg = _( + 'No suitable Edge found for listener %s') % pol.listener_id + raise n_exc.BadRequest(resource='edge-lbaas', msg=msg) + edge_id = lb_binding['edge_id'] + app_rule = policy_to_application_rule(pol) + app_rule_id = None + try: + with locking.LockManager.get_lock(edge_id): + # create the backend application rule for this policy + h = (self.vcns.create_app_rule(edge_id, app_rule))[0] + app_rule_id = lb_common.extract_resource_id(h['location']) + + # add the nsx application rule (neutron policy) to the nsx + # virtual server (neutron listener) + listener_binding = nsxv_db.get_nsxv_lbaas_listener_binding( + context.session, lb_id, pol.listener.id) + if listener_binding: + self._add_app_rule_to_virtual_server( + edge_id, listener_binding['vse_id'], app_rule_id, + pol.position) + except Exception as e: + with excutils.save_and_reraise_exception(): + self.lbv2_driver.l7policy.failed_completion(context, pol) + LOG.error(_LE('Failed to create L7policy on edge %(edge)s: ' + '%(err)s'), + {'edge': edge_id, 'err': e}) + if app_rule_id: + # Failed to add the rule to the vip: delete the rule + # from the backend. + try: + self.vcns.delete_app_rule(edge_id, app_rule_id) + except Exception: + pass + + # save the nsx application rule id in the DB + nsxv_db.add_nsxv_lbaas_l7policy_binding(context.session, pol.id, + edge_id, app_rule_id) + # complete the transaction + self.lbv2_driver.l7policy.successful_completion(context, pol) + + @log_helpers.log_method_call + def update(self, context, old_pol, new_pol): + # get the nsx application rule id and edge id from the nsx DB + edge_id, app_rule_id = policy_to_edge_and_rule_id(context, new_pol.id) + # create the script for the new policy data + app_rule = policy_to_application_rule(new_pol) + try: + with locking.LockManager.get_lock(edge_id): + # update the backend application rule for the new policy + self.vcns.update_app_rule(edge_id, app_rule_id, app_rule) + except Exception as e: + with excutils.save_and_reraise_exception(): + self.lbv2_driver.l7policy.failed_completion(context, new_pol) + LOG.error(_LE('Failed to update L7policy on edge %(edge)s: ' + '%(err)s'), + {'edge': edge_id, 'err': e}) + + # complete the transaction + self.lbv2_driver.l7policy.successful_completion(context, new_pol) + + @log_helpers.log_method_call + def delete(self, context, pol): + # get the nsx application rule id and edge id from the nsx DB + try: + edge_id, app_rule_id = policy_to_edge_and_rule_id(context, pol.id) + except n_exc.BadRequest: + # This is probably a policy that we failed to create properly. + # We should allow deleting it + self.lbv2_driver.l7policy.successful_completion(context, pol, + delete=True) + return + + with locking.LockManager.get_lock(edge_id): + try: + # remove the nsx application rule from the virtual server + lb_id = pol.listener.loadbalancer_id + listener_binding = nsxv_db.get_nsxv_lbaas_listener_binding( + context.session, lb_id, pol.listener.id) + if listener_binding: + vse_id = listener_binding['vse_id'] + self._del_app_rule_from_virtual_server( + edge_id, vse_id, app_rule_id) + + # delete the nsx application rule + self.vcns.delete_app_rule(edge_id, app_rule_id) + except Exception as e: + with excutils.save_and_reraise_exception(): + self.lbv2_driver.l7policy.failed_completion(context, pol) + LOG.error(_LE('Failed to delete L7policy on edge ' + '%(edge)s: %(err)s'), + {'edge': edge_id, 'err': e}) + + # delete the nsxv db entry + nsxv_db.del_nsxv_lbaas_l7policy_binding(context.session, pol.id) + + # complete the transaction + self.lbv2_driver.l7policy.successful_completion(context, pol, + delete=True) diff --git a/vmware_nsx/services/lbaas/nsx_v/v2/l7rule_mgr.py b/vmware_nsx/services/lbaas/nsx_v/v2/l7rule_mgr.py new file mode 100644 index 0000000000..7893882156 --- /dev/null +++ b/vmware_nsx/services/lbaas/nsx_v/v2/l7rule_mgr.py @@ -0,0 +1,67 @@ +# Copyright 2017 VMware, Inc. +# All Rights Reserved +# +# 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 oslo_log import helpers as log_helpers +from oslo_log import log as logging +from oslo_utils import excutils + +from vmware_nsx._i18n import _LE +from vmware_nsx.common import locking +from vmware_nsx.services.lbaas.nsx_v.v2 import base_mgr +from vmware_nsx.services.lbaas.nsx_v.v2 import l7policy_mgr + +LOG = logging.getLogger(__name__) + + +class EdgeL7RuleManager(base_mgr.EdgeLoadbalancerBaseManager): + @log_helpers.log_method_call + def __init__(self, vcns_driver): + super(EdgeL7RuleManager, self).__init__(vcns_driver) + + def _handle_l7policy_rules_change(self, context, rule, delete=False): + # Get the nsx application rule id and edge id + edge_id, app_rule_id = l7policy_mgr.policy_to_edge_and_rule_id( + context, rule.l7policy_id) + + # Create the script for the new policy data. + # The policy obj on the rule is already updated with the + # created/updated/deleted rule. + app_rule = l7policy_mgr.policy_to_application_rule(rule.policy) + try: + with locking.LockManager.get_lock(edge_id): + # update the backend application rule for the updated policy + self.vcns.update_app_rule(edge_id, app_rule_id, app_rule) + except Exception as e: + with excutils.save_and_reraise_exception(): + self.lbv2_driver.l7rule.failed_completion(context, rule) + LOG.error(_LE('Failed to update L7rules on edge %(edge)s: ' + '%(err)s'), + {'edge': edge_id, 'err': e}) + + # complete the transaction + self.lbv2_driver.l7rule.successful_completion(context, rule, + delete=delete) + + @log_helpers.log_method_call + def create(self, context, rule): + self._handle_l7policy_rules_change(context, rule) + + @log_helpers.log_method_call + def update(self, context, old_rule, new_rule): + self._handle_l7policy_rules_change(context, new_rule) + + @log_helpers.log_method_call + def delete(self, context, rule): + self._handle_l7policy_rules_change(context, rule, delete=True) diff --git a/vmware_nsx/services/lbaas/nsx_v/v2/listener_mgr.py b/vmware_nsx/services/lbaas/nsx_v/v2/listener_mgr.py index c53eb41132..028a7261ec 100644 --- a/vmware_nsx/services/lbaas/nsx_v/v2/listener_mgr.py +++ b/vmware_nsx/services/lbaas/nsx_v/v2/listener_mgr.py @@ -71,13 +71,14 @@ def listener_to_edge_app_profile(listener, edge_cert_id): return edge_app_profile -def listener_to_edge_vse(listener, vip_address, default_pool, app_profile_id): +def listener_to_edge_vse(context, listener, vip_address, default_pool, + app_profile_id): if listener.connection_limit: connection_limit = max(0, listener.connection_limit) else: connection_limit = 0 - return { + vse = { 'name': 'vip_' + listener.id, 'description': listener.description, 'ipAddress': vip_address, @@ -89,6 +90,18 @@ def listener_to_edge_vse(listener, vip_address, default_pool, app_profile_id): listener.protocol == lb_const.LB_PROTOCOL_TCP), 'applicationProfileId': app_profile_id} + # Add the L7 policies + if listener.l7_policies: + app_rule_ids = [] + for pol in listener.l7_policies: + binding = nsxv_db.get_nsxv_lbaas_l7policy_binding( + context.session, pol.id) + if binding: + app_rule_ids.append(binding['edge_app_rule_id']) + vse['applicationRuleId'] = app_rule_ids + + return vse + class EdgeListenerManager(base_mgr.EdgeLoadbalancerBaseManager): @log_helpers.log_method_call @@ -157,7 +170,8 @@ class EdgeListenerManager(base_mgr.EdgeLoadbalancerBaseManager): LOG.error(_LE('Failed to create app profile on edge: %s'), lb_binding['edge_id']) - vse = listener_to_edge_vse(listener, lb_binding['vip_address'], + vse = listener_to_edge_vse(context, listener, + lb_binding['vip_address'], default_pool, app_profile_id) @@ -227,7 +241,7 @@ class EdgeListenerManager(base_mgr.EdgeLoadbalancerBaseManager): self.vcns.update_app_profile( edge_id, app_profile_id, app_profile) - vse = listener_to_edge_vse(new_listener, + vse = listener_to_edge_vse(context, new_listener, lb_binding['vip_address'], default_pool, app_profile_id) diff --git a/vmware_nsx/services/lbaas/nsx_v/v2/member_mgr.py b/vmware_nsx/services/lbaas/nsx_v/v2/member_mgr.py index 711bc50120..98bd7547a1 100644 --- a/vmware_nsx/services/lbaas/nsx_v/v2/member_mgr.py +++ b/vmware_nsx/services/lbaas/nsx_v/v2/member_mgr.py @@ -33,10 +33,17 @@ class EdgeMemberManager(base_mgr.EdgeLoadbalancerBaseManager): super(EdgeMemberManager, self).__init__(vcns_driver) self._fw_section_id = None + def _get_pool_lb_id(self, member): + listener = member.pool.listener + if listener: + lb_id = listener.loadbalancer_id + else: + lb_id = member.pool.loadbalancer.id + return lb_id + @log_helpers.log_method_call def create(self, context, member): - listener = member.pool.listener - lb_id = listener.loadbalancer_id + lb_id = self._get_pool_lb_id(member) lb_binding = nsxv_db.get_nsxv_lbaas_loadbalancer_binding( context.session, lb_id) pool_binding = nsxv_db.get_nsxv_lbaas_pool_binding( @@ -72,8 +79,7 @@ class EdgeMemberManager(base_mgr.EdgeLoadbalancerBaseManager): @log_helpers.log_method_call def update(self, context, old_member, new_member): - listener = new_member.pool.listener - lb_id = listener.loadbalancer_id + lb_id = self._get_pool_lb_id(new_member) lb_binding = nsxv_db.get_nsxv_lbaas_loadbalancer_binding( context.session, lb_id) pool_binding = nsxv_db.get_nsxv_lbaas_pool_binding(context.session, @@ -121,8 +127,7 @@ class EdgeMemberManager(base_mgr.EdgeLoadbalancerBaseManager): @log_helpers.log_method_call def delete(self, context, member): - listener = member.pool.listener - lb_id = listener.loadbalancer_id + lb_id = self._get_pool_lb_id(member) lb_binding = nsxv_db.get_nsxv_lbaas_loadbalancer_binding( context.session, lb_id) pool_binding = nsxv_db.get_nsxv_lbaas_pool_binding( diff --git a/vmware_nsx/services/lbaas/nsx_v/v2/pool_mgr.py b/vmware_nsx/services/lbaas/nsx_v/v2/pool_mgr.py index 02ead80bb7..472a88578c 100644 --- a/vmware_nsx/services/lbaas/nsx_v/v2/pool_mgr.py +++ b/vmware_nsx/services/lbaas/nsx_v/v2/pool_mgr.py @@ -17,6 +17,8 @@ from oslo_log import helpers as log_helpers from oslo_log import log as logging from oslo_utils import excutils +from neutron_lib import exceptions as n_exc + from vmware_nsx._i18n import _LE from vmware_nsx.common import locking from vmware_nsx.db import nsxv_db @@ -48,7 +50,10 @@ class EdgePoolManager(base_mgr.EdgeLoadbalancerBaseManager): lb_id = pool.loadbalancer_id lb_binding = nsxv_db.get_nsxv_lbaas_loadbalancer_binding( context.session, lb_id) - + if not lb_binding: + msg = _( + 'No suitable Edge found for pool %s') % pool.id + raise n_exc.BadRequest(resource='edge-lbaas', msg=msg) edge_id = lb_binding['edge_id'] try: @@ -64,6 +69,7 @@ class EdgePoolManager(base_mgr.EdgeLoadbalancerBaseManager): context.session, lb_id, pool.listener.id) # Associate listener with pool vse = listener_mgr.listener_to_edge_vse( + context, pool.listener, lb_binding['vip_address'], edge_pool_id, @@ -133,6 +139,7 @@ class EdgePoolManager(base_mgr.EdgeLoadbalancerBaseManager): listener_binding = nsxv_db.get_nsxv_lbaas_listener_binding( context.session, lb_id, listener.id) vse = listener_mgr.listener_to_edge_vse( + context, listener, lb_binding['vip_address'], None, diff --git a/vmware_nsx/tests/unit/nsx_v/test_edge_loadbalancer_driver_v2.py b/vmware_nsx/tests/unit/nsx_v/test_edge_loadbalancer_driver_v2.py index bdd480937f..e23b9c9a94 100644 --- a/vmware_nsx/tests/unit/nsx_v/test_edge_loadbalancer_driver_v2.py +++ b/vmware_nsx/tests/unit/nsx_v/test_edge_loadbalancer_driver_v2.py @@ -72,6 +72,17 @@ HM_BINDING = {'loadbalancer_id': LB_ID, 'edge_id': LB_EDGE_ID, 'edge_mon_id': EDGE_HM_ID} +L7POL_ID = 'l7pol-l7pol' +EDGE_RULE_ID = 'app-rule-xx' +L7POL_BINDING = {'policy_id': L7POL_ID, + 'edge_id': LB_EDGE_ID, + 'edge_app_rule_id': EDGE_RULE_ID} +EDGE_L7POL_DEF = {'script': 'tcp-request content reject if TRUE', + 'name': 'pol_' + L7POL_ID} + +L7RULE_ID1 = 'l7rule-111' +L7RULE_ID2 = 'l7rule-222' + class BaseTestEdgeLbaasV2(base.BaseTestCase): def _tested_entity(self): @@ -106,6 +117,28 @@ class BaseTestEdgeLbaasV2(base.BaseTestCase): MEMBER_ADDRESS, 80, 1, pool=self.pool) self.hm = lb_models.HealthMonitor(HM_ID, LB_TENANT_ID, 'PING', 3, 3, 1, pool=self.pool) + self.l7policy = lb_models.L7Policy(L7POL_ID, LB_TENANT_ID, + name='policy-test', + description='policy-desc', + listener_id=LISTENER_ID, + action='REJECT', + listener=self.listener, + position=1) + self.l7rule1 = lb_models.L7Rule(L7RULE_ID1, LB_TENANT_ID, + l7policy_id=L7POL_ID, + compare_type='EQUAL_TO', + invert=False, + type='HEADER', + key='key1', + value='val1', + policy=self.l7policy) + self.l7rule2 = lb_models.L7Rule(L7RULE_ID2, LB_TENANT_ID, + l7policy_id=L7POL_ID, + compare_type='STARTS_WITH', + invert=True, + type='PATH', + value='/images', + policy=self.l7policy) def tearDown(self): self._unpatch_lb_plugin(self.lbv2_driver, self._tested_entity) @@ -606,3 +639,228 @@ class TestEdgeLbaasV2HealthMonitor(BaseTestEdgeLbaasV2): mock_successful_completion.assert_called_with(self.context, self.hm, delete=True) + + +class TestEdgeLbaasV2L7Policy(BaseTestEdgeLbaasV2): + def setUp(self): + super(TestEdgeLbaasV2L7Policy, self).setUp() + + @property + def _tested_entity(self): + return 'l7policy' + + def test_create(self): + with mock.patch.object(nsxv_db, 'get_nsxv_lbaas_l7policy_binding' + ) as mock_get_l7policy_binding, \ + mock.patch.object(nsxv_db, 'get_nsxv_lbaas_loadbalancer_binding' + ) as mock_get_lb_binding, \ + mock.patch.object(nsxv_db, 'get_nsxv_lbaas_listener_binding' + ) as mock_get_listener_binding, \ + mock.patch.object(nsxv_db, 'add_nsxv_lbaas_l7policy_binding' + ) as mock_add_l7policy_binding,\ + mock.patch.object(self.edge_driver.vcns, 'create_app_rule' + ) as mock_create_rule, \ + mock.patch.object(self.edge_driver.vcns, 'get_vip' + ) as mock_get_vip, \ + mock.patch.object(self.edge_driver.vcns, 'update_vip' + ) as mock_upd_vip: + mock_get_lb_binding.return_value = LB_BINDING + mock_get_l7policy_binding.return_value = L7POL_BINDING + mock_get_listener_binding.return_value = LISTENER_BINDING + mock_create_rule.return_value = ( + {'location': 'x/' + EDGE_RULE_ID}, None) + mock_get_vip.return_value = (None, EDGE_VIP_DEF.copy()) + + self.edge_driver.l7policy.create(self.context, self.l7policy) + + mock_create_rule.assert_called_with(LB_EDGE_ID, + EDGE_L7POL_DEF.copy()) + mock_add_l7policy_binding.assert_called_with( + self.context.session, L7POL_ID, LB_EDGE_ID, EDGE_RULE_ID) + + edge_vip_def = EDGE_VIP_DEF.copy() + edge_vip_def['applicationRuleId'] = [EDGE_RULE_ID] + mock_upd_vip.assert_called_with(LB_EDGE_ID, EDGE_VIP_ID, + edge_vip_def) + mock_successful_completion = ( + self.lbv2_driver.l7policy.successful_completion) + mock_successful_completion.assert_called_with(self.context, + self.l7policy) + + def test_update(self): + url = 'http://www.test.com' + new_pol = lb_models.L7Policy(L7POL_ID, LB_TENANT_ID, + name='policy-test', + description='policy-desc', + listener_id=LISTENER_ID, + action='REDIRECT_TO_URL', + redirect_url=url, + listener=self.listener, + position=1) + + with mock.patch.object(nsxv_db, 'get_nsxv_lbaas_l7policy_binding' + ) as mock_get_l7policy_binding, \ + mock.patch.object(nsxv_db, 'get_nsxv_lbaas_loadbalancer_binding' + ) as mock_get_lb_binding, \ + mock.patch.object(self.edge_driver.vcns, 'update_app_rule' + ) as mock_update_rule: + mock_get_lb_binding.return_value = LB_BINDING + mock_get_l7policy_binding.return_value = L7POL_BINDING + + self.edge_driver.l7policy.update(self.context, self.l7policy, + new_pol) + + edge_rule_def = EDGE_L7POL_DEF.copy() + edge_rule_def['script'] = "redirect location %s if TRUE" % url + mock_update_rule.assert_called_with( + LB_EDGE_ID, EDGE_RULE_ID, edge_rule_def) + mock_successful_completion = ( + self.lbv2_driver.l7policy.successful_completion) + mock_successful_completion.assert_called_with(self.context, + new_pol) + + def test_delete(self): + with mock.patch.object(nsxv_db, 'get_nsxv_lbaas_l7policy_binding' + ) as mock_get_l7policy_binding, \ + mock.patch.object(nsxv_db, 'del_nsxv_lbaas_l7policy_binding' + ) as mock_del_l7policy_binding, \ + mock.patch.object(nsxv_db, 'get_nsxv_lbaas_loadbalancer_binding' + ) as mock_get_lb_binding, \ + mock.patch.object(nsxv_db, 'get_nsxv_lbaas_pool_binding' + ) as mock_get_pool_binding,\ + mock.patch.object(nsxv_db, 'get_nsxv_lbaas_listener_binding' + ) as mock_get_listener_binding, \ + mock.patch.object(self.edge_driver.vcns, 'delete_app_rule' + ) as mock_del_app_rule, \ + mock.patch.object(self.edge_driver.vcns, 'get_vip' + ) as mock_get_vip, \ + mock.patch.object(self.edge_driver.vcns, 'update_vip' + ) as mock_upd_vip: + mock_get_lb_binding.return_value = LB_BINDING + mock_get_pool_binding.return_value = POOL_BINDING + mock_get_listener_binding.return_value = LISTENER_BINDING + mock_get_l7policy_binding.return_value = L7POL_BINDING + edge_vip_def = EDGE_VIP_DEF.copy() + edge_vip_def['applicationRuleId'] = [EDGE_RULE_ID] + mock_get_vip.return_value = (None, edge_vip_def) + + self.edge_driver.l7policy.delete(self.context, self.l7policy) + + edge_vip_def2 = EDGE_VIP_DEF.copy() + edge_vip_def2['applicationRuleId'] = [] + mock_upd_vip.assert_called_with(LB_EDGE_ID, EDGE_VIP_ID, + edge_vip_def2) + mock_del_app_rule.assert_called_with(LB_EDGE_ID, EDGE_RULE_ID) + mock_del_l7policy_binding.assert_called_with( + self.context.session, L7POL_ID) + mock_successful_completion = ( + self.lbv2_driver.l7policy.successful_completion) + mock_successful_completion.assert_called_with(self.context, + self.l7policy, + delete=True) + + +class TestEdgeLbaasV2L7Rule(BaseTestEdgeLbaasV2): + def setUp(self): + super(TestEdgeLbaasV2L7Rule, self).setUp() + + @property + def _tested_entity(self): + return 'l7rule' + + def test_create(self): + with mock.patch.object(nsxv_db, 'get_nsxv_lbaas_l7policy_binding' + ) as mock_get_l7policy_binding, \ + mock.patch.object(self.edge_driver.vcns, 'update_app_rule' + ) as mock_update_rule: + mock_get_l7policy_binding.return_value = L7POL_BINDING + + # Create the first rule + self.l7rule1.policy.rules = [self.l7rule1] + self.edge_driver.l7rule.create(self.context, self.l7rule1) + + edge_rule_def = EDGE_L7POL_DEF.copy() + edge_rule_def['script'] = ( + "acl %(rule_id)s hdr(key1) -i val1\n" + "tcp-request content reject if %(rule_id)s" % + {'rule_id': L7RULE_ID1}) + mock_update_rule.assert_called_with( + LB_EDGE_ID, EDGE_RULE_ID, edge_rule_def) + + mock_successful_completion = ( + self.lbv2_driver.l7rule.successful_completion) + mock_successful_completion.assert_called_with( + self.context, self.l7rule1, delete=False) + + # Create the 2nd rule + self.l7rule2.policy.rules = [self.l7rule1, self.l7rule2] + self.edge_driver.l7rule.create(self.context, self.l7rule2) + + edge_rule_def = EDGE_L7POL_DEF.copy() + edge_rule_def['script'] = ( + "acl %(rule_id1)s hdr(key1) -i val1\n" + "acl %(rule_id2)s path_beg -i /images\n" + "tcp-request content reject if %(rule_id1)s !%(rule_id2)s" % + {'rule_id1': L7RULE_ID1, + 'rule_id2': L7RULE_ID2}) + mock_update_rule.assert_called_with( + LB_EDGE_ID, EDGE_RULE_ID, edge_rule_def) + + mock_successful_completion = ( + self.lbv2_driver.l7rule.successful_completion) + mock_successful_completion.assert_called_with( + self.context, self.l7rule2, delete=False) + + def test_update(self): + new_rule = lb_models.L7Rule(L7RULE_ID1, LB_TENANT_ID, + l7policy_id=L7POL_ID, + compare_type='EQUAL_TO', + invert=False, + type='HEADER', + key='key2', + value='val1', + policy=self.l7policy) + + with mock.patch.object(nsxv_db, 'get_nsxv_lbaas_l7policy_binding' + ) as mock_get_l7policy_binding, \ + mock.patch.object(self.edge_driver.vcns, 'update_app_rule' + ) as mock_update_rule: + mock_get_l7policy_binding.return_value = L7POL_BINDING + + new_rule.policy.rules = [new_rule] + self.edge_driver.l7rule.update( + self.context, self.l7rule1, new_rule) + + edge_rule_def = EDGE_L7POL_DEF.copy() + edge_rule_def['script'] = ( + "acl %(rule_id)s hdr(key2) -i val1\n" + "tcp-request content reject if %(rule_id)s" % + {'rule_id': L7RULE_ID1}) + mock_update_rule.assert_called_with( + LB_EDGE_ID, EDGE_RULE_ID, edge_rule_def) + + mock_successful_completion = ( + self.lbv2_driver.l7rule.successful_completion) + mock_successful_completion.assert_called_with( + self.context, new_rule, delete=False) + + def test_delete(self): + with mock.patch.object(nsxv_db, 'get_nsxv_lbaas_l7policy_binding' + ) as mock_get_l7policy_binding, \ + mock.patch.object(self.edge_driver.vcns, 'update_app_rule' + ) as mock_update_rule: + mock_get_l7policy_binding.return_value = L7POL_BINDING + + self.l7rule1.policy.rules = [] + self.edge_driver.l7rule.delete(self.context, self.l7rule1) + + edge_rule_def = EDGE_L7POL_DEF.copy() + edge_rule_def['script'] = ( + "tcp-request content reject if TRUE") + mock_update_rule.assert_called_with( + LB_EDGE_ID, EDGE_RULE_ID, edge_rule_def) + + mock_successful_completion = ( + self.lbv2_driver.l7rule.successful_completion) + mock_successful_completion.assert_called_with( + self.context, self.l7rule1, delete=True) diff --git a/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py b/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py index 5540e7fd80..ff61aa7ff8 100644 --- a/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py +++ b/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py @@ -735,6 +735,21 @@ class FakeVcns(object): del self._fake_app_profiles_dict[edge_id][app_profileid] return self.return_helper(header, response) + def create_app_rule(self, edge_id, app_rule): + app_ruleid = uuidutils.generate_uuid() + header = { + 'status': 204, + 'location': "https://host/api/4.0/edges/edge_id" + "/loadbalancer/config/%s" % app_ruleid} + response = "" + return self.return_helper(header, response) + + def update_app_rule(self, edge_id, app_ruleid, app_rule): + pass + + def delete_app_rule(self, edge_id, app_ruleid): + pass + def get_loadbalancer_config(self, edge_id): header = {'status': 204} response = {'config': False}