]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
BigSwitch: Add agent to support neutron sec groups
authorKevin Benton <blak111@gmail.com>
Tue, 11 Feb 2014 03:36:22 +0000 (19:36 -0800)
committerKevin Benton <blak111@gmail.com>
Sat, 1 Mar 2014 18:32:12 +0000 (10:32 -0800)
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

etc/neutron/plugins/bigswitch/restproxy.ini
neutron/db/migration/alembic_migrations/versions/f44ab9871cd6_bsn_security_groups.py [new file with mode: 0644]
neutron/plugins/bigswitch/agent/__init__.py [new file with mode: 0644]
neutron/plugins/bigswitch/agent/restproxy_agent.py [new file with mode: 0644]
neutron/plugins/bigswitch/config.py
neutron/plugins/bigswitch/plugin.py
neutron/tests/unit/bigswitch/test_base.py
neutron/tests/unit/bigswitch/test_restproxy_agent.py [new file with mode: 0644]
neutron/tests/unit/bigswitch/test_security_groups.py [new file with mode: 0644]
setup.cfg

index a89f84fa8760fb6fbdf8edb1506a9c1fde0fe19f..a982010f637ff521c6548905f535c605f1e2786c 100644 (file)
@@ -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 (file)
index 0000000..b75bf42
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/neutron/plugins/bigswitch/agent/restproxy_agent.py b/neutron/plugins/bigswitch/agent/restproxy_agent.py
new file mode 100644 (file)
index 0000000..81d3032
--- /dev/null
@@ -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()
index 7c46e105dad91dbb876f8244d9f5e2519268e6c3..5094617b7914d82690459f85d74de3fa7d2f0a89 100644 (file)
@@ -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)
index 0a723ae1db579194e8359191448cc477fe769a58..12a80955d4c2ace9a838f58837506d52c32e4394 100644 (file)
@@ -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):
index 1dd40ec0b6873290878fb963624fa80cc42394e6..77cea72d038a56988b61ec7a2eec0b039eac472c 100644 (file)
@@ -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 (file)
index 0000000..385683f
--- /dev/null
@@ -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 (file)
index 0000000..c820329
--- /dev/null
@@ -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
index 27f70b6e0e63de51526291bf67a88b3d95e8cba0..22e3e639d9075648e459c66719dbcfae9e39aadb 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -96,6 +96,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