From: Kevin Benton Date: Tue, 11 Feb 2014 03:36:22 +0000 (-0800) Subject: BigSwitch: Add agent to support neutron sec groups X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=65fc2607700bf3818c394c1a2591a2c9557e096e;p=openstack-build%2Fneutron-build.git BigSwitch: Add agent to support neutron sec groups Adds a BigSwitch Agent responsible for supporting neutron security groups on the compute node. Adds the mixin classes to the plugin to support the security group calls. Implements: blueprint bsn-neutron-sec-groups Change-Id: I3a09888a3ba7d565c2dce8293821919c1e5d0d15 --- diff --git a/etc/neutron/plugins/bigswitch/restproxy.ini b/etc/neutron/plugins/bigswitch/restproxy.ini index a89f84fa8..a982010f6 100644 --- a/etc/neutron/plugins/bigswitch/restproxy.ini +++ b/etc/neutron/plugins/bigswitch/restproxy.ini @@ -60,3 +60,20 @@ servers=localhost:8080 # Maximum number of rules that a single router may have # Default is 200 # max_router_rules=200 + +[restproxyagent] + +# Specify the name of the bridge used on compute nodes +# for attachment. +# Default: br-int +# integration_bridge=br-int + +# Change the frequency of polling by the restproxy agent. +# Value is seconds +# Default: 5 +# polling_interval=5 + +# Virtual switch type on the compute node. +# Options: ovs or ivs +# Default: ovs +# virtual_switch_type = ovs diff --git a/neutron/db/migration/alembic_migrations/versions/f44ab9871cd6_bsn_security_groups.py b/neutron/db/migration/alembic_migrations/versions/f44ab9871cd6_bsn_security_groups.py new file mode 100644 index 000000000..b75bf4201 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/f44ab9871cd6_bsn_security_groups.py @@ -0,0 +1,95 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +"""bsn_security_groups + +Revision ID: f44ab9871cd6 +Revises: e766b19a3bb +Create Date: 2014-02-26 17:43:43.051078 + +""" + +# revision identifiers, used by Alembic. +revision = 'f44ab9871cd6' +down_revision = 'e766b19a3bb' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + 'neutron.plugins.bigswitch.plugin.NeutronRestProxyV2', +] + +from alembic import op +import sqlalchemy as sa + +from neutron.db import migration + + +def upgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + ### commands auto generated by Alembic - please adjust! ### + op.create_table( + 'securitygroups', + sa.Column('tenant_id', sa.String(length=255), nullable=True), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('description', sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table( + 'securitygrouprules', + sa.Column('tenant_id', sa.String(length=255), nullable=True), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('security_group_id', sa.String(length=36), nullable=False), + sa.Column('remote_group_id', sa.String(length=36), nullable=True), + sa.Column('direction', + sa.Enum('ingress', 'egress', + name='securitygrouprules_direction'), + nullable=True), + sa.Column('ethertype', sa.String(length=40), nullable=True), + sa.Column('protocol', sa.String(length=40), nullable=True), + sa.Column('port_range_min', sa.Integer(), nullable=True), + sa.Column('port_range_max', sa.Integer(), nullable=True), + sa.Column('remote_ip_prefix', sa.String(length=255), nullable=True), + sa.ForeignKeyConstraint(['security_group_id'], ['securitygroups.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['remote_group_id'], ['securitygroups.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table( + 'securitygroupportbindings', + sa.Column('port_id', sa.String(length=36), nullable=False), + sa.Column('security_group_id', sa.String(length=36), nullable=False), + sa.ForeignKeyConstraint(['port_id'], ['ports.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['security_group_id'], ['securitygroups.id']), + sa.PrimaryKeyConstraint('port_id', 'security_group_id') + ) + ### end Alembic commands ### + + +def downgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('securitygroupportbindings') + op.drop_table('securitygrouprules') + op.drop_table('securitygroups') + ### end Alembic commands ### diff --git a/neutron/plugins/bigswitch/agent/__init__.py b/neutron/plugins/bigswitch/agent/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/plugins/bigswitch/agent/restproxy_agent.py b/neutron/plugins/bigswitch/agent/restproxy_agent.py new file mode 100644 index 000000000..81d3032d5 --- /dev/null +++ b/neutron/plugins/bigswitch/agent/restproxy_agent.py @@ -0,0 +1,179 @@ +# Copyright 2014 Big Switch Networks, Inc. +# All Rights Reserved. +# +# Copyright 2011 Nicira Networks, 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. +# @author: Kevin Benton, kevin.benton@bigswitch.com + +import eventlet +import sys +import time + +from oslo.config import cfg + +from neutron.agent.linux import ovs_lib +from neutron.agent.linux import utils +from neutron.agent import rpc as agent_rpc +from neutron.agent import securitygroups_rpc as sg_rpc +from neutron.common import config +from neutron.common import topics +from neutron import context as q_context +from neutron.extensions import securitygroup as ext_sg +from neutron.openstack.common import excutils +from neutron.openstack.common import log +from neutron.openstack.common.rpc import dispatcher +from neutron.plugins.bigswitch import config as pl_config + +LOG = log.getLogger(__name__) + + +class IVSBridge(ovs_lib.OVSBridge): + ''' + This class does not provide parity with OVS using IVS. + It's only the bare minimum necessary to use IVS with this agent. + ''' + def run_vsctl(self, args, check_error=False): + full_args = ["ivs-ctl"] + args + try: + return utils.execute(full_args, root_helper=self.root_helper) + except Exception as e: + with excutils.save_and_reraise_exception() as ctxt: + LOG.error(_("Unable to execute %(cmd)s. " + "Exception: %(exception)s"), + {'cmd': full_args, 'exception': e}) + if not check_error: + ctxt.reraise = False + + def get_vif_port_set(self): + port_names = self.get_port_name_list() + edge_ports = set(port_names) + return edge_ports + + def get_vif_port_by_id(self, port_id): + # IVS in nova uses hybrid method with last 14 chars of UUID + name = 'qvo%s' % port_id[:14] + if name in self.get_vif_port_set(): + return name + return False + + +class PluginApi(agent_rpc.PluginApi, + sg_rpc.SecurityGroupServerRpcApiMixin): + pass + + +class SecurityGroupAgent(sg_rpc.SecurityGroupAgentRpcMixin): + def __init__(self, context, plugin_rpc, root_helper): + self.context = context + self.plugin_rpc = plugin_rpc + self.root_helper = root_helper + self.init_firewall() + + +class RestProxyAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): + + RPC_API_VERSION = '1.1' + + def __init__(self, integ_br, polling_interval, root_helper, vs='ovs'): + super(RestProxyAgent, self).__init__() + self.polling_interval = polling_interval + self._setup_rpc() + self.sg_agent = SecurityGroupAgent(self.context, + self.plugin_rpc, + root_helper) + if vs == 'ivs': + self.int_br = IVSBridge(integ_br, root_helper) + else: + self.int_br = ovs_lib.OVSBridge(integ_br, root_helper) + + def _setup_rpc(self): + self.topic = topics.AGENT + self.plugin_rpc = PluginApi(topics.PLUGIN) + self.context = q_context.get_admin_context_without_session() + self.dispatcher = dispatcher.RpcDispatcher([self]) + consumers = [[topics.PORT, topics.UPDATE], + [topics.SECURITY_GROUP, topics.UPDATE]] + self.connection = agent_rpc.create_consumers(self.dispatcher, + self.topic, + consumers) + + def port_update(self, context, **kwargs): + LOG.debug(_("Port update received")) + port = kwargs.get('port') + vif_port = self.int_br.get_vif_port_by_id(port['id']) + if not vif_port: + LOG.debug(_("Port %s is not present on this host."), port['id']) + return + + LOG.debug(_("Port %s found. Refreshing firewall."), port['id']) + if ext_sg.SECURITYGROUPS in port: + self.sg_agent.refresh_firewall() + + def _update_ports(self, registered_ports): + ports = self.int_br.get_vif_port_set() + if ports == registered_ports: + return + added = ports - registered_ports + removed = registered_ports - ports + return {'current': ports, + 'added': added, + 'removed': removed} + + def _process_devices_filter(self, port_info): + if 'added' in port_info: + self.sg_agent.prepare_devices_filter(port_info['added']) + if 'removed' in port_info: + self.sg_agent.remove_devices_filter(port_info['removed']) + + def daemon_loop(self): + ports = set() + + while True: + start = time.time() + try: + port_info = self._update_ports(ports) + if port_info: + LOG.debug(_("Agent loop has new device")) + self._process_devices_filter(port_info) + ports = port_info['current'] + except Exception: + LOG.exception(_("Error in agent event loop")) + + elapsed = max(time.time() - start, 0) + if (elapsed < self.polling_interval): + time.sleep(self.polling_interval - elapsed) + else: + LOG.debug(_("Loop iteration exceeded interval " + "(%(polling_interval)s vs. %(elapsed)s)!"), + {'polling_interval': self.polling_interval, + 'elapsed': elapsed}) + + +def main(): + eventlet.monkey_patch() + cfg.CONF(project='neutron') + config.setup_logging(cfg.CONF) + pl_config.register_config() + + integ_br = cfg.CONF.RESTPROXYAGENT.integration_bridge + polling_interval = cfg.CONF.RESTPROXYAGENT.polling_interval + root_helper = cfg.CONF.AGENT.root_helper + bsnagent = RestProxyAgent(integ_br, polling_interval, root_helper, + cfg.CONF.RESTPROXYAGENT.virtual_switch_type) + bsnagent.daemon_loop() + sys.exit(0) + +if __name__ == "__main__": + main() diff --git a/neutron/plugins/bigswitch/config.py b/neutron/plugins/bigswitch/config.py index 7c46e105d..5094617b7 100644 --- a/neutron/plugins/bigswitch/config.py +++ b/neutron/plugins/bigswitch/config.py @@ -24,6 +24,7 @@ This module manages configuration options from oslo.config import cfg +from neutron.agent.common import config as agconfig from neutron.common import utils from neutron.extensions import portbindings @@ -80,8 +81,20 @@ nova_opts.append(cfg.ListOpt('vif_types', default=portbindings.VIF_TYPES, help=_('List of allowed vif_type values.'))) +agent_opts = [ + cfg.StrOpt('integration_bridge', default='br-int', + help=_('Name of integration bridge on compute ' + 'nodes used for security group insertion.')), + cfg.IntOpt('polling_interval', default=5, + help=_('Seconds between agent checks for port changes')), + cfg.StrOpt('virtual_switch_type', default='ovs', + help=_('Virtual switch type.')) +] + def register_config(): cfg.CONF.register_opts(restproxy_opts, "RESTPROXY") cfg.CONF.register_opts(router_opts, "ROUTER") cfg.CONF.register_opts(nova_opts, "NOVA") + cfg.CONF.register_opts(agent_opts, "RESTPROXYAGENT") + agconfig.register_root_helper(cfg.CONF) diff --git a/neutron/plugins/bigswitch/plugin.py b/neutron/plugins/bigswitch/plugin.py index 0a723ae1d..12a80955d 100644 --- a/neutron/plugins/bigswitch/plugin.py +++ b/neutron/plugins/bigswitch/plugin.py @@ -45,9 +45,11 @@ on port-attach) on an additional PUT to do a bulk dump of all persistent data. """ import copy +import re from oslo.config import cfg +from neutron.agent import securitygroups_rpc as sg_rpc from neutron.api import extensions as neutron_extensions from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api from neutron.common import constants as const @@ -57,15 +59,20 @@ from neutron.common import topics from neutron import context as qcontext from neutron.db import agents_db from neutron.db import agentschedulers_db +from neutron.db import api as db from neutron.db import db_base_plugin_v2 from neutron.db import dhcp_rpc_base from neutron.db import external_net_db from neutron.db import extradhcpopt_db from neutron.db import l3_db +from neutron.db import models_v2 +from neutron.db import securitygroups_db as sg_db +from neutron.db import securitygroups_rpc_base as sg_rpc_base from neutron.extensions import external_net from neutron.extensions import extra_dhcp_opt as edo_ext from neutron.extensions import l3 from neutron.extensions import portbindings +from neutron import manager from neutron.openstack.common import excutils from neutron.openstack.common import importutils from neutron.openstack.common import log as logging @@ -84,7 +91,26 @@ SYNTAX_ERROR_MESSAGE = _('Syntax error in server config file, aborting plugin') METADATA_SERVER_IP = '169.254.169.254' -class RpcProxy(dhcp_rpc_base.DhcpRpcCallbackMixin): +class AgentNotifierApi(rpc.proxy.RpcProxy, + sg_rpc.SecurityGroupAgentRpcApiMixin): + + BASE_RPC_API_VERSION = '1.1' + + def __init__(self, topic): + super(AgentNotifierApi, self).__init__( + topic=topic, default_version=self.BASE_RPC_API_VERSION) + self.topic_port_update = topics.get_topic_name( + topic, topics.PORT, topics.UPDATE) + + def port_update(self, context, port): + self.fanout_cast(context, + self.make_msg('port_update', + port=port), + topic=self.topic_port_update) + + +class RestProxyCallbacks(sg_rpc_base.SecurityGroupServerRpcCallbackMixin, + dhcp_rpc_base.DhcpRpcCallbackMixin): RPC_API_VERSION = '1.1' @@ -92,6 +118,42 @@ class RpcProxy(dhcp_rpc_base.DhcpRpcCallbackMixin): return q_rpc.PluginRpcDispatcher([self, agents_db.AgentExtRpcCallback()]) + def get_port_from_device(self, device): + port_id = re.sub(r"^tap", "", device) + port = self.get_port_and_sgs(port_id) + if port: + port['device'] = device + return port + + def get_port_and_sgs(self, port_id): + """Get port from database with security group info.""" + + LOG.debug(_("get_port_and_sgs() called for port_id %s"), port_id) + session = db.get_session() + sg_binding_port = sg_db.SecurityGroupPortBinding.port_id + + with session.begin(subtransactions=True): + query = session.query( + models_v2.Port, + sg_db.SecurityGroupPortBinding.security_group_id + ) + query = query.outerjoin(sg_db.SecurityGroupPortBinding, + models_v2.Port.id == sg_binding_port) + query = query.filter(models_v2.Port.id.startswith(port_id)) + port_and_sgs = query.all() + if not port_and_sgs: + return + port = port_and_sgs[0][0] + plugin = manager.NeutronManager.get_plugin() + port_dict = plugin._make_port_dict(port) + port_dict['security_groups'] = [ + sg_id for port_, sg_id in port_and_sgs if sg_id] + port_dict['security_group_rules'] = [] + port_dict['security_group_source_groups'] = [] + port_dict['fixed_ips'] = [ip['ip_address'] + for ip in port['fixed_ips']] + return port_dict + class NeutronRestProxyV2Base(db_base_plugin_v2.NeutronDbPluginV2, external_net_db.External_net_db_mixin, @@ -320,11 +382,21 @@ class NeutronRestProxyV2Base(db_base_plugin_v2.NeutronDbPluginV2, class NeutronRestProxyV2(NeutronRestProxyV2Base, extradhcpopt_db.ExtraDhcpOptMixin, - agentschedulers_db.DhcpAgentSchedulerDbMixin): - - supported_extension_aliases = ["external-net", "router", "binding", - "router_rules", "extra_dhcp_opt", "quotas", - "dhcp_agent_scheduler", "agent"] + agentschedulers_db.DhcpAgentSchedulerDbMixin, + sg_rpc_base.SecurityGroupServerRpcMixin): + + _supported_extension_aliases = ["external-net", "router", "binding", + "router_rules", "extra_dhcp_opt", "quotas", + "dhcp_agent_scheduler", "agent", + "security-group"] + + @property + def supported_extension_aliases(self): + if not hasattr(self, '_aliases'): + aliases = self._supported_extension_aliases[:] + sg_rpc.disable_security_group_extension_if_noop_driver(aliases) + self._aliases = aliases + return self._aliases def __init__(self, server_timeout=None): super(NeutronRestProxyV2, self).__init__() @@ -340,26 +412,33 @@ class NeutronRestProxyV2(NeutronRestProxyV2Base, # init network ctrl connections self.servers = servermanager.ServerPool(server_timeout) - # init dhcp support - self.topic = topics.PLUGIN self.network_scheduler = importutils.import_object( cfg.CONF.network_scheduler_driver ) + + # setup rpc for security and DHCP agents + self._setup_rpc() + + if cfg.CONF.RESTPROXY.sync_data: + self._send_all_data() + + LOG.debug(_("NeutronRestProxyV2: initialization done")) + + def _setup_rpc(self): + self.conn = rpc.create_connection(new=True) + self.topic = topics.PLUGIN + self.notifier = AgentNotifierApi(topics.AGENT) + # init dhcp agent support self._dhcp_agent_notifier = dhcp_rpc_agent_api.DhcpAgentNotifyAPI() self.agent_notifiers[const.AGENT_TYPE_DHCP] = ( self._dhcp_agent_notifier ) - self.conn = rpc.create_connection(new=True) - self.callbacks = RpcProxy() + self.callbacks = RestProxyCallbacks() self.dispatcher = self.callbacks.create_rpc_dispatcher() self.conn.create_consumer(self.topic, self.dispatcher, fanout=False) # Consume from all consumers in a thread self.conn.consume_in_thread() - if cfg.CONF.RESTPROXY.sync_data: - self._send_all_data() - - LOG.debug(_("NeutronRestProxyV2: initialization done")) def create_network(self, context, network): """Create a network. @@ -390,6 +469,10 @@ class NeutronRestProxyV2(NeutronRestProxyV2Base, self._warn_on_state_status(network['network']) with context.session.begin(subtransactions=True): + self._ensure_default_security_group( + context, + network['network']["tenant_id"] + ) # create network in DB new_net = super(NeutronRestProxyV2, self).create_network(context, network) @@ -499,6 +582,8 @@ class NeutronRestProxyV2(NeutronRestProxyV2Base, # Update DB in new session so exceptions rollback changes with context.session.begin(subtransactions=True): + self._ensure_default_security_group_on_port(context, port) + sgids = self._get_security_groups_on_port(context, port) dhcp_opts = port['port'].get(edo_ext.EXTRADHCPOPTS, []) new_port = super(NeutronRestProxyV2, self).create_port(context, port) @@ -521,6 +606,8 @@ class NeutronRestProxyV2(NeutronRestProxyV2Base, self.servers.rest_create_port(net_tenant_id, new_port["network_id"], mapped_port) + self._process_port_create_security_group(context, new_port, sgids) + self.notify_security_groups_member_updated(context, new_port) return new_port def get_port(self, context, id, fields=None): @@ -600,13 +687,16 @@ class NeutronRestProxyV2(NeutronRestProxyV2Base, self.servers.rest_update_port(net_tenant_id, new_port["network_id"], mapped_port) + agent_update_required = self.update_security_group_on_port( + context, port_id, port, orig_port, new_port) + agent_update_required |= self.is_security_group_member_updated( + context, orig_port, new_port) # return new_port return new_port def delete_port(self, context, port_id, l3_port_check=True): """Delete a port. - :param context: neutron api request context :param id: UUID representing the port to delete. @@ -623,6 +713,7 @@ class NeutronRestProxyV2(NeutronRestProxyV2Base, self.prevent_l3_port_deletion(context, port_id) with context.session.begin(subtransactions=True): self.disassociate_floatingips(context, port_id) + self._delete_port_security_group_bindings(context, port_id) super(NeutronRestProxyV2, self).delete_port(context, port_id) def _delete_port(self, context, port_id): diff --git a/neutron/tests/unit/bigswitch/test_base.py b/neutron/tests/unit/bigswitch/test_base.py index 1dd40ec0b..77cea72d0 100644 --- a/neutron/tests/unit/bigswitch/test_base.py +++ b/neutron/tests/unit/bigswitch/test_base.py @@ -26,7 +26,9 @@ from neutron.plugins.bigswitch import config from neutron.tests.unit.bigswitch import fake_server RESTPROXY_PKG_PATH = 'neutron.plugins.bigswitch.plugin' -NOTIFIER = 'neutron.plugins.bigswitch.plugin.RpcProxy' +NOTIFIER = 'neutron.plugins.bigswitch.plugin.AgentNotifierApi' +CALLBACKS = 'neutron.plugins.bigswitch.plugin.RestProxyCallbacks' +CERTFETCH = 'neutron.plugins.bigswitch.servermanager.ServerPool._fetch_cert' HTTPCON = 'httplib.HTTPConnection' @@ -45,7 +47,9 @@ class BigSwitchTestBase(object): self.httpPatch = mock.patch(HTTPCON, create=True, new=fake_server.HTTPConnectionMock) self.plugin_notifier_p = mock.patch(NOTIFIER) + self.callbacks_p = mock.patch(CALLBACKS) self.addCleanup(mock.patch.stopall) self.addCleanup(db.clear_db) + self.callbacks_p.start() self.plugin_notifier_p.start() self.httpPatch.start() diff --git a/neutron/tests/unit/bigswitch/test_restproxy_agent.py b/neutron/tests/unit/bigswitch/test_restproxy_agent.py new file mode 100644 index 000000000..385683ffe --- /dev/null +++ b/neutron/tests/unit/bigswitch/test_restproxy_agent.py @@ -0,0 +1,188 @@ +# Copyright 2014 Big Switch Networks, 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. +# +# @author: Kevin Benton, Big Switch Networks + +from contextlib import nested + +import mock + +from neutron.openstack.common import importutils +from neutron.tests import base + +OVSBRIDGE = 'neutron.agent.linux.ovs_lib.OVSBridge' +PLUGINAPI = 'neutron.plugins.bigswitch.agent.restproxy_agent.PluginApi' +CONTEXT = 'neutron.context' +CONSUMERCREATE = 'neutron.agent.rpc.create_consumers' +SGRPC = 'neutron.agent.securitygroups_rpc' +SGAGENT = 'neutron.plugins.bigswitch.agent.restproxy_agent.SecurityGroupAgent' +AGENTMOD = 'neutron.plugins.bigswitch.agent.restproxy_agent' +NEUTRONCFG = 'neutron.common.config' +PLCONFIG = 'neutron.plugins.bigswitch.config' + + +class BaseAgentTestCase(base.BaseTestCase): + + def setUp(self): + super(BaseAgentTestCase, self).setUp() + self.addCleanup(mock.patch.stopall) + self.mod_agent = importutils.import_module(AGENTMOD) + + +class TestRestProxyAgentOVS(BaseAgentTestCase): + def setUp(self): + super(TestRestProxyAgentOVS, self).setUp() + self.plapi = mock.patch(PLUGINAPI).start() + self.ovsbridge = mock.patch(OVSBRIDGE).start() + self.context = mock.patch(CONTEXT).start() + self.rpc = mock.patch(CONSUMERCREATE).start() + self.sg_rpc = mock.patch(SGRPC).start() + self.sg_agent = mock.patch(SGAGENT).start() + + def mock_agent(self): + mock_context = mock.Mock(return_value='abc') + self.context.get_admin_context_without_session = mock_context + return self.mod_agent.RestProxyAgent('int-br', 2, 'helper') + + def mock_port_update(self, **kwargs): + agent = self.mock_agent() + agent.port_update(mock.Mock(), **kwargs) + + def test_port_update(self): + port = {'id': 1, 'security_groups': 'default'} + + with mock.patch.object(self.ovsbridge.return_value, + 'get_vif_port_by_id', + return_value=1) as get_vif: + self.mock_port_update(port=port) + + get_vif.assert_called_once_with(1) + self.sg_agent.assert_has_calls([ + mock.call().refresh_firewall() + ]) + + def test_port_update_not_vifport(self): + port = {'id': 1, 'security_groups': 'default'} + + with mock.patch.object(self.ovsbridge.return_value, + 'get_vif_port_by_id', + return_value=0) as get_vif: + self.mock_port_update(port=port) + + get_vif.assert_called_once_with(1) + self.assertFalse(self.sg_agent.return_value.refresh_firewall.called) + + def test_port_update_without_secgroup(self): + port = {'id': 1} + + with mock.patch.object(self.ovsbridge.return_value, + 'get_vif_port_by_id', + return_value=1) as get_vif: + self.mock_port_update(port=port) + + get_vif.assert_called_once_with(1) + self.assertFalse(self.sg_agent.return_value.refresh_firewall.called) + + def mock_update_ports(self, vif_port_set=None, registered_ports=None): + with mock.patch.object(self.ovsbridge.return_value, + 'get_vif_port_set', + return_value=vif_port_set): + agent = self.mock_agent() + return agent._update_ports(registered_ports) + + def test_update_ports_unchanged(self): + self.assertIsNone(self.mock_update_ports()) + + def test_update_ports_changed(self): + vif_port_set = set([1, 3]) + registered_ports = set([1, 2]) + expected = dict(current=vif_port_set, + added=set([3]), + removed=set([2])) + + actual = self.mock_update_ports(vif_port_set, registered_ports) + + self.assertEqual(expected, actual) + + def mock_process_devices_filter(self, port_info): + agent = self.mock_agent() + agent._process_devices_filter(port_info) + + def test_process_devices_filter_add(self): + port_info = {'added': 1} + + self.mock_process_devices_filter(port_info) + + self.sg_agent.assert_has_calls([ + mock.call().prepare_devices_filter(1) + ]) + + def test_process_devices_filter_remove(self): + port_info = {'removed': 2} + + self.mock_process_devices_filter(port_info) + + self.sg_agent.assert_has_calls([ + mock.call().remove_devices_filter(2) + ]) + + def test_process_devices_filter_both(self): + port_info = {'added': 1, 'removed': 2} + + self.mock_process_devices_filter(port_info) + + self.sg_agent.assert_has_calls([ + mock.call().prepare_devices_filter(1), + mock.call().remove_devices_filter(2) + ]) + + def test_process_devices_filter_none(self): + port_info = {} + + self.mock_process_devices_filter(port_info) + + self.assertFalse( + self.sg_agent.return_value.prepare_devices_filter.called) + self.assertFalse( + self.sg_agent.return_value.remove_devices_filter.called) + + +class TestRestProxyAgent(BaseAgentTestCase): + def mock_main(self): + cfg_attrs = {'CONF.RESTPROXYAGENT.integration_bridge': 'integ_br', + 'CONF.RESTPROXYAGENT.polling_interval': 5, + 'CONF.RESTPROXYAGENT.virtual_switch_type': 'ovs', + 'CONF.AGENT.root_helper': 'helper'} + with nested( + mock.patch(AGENTMOD + '.cfg', **cfg_attrs), + mock.patch(NEUTRONCFG), + mock.patch(PLCONFIG), + ) as (mock_conf, mock_log_conf, mock_pluginconf): + self.mod_agent.main() + + mock_log_conf.assert_has_calls([ + mock.call(mock_conf), + ]) + + def test_main(self): + agent_attrs = {'daemon_loop.side_effect': SystemExit(0)} + with mock.patch(AGENTMOD + '.RestProxyAgent', + **agent_attrs) as mock_agent: + self.assertRaises(SystemExit, self.mock_main) + + mock_agent.assert_has_calls([ + mock.call('integ_br', 5, 'helper', 'ovs'), + mock.call().daemon_loop() + ]) diff --git a/neutron/tests/unit/bigswitch/test_security_groups.py b/neutron/tests/unit/bigswitch/test_security_groups.py new file mode 100644 index 000000000..c82032950 --- /dev/null +++ b/neutron/tests/unit/bigswitch/test_security_groups.py @@ -0,0 +1,46 @@ +# Copyright 2014, Big Switch Networks +# 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 import manager +from neutron.tests.unit.bigswitch import test_base +from neutron.tests.unit import test_extension_security_group as test_sg +from neutron.tests.unit import test_security_groups_rpc as test_sg_rpc + + +class RestProxySecurityGroupsTestCase(test_sg.SecurityGroupDBTestCase, + test_base.BigSwitchTestBase): + plugin_str = ('%s.NeutronRestProxyV2' % + test_base.RESTPROXY_PKG_PATH) + + def setUp(self, plugin=None): + test_sg_rpc.set_firewall_driver(test_sg_rpc.FIREWALL_HYBRID_DRIVER) + self.setup_config_files() + self.setup_patches() + self._attribute_map_bk_ = {} + super(RestProxySecurityGroupsTestCase, self).setUp(self.plugin_str) + plugin = manager.NeutronManager.get_plugin() + self.notifier = plugin.notifier + self.rpc = plugin.callbacks + + +class TestSecServerRpcCallBack(test_sg_rpc.SGServerRpcCallBackMixinTestCase, + RestProxySecurityGroupsTestCase): + pass + + +class TestSecurityGroupsMixin(test_sg.TestSecurityGroups, + test_sg_rpc.SGNotificationTestMixin, + RestProxySecurityGroupsTestCase): + pass diff --git a/setup.cfg b/setup.cfg index 885ebde4c..56b98aeaf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -98,6 +98,7 @@ console_scripts = neutron-nsx-manage = neutron.plugins.nicira.shell:main neutron-openvswitch-agent = neutron.plugins.openvswitch.agent.ovs_neutron_agent:main neutron-ovs-cleanup = neutron.agent.ovs_cleanup_util:main + neutron-restproxy-agent = neutron.plugins.bigswitch.agent.restproxy_agent:main neutron-ryu-agent = neutron.plugins.ryu.agent.ryu_neutron_agent:main neutron-server = neutron.server:main neutron-rootwrap = oslo.rootwrap.cmd:main