From f10dcfe82da5094c0113fcd8bc68ab04b068aa09 Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Tue, 21 Mar 2017 11:15:32 +0200 Subject: [PATCH] NSX-V FWaaS(V1) support The nsx-v FWaaS driver will add the configured firewall rules to the router edges. Currently there is not support for shared routers. The rules will be edded after the current rules (NAT, LBaaS, external traffic) for exclusive routers edges and distributed routers PLR edged. Change-Id: I82ba90070ef4e739a0b5c4463ef03a807e26adfb --- doc/source/devstack.rst | 18 ++ .../nsxv-fwaas-driver-4c457dee3fc3bae2.yaml | 6 + tools/tox_install.sh | 1 + .../nsx_v/drivers/shared_router_driver.py | 2 +- vmware_nsx/plugins/nsx_v/plugin.py | 27 ++- .../nsx_v/vshield/edge_firewall_driver.py | 116 +++++----- vmware_nsx/services/fwaas/__init__.py | 0 vmware_nsx/services/fwaas/nsx_v/__init__.py | 0 .../services/fwaas/nsx_v/edge_fwaas_driver.py | 202 ++++++++++++++++++ .../services/fwaas/nsx_v/fwaas_callbacks.py | 115 ++++++++++ vmware_nsx/tests/unit/db/test_migrations.py | 7 +- .../tests/unit/nsx_v/test_fwaas_driver.py | 142 ++++++++++++ vmware_nsx/tests/unit/nsx_v/test_plugin.py | 6 +- 13 files changed, 568 insertions(+), 74 deletions(-) create mode 100644 releasenotes/notes/nsxv-fwaas-driver-4c457dee3fc3bae2.yaml create mode 100644 vmware_nsx/services/fwaas/__init__.py create mode 100644 vmware_nsx/services/fwaas/nsx_v/__init__.py create mode 100644 vmware_nsx/services/fwaas/nsx_v/edge_fwaas_driver.py create mode 100644 vmware_nsx/services/fwaas/nsx_v/fwaas_callbacks.py create mode 100644 vmware_nsx/tests/unit/nsx_v/test_fwaas_driver.py diff --git a/doc/source/devstack.rst b/doc/source/devstack.rst index ca51bfa6cd..a365d1f804 100644 --- a/doc/source/devstack.rst +++ b/doc/source/devstack.rst @@ -89,6 +89,24 @@ lines to the policy.json file:: "delete_flow_classifier": "rule:admin_only", "get_flow_classifier": "rule:admin_only" +FWAAS (V1) Driver: +~~~~~~~~~~~~~ + +Add neutron-fwaas repo as an external repository and configure following flags in ``local.conf``:: + + [[local|localrc]] + enable_plugin neutron-fwaas https://git.openstack.org/openstack/neutron-fwaas + ENABLED_SERVICES+=,q-fwaas + + [[post-config|$NEUTRON_CONF]] + [DEFAULT] + service_plugins = neutron_fwaas.services.firewall.fwaas_plugin.FirewallPlugin + + [fwaas] + enabled = True + driver = vmware_nsx.services.fwaas.nsx_v.edge_fwaas_driver.EdgeFwaasDriver + + NSXv3 ----- diff --git a/releasenotes/notes/nsxv-fwaas-driver-4c457dee3fc3bae2.yaml b/releasenotes/notes/nsxv-fwaas-driver-4c457dee3fc3bae2.yaml new file mode 100644 index 0000000000..c179dea429 --- /dev/null +++ b/releasenotes/notes/nsxv-fwaas-driver-4c457dee3fc3bae2.yaml @@ -0,0 +1,6 @@ +--- +prelude: > + The NSX-V plugin can suppport FWaaS-V1 for setting router edges firewall rules. +features: + - | + The NSX-V plugin can suppport FWaaS-V1 for setting router edges firewall rules. diff --git a/tools/tox_install.sh b/tools/tox_install.sh index f9072db66c..71ca229fab 100755 --- a/tools/tox_install.sh +++ b/tools/tox_install.sh @@ -8,6 +8,7 @@ ${DIR}/tox_install_project.sh networking-l2gw networking_l2gw $* ${DIR}/tox_install_project.sh networking-sfc networking_sfc $* ${DIR}/tox_install_project.sh neutron-lbaas neutron_lbaas $* ${DIR}/tox_install_project.sh vmware-nsxlib vmware_nsxlib $* +${DIR}/tox_install_project.sh neutron-fwaas neutron_fwaas $* CONSTRAINTS_FILE=$1 shift diff --git a/vmware_nsx/plugins/nsx_v/drivers/shared_router_driver.py b/vmware_nsx/plugins/nsx_v/drivers/shared_router_driver.py index db5a8e86d0..9bac17873b 100644 --- a/vmware_nsx/plugins/nsx_v/drivers/shared_router_driver.py +++ b/vmware_nsx/plugins/nsx_v/drivers/shared_router_driver.py @@ -317,7 +317,7 @@ class RouterSharedDriver(router_driver.RouterBaseDriver): fake_fw_rules += ( nsx_v_md_proxy.get_router_fw_rules()) - # TODO(berlin): Add fw rules if fw service is supported + # TODO(asarfaty): Add fwaas rules when fwaas supports shared routers fake_fw = {'firewall_rule_list': fake_fw_rules} edge_utils.update_firewall(self.nsx_v, context, target_router_id, fake_fw, allow_external=allow_external) diff --git a/vmware_nsx/plugins/nsx_v/plugin.py b/vmware_nsx/plugins/nsx_v/plugin.py index e9efd0a947..829162606f 100644 --- a/vmware_nsx/plugins/nsx_v/plugin.py +++ b/vmware_nsx/plugins/nsx_v/plugin.py @@ -127,6 +127,7 @@ from vmware_nsx.plugins.nsx_v.vshield import edge_utils from vmware_nsx.plugins.nsx_v.vshield import securitygroup_utils from vmware_nsx.plugins.nsx_v.vshield import vcns_driver from vmware_nsx.services.flowclassifier.nsx_v import utils as fc_utils +from vmware_nsx.services.fwaas.nsx_v import fwaas_callbacks LOG = logging.getLogger(__name__) PORTGROUP_PREFIX = 'dvportgroup' @@ -272,6 +273,9 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, # will happen only once self.start_rpc_listeners_called = False + # Init the FWaaS support + self._init_fwaas() + # Service insertion driver register self._si_handler = fc_utils.NsxvServiceInsertionHandler(self) registry.subscribe(self.add_vms_to_service_insertion, @@ -377,6 +381,10 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, self.start_rpc_listeners_called = True return self.conn.consume_in_threads() + def _init_fwaas(self): + # Bind FWaaS callbacks to the driver + self.fwaas_callbacks = fwaas_callbacks.NsxvFwaasCallbacks() + def _ext_extend_network_dict(self, result, netdb): ctx = n_context.get_admin_context() with db_api.context_manager.writer.using(ctx): @@ -3459,7 +3467,7 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, def _update_subnets_and_dnat_firewall(self, context, router, router_id=None, allow_external=True): - fake_fw_rules = [] + fw_rules = [] if not router_id: router_id = router['id'] subnet_cidrs = self._find_router_subnets_cidrs(context, router['id']) @@ -3473,12 +3481,12 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, 'enabled': True, 'source_ip_address': subnet_cidrs, 'destination_ip_address': subnet_cidrs} - fake_fw_rules.append(fake_subnet_fw_rule) + fw_rules.append(fake_subnet_fw_rule) _, dnat_rules = self._get_nat_rules(context, router) # If metadata service is enabled, block access to inter-edge network if self.metadata_proxy_handler: - fake_fw_rules += nsx_v_md_proxy.get_router_fw_rules() + fw_rules += nsx_v_md_proxy.get_router_fw_rules() dnat_cidrs = [rule['dst'] for rule in dnat_rules] if dnat_cidrs: @@ -3487,10 +3495,10 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, 'action': 'allow', 'enabled': True, 'destination_ip_address': dnat_cidrs} - fake_fw_rules.append(fake_dnat_fw_rule) + fw_rules.append(fake_dnat_fw_rule) nosnat_fw_rules = self._get_nosnat_subnets_fw_rules( context, router) - fake_fw_rules.extend(nosnat_fw_rules) + fw_rules.extend(nosnat_fw_rules) # Get the load balancer rules in case they are refreshed edge_id = self._get_edge_id_by_rtr_id(context, router_id) @@ -3506,10 +3514,13 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, 'name': vsm_rule['name'], 'ruleId': vsm_rule['ruleId'] } - fake_fw_rules.append(lb_fw_rule) + fw_rules.append(lb_fw_rule) - # TODO(berlin): Add fw rules if fw service is supported - fake_fw = {'firewall_rule_list': fake_fw_rules} + # Add fw rules if FWaaS is enabled + fw_rules.extend(self.fwaas_callbacks.get_fwaas_rules_for_router( + context, router, router_id)) + + fake_fw = {'firewall_rule_list': fw_rules} try: edge_utils.update_firewall(self.nsx_v, context, router_id, fake_fw, allow_external=allow_external) diff --git a/vmware_nsx/plugins/nsx_v/vshield/edge_firewall_driver.py b/vmware_nsx/plugins/nsx_v/vshield/edge_firewall_driver.py index cfc397e395..71129b1185 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/edge_firewall_driver.py +++ b/vmware_nsx/plugins/nsx_v/vshield/edge_firewall_driver.py @@ -25,9 +25,11 @@ LOG = logging.getLogger(__name__) VSE_FWAAS_ALLOW = "accept" VSE_FWAAS_DENY = "deny" +VSE_FWAAS_REJECT = "reject" FWAAS_ALLOW = "allow" FWAAS_DENY = "deny" +FWAAS_REJECT = "reject" class EdgeFirewallDriver(object): @@ -43,6 +45,8 @@ class EdgeFirewallDriver(object): return VSE_FWAAS_ALLOW elif action == FWAAS_DENY: return VSE_FWAAS_DENY + elif action == FWAAS_REJECT: + return VSE_FWAAS_REJECT else: msg = _("Invalid action value %s in a firewall rule") % action raise vcns_exc.VcnsBadRequest(resource='firewall_rule', msg=msg) @@ -52,12 +56,14 @@ class EdgeFirewallDriver(object): return FWAAS_ALLOW elif action == VSE_FWAAS_DENY: return FWAAS_DENY + elif action == VSE_FWAAS_REJECT: + return FWAAS_REJECT else: msg = (_("Invalid action value %s in " "a vshield firewall rule") % action) raise vcns_exc.VcnsBadRequest(resource='firewall_rule', msg=msg) - def _get_port_range_from_min_max_ports(self, min_port, max_port): + def _get_port_range(self, min_port, max_port): if not min_port: return None if min_port == max_port: @@ -74,7 +80,7 @@ class EdgeFirewallDriver(object): "num1:num2" - a range "num1,num2,num3" - a list """ - if not port_str: + if not port_str or port_str == 'any': return [] if ':' in port_str: min_port, sep, max_port = port_str.partition(":") @@ -91,7 +97,7 @@ class EdgeFirewallDriver(object): else: return [int(port_str.strip())] - def _convert_firewall_rule(self, context, rule, index=None): + def _convert_firewall_rule(self, rule, index=None): vcns_rule = { "action": self._convert_firewall_action(rule['action']), "enabled": rule.get('enabled', True)} @@ -141,47 +147,59 @@ class EdgeFirewallDriver(object): vcns_rule['ruleTag'] = index return vcns_rule - def _restore_firewall_rule(self, context, edge_id, response): - rule = response - rule_binding = nsxv_db.get_nsxv_edge_firewallrule_binding_by_vseid( - context.session, edge_id, rule['ruleId']) - service = rule['application']['service'][0] - src_port_range = self._get_port_range_from_min_max_ports( - service['sourcePort'][0], service['sourcePort'][-1]) - dst_port_range = self._get_port_range_from_min_max_ports( - service['port'][0], service['port'][-1]) - fw_rule = { - 'firewall_rule': { - 'id': rule_binding['rule_id'], - 'source_ip_address': rule['source']['ipAddress'], - 'destination_ip_address': rule['destination']['ipAddress'], - 'protocol': service['protocol'], - 'destination_port': dst_port_range, - 'source_port': src_port_range, - 'action': self._restore_firewall_action(rule['action']), - 'enabled': rule['enabled']}} + def _restore_firewall_rule(self, context, edge_id, rule): + fw_rule = {} + try: + rule_binding = ( + nsxv_db.get_nsxv_edge_firewallrule_binding_by_vseid( + context.session, edge_id, rule['ruleId'])) + except nsx_exc.NsxPluginException: + rule_binding = None + fw_rule['id'] = (rule_binding['rule_id'] + if rule_binding else rule['ruleId']) + fw_rule['ruleId'] = rule['ruleId'] + if rule.get('source'): + src = rule['source'] + fw_rule['source_ip_address'] = src['ipAddress'] + fw_rule['source_vnic_groups'] = src['vnicGroupId'] + + if rule.get('destination'): + dest = rule['destination'] + fw_rule['destination_ip_address'] = dest['ipAddress'] + fw_rule['destination_vnic_groups'] = dest['vnicGroupId'] + + if 'application' in rule and 'service' in rule['application']: + service = rule['application']['service'][0] + fw_rule['protocol'] = service['protocol'] + fw_rule['source_port'] = self._get_port_range( + service['sourcePort'][0], service['sourcePort'][-1]) + fw_rule['destination_port'] = self._get_port_range( + service['port'][0], service['port'][-1]) + + fw_rule['action'] = self._restore_firewall_action(rule['action']) + fw_rule['enabled'] = rule['enabled'] if rule.get('name'): - fw_rule['firewall_rule']['name'] = rule['name'] + fw_rule['name'] = rule['name'] if rule.get('description'): - fw_rule['firewall_rule']['description'] = rule['description'] + fw_rule['description'] = rule['description'] return fw_rule - def _convert_firewall(self, context, firewall, allow_external=False): + def _convert_firewall(self, firewall, allow_external=False): #bulk configuration on firewall and rescheduling the rule binding ruleTag = 1 vcns_rules = [] - for rule in firewall['firewall_rule_list']: - tag = rule.get('ruleTag', ruleTag) - vcns_rule = self._convert_firewall_rule(context, rule, tag) - vcns_rules.append(vcns_rule) - if not rule.get('ruleTag'): - ruleTag += 1 if allow_external: vcns_rules.append( {'action': "accept", 'enabled': True, 'destination': {'vnicGroupId': ["external"]}, 'ruleTag': ruleTag}) + for rule in firewall['firewall_rule_list']: + tag = rule.get('ruleTag', ruleTag) + vcns_rule = self._convert_firewall_rule(rule, tag) + vcns_rules.append(vcns_rule) + if not rule.get('ruleTag'): + ruleTag += 1 return { 'featureType': "firewall_4.0", 'globalConfig': {'tcpTimeoutEstablished': 7200}, @@ -192,32 +210,10 @@ class EdgeFirewallDriver(object): res = {} res['firewall_rule_list'] = [] for rule in response['firewallRules']['firewallRules']: - rule_binding = ( - nsxv_db.get_nsxv_edge_firewallrule_binding_by_vseid( - context.session, edge_id, rule['ruleId'])) - if rule_binding is None: + if rule.get('ruleType') == 'default_policy': continue - service = rule['application']['service'][0] - src_port_range = self._get_port_range_from_min_max_ports( - service['sourcePort'][0], service['sourcePort'][-1]) - dst_port_range = self._get_port_range_from_min_max_ports( - service['port'][0], service['port'][-1]) - item = { - 'firewall_rule': { - 'id': rule_binding['rule_id'], - 'source_ip_address': rule['source']['ipAddress'], - 'destination_ip_address': rule[ - 'destination']['ipAddress'], - 'protocol': service['protocol'], - 'destination_port': dst_port_range, - 'source_port': src_port_range, - 'action': self._restore_firewall_action(rule['action']), - 'enabled': rule['enabled']}} - if rule.get('name'): - item['firewall_rule']['name'] = rule['name'] - if rule.get('description'): - item['firewall_rule']['description'] = rule['description'] - res['firewall_rule_list'].append(item) + firewall_rule = self._restore_firewall_rule(context, edge_id, rule) + res['firewall_rule_list'].append({'firewall_rule': firewall_rule}) return res def _get_firewall(self, edge_id): @@ -277,7 +273,7 @@ class EdgeFirewallDriver(object): rule_map = nsxv_db.get_nsxv_edge_firewallrule_binding( context.session, id, edge_id) vcns_rule_id = rule_map.rule_vseid - fwr_req = self._convert_firewall_rule(context, firewall_rule) + fwr_req = self._convert_firewall_rule(firewall_rule) try: self.vcns.update_firewall_rule(edge_id, vcns_rule_id, fwr_req) except vcns_exc.VcnsApiException: @@ -308,7 +304,7 @@ class EdgeFirewallDriver(object): rule_map = nsxv_db.get_nsxv_edge_firewallrule_binding( context.session, ref_rule_id, edge_id) ref_vcns_rule_id = rule_map.rule_vseid - fwr_req = self._convert_firewall_rule(context, firewall_rule) + fwr_req = self._convert_firewall_rule(firewall_rule) try: header = self.vcns.add_firewall_rule_above( edge_id, ref_vcns_rule_id, fwr_req)[0] @@ -334,7 +330,7 @@ class EdgeFirewallDriver(object): ref_vcns_rule_id = rule_map.rule_vseid fwr_vse_next = self._get_firewall_rule_next( context, edge_id, ref_vcns_rule_id) - fwr_req = self._convert_firewall_rule(context, firewall_rule) + fwr_req = self._convert_firewall_rule(firewall_rule) if fwr_vse_next: ref_vcns_rule_id = fwr_vse_next['ruleId'] try: @@ -379,7 +375,7 @@ class EdgeFirewallDriver(object): raise vcns_exc.VcnsBadRequest(resource='firewall_rule', msg=msg) def update_firewall(self, edge_id, firewall, context, allow_external=True): - config = self._convert_firewall(None, firewall, + config = self._convert_firewall(firewall, allow_external=allow_external) try: diff --git a/vmware_nsx/services/fwaas/__init__.py b/vmware_nsx/services/fwaas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/fwaas/nsx_v/__init__.py b/vmware_nsx/services/fwaas/nsx_v/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/fwaas/nsx_v/edge_fwaas_driver.py b/vmware_nsx/services/fwaas/nsx_v/edge_fwaas_driver.py new file mode 100644 index 0000000000..11502fc103 --- /dev/null +++ b/vmware_nsx/services/fwaas/nsx_v/edge_fwaas_driver.py @@ -0,0 +1,202 @@ +# 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 neutron_lib import context as n_context +from neutron_lib.plugins import directory +from oslo_log import helpers as log_helpers +from oslo_log import log as logging + +from neutron_fwaas.extensions import firewall as fw_ext +from neutron_fwaas.services.firewall.drivers import fwaas_base + +from vmware_nsx.common import locking +from vmware_nsx.plugins.nsx_v.vshield.common import ( + exceptions as vcns_exc) +from vmware_nsx.plugins.nsx_v.vshield import edge_utils +from vmware_nsx.plugins.nsx_v.vshield import vcns_driver + +LOG = logging.getLogger(__name__) +FWAAS_DRIVER_NAME = 'Fwaas NSX-V driver' +RULE_NAME_PREFIX = 'Fwaas-' + + +class EdgeFwaasDriver(fwaas_base.FwaasDriverBase): + """NSX-V driver for Firewall As A Service - V1.""" + + @property + def edge_manager(self): + return directory.get_plugin().edge_manager + + def __init__(self): + LOG.debug("Loading FWaaS NsxVDriver.") + super(EdgeFwaasDriver, self).__init__() + self._nsxv = vcns_driver.VcnsDriver(None) + + def _get_routers_edges(self, context, apply_list): + # Get edges for all the routers in the apply list. + # note that shared routers are currently not supported + edge_manager = self.edge_manager + edges = [] + for router_info in apply_list: + lookup_id = None + router_id = router_info.router_id + if router_info.router.get('distributed'): + # we need the plr edge id + lookup_id = edge_manager.get_plr_by_tlr_id( + context, router_id) + if router_info.router.get('router_type') == 'shared': + LOG.info("Cannot apply firewall to shared router %s", + router_id) + else: + # exclusive router + lookup_id = router_id + if lookup_id: + # look for the edge id in the DB + edge_id = edge_utils.get_router_edge_id(context, lookup_id) + if edge_id: + edges.append(edge_id) + return edges + + def _translate_rules(self, fwaas_rules): + translated_rules = [] + for rule in fwaas_rules: + if not rule['enabled']: + # skip disabled rules + continue + # Make sure the rule has a name, and it starts with the prefix + # (backend max name length is 30) + if rule.get('name'): + rule['name'] = RULE_NAME_PREFIX + rule['name'] + else: + rule['name'] = RULE_NAME_PREFIX + rule['id'] + rule['name'] = rule['name'][:30] + # source & destination should be lists + if rule.get('destination_ip_address'): + rule['destination_ip_address'] = [ + rule['destination_ip_address']] + if rule.get('source_ip_address'): + rule['source_ip_address'] = [rule['source_ip_address']] + translated_rules.append(rule) + + return translated_rules + + def _get_other_backend_rules(self, context, edge_id): + """Get a list of current backend rules from other applications + + Those rules should stay on the backend firewall, when updating the + FWaaS rules. + """ + try: + backend_fw = self._nsxv.get_firewall(context, edge_id) + backend_rules = backend_fw['firewall_rule_list'] + except vcns_exc.VcnsApiException: + # Need to create a new one + backend_rules = [] + + # remove old FWaaS rules from the rules list + relevant_rules = [] + for rule_item in backend_rules: + rule = rule_item['firewall_rule'] + if not rule.get('name', '').startswith(RULE_NAME_PREFIX): + relevant_rules.append(rule) + + return relevant_rules + + def _set_rules_on_edge(self, context, edge_id, fw_id, translated_rules): + """delete old FWaaS rules from the Edge, and add new ones + + Note that the edge might have other FW rules like NAT or LBaas + that should remain there. + """ + # Get the existing backend rules which do not belong to FWaaS + backend_rules = self._get_other_backend_rules(context, edge_id) + + # add new FWaaS rules at the end by their original order + backend_rules.extend(translated_rules) + + # update the backend + # allow_external is False because it was already added + try: + with locking.LockManager.get_lock(str(edge_id)): + self._nsxv.update_firewall( + edge_id, + {'firewall_rule_list': backend_rules}, + context, + allow_external=False) + except Exception as e: + # catch known library exceptions and raise Fwaas generic exception + LOG.error("Failed to update backend firewall %(fw)s: " + "%(e)s", {'e': e, 'fw': fw_id}) + raise fw_ext.FirewallInternalDriverError(driver=FWAAS_DRIVER_NAME) + + def _create_or_update_firewall(self, agent_mode, apply_list, firewall): + # admin state down means default block rule firewall + if not firewall['admin_state_up']: + self.apply_default_policy(agent_mode, apply_list, firewall) + return + + context = n_context.get_admin_context() + + # Find out the relevant edges + router_edges = self._get_routers_edges(context, apply_list) + if not router_edges: + LOG.warning("Cannot apply the firewall to any of the routers %s", + apply_list) + return + + rules = self._translate_rules(firewall['firewall_rule_list']) + # update each edge + for edge_id in router_edges: + self._set_rules_on_edge( + context, edge_id, firewall['id'], rules) + + @log_helpers.log_method_call + def create_firewall(self, agent_mode, apply_list, firewall): + """Create the Firewall with a given policy. """ + self._create_or_update_firewall(agent_mode, apply_list, firewall) + + @log_helpers.log_method_call + def update_firewall(self, agent_mode, apply_list, firewall): + """Remove previous policy and apply the new policy.""" + self._create_or_update_firewall(agent_mode, apply_list, firewall) + + def _delete_firewall_or_set_default_policy(self, apply_list, firewall): + context = n_context.get_admin_context() + router_edges = self._get_routers_edges(context, apply_list) + if router_edges: + for edge_id in router_edges: + self._set_rules_on_edge(context, edge_id, firewall['id'], []) + + @log_helpers.log_method_call + def delete_firewall(self, agent_mode, apply_list, firewall): + """Delete firewall. + + Removes rules created by this instance from the backend firewall. + """ + self._delete_firewall_or_set_default_policy(apply_list, firewall) + + @log_helpers.log_method_call + def apply_default_policy(self, agent_mode, apply_list, firewall): + """Apply the default policy (deny all). + + The backend firewall always has this policy as default, so we only + need to delete the current rules. + """ + self._delete_firewall_or_set_default_policy(apply_list, firewall) + + def get_firewall_translated_rules(self, firewall): + if firewall['admin_state_up']: + return self._translate_rules(firewall['firewall_rule_list']) + return [] diff --git a/vmware_nsx/services/fwaas/nsx_v/fwaas_callbacks.py b/vmware_nsx/services/fwaas/nsx_v/fwaas_callbacks.py new file mode 100644 index 0000000000..408c591990 --- /dev/null +++ b/vmware_nsx/services/fwaas/nsx_v/fwaas_callbacks.py @@ -0,0 +1,115 @@ +# 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_config import cfg + +from neutron.agent.l3 import router_info +from neutron.common import config as neutron_config # noqa +from neutron_fwaas.db.firewall import firewall_db # noqa +from neutron_fwaas.db.firewall import firewall_router_insertion_db \ + as fw_r_ins_db +from neutron_fwaas.services.firewall.agents.l3reference \ + import firewall_l3_agent +from neutron_lib import context as n_context +from neutron_lib.plugins import directory + + +class NsxvFwaasCallbacks(firewall_l3_agent.L3WithFWaaS): + """NSX-V RPC callbacks for Firewall As A Service - V1.""" + def __init__(self): + # The super code needs a configuration object with the neutron host + # and an agent_mode, hich our driver doesn't use. + neutron_conf = cfg.CONF + neutron_conf.agent_mode = 'nsx' + super(NsxvFwaasCallbacks, self).__init__(conf=neutron_conf) + + @property + def core_plugin(self): + return directory.get_plugin() + + # Override functions using the agent_api that is not used by our plugin + def _get_router_ids_for_fw(self, context, fw, to_delete=False): + """Return the router_ids either from fw dict or tenant routers.""" + if self._has_router_insertion_fields(fw): + # it is a new version of plugin + return (fw['del-router-ids'] if to_delete + else fw['add-router-ids']) + else: + return [router['id'] for router in + self._get_routers_in_project(context, fw['tenant_id'])] + + def _get_routers_in_project(self, context, project_id): + return self.core_plugin.get_routers( + context, + filters={'tenant_id': [project_id]}) + + def _router_dict_to_obj(self, r): + # The callbacks expect a router-info object + return router_info.RouterInfo( + None, r['id'], router=r, + agent_conf=None, + interface_driver=None, + use_ipv6=False) + + def _get_router_info_list_for_tenant(self, router_ids, tenant_id): + """Returns the list of router info objects on which to apply the fw.""" + context = n_context.get_admin_context() + tenant_routers = self._get_routers_in_project(context, tenant_id) + return [self._router_dict_to_obj(ri) for ri in tenant_routers + if ri['id'] in router_ids] + + def get_fwaas_rules_for_router(self, context, router, router_id): + """Return the list of (translated) fwaas rules for this router.""" + if not self.fwaas_enabled: + return [] + + ctx_elevated = context.elevated() + + # get all the relevant router info + # ("router" does not have all the fields) + router_data = self.core_plugin.get_router(ctx_elevated, router['id']) + if not router_data: + return [] + if router_data.get('distributed'): + # in case of distributed router router['id'] is the id of the + # neutron router + # and router_id is the plr/tlr (the one that is being updated) + if router_id == router['id']: + # Do not add firewall rules on the tlr router. + return [] + if router_data.get('router_type') == 'shared': + # Currently there is no FWaaS support for shared routers + return [] + + # Exclusive router or PLR + fw_id = self._get_router_firewall_id(ctx_elevated, router['id']) + if fw_id: + return self._get_fw_applicable_rules(ctx_elevated, fw_id) + return [] + + # TODO(asarfaty): add this api to fwaas firewall-router-insertion-db + def _get_router_firewall_id(self, context, router_id): + entry = context.session.query( + fw_r_ins_db.FirewallRouterAssociation).filter_by( + router_id=router_id).first() + if entry: + return entry.fw_id + + def _get_fw_applicable_rules(self, context, fw_id): + fw_list = self.fwplugin_rpc.get_firewalls_for_tenant(context) + for fw in fw_list: + if fw['id'] == fw_id: + return self.fwaas_driver.get_firewall_translated_rules(fw) + return [] diff --git a/vmware_nsx/tests/unit/db/test_migrations.py b/vmware_nsx/tests/unit/db/test_migrations.py index 639a2a43cf..9d012a59f6 100644 --- a/vmware_nsx/tests/unit/db/test_migrations.py +++ b/vmware_nsx/tests/unit/db/test_migrations.py @@ -86,10 +86,15 @@ TAAS_TABLES = { 'tap_id_associations', } +FWAAS_TABLES = { + 'firewall_router_associations', + 'cisco_firewall_associations', +} + # EXTERNAL_TABLES should contain all names of tables that are not related to # current repo. EXTERNAL_TABLES = (set(external.TABLES) | LBAAS_TABLES | - L2GW_TABLES | SFC_TABLES | TAAS_TABLES) + L2GW_TABLES | SFC_TABLES | TAAS_TABLES | FWAAS_TABLES) class _TestModelsMigrationsFoo(test_migrations._TestModelsMigrations): diff --git a/vmware_nsx/tests/unit/nsx_v/test_fwaas_driver.py b/vmware_nsx/tests/unit/nsx_v/test_fwaas_driver.py new file mode 100644 index 0000000000..f9ce643029 --- /dev/null +++ b/vmware_nsx/tests/unit/nsx_v/test_fwaas_driver.py @@ -0,0 +1,142 @@ +# 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. + +import copy +import mock + +from vmware_nsx.services.fwaas.nsx_v import edge_fwaas_driver +from vmware_nsx.tests.unit.nsx_v import test_plugin as test_v_plugin + +FAKE_FW_ID = 'fake_fw_uuid' + + +class NsxvFwaasTestCase(test_v_plugin.NsxVPluginV2TestCase): + def setUp(self): + super(NsxvFwaasTestCase, self).setUp() + self.firewall = edge_fwaas_driver.EdgeFwaasDriver() + self.firewall._get_routers_edges = mock.Mock() + self.firewall._get_routers_edges.return_value = ['edge-1'] + + def _fake_rules_v4(self): + rule1 = {'enabled': True, + 'action': 'allow', + 'ip_version': 4, + 'protocol': 'tcp', + 'destination_port': '80', + 'source_ip_address': '10.24.4.2', + 'id': 'fake-fw-rule1'} + rule2 = {'enabled': True, + 'action': 'deny', + 'ip_version': 4, + 'protocol': 'tcp', + 'destination_port': '22', + 'id': 'fake-fw-rule2'} + rule3 = {'enabled': True, + 'action': 'reject', + 'ip_version': 4, + 'protocol': 'tcp', + 'destination_port': '23', + 'id': 'fake-fw-rule3'} + return [rule1, rule2, rule3] + + def _fake_firewall_no_rule(self): + rule_list = [] + fw_inst = {'id': FAKE_FW_ID, + 'admin_state_up': True, + 'tenant_id': 'tenant-uuid', + 'firewall_rule_list': rule_list} + return fw_inst + + def _fake_firewall(self, rule_list): + _rule_list = copy.deepcopy(rule_list) + for rule in _rule_list: + rule['position'] = str(_rule_list.index(rule)) + fw_inst = {'id': FAKE_FW_ID, + 'admin_state_up': True, + 'tenant_id': 'tenant-uuid', + 'firewall_rule_list': _rule_list} + return fw_inst + + def _fake_firewall_with_admin_down(self, rule_list): + fw_inst = {'id': FAKE_FW_ID, + 'admin_state_up': False, + 'tenant_id': 'tenant-uuid', + 'firewall_rule_list': rule_list} + return fw_inst + + def _fake_apply_list(self, router_count=1): + apply_list = [] + while router_count > 0: + router_inst = {} + router_info_inst = mock.Mock() + router_info_inst.router = router_inst + apply_list.append(router_info_inst) + router_count -= 1 + return apply_list + + def _setup_firewall_with_rules(self, func, router_count=1): + apply_list = self._fake_apply_list(router_count=router_count) + rule_list = self._fake_rules_v4() + firewall = self._fake_firewall(rule_list) + edges = ['edge-1'] * router_count + with mock.patch.object(self.firewall._nsxv, + "update_firewall") as update_fw,\ + mock.patch.object(self.firewall, + "_get_routers_edges", return_value=edges): + func('nsx', apply_list, firewall) + self.assertEqual(router_count, update_fw.call_count) + bakend_rules = update_fw.call_args[0][1]['firewall_rule_list'] + self.assertEqual(len(rule_list), len(bakend_rules)) + + def test_create_firewall_no_rules(self): + apply_list = self._fake_apply_list() + firewall = self._fake_firewall_no_rule() + with mock.patch.object(self.firewall._nsxv, + "update_firewall") as update_fw: + self.firewall.create_firewall('nsx', apply_list, firewall) + self.assertEqual(1, update_fw.call_count) + bakend_rules = update_fw.call_args[0][1]['firewall_rule_list'] + self.assertEqual(0, len(bakend_rules)) + + def test_create_firewall_with_rules(self): + self._setup_firewall_with_rules(self.firewall.create_firewall) + + def test_create_firewall_with_rules_two_routers(self): + self._setup_firewall_with_rules(self.firewall.create_firewall, + router_count=2) + + def test_update_firewall_with_rules(self): + self._setup_firewall_with_rules(self.firewall.update_firewall) + + def test_delete_firewall(self): + apply_list = self._fake_apply_list() + firewall = self._fake_firewall_no_rule() + with mock.patch.object(self.firewall._nsxv, + "update_firewall") as update_fw: + self.firewall.delete_firewall('nsx', apply_list, firewall) + self.assertEqual(1, update_fw.call_count) + bakend_rules = update_fw.call_args[0][1]['firewall_rule_list'] + self.assertEqual(0, len(bakend_rules)) + + def test_create_firewall_with_admin_down(self): + apply_list = self._fake_apply_list() + rule_list = self._fake_rules_v4() + firewall = self._fake_firewall_with_admin_down(rule_list) + with mock.patch.object(self.firewall._nsxv, + "update_firewall") as update_fw: + self.firewall.create_firewall('nsx', apply_list, firewall) + self.assertEqual(1, update_fw.call_count) + bakend_rules = update_fw.call_args[0][1]['firewall_rule_list'] + self.assertEqual(0, len(bakend_rules)) diff --git a/vmware_nsx/tests/unit/nsx_v/test_plugin.py b/vmware_nsx/tests/unit/nsx_v/test_plugin.py index 90238c0283..3a23936290 100644 --- a/vmware_nsx/tests/unit/nsx_v/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v/test_plugin.py @@ -3097,8 +3097,7 @@ class TestExclusiveRouterTestCase(L3NatTest, L3NatTestCaseBase, # Also test the md_srvip conversion: drv = edge_firewall_driver.EdgeFirewallDriver() - rule = drv._convert_firewall_rule( - context.get_admin_context(), md_srvip) + rule = drv._convert_firewall_rule(md_srvip) exp_service = {'service': [{'port': [80, 443, 8775], 'protocol': 'tcp'}]} exp_rule = {'action': 'accept', @@ -3156,8 +3155,7 @@ class TestExclusiveRouterTestCase(L3NatTest, L3NatTestCaseBase, # Also test the rule conversion # Ports should be sorted & unique, and ignore non numeric values drv = edge_firewall_driver.EdgeFirewallDriver() - rule = drv._convert_firewall_rule( - context.get_admin_context(), md_srvip) + rule = drv._convert_firewall_rule(md_srvip) exp_service = {'service': [{'port': [55, 66, 80, 443, 8775], 'protocol': 'tcp'}]} exp_rule = {'action': 'accept',