From fa89d1a1cccc6a8a14cf6fbac24cfbf4a0b6b785 Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Mon, 14 Jan 2013 22:45:41 -0800 Subject: [PATCH] Add NVP port security implementation Implements blueprint nvp-port-security-extension Change-Id: I9f09424b963acb4f4847d11e9a98432094f8c164 --- .../versions/1149d7de0cfa_port_security.py | 73 ++++++++++ .../nicira/nicira_nvp_plugin/QuantumPlugin.py | 126 ++++++++++++++---- .../nicira/nicira_nvp_plugin/nvplib.py | 69 ++++++---- .../tests/unit/nicira/test_nicira_plugin.py | 33 +++++ 4 files changed, 246 insertions(+), 55 deletions(-) create mode 100644 quantum/db/migration/alembic_migrations/versions/1149d7de0cfa_port_security.py diff --git a/quantum/db/migration/alembic_migrations/versions/1149d7de0cfa_port_security.py b/quantum/db/migration/alembic_migrations/versions/1149d7de0cfa_port_security.py new file mode 100644 index 0000000000..bcb4596c0a --- /dev/null +++ b/quantum/db/migration/alembic_migrations/versions/1149d7de0cfa_port_security.py @@ -0,0 +1,73 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 OpenStack LLC +# +# 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. +# + +"""inital port security + +Revision ID: 1149d7de0cfa +Revises: 1b693c095aa3 +Create Date: 2013-01-22 14:05:20.696502 + +""" + +# revision identifiers, used by Alembic. +revision = '1149d7de0cfa' +down_revision = '1b693c095aa3' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + 'quantum.plugins.nicira.nicira_nvp_plugin.QuantumPlugin.NvpPluginV2' +] + +from alembic import op +import sqlalchemy as sa + +from quantum.db import migration + + +def upgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + ### commands auto generated by Alembic - please adjust! ### + op.create_table('networksecuritybindings', + sa.Column('network_id', sa.String(length=36), + nullable=False), + sa.Column('port_security_enabled', sa.Boolean(), + nullable=False), + sa.ForeignKeyConstraint(['network_id'], ['networks.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('network_id')) + op.create_table('portsecuritybindings', + sa.Column('port_id', sa.String(length=36), + nullable=False), + sa.Column('port_security_enabled', sa.Boolean(), + nullable=False), + sa.ForeignKeyConstraint(['port_id'], ['ports.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('port_id')) + ### end Alembic commands ### + + +def downgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('portsecuritybindings') + op.drop_table('networksecuritybindings') + ### end Alembic commands ### diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py b/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py index e1188d597b..1d21da3a99 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py @@ -34,8 +34,10 @@ from quantum.common import topics from quantum.db import api as db from quantum.db import db_base_plugin_v2 from quantum.db import dhcp_rpc_base +from quantum.db import portsecurity_db # NOTE: quota_db cannot be removed, it is for db model from quantum.db import quota_db +from quantum.extensions import portsecurity as psec from quantum.extensions import providernet as pnet from quantum.openstack.common import cfg from quantum.openstack.common import rpc @@ -105,16 +107,22 @@ class NVPRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin): return q_rpc.PluginRpcDispatcher([self]) -class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): +class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, + portsecurity_db.PortSecurityDbMixin): """ NvpPluginV2 is a Quantum plugin that provides L2 Virtual Network functionality using NVP. """ - supported_extension_aliases = ["provider", "quotas"] + supported_extension_aliases = ["provider", "quotas", "port-security"] # Default controller cluster default_cluster = None + provider_network_view = "extension:provider_network:view" + provider_network_set = "extension:provider_network:set" + port_security_enabled_create = "create_port:port_security_enabled" + port_security_enabled_update = "update_port:port_security_enabled" + def __init__(self, loglevel=None): if loglevel: logging.basicConfig(level=loglevel) @@ -216,15 +224,11 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): else: return self.default_cluster - def _check_provider_view_auth(self, context, network): - return policy.check(context, - "extension:provider_network:view", - network) + def _check_view_auth(self, context, resource, action): + return policy.check(context, action, resource) - def _enforce_provider_set_auth(self, context, network): - return policy.enforce(context, - "extension:provider_network:set", - network) + def _enforce_set_auth(self, context, resource, action): + return policy.enforce(context, action, resource) def _handle_provider_create(self, context, attrs): # NOTE(salvatore-orlando): This method has been borrowed from @@ -240,7 +244,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): return # Authorize before exposing plugin details to client - self._enforce_provider_set_auth(context, attrs) + self._enforce_set_auth(context, attrs, self.provider_network_set) err_msg = None if not network_type_set: err_msg = _("%s required") % pnet.NETWORK_TYPE @@ -273,7 +277,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): # which should be specified in physical_network def _extend_network_dict_provider(self, context, network, binding=None): - if self._check_provider_view_auth(context, network): + if self._check_view_auth(context, network, self.provider_network_view): if not binding: binding = nicira_db.get_network_binding(context.session, network['id']) @@ -365,6 +369,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): with context.session.begin(subtransactions=True): new_net = super(NvpPluginV2, self).create_network(context, network) + self._process_network_create_port_security(context, + network['network']) if net_data.get(pnet.NETWORK_TYPE): net_binding = nicira_db.add_network_binding( context.session, new_net['id'], @@ -373,6 +379,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): net_data.get(pnet.SEGMENTATION_ID)) self._extend_network_dict_provider(context, new_net, net_binding) + self._extend_network_port_security_dict(context, new_net) return new_net def delete_network(self, context, id): @@ -409,6 +416,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): network = self._get_network(context, id) net_result = self._make_network_dict(network, None) self._extend_network_dict_provider(context, net_result) + self._extend_network_port_security_dict(context, net_result) + # verify the fabric status of the corresponding # logical switch(es) in nvp try: @@ -441,6 +450,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): super(NvpPluginV2, self).get_networks(context, filters)) for net in quantum_lswitches: self._extend_network_dict_provider(context, net) + self._extend_network_port_security_dict(context, net) if context.is_admin and not filters.get("tenant_id"): tenant_filter = "" @@ -516,10 +526,23 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): raise q_exc.NotImplementedError(_("admin_state_up=False " "networks are not " "supported.")) - return super(NvpPluginV2, self).update_network(context, id, network) + with context.session.begin(subtransactions=True): + quantum_db = super(NvpPluginV2, self).update_network( + context, id, network) + if psec.PORTSECURITY in network['network']: + self._update_network_security_binding( + context, id, network['network'][psec.PORTSECURITY]) + self._extend_network_port_security_dict( + context, quantum_db) + return quantum_db def get_ports(self, context, filters=None, fields=None): - quantum_lports = super(NvpPluginV2, self).get_ports(context, filters) + with context.session.begin(subtransactions=True): + quantum_lports = super(NvpPluginV2, self).get_ports( + context, filters) + for quantum_lport in quantum_lports: + self._extend_port_port_security_dict(context, quantum_lport) + vm_filter = "" tenant_filter = "" # This is used when calling delete_network. Quantum checks to see if @@ -607,12 +630,28 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): return lports def create_port(self, context, port): + # If PORTSECURITY is not the default value ATTR_NOT_SPECIFIED + # then we pass the port to the policy engine. The reason why we don't + # pass the value to the policy engine when the port is + # ATTR_NOT_SPECIFIED is for the case where a port is created on a + # shared network that is not owned by the tenant. + # TODO(arosen) fix policy engine to do this for us automatically. + if attributes.is_attr_set(port['port'].get(psec.PORTSECURITY)): + self._enforce_set_auth(context, port, + self.port_security_enabled_create) + port_data = port['port'] with context.session.begin(subtransactions=True): # First we allocate port in quantum database quantum_db = super(NvpPluginV2, self).create_port(context, port) # Update fields obtained from quantum db (eg: MAC address) port["port"].update(quantum_db) - port_data = port['port'] + + # port security extension checks + (port_security, has_ip) = self._determine_port_security_and_has_ip( + context, port_data) + port_data[psec.PORTSECURITY] = port_security + self._process_port_security_create(context, port_data) + # provider networking extension checks # Fetch the network and network binding from Quantum db network = self._get_network(context, port_data['network_id']) network_binding = nicira_db.get_network_binding( @@ -639,7 +678,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): port_data['device_id'], port_data['admin_state_up'], port_data['mac_address'], - port_data['fixed_ips']) + port_data['fixed_ips'], + port_data[psec.PORTSECURITY]) # Get NVP ls uuid for quantum network nvplib.plug_interface(cluster, selected_lswitch['uuid'], lport['uuid'], "VifAttachment", @@ -660,20 +700,50 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): LOG.debug(_("create_port completed on NVP for tenant " "%(tenant_id)s: (%(id)s)"), port_data) - return port_data + self._extend_port_port_security_dict(context, port_data) + return port_data def update_port(self, context, id, port): - params = {} - port_quantum = super(NvpPluginV2, self).get_port(context, id) - port_nvp, cluster = ( - nvplib.get_port_by_quantum_tag(self.clusters.itervalues(), - port_quantum["network_id"], id)) - params["cluster"] = cluster - params["port"] = port_quantum - LOG.debug(_("Update port request: %s"), params) - nvplib.update_port(port_quantum['network_id'], - port_nvp['uuid'], **params) - return super(NvpPluginV2, self).update_port(context, id, port) + self._enforce_set_auth(context, port, + self.port_security_enabled_update) + tenant_id = self._get_tenant_id_for_create(context, port) + with context.session.begin(subtransactions=True): + ret_port = super(NvpPluginV2, self).update_port( + context, id, port) + # copy values over + ret_port.update(port['port']) + + # Handle port security + if psec.PORTSECURITY in port['port']: + self._update_port_security_binding( + context, id, ret_port[psec.PORTSECURITY]) + # populate with value + else: + ret_port[psec.PORTSECURITY] = self._get_port_security_binding( + context, id) + + port_nvp, cluster = ( + nvplib.get_port_by_quantum_tag(self.clusters.itervalues(), + ret_port["network_id"], id)) + LOG.debug(_("Update port request: %s"), port) + nvplib.update_port(cluster, ret_port['network_id'], + port_nvp['uuid'], id, tenant_id, + ret_port['name'], ret_port['device_id'], + ret_port['admin_state_up'], + ret_port['mac_address'], + ret_port['fixed_ips'], + ret_port[psec.PORTSECURITY]) + + # Update the port status from nvp. If we fail here hide it since + # the port was successfully updated but we were not able to retrieve + # the status. + try: + ret_port['status'] = nvplib.get_port_status( + cluster, ret_port['network_id'], port_nvp['uuid']) + except: + LOG.warn(_("Unable to retrieve port status for: %s."), + port_nvp['uuid']) + return ret_port def delete_port(self, context, id): # TODO(salvatore-orlando): pass only actual cluster diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py b/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py index df4a825ec0..431da9a592 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py @@ -290,7 +290,6 @@ def get_all_networks(cluster, tenant_id, networks): raise exception.QuantumException() if not resp_obj: return [] - lswitches = json.loads(resp_obj)["results"] networks_result = copy(networks) return networks_result @@ -371,7 +370,7 @@ def get_port_by_quantum_tag(clusters, lswitch, quantum_tag): for c in clusters: try: res_obj = do_single_request('GET', query, cluster=c) - except Exception as e: + except Exception: continue res = json.loads(res_obj) if len(res["results"]) == 1: @@ -417,44 +416,56 @@ def get_port(cluster, network, port, relations=None): return port -def update_port(network, port_id, **params): - cluster = params["cluster"] - lport_obj = {} +def _configure_extensions(lport_obj, mac_address, fixed_ips, + port_security_enabled): + lport_obj['allowed_address_pairs'] = [] + if port_security_enabled: + for fixed_ip in fixed_ips: + ip_address = fixed_ip.get('ip_address') + if ip_address: + lport_obj['allowed_address_pairs'].append( + {'mac_address': mac_address, 'ip_address': ip_address}) + # add address pair allowing src_ip 0.0.0.0 to leave + # this is required for outgoing dhcp request + lport_obj["allowed_address_pairs"].append( + {"mac_address": mac_address, + "ip_address": "0.0.0.0"}) - admin_state_up = params['port'].get('admin_state_up') - name = params["port"].get("name") - device_id = params["port"].get("device_id") - if admin_state_up: - lport_obj["admin_status_enabled"] = admin_state_up - if name: - lport_obj["display_name"] = name - if device_id: - # device_id can be longer than 40 so we rehash it - device_id = hashlib.sha1(device_id).hexdigest() - lport_obj["tags"] = ( - [dict(scope='os_tid', tag=params["port"].get("tenant_id")), - dict(scope='q_port_id', tag=params["port"]["id"]), - dict(scope='vm_id', tag=device_id)]) +def update_port(cluster, lswitch_uuid, lport_uuid, quantum_port_id, tenant_id, + display_name, device_id, admin_status_enabled, + mac_address=None, fixed_ips=None, port_security_enabled=None): - uri = "/ws.v1/lswitch/" + network + "/lport/" + port_id + # device_id can be longer than 40 so we rehash it + hashed_device_id = hashlib.sha1(device_id).hexdigest() + lport_obj = dict( + admin_status_enabled=admin_status_enabled, + display_name=display_name, + tags=[dict(scope='os_tid', tag=tenant_id), + dict(scope='q_port_id', tag=quantum_port_id), + dict(scope='vm_id', tag=hashed_device_id)]) + + _configure_extensions(lport_obj, mac_address, fixed_ips, + port_security_enabled) + + path = "/ws.v1/lswitch/" + lswitch_uuid + "/lport/" + lport_uuid try: - resp_obj = do_single_request("PUT", uri, json.dumps(lport_obj), + resp_obj = do_single_request("PUT", path, json.dumps(lport_obj), cluster=cluster) except NvpApiClient.ResourceNotFound as e: LOG.error(_("Port or Network not found, Error: %s"), str(e)) - raise exception.PortNotFound(port_id=port_id, net_id=network) + raise exception.PortNotFound(port_id=lport_uuid, net_id=lswitch_uuid) except NvpApiClient.NvpApiException as e: raise exception.QuantumException() - - obj = json.loads(resp_obj) - obj["port-op-status"] = get_port_status(cluster, network, obj["uuid"]) - return obj + result = json.loads(resp_obj) + LOG.debug(_("Updated logical port %(result)s on logical swtich %(uuid)s"), + {'result': result['uuid'], 'uuid': lswitch_uuid}) + return result def create_lport(cluster, lswitch_uuid, tenant_id, quantum_port_id, display_name, device_id, admin_status_enabled, - mac_address=None, fixed_ips=None): + mac_address=None, fixed_ips=None, port_security_enabled=None): """ Creates a logical port on the assigned logical switch """ # device_id can be longer than 40 so we rehash it hashed_device_id = hashlib.sha1(device_id).hexdigest() @@ -465,6 +476,10 @@ def create_lport(cluster, lswitch_uuid, tenant_id, quantum_port_id, dict(scope='q_port_id', tag=quantum_port_id), dict(scope='vm_id', tag=hashed_device_id)], ) + + _configure_extensions(lport_obj, mac_address, fixed_ips, + port_security_enabled) + path = _build_uri_path(LPORT_RESOURCE, parent_resource_id=lswitch_uuid) try: resp_obj = do_single_request("POST", path, diff --git a/quantum/tests/unit/nicira/test_nicira_plugin.py b/quantum/tests/unit/nicira/test_nicira_plugin.py index 72bb541d69..c20beebbc7 100644 --- a/quantum/tests/unit/nicira/test_nicira_plugin.py +++ b/quantum/tests/unit/nicira/test_nicira_plugin.py @@ -27,6 +27,8 @@ from quantum.openstack.common import cfg from quantum.plugins.nicira.nicira_nvp_plugin import nvplib from quantum.tests.unit.nicira import fake_nvpapiclient import quantum.tests.unit.test_db_plugin as test_plugin +import quantum.tests.unit.test_extension_portsecurity as psec + LOG = logging.getLogger(__name__) NICIRA_PKG_PATH = 'quantum.plugins.nicira.nicira_nvp_plugin' @@ -152,3 +154,34 @@ class TestNiciraNetworksV2(test_plugin.TestNetworksV2, with self.assertRaises(webob.exc.HTTPClientError) as ctx_manager: self._test_create_bridge_network(vlan_id=5000) self.assertEquals(ctx_manager.exception.code, 400) + + +class NiciraPortSecurityTestCase(psec.PortSecurityDBTestCase): + + _plugin_name = ('%s.QuantumPlugin.NvpPluginV2' % NICIRA_PKG_PATH) + + def setUp(self): + etc_path = os.path.join(os.path.dirname(__file__), 'etc') + test_lib.test_config['config_files'] = [os.path.join(etc_path, + 'nvp.ini.test')] + # mock nvp api client + fc = fake_nvpapiclient.FakeClient(etc_path) + self.mock_nvpapi = mock.patch('%s.NvpApiClient.NVPApiHelper' + % NICIRA_PKG_PATH, autospec=True) + instance = self.mock_nvpapi.start() + instance.return_value.login.return_value = "the_cookie" + + def _fake_request(*args, **kwargs): + return fc.fake_request(*args, **kwargs) + + instance.return_value.request.side_effect = _fake_request + super(NiciraPortSecurityTestCase, self).setUp(self._plugin_name) + + def tearDown(self): + super(NiciraPortSecurityTestCase, self).tearDown() + self.mock_nvpapi.stop() + + +class TestNiciraPortSecurity(psec.TestPortSecurity, + NiciraPortSecurityTestCase): + pass