]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Reference driver implementation (IPsec) for VPNaaS
authorNachi Ueno <nachi@ntti3.com>
Wed, 5 Jun 2013 23:24:38 +0000 (16:24 -0700)
committerNachi Ueno <nachi@ntti3.com>
Wed, 4 Sep 2013 07:32:39 +0000 (00:32 -0700)
Implements blueprint ipsec-vpn-reference

This patch implements reference driver implementation for VPNaaS.
The driver uses openswan to manage vpn connections.

Future work: Support ikepolicy and ipsec update
Support service type framework
Intelligent updating of resources

This commit adds jinja2 for requirements.txt for
generating cofig file.

Change-Id: I8c5ed800a71ca014dc7bdbb6a57c4f8d18fa82e0

28 files changed:
etc/neutron/rootwrap.d/vpnaas.filters [new file with mode: 0644]
etc/vpn_agent.ini [new file with mode: 0644]
neutron/agent/l3_agent.py
neutron/db/vpn/vpn_db.py
neutron/extensions/vpnaas.py
neutron/plugins/common/constants.py
neutron/plugins/common/utils.py
neutron/services/vpn/agent.py [new file with mode: 0644]
neutron/services/vpn/common/__init__.py [new file with mode: 0644]
neutron/services/vpn/common/topics.py [new file with mode: 0644]
neutron/services/vpn/device_drivers/__init__.py [new file with mode: 0644]
neutron/services/vpn/device_drivers/ipsec.py [new file with mode: 0644]
neutron/services/vpn/device_drivers/template/openswan/ipsec.conf.template [new file with mode: 0644]
neutron/services/vpn/device_drivers/template/openswan/ipsec.secret.template [new file with mode: 0644]
neutron/services/vpn/plugin.py
neutron/services/vpn/service_drivers/__init__.py [new file with mode: 0644]
neutron/services/vpn/service_drivers/ipsec.py [new file with mode: 0644]
neutron/tests/unit/metaplugin/test_basic.py
neutron/tests/unit/nicira/test_nicira_plugin.py
neutron/tests/unit/services/vpn/device_drivers/__init__.py [new file with mode: 0644]
neutron/tests/unit/services/vpn/device_drivers/test_ipsec.py [new file with mode: 0644]
neutron/tests/unit/services/vpn/service_drivers/__init__.py [new file with mode: 0644]
neutron/tests/unit/services/vpn/service_drivers/test_ipsec.py [new file with mode: 0644]
neutron/tests/unit/services/vpn/test_vpn_agent.py [new file with mode: 0644]
neutron/tests/unit/services/vpn/test_vpnaas_driver_plugin.py [new file with mode: 0644]
neutron/tests/unit/test_l3_plugin.py
requirements.txt
setup.cfg

diff --git a/etc/neutron/rootwrap.d/vpnaas.filters b/etc/neutron/rootwrap.d/vpnaas.filters
new file mode 100644 (file)
index 0000000..7848136
--- /dev/null
@@ -0,0 +1,13 @@
+# neutron-rootwrap command filters for nodes on which neutron is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+ip: IpFilter, ip, root
+ip_exec: IpNetnsExecFilter, ip, root
+openswan: CommandFilter, ipsec, root
diff --git a/etc/vpn_agent.ini b/etc/vpn_agent.ini
new file mode 100644 (file)
index 0000000..3f8f61b
--- /dev/null
@@ -0,0 +1,13 @@
+[DEFAULT]
+# VPN-Agent configuration file
+# Note vpn-agent inherits l3-agent, so you can use configs on l3-agent also
+
+[vpnagent]
+#vpn device drivers which vpn agent will use
+#If we want to use multiple drivers,  we need to define this option multiple times.
+#vpn_device_driver=neutron.services.vpn.device_drivers.ipsec.OpenSwanDriver
+#vpn_device_driver=another_driver
+
+[ipsec]
+#Status check interval
+#ipsec_status_check_interval=60
index c53d930e0141347369071c833eb11e4fc5c8b817..1e9086dc62e9e386f1fdf49f231e59c3229c8959 100644 (file)
@@ -824,7 +824,7 @@ class L3NATAgentWithStateReport(L3NATAgent):
         LOG.info(_("agent_updated by server side %s!"), payload)
 
 
-def main():
+def main(manager='neutron.agent.l3_agent.L3NATAgentWithStateReport'):
     eventlet.monkey_patch()
     conf = cfg.CONF
     conf.register_opts(L3NATAgent.OPTS)
@@ -839,5 +839,5 @@ def main():
         binary='neutron-l3-agent',
         topic=topics.L3_AGENT,
         report_interval=cfg.CONF.AGENT.report_interval,
-        manager='neutron.agent.l3_agent.L3NATAgentWithStateReport')
+        manager=manager)
     service.launch(server).wait()
index 727ee85ccabb25761a8fa97c9dd8eeabd8463183..8061b9b22031ee41af3f6a34a9a8a2ae148b73f6 100644 (file)
@@ -21,7 +21,7 @@ import sqlalchemy as sa
 from sqlalchemy import orm
 from sqlalchemy.orm import exc
 
-from neutron.common import constants as q_constants
+from neutron.common import constants as n_constants
 from neutron.db import agentschedulers_db as agent_db
 from neutron.db import api as qdbapi
 from neutron.db import db_base_plugin_v2 as base_db
@@ -34,6 +34,7 @@ from neutron import manager
 from neutron.openstack.common import log as logging
 from neutron.openstack.common import uuidutils
 from neutron.plugins.common import constants
+from neutron.plugins.common import utils
 
 LOG = logging.getLogger(__name__)
 
@@ -190,7 +191,7 @@ class VPNPluginDb(VPNPluginBase, base_db.CommonDbMixin):
 
     def assert_update_allowed(self, obj):
         status = getattr(obj, 'status', None)
-        if status != constants.ACTIVE:
+        if utils.in_pending_status(status):
             raise vpnaas.VPNStateInvalid(id=id, state=status)
 
     def _make_ipsec_site_connection_dict(self, ipsec_site_conn, fields=None):
@@ -330,11 +331,15 @@ class VPNPluginDb(VPNPluginBase, base_db.CommonDbMixin):
             )
             context.session.delete(ipsec_site_conn_db)
 
+    def _get_ipsec_site_connection(
+            self, context, ipsec_site_conn_id):
+        return self._get_resource(
+            context, IPsecSiteConnection, ipsec_site_conn_id)
+
     def get_ipsec_site_connection(self, context,
                                   ipsec_site_conn_id, fields=None):
-        ipsec_site_conn_db = self._get_resource(
-            context, IPsecSiteConnection, ipsec_site_conn_id
-        )
+        ipsec_site_conn_db = self._get_ipsec_site_connection(
+            context, ipsec_site_conn_id)
         return self._make_ipsec_site_connection_dict(
             ipsec_site_conn_db, fields)
 
@@ -533,10 +538,6 @@ class VPNPluginDb(VPNPluginBase, base_db.CommonDbMixin):
     def update_vpnservice(self, context, vpnservice_id, vpnservice):
         vpns = vpnservice['vpnservice']
         with context.session.begin(subtransactions=True):
-            vpnservice = context.session.query(IPsecSiteConnection).filter_by(
-                vpnservice_id=vpnservice_id).first()
-            if vpnservice:
-                raise vpnaas.VPNServiceInUse(vpnservice_id=vpnservice_id)
             vpns_db = self._get_resource(context, VPNService, vpnservice_id)
             self.assert_update_allowed(vpns_db)
             if vpns:
@@ -570,7 +571,7 @@ class VPNPluginRpcDbMixin():
 
         plugin = manager.NeutronManager.get_plugin()
         agent = plugin._get_agent_by_type_and_host(
-            context, q_constants.AGENT_TYPE_L3, host)
+            context, n_constants.AGENT_TYPE_L3, host)
         if not agent.admin_state_up:
             return []
         query = context.session.query(VPNService)
@@ -585,14 +586,44 @@ class VPNPluginRpcDbMixin():
             agent_db.RouterL3AgentBinding.l3_agent_id == agent.id)
         return query
 
-    def update_status_on_host(self, context, host, active_services):
+    def update_status_by_agent(self, context, service_status_info_list):
+        """Updating vpnservice and vpnconnection status.
+
+        :param context: context variable
+        :param service_status_info_list: list of status
+        The structure is
+        [{id: vpnservice_id,
+          status: ACTIVE|DOWN|ERROR,
+          updated_pending_status: True|False
+          ipsec_site_connections: {
+              ipsec_site_connection_id: {
+                  status: ACTIVE|DOWN|ERROR,
+                  updated_pending_status: True|False
+              }
+          }]
+        The agent will set updated_pending_status as True,
+        when agent update any pending status.
+        """
         with context.session.begin(subtransactions=True):
-            vpnservices = self._get_agent_hosting_vpn_services(
-                context, host)
-            for vpnservice in vpnservices:
-                if vpnservice.id in active_services:
-                    if vpnservice.status != constants.ACTIVE:
-                        vpnservice.status = constants.ACTIVE
-                else:
-                    if vpnservice.status != constants.ERROR:
-                        vpnservice.status = constants.ERROR
+            for vpnservice in service_status_info_list:
+                try:
+                    vpnservice_db = self._get_vpnservice(
+                        context, vpnservice['id'])
+                except vpnaas.VPNServiceNotFound:
+                    LOG.warn(_('vpnservice %s in db is already deleted'),
+                             vpnservice['id'])
+                    continue
+
+                if (not utils.in_pending_status(vpnservice_db.status)
+                    or vpnservice['updated_pending_status']):
+                    vpnservice_db.status = vpnservice['status']
+                for conn_id, conn in vpnservice[
+                    'ipsec_site_connections'].items():
+                    try:
+                        conn_db = self._get_ipsec_site_connection(
+                            context, conn_id)
+                    except vpnaas.IPsecSiteConnectionNotFound:
+                        continue
+                    if (not utils.in_pending_status(conn_db.status)
+                        or conn['updated_pending_status']):
+                        conn_db.status = conn['status']
index b02890d24e4b3cd41e5d1f4039312d826da80994..a1bbd9d51958cb4cb62a55bea3d859d7f52f6347 100644 (file)
@@ -68,6 +68,10 @@ class IPsecPolicyInUse(qexception.InUse):
     message = _("IPsecPolicy %(ipsecpolicy_id)s is still in use")
 
 
+class DeviceDriverImportError(qexception.NeutronException):
+    message = _("Can not load driver :%(device_driver)s")
+
+
 vpn_supported_initiators = ['bi-directional', 'response-only']
 vpn_supported_encryption_algorithms = ['3des', 'aes-128',
                                        'aes-192', 'aes-256']
@@ -76,7 +80,8 @@ vpn_dpd_supported_actions = [
 ]
 vpn_supported_transform_protocols = ['esp', 'ah', 'ah-esp']
 vpn_supported_encapsulation_mode = ['tunnel', 'transport']
-vpn_supported_lifetime_units = ['seconds', 'kilobytes']
+#TODO(nati) add kilobytes when we support it
+vpn_supported_lifetime_units = ['seconds']
 vpn_supported_pfs = ['group2', 'group5', 'group14']
 vpn_supported_ike_versions = ['v1', 'v2']
 vpn_supported_auth_mode = ['psk']
index 7a7d36bee2ea43f3ef51313a421328c5410312bc..688de5ddd6444fbd787dacc596b5bb5b6d0fdc5c 100644 (file)
@@ -46,6 +46,7 @@ COMMON_PREFIXES = {
 
 # Service operation status constants
 ACTIVE = "ACTIVE"
+DOWN = "DOWN"
 PENDING_CREATE = "PENDING_CREATE"
 PENDING_UPDATE = "PENDING_UPDATE"
 PENDING_DELETE = "PENDING_DELETE"
index 0daffad22af70a276c1f773b249f1bbe60714fda..55b767f96e4007035818df9d0234e5cba23ab1b0 100644 (file)
@@ -20,6 +20,7 @@ Common utilities and helper functions for Openstack Networking Plugins.
 
 from neutron.common import exceptions as q_exc
 from neutron.common import utils
+from neutron.plugins.common import constants
 
 
 def verify_vlan_range(vlan_range):
@@ -60,3 +61,9 @@ def parse_network_vlan_ranges(network_vlan_ranges_cfg_entries):
         else:
             networks.setdefault(network, [])
     return networks
+
+
+def in_pending_status(status):
+    return status in (constants.PENDING_CREATE,
+                      constants.PENDING_UPDATE,
+                      constants.PENDING_DELETE)
diff --git a/neutron/services/vpn/agent.py b/neutron/services/vpn/agent.py
new file mode 100644 (file)
index 0000000..ffb4d74
--- /dev/null
@@ -0,0 +1,148 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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 import l3_agent
+from neutron.extensions import vpnaas
+from neutron.openstack.common import importutils
+
+vpn_agent_opts = [
+    cfg.MultiStrOpt(
+        'vpn_device_driver',
+        default=['neutron.services.vpn.device_drivers.'
+                 'ipsec.OpenSwanDriver'],
+        help=_("The vpn device drivers Neutron will use")),
+]
+cfg.CONF.register_opts(vpn_agent_opts, 'vpnagent')
+
+
+class VPNAgent(l3_agent.L3NATAgentWithStateReport):
+    """VPNAgent class which can handle vpn service drivers."""
+    def __init__(self, host, conf=None):
+        super(VPNAgent, self).__init__(host=host, conf=conf)
+        self.setup_device_drivers(host)
+
+    def setup_device_drivers(self, host):
+        """Setting up device drivers.
+
+        :param host: hostname. This is needed for rpc
+        Each devices will stays as processes.
+        They will communiate with
+        server side service plugin using rpc with
+        device specific rpc topic.
+        :returns: None
+        """
+        device_drivers = cfg.CONF.vpnagent.vpn_device_driver
+        self.devices = []
+        for device_driver in device_drivers:
+            try:
+                self.devices.append(
+                    importutils.import_object(device_driver, self, host))
+            except ImportError:
+                raise vpnaas.DeviceDriverImportError(
+                    device_driver=device_driver)
+
+    def get_namespace(self, router_id):
+        """Get namespace of router.
+
+        :router_id: router_id
+        :returns: namespace string.
+            Note if the router is not exist, this function
+            returns None
+        """
+        router_info = self.router_info.get(router_id)
+        if not router_info:
+            return
+        return router_info.ns_name()
+
+    def add_nat_rule(self, router_id, chain, rule, top=False):
+        """Add nat rule in namespace.
+
+        :param router_id: router_id
+        :param chain: a string of chain name
+        :param rule: a string of rule
+        :param top: if top is true, the rule
+            will be placed on the top of chain
+            Note if there is no rotuer, this method do nothing
+        """
+        router_info = self.router_info.get(router_id)
+        if not router_info:
+            return
+        router_info.iptables_manager.ipv4['nat'].add_rule(
+            chain, rule, top=top)
+
+    def remove_nat_rule(self, router_id, chain, rule, top=False):
+        """Remove nat rule in namespace.
+
+        :param router_id: router_id
+        :param chain: a string of chain name
+        :param rule: a string of rule
+        :param top: unused
+            needed to have same argument with add_nat_rule
+        """
+        router_info = self.router_info.get(router_id)
+        if not router_info:
+            return
+        router_info.iptables_manager.ipv4['nat'].remove_rule(
+            chain, rule)
+
+    def iptables_apply(self, router_id):
+        """Apply IPtables.
+
+        :param router_id: router_id
+        This method do nothing if there is no router
+        """
+        router_info = self.router_info.get(router_id)
+        if not router_info:
+            return
+        router_info.iptables_manager.apply()
+
+    def _router_added(self, router_id, router):
+        """Router added event.
+
+        This method overwrites parent class method.
+        :param router_id: id of added router
+        :param router: dict of rotuer
+        """
+        super(VPNAgent, self)._router_added(router_id, router)
+        for device in self.devices:
+            device.create_router(router_id)
+
+    def _router_removed(self, router_id):
+        """Router removed event.
+
+        This method overwrites parent class method.
+        :param router_id: id of removed router
+        """
+        super(VPNAgent, self)._router_removed(router_id)
+        for device in self.devices:
+            device.destroy_router(router_id)
+
+    def _process_routers(self, routers, all_routers=False):
+        """Router sync event.
+
+        This method overwrites parent class method.
+        :param routers: list of routers
+        """
+        super(VPNAgent, self)._process_routers(routers, all_routers)
+        for device in self.devices:
+            device.sync(self.context, routers)
+
+
+def main():
+    l3_agent.main(
+        manager='neutron.services.vpn.agent.VPNAgent')
diff --git a/neutron/services/vpn/common/__init__.py b/neutron/services/vpn/common/__init__.py
new file mode 100644 (file)
index 0000000..9b27a75
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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.
diff --git a/neutron/services/vpn/common/topics.py b/neutron/services/vpn/common/topics.py
new file mode 100644 (file)
index 0000000..87df69c
--- /dev/null
@@ -0,0 +1,20 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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.
+
+
+IPSEC_DRIVER_TOPIC = 'ipsec_driver'
+IPSEC_AGENT_TOPIC = 'ipsec_agent'
diff --git a/neutron/services/vpn/device_drivers/__init__.py b/neutron/services/vpn/device_drivers/__init__.py
new file mode 100644 (file)
index 0000000..c15fcbf
--- /dev/null
@@ -0,0 +1,36 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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 abc
+
+
+class DeviceDriver(object):
+    __metaclass__ = abc.ABCMeta
+
+    def __init__(self, agent, host):
+        pass
+
+    @abc.abstractmethod
+    def sync(self, context, processes):
+        pass
+
+    @abc.abstractmethod
+    def create_router(self, process_id):
+        pass
+
+    @abc.abstractmethod
+    def destroy_router(self, process_id):
+        pass
diff --git a/neutron/services/vpn/device_drivers/ipsec.py b/neutron/services/vpn/device_drivers/ipsec.py
new file mode 100644 (file)
index 0000000..1ede754
--- /dev/null
@@ -0,0 +1,687 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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 abc
+import copy
+import os
+import re
+import shutil
+
+import jinja2
+import netaddr
+from oslo.config import cfg
+
+from neutron.agent.linux import ip_lib
+from neutron.agent.linux import utils
+from neutron.common import rpc as q_rpc
+from neutron import context
+from neutron.openstack.common import lockutils
+from neutron.openstack.common import log as logging
+from neutron.openstack.common import loopingcall
+from neutron.openstack.common import rpc
+from neutron.openstack.common.rpc import proxy
+from neutron.plugins.common import constants
+from neutron.plugins.common import utils as plugin_utils
+from neutron.services.vpn.common import topics
+from neutron.services.vpn import device_drivers
+
+LOG = logging.getLogger(__name__)
+TEMPLATE_PATH = os.path.dirname(__file__)
+
+ipsec_opts = [
+    cfg.StrOpt(
+        'config_base_dir',
+        default='$state_path/ipsec',
+        help=_('Location to store ipsec server config files')),
+    cfg.IntOpt('ipsec_status_check_interval',
+               default=60,
+               help=_("Interval for checking ipsec status"))
+]
+cfg.CONF.register_opts(ipsec_opts, 'ipsec')
+
+openswan_opts = [
+    cfg.StrOpt(
+        'ipsec_config_template',
+        default=os.path.join(
+            TEMPLATE_PATH,
+            'template/openswan/ipsec.conf.template'),
+        help='Template file for ipsec configuration'),
+    cfg.StrOpt(
+        'ipsec_secret_template',
+        default=os.path.join(
+            TEMPLATE_PATH,
+            'template/openswan/ipsec.secret.template'),
+        help='Template file for ipsec secret configuration')
+]
+
+cfg.CONF.register_opts(openswan_opts, 'openswan')
+
+JINJA_ENV = None
+
+STATUS_MAP = {
+    'erouted': constants.ACTIVE,
+    'unrouted': constants.DOWN
+}
+
+
+def _get_template(template_file):
+    global JINJA_ENV
+    if not JINJA_ENV:
+        templateLoader = jinja2.FileSystemLoader(searchpath="/")
+        JINJA_ENV = jinja2.Environment(loader=templateLoader)
+    return JINJA_ENV.get_template(template_file)
+
+
+class BaseSwanProcess():
+    """Swan Family Process Manager
+
+    This class manages start/restart/stop ipsec process.
+    This class create/delete config template
+    """
+    __metaclass__ = abc.ABCMeta
+
+    binary = "ipsec"
+    CONFIG_DIRS = [
+        'var/run',
+        'log',
+        'etc',
+        'etc/ipsec.d/aacerts',
+        'etc/ipsec.d/acerts',
+        'etc/ipsec.d/cacerts',
+        'etc/ipsec.d/certs',
+        'etc/ipsec.d/crls',
+        'etc/ipsec.d/ocspcerts',
+        'etc/ipsec.d/policies',
+        'etc/ipsec.d/private',
+        'etc/ipsec.d/reqs',
+        'etc/pki/nssdb/'
+    ]
+
+    DIALECT_MAP = {
+        "3des": "3des",
+        "aes-128": "aes128",
+        "aes-256": "aes256",
+        "aes-192": "aes192",
+        "group2": "modp1024",
+        "group5": "modp1536",
+        "group14": "modp2048",
+        "group15": "modp3072",
+        "bi-directional": "start",
+        "response-only": "add",
+        "v2": "insist",
+        "v1": "never"
+    }
+
+    def __init__(self, conf, root_helper, process_id,
+                 vpnservice, namespace):
+        self.conf = conf
+        self.id = process_id
+        self.root_helper = root_helper
+        self.vpnservice = vpnservice
+        self.updated_pending_status = False
+        self.namespace = namespace
+        self.connection_status = {}
+        self.config_dir = os.path.join(
+            cfg.CONF.ipsec.config_base_dir, self.id)
+        self.etc_dir = os.path.join(self.config_dir, 'etc')
+        self.translate_dialect()
+
+    def translate_dialect(self):
+        if not self.vpnservice:
+            return
+        for ipsec_site_conn in self.vpnservice['ipsec_site_connections']:
+            self._dialect(ipsec_site_conn, 'initiator')
+            self._dialect(ipsec_site_conn['ikepolicy'], 'ike_version')
+            for key in ['encryption_algorithm',
+                        'auth_algorithm',
+                        'pfs']:
+                self._dialect(ipsec_site_conn['ikepolicy'], key)
+                self._dialect(ipsec_site_conn['ipsecpolicy'], key)
+
+    def _dialect(self, obj, key):
+        obj[key] = self.DIALECT_MAP.get(obj[key], obj[key])
+
+    @abc.abstractmethod
+    def ensure_configs(self):
+        pass
+
+    def ensure_config_file(self, kind, template, vpnservice):
+        """Update config file,  based on current settings for service."""
+        config_str = self._gen_config_content(template, vpnservice)
+        config_file_name = self._get_config_filename(kind)
+        utils.replace_file(config_file_name, config_str)
+
+    def remove_config(self):
+        """Remove whole config file."""
+        shutil.rmtree(self.config_dir, ignore_errors=True)
+
+    def _get_config_filename(self, kind):
+        config_dir = self.etc_dir
+        return os.path.join(config_dir, kind)
+
+    def _ensure_dir(self, dir_path):
+        if not os.path.isdir(dir_path):
+            os.makedirs(dir_path, 0o755)
+
+    def ensure_config_dir(self, vpnservice):
+        """Create config directory if it does not exist."""
+        self._ensure_dir(self.config_dir)
+        for subdir in self.CONFIG_DIRS:
+            dir_path = os.path.join(self.config_dir, subdir)
+            self._ensure_dir(dir_path)
+
+    def _gen_config_content(self, template_file, vpnservice):
+        template = _get_template(template_file)
+        return template.render(
+            {'vpnservice': vpnservice,
+             'state_path': cfg.CONF.state_path})
+
+    @abc.abstractmethod
+    def get_status(self):
+        pass
+
+    @property
+    def status(self):
+        if self.active:
+            return constants.ACTIVE
+        return constants.DOWN
+
+    @property
+    def active(self):
+        """Check if the process is active or not."""
+        if not self.namespace:
+            return False
+        try:
+            status = self.get_status()
+            self._update_connection_status(status)
+        except RuntimeError:
+            return False
+        return True
+
+    def update(self):
+        """Update Status based on vpnservice configuration."""
+        if self.vpnservice and not self.vpnservice['admin_state_up']:
+            self.disable()
+        else:
+            self.enable()
+
+        if plugin_utils.in_pending_status(self.vpnservice['status']):
+            self.updated_pending_status = True
+
+        self.vpnservice['status'] = self.status
+        for ipsec_site_conn in self.vpnservice['ipsec_site_connections']:
+            if plugin_utils.in_pending_status(ipsec_site_conn['status']):
+                conn_id = ipsec_site_conn['id']
+                conn_status = self.connection_status.get(conn_id)
+                if not conn_status:
+                    continue
+                conn_status['updated_pending_status'] = True
+                ipsec_site_conn['status'] = conn_status['status']
+
+    def enable(self):
+        """Enabling the process."""
+        try:
+            self.ensure_configs()
+            if self.active:
+                self.restart()
+            else:
+                self.start()
+        except RuntimeError:
+            LOG.exception(
+                _("Failed to enable vpn process on router %s"),
+                self.id)
+
+    def disable(self):
+        """Disabling the process."""
+        try:
+            if self.active:
+                self.stop()
+            self.remove_config()
+        except RuntimeError:
+            LOG.exception(
+                _("Failed to disable vpn process on router %s"),
+                self.id)
+
+    @abc.abstractmethod
+    def restart(self):
+        """Restart process."""
+
+    @abc.abstractmethod
+    def start(self):
+        """Start process."""
+
+    @abc.abstractmethod
+    def stop(self):
+        """Stop process."""
+
+    def _update_connection_status(self, status_output):
+        for line in status_output.split('\n'):
+            m = re.search('\d\d\d "([a-f0-9\-]+).* (unrouted|erouted);', line)
+            if not m:
+                continue
+            connection_id = m.group(1)
+            status = m.group(2)
+            if not self.connection_status.get(connection_id):
+                self.connection_status[connection_id] = {
+                    'status': None,
+                    'updated_pending_status': False
+                }
+            self.connection_status[
+                connection_id]['status'] = STATUS_MAP[status]
+
+
+class OpenSwanProcess(BaseSwanProcess):
+    """OpenSwan Process manager class.
+
+    This process class uses three commands
+    (1) ipsec pluto:  IPsec IKE keying daemon
+    (2) ipsec addconn: Adds new ipsec addconn
+    (3) ipsec whack:  control interface for IPSEC keying daemon
+    """
+    def __init__(self, conf, root_helper, process_id,
+                 vpnservice, namespace):
+        super(OpenSwanProcess, self).__init__(
+            conf, root_helper, process_id,
+            vpnservice, namespace)
+        self.secrets_file = os.path.join(
+            self.etc_dir, 'ipsec.secrets')
+        self.config_file = os.path.join(
+            self.etc_dir, 'ipsec.conf')
+        self.pid_path = os.path.join(
+            self.config_dir, 'var', 'run', 'pluto')
+
+    def _execute(self, cmd, check_exit_code=True):
+        """Execute command on namespace."""
+        ip_wrapper = ip_lib.IPWrapper(self.root_helper, self.namespace)
+        return ip_wrapper.netns.execute(
+            cmd,
+            check_exit_code=check_exit_code)
+
+    def ensure_configs(self):
+        """Generate config files which are needed for OpenSwan.
+
+        If there is no directory, this function will create
+        dirs.
+        """
+        self.ensure_config_dir(self.vpnservice)
+        self.ensure_config_file(
+            'ipsec.conf',
+            self.conf.openswan.ipsec_config_template,
+            self.vpnservice)
+        self.ensure_config_file(
+            'ipsec.secrets',
+            self.conf.openswan.ipsec_secret_template,
+            self.vpnservice)
+
+    def get_status(self):
+        return self._execute([self.binary,
+                              'whack',
+                              '--ctlbase',
+                              self.pid_path,
+                              '--status'])
+
+    def restart(self):
+        """Restart the process."""
+        self.stop()
+        self.start()
+        return
+
+    def _get_nexthop(self, address):
+        routes = self._execute(
+            ['ip', 'route', 'get', address])
+        if routes.find('via') >= 0:
+            return routes.split(' ')[2]
+        return address
+
+    def _virtual_privates(self):
+        """Returns line of virtual_privates.
+
+        virtual_private contains the networks
+        that are allowed as subnet for the remote client.
+        """
+        virtual_privates = []
+        nets = [self.vpnservice['subnet']['cidr']]
+        for ipsec_site_conn in self.vpnservice['ipsec_site_connections']:
+            nets += ipsec_site_conn['peer_cidrs']
+        for net in nets:
+            version = netaddr.IPNetwork(net).version
+            virtual_privates.append('%%v%s:%s' % (version, net))
+        return ','.join(virtual_privates)
+
+    def start(self):
+        """Start the process.
+
+        Note: if there is not namespace yet,
+        just do nothing, and wait next event.
+        """
+        if not self.namespace:
+            return
+        virtual_private = self._virtual_privates()
+        #start pluto IKE keying daemon
+        self._execute([self.binary,
+                       'pluto',
+                       '--ctlbase', self.pid_path,
+                       '--ipsecdir', self.etc_dir,
+                       '--use-netkey',
+                       '--uniqueids',
+                       '--nat_traversal',
+                       '--secretsfile', self.secrets_file,
+                       '--virtual_private', virtual_private
+                       ])
+        #add connections
+        for ipsec_site_conn in self.vpnservice['ipsec_site_connections']:
+            nexthop = self._get_nexthop(ipsec_site_conn['peer_address'])
+            self._execute([self.binary,
+                           'addconn',
+                           '--ctlbase', '%s.ctl' % self.pid_path,
+                           '--defaultroutenexthop', nexthop,
+                           '--config', self.config_file,
+                           ipsec_site_conn['id']
+                           ])
+        #TODO(nati) fix this when openswan is fixed
+        #Due to openswan bug, this command always exit with 3
+        #start whack ipsec keying daemon
+        self._execute([self.binary,
+                       'whack',
+                       '--ctlbase', self.pid_path,
+                       '--listen',
+                       ], check_exit_code=False)
+
+        for ipsec_site_conn in self.vpnservice['ipsec_site_connections']:
+            if not ipsec_site_conn['initiator'] == 'start':
+                continue
+            #initiate ipsec connection
+            self._execute([self.binary,
+                           'whack',
+                           '--ctlbase', self.pid_path,
+                           '--name', ipsec_site_conn['id'],
+                           '--asynchronous',
+                           '--initiate'
+                           ])
+
+    def disconnect(self):
+        if not self.namespace:
+            return
+        if not self.vpnservice:
+            return
+        for conn_id in self.connection_status:
+            self._execute([self.binary,
+                           'whack',
+                           '--ctlbase', self.pid_path,
+                           '--name', '%s/0x1' % conn_id,
+                           '--terminate'
+                           ])
+
+    def stop(self):
+        #Stop process using whack
+        #Note this will also stop pluto
+        self.disconnect()
+        self._execute([self.binary,
+                       'whack',
+                       '--ctlbase', self.pid_path,
+                       '--shutdown',
+                       ])
+
+
+class IPsecVpnDriverApi(proxy.RpcProxy):
+    """IPSecVpnDriver RPC api."""
+    IPSEC_PLUGIN_VERSION = '1.0'
+
+    def get_vpn_services_on_host(self, context, host):
+        """Get list of vpnservices.
+
+        The vpnservices including related ipsec_site_connection,
+        ikepolicy and ipsecpolicy on this host
+        """
+        return self.call(context,
+                         self.make_msg('get_vpn_services_on_host',
+                                       host=host),
+                         version=self.IPSEC_PLUGIN_VERSION,
+                         topic=self.topic)
+
+    def update_status(self, context, status):
+        """Update local status.
+
+        This method call updates status attribute of
+        VPNServices.
+        """
+        return self.cast(context,
+                         self.make_msg('update_status',
+                                       status=status),
+                         version=self.IPSEC_PLUGIN_VERSION,
+                         topic=self.topic)
+
+
+class IPsecDriver(device_drivers.DeviceDriver):
+    """VPN Device Driver for IPSec.
+
+    This class is designed for use with L3-agent now.
+    However this driver will be used with another agent in future.
+    so the use of "Router" is kept minimul now.
+    Insted of router_id,  we are using process_id in this code.
+    """
+
+    # history
+    #   1.0 Initial version
+
+    RPC_API_VERSION = '1.0'
+    __metaclass__ = abc.ABCMeta
+
+    def __init__(self, agent, host):
+        self.agent = agent
+        self.conf = self.agent.conf
+        self.root_helper = self.agent.root_helper
+        self.host = host
+        self.conn = rpc.create_connection(new=True)
+        self.context = context.get_admin_context_without_session()
+        self.topic = topics.IPSEC_AGENT_TOPIC
+        node_topic = '%s.%s' % (self.topic, self.host)
+
+        self.processes = {}
+        self.process_status_cache = {}
+
+        self.conn.create_consumer(
+            node_topic,
+            self.create_rpc_dispatcher(),
+            fanout=False)
+        self.conn.consume_in_thread()
+        self.agent_rpc = IPsecVpnDriverApi(topics.IPSEC_DRIVER_TOPIC, '1.0')
+        self.process_status_cache_check = loopingcall.FixedIntervalLoopingCall(
+            self.report_status, self.context)
+        self.process_status_cache_check.start(
+            interval=self.conf.ipsec.ipsec_status_check_interval)
+
+    def create_rpc_dispatcher(self):
+        return q_rpc.PluginRpcDispatcher([self])
+
+    def _update_nat(self, vpnservice, func):
+        """Setting up nat rule in iptables.
+
+        We need to setup nat rule for ipsec packet.
+        :param vpnservice: vpnservices
+        :param func: self.add_nat_rule or self.remove_nat_rule
+        """
+        local_cidr = vpnservice['subnet']['cidr']
+        router_id = vpnservice['router_id']
+        for ipsec_site_connection in vpnservice['ipsec_site_connections']:
+            for peer_cidr in ipsec_site_connection['peer_cidrs']:
+                func(
+                    router_id,
+                    'POSTROUTING',
+                    '-s %s -d %s -m policy '
+                    '--dir out --pol ipsec '
+                    '-j ACCEPT ' % (local_cidr, peer_cidr),
+                    top=True)
+        self.agent.iptables_apply(router_id)
+
+    def vpnservice_updated(self, context, **kwargs):
+        """Vpnservice updated rpc handler
+
+        VPN Service Driver will call this method
+        when vpnservices updated.
+        Then this method start sync with server.
+        """
+        self.sync(context, [])
+
+    @abc.abstractmethod
+    def create_process(self, process_id, vpnservice, namespace):
+        pass
+
+    def ensure_process(self, process_id, vpnservice=None):
+        """Ensuring process.
+
+        If the process dosen't exist, it will create process
+        and store it in self.processs
+        """
+        process = self.processes.get(process_id)
+        if not process or not process.namespace:
+            namespace = self.agent.get_namespace(process_id)
+            process = self.create_process(
+                process_id,
+                vpnservice,
+                namespace)
+            self.processes[process_id] = process
+        return process
+
+    def create_router(self, process_id):
+        """Handling create router event.
+
+        Agent calls this method, when the process namespace
+        is ready.
+        """
+        if process_id in self.processes:
+            # In case of vpnservice is created
+            # before router's namespace
+            process = self.processes[process_id]
+            self._update_nat(process.vpnservice, self.agent.add_nat_rule)
+            process.enable()
+
+    def destroy_router(self, process_id):
+        """Handling destroy_router event.
+
+        Agent calls this method, when the process namespace
+        is deleted.
+        """
+        if process_id in self.processes:
+            process = self.processes[process_id]
+            process.disable()
+            vpnservice = process.vpnservice
+            if vpnservice:
+                self._update_nat(vpnservice, self.agent.remove_nat_rule)
+            del self.processes[process_id]
+
+    def get_process_status_cache(self, process):
+        if not self.process_status_cache.get(process.id):
+            self.process_status_cache[process.id] = {
+                'status': None,
+                'id': process.vpnservice['id'],
+                'updated_pending_status': False,
+                'ipsec_site_connections': {}}
+        return self.process_status_cache[process.id]
+
+    def is_status_updated(self, process, previous_status):
+        if process.updated_pending_status:
+            return True
+        if process.status != previous_status['status']:
+            return True
+        if (process.connection_status !=
+            previous_status['ipsec_site_connections']):
+            return True
+
+    def unset_updated_pending_status(self, process):
+        process.updated_pending_status = False
+        for connection_status in process.connection_status.values():
+            connection_status['updated_pending_status'] = False
+
+    def copy_process_status(self, process):
+        return {
+            'id': process.vpnservice['id'],
+            'status': process.status,
+            'updated_pending_status': process.updated_pending_status,
+            'ipsec_site_connections': copy.deepcopy(process.connection_status)
+        }
+
+    def report_status(self, context):
+        status_changed_vpn_services = []
+        for process in self.processes.values():
+            previous_status = self.get_process_status_cache(process)
+            if self.is_status_updated(process, previous_status):
+                new_status = self.copy_process_status(process)
+                self.process_status_cache[process.id] = new_status
+                status_changed_vpn_services.append(new_status)
+                # We need unset updated_pending status after it
+                # is reported to the server side
+                self.unset_updated_pending_status(process)
+
+        if status_changed_vpn_services:
+            self.agent_rpc.update_status(
+                context,
+                status_changed_vpn_services)
+
+    @lockutils.synchronized('vpn-agent', 'neutron-')
+    def sync(self, context, routers):
+        """Sync status with server side.
+
+        :param context: context object for RPC call
+        :param routers: Router objects which is created in this sync event
+
+        There could be many failure cases should be
+        considered including the followings.
+        1) Agent class restarted
+        2) Failure on process creation
+        3) VpnService is deleted during agent down
+        4) RPC failure
+
+        In order to handle, these failure cases,
+        This driver takes simple sync strategies.
+        """
+        vpnservices = self.agent_rpc.get_vpn_services_on_host(
+            context, self.host)
+        router_ids = [vpnservice['router_id'] for vpnservice in vpnservices]
+        # Ensure the ipsec process is enabled
+        for vpnservice in vpnservices:
+            process = self.ensure_process(vpnservice['router_id'],
+                                          vpnservice=vpnservice)
+            self._update_nat(vpnservice, self.agent.add_nat_rule)
+            process.update()
+
+        # Delete any IPSec processes that are
+        # associated with routers, but are not running the VPN service.
+        for router in routers:
+            #We are using router id as process_id
+            process_id = router['id']
+            if process_id not in router_ids:
+                process = self.ensure_process(process_id)
+                self.destroy_router(process_id)
+
+        # Delete any IPSec processes running
+        # VPN that do not have an associated router.
+        process_ids = [process_id
+                       for process_id in self.processes
+                       if process_id not in router_ids]
+        for process_id in process_ids:
+            self.destroy_router(process_id)
+        self.report_status(context)
+
+
+class OpenSwanDriver(IPsecDriver):
+    def create_process(self, process_id, vpnservice, namespace):
+        return OpenSwanProcess(
+            self.conf,
+            self.root_helper,
+            process_id,
+            vpnservice,
+            namespace)
diff --git a/neutron/services/vpn/device_drivers/template/openswan/ipsec.conf.template b/neutron/services/vpn/device_drivers/template/openswan/ipsec.conf.template
new file mode 100644 (file)
index 0000000..180b4a1
--- /dev/null
@@ -0,0 +1,64 @@
+# Configuration for {{vpnservice.name}}
+config setup
+    nat_traversal=yes
+    listen={{vpnservice.external_ip}}
+conn %default
+    ikelifetime=480m
+    keylife=60m
+    keyingtries=%forever
+{% for ipsec_site_connection in vpnservice.ipsec_site_connections
+%}conn {{ipsec_site_connection.id}}
+    # NOTE: a default route is required for %defaultroute to work...
+    left={{vpnservice.external_ip}}
+    leftid={{vpnservice.external_ip}}
+    auto={{ipsec_site_connection.initiator}}
+    # NOTE:REQUIRED
+    # [subnet]
+    leftsubnet={{vpnservice.subnet.cidr}}
+    # leftsubnet=networkA/netmaskA, networkB/netmaskB (IKEv2 only)
+    leftnexthop=%defaultroute
+    ######################
+    # ipsec_site_connections
+    ######################
+    # [peer_address]
+    right={{ipsec_site_connection.peer_address}}
+    # [peer_id]
+    rightid={{ipsec_site_connection.peer_id}}
+    # [peer_cidrs]
+    rightsubnets={ {{ipsec_site_connection['peer_cidrs']|join(' ')}} }
+    # rightsubnet=networkA/netmaskA, networkB/netmaskB (IKEv2 only)
+    rightnexthop=%defaultroute
+    # [mtu]
+    # Note It looks like not supported in the strongswan driver
+    # ignore it now
+    # [dpd_action]
+    dpdaction={{ipsec_site_connection.dpd_action}}
+    # [dpd_interval]
+    dpddelay={{ipsec_site_connection.dpd_interval}}
+    # [dpd_timeout]
+    dpdtimeout={{ipsec_site_connection.dpd_timeout}}
+    # [auth_mode]
+    authby=secret
+    ######################
+    # IKEPolicy params
+    ######################
+    #ike version
+    ikev2={{ipsec_site_connection.ikepolicy.ike_version}}
+    # [encryption_algorithm]-[auth_algorithm]-[pfs]
+    ike={{ipsec_site_connection.ikepolicy.encryption_algorithm}}-{{ipsec_site_connection.ikepolicy.auth_algorithm}};{{ipsec_site_connection.ikepolicy.pfs}}
+    # [lifetime_value]
+    ikelifetime={{ipsec_site_connection.ikepolicy.lifetime_value}}s
+    # NOTE: it looks lifetime_units=kilobytes can't be enforced (could be seconds,  hours,  days...)
+    ##########################
+    # IPsecPolicys params
+    ##########################
+    # [transform_protocol]
+    auth={{ipsec_site_connection.ipsecpolicy.transform_protocol}}
+    # [encryption_algorithm]-[auth_algorithm]-[pfs]
+    phase2alg={{ipsec_site_connection.ipsecpolicy.encryption_algorithm}}-{{ipsec_site_connection.ipsecpolicy.auth_algorithm}};{{ipsec_site_connection.ipsecpolicy.pfs}}
+    # [encapsulation_mode]
+    type={{ipsec_site_connection.ipsecpolicy.encapsulation_mode}}
+    # [lifetime_value]
+    lifetime={{ipsec_site_connection.ipsecpolicy.lifetime_value}}s
+    # lifebytes=100000 if lifetime_units=kilobytes (IKEv2 only)
+{% endfor %}
diff --git a/neutron/services/vpn/device_drivers/template/openswan/ipsec.secret.template b/neutron/services/vpn/device_drivers/template/openswan/ipsec.secret.template
new file mode 100644 (file)
index 0000000..8302e85
--- /dev/null
@@ -0,0 +1,3 @@
+# Configuration for {{vpnservice.name}} {% for ipsec_site_connection in vpnservice.ipsec_site_connections %}
+{{vpnservice.external_ip}} {{ipsec_site_connection.peer_id}} : PSK "{{ipsec_site_connection.psk}}"
+{% endfor %}
index 6ba7c093329daedc5e11b250e3fae1ee336baacf..3d58354674efdb5ead4ab68d8f103e4e023b50f7 100644 (file)
@@ -19,6 +19,7 @@
 # @author: Swaminathan Vasudevan, Hewlett-Packard
 
 from neutron.db.vpn import vpn_db
+from neutron.services.vpn.service_drivers import ipsec as ipsec_driver
 
 
 class VPNPlugin(vpn_db.VPNPluginDb):
@@ -30,3 +31,60 @@ class VPNPlugin(vpn_db.VPNPluginDb):
     vpn_db.VPNPluginDb.
     """
     supported_extension_aliases = ["vpnaas"]
+
+
+class VPNDriverPlugin(VPNPlugin, vpn_db.VPNPluginRpcDbMixin):
+    """VpnPlugin which supports VPN Service Drivers."""
+    #TODO(nati) handle ikepolicy and ipsecpolicy update usecase
+    def __init__(self):
+        super(VPNDriverPlugin, self).__init__()
+        self.ipsec_driver = ipsec_driver.IPsecVPNDriver(self)
+
+    def _get_driver_for_vpnservice(self, vpnservice):
+        return self.ipsec_driver
+
+    def _get_driver_for_ipsec_site_connection(self, context,
+                                              ipsec_site_connection):
+        #TODO(nati) get vpnservice when we support service type framework
+        vpnservice = None
+        return self._get_driver_for_vpnservice(vpnservice)
+
+    def create_ipsec_site_connection(self, context, ipsec_site_connection):
+        ipsec_site_connection = super(
+            VPNDriverPlugin, self).create_ipsec_site_connection(
+                context, ipsec_site_connection)
+        driver = self._get_driver_for_ipsec_site_connection(
+            context, ipsec_site_connection)
+        driver.create_ipsec_site_connection(context, ipsec_site_connection)
+        return ipsec_site_connection
+
+    def delete_ipsec_site_connection(self, context, ipsec_conn_id):
+        ipsec_site_connection = self.get_ipsec_site_connection(
+            context, ipsec_conn_id)
+        super(VPNDriverPlugin, self).delete_ipsec_site_connection(
+            context, ipsec_conn_id)
+        driver = self._get_driver_for_ipsec_site_connection(
+            context, ipsec_site_connection)
+        driver.delete_ipsec_site_connection(context, ipsec_site_connection)
+
+    def update_ipsec_site_connection(
+            self, context,
+            ipsec_conn_id, ipsec_site_connection):
+        old_ipsec_site_connection = self.get_ipsec_site_connection(
+            context, ipsec_conn_id)
+        ipsec_site_connection = super(
+            VPNDriverPlugin, self).update_ipsec_site_connection(
+                context,
+                ipsec_conn_id,
+                ipsec_site_connection)
+        driver = self._get_driver_for_ipsec_site_connection(
+            context, ipsec_site_connection)
+        driver.update_ipsec_site_connection(
+            context, old_ipsec_site_connection, ipsec_site_connection)
+        return ipsec_site_connection
+
+    def delete_vpnservice(self, context, vpnservice_id):
+        vpnservice = self._get_vpnservice(context, vpnservice_id)
+        super(VPNDriverPlugin, self).delete_vpnservice(context, vpnservice_id)
+        driver = self._get_driver_for_vpnservice(vpnservice)
+        driver.delete_vpnservice(context, vpnservice)
diff --git a/neutron/services/vpn/service_drivers/__init__.py b/neutron/services/vpn/service_drivers/__init__.py
new file mode 100644 (file)
index 0000000..4996882
--- /dev/null
@@ -0,0 +1,39 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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 abc
+
+
+class VpnDriver(object):
+    __metaclass__ = abc.ABCMeta
+
+    @property
+    def service_type(self):
+        pass
+
+    @abc.abstractmethod
+    def create_vpnservice(self, context, vpnservice):
+        pass
+
+    @abc.abstractmethod
+    def update_vpnservice(
+        self, context, old_vpnservice, vpnservice):
+        pass
+
+    @abc.abstractmethod
+    def delete_vpnservice(self, context, vpnservice):
+        pass
diff --git a/neutron/services/vpn/service_drivers/ipsec.py b/neutron/services/vpn/service_drivers/ipsec.py
new file mode 100644 (file)
index 0000000..2fea0a0
--- /dev/null
@@ -0,0 +1,189 @@
+# vim: tabstop=10 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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 netaddr
+
+from neutron.common import rpc as n_rpc
+from neutron import manager
+from neutron.openstack.common import log as logging
+from neutron.openstack.common import rpc
+from neutron.openstack.common.rpc import proxy
+from neutron.services.vpn.common import topics
+from neutron.services.vpn import service_drivers
+
+
+LOG = logging.getLogger(__name__)
+
+IPSEC = 'ipsec'
+BASE_IPSEC_VERSION = '1.0'
+
+
+class IPsecVpnDriverCallBack(object):
+    """Callback for IPSecVpnDriver rpc."""
+
+    # history
+    #   1.0 Initial version
+
+    RPC_API_VERSION = BASE_IPSEC_VERSION
+
+    def __init__(self, driver):
+        self.driver = driver
+
+    def create_rpc_dispatcher(self):
+        return n_rpc.PluginRpcDispatcher([self])
+
+    def get_vpn_services_on_host(self, context, host=None):
+        """Retuns the vpnservices on the host."""
+        plugin = self.driver.service_plugin
+        vpnservices = plugin._get_agent_hosting_vpn_services(
+            context, host)
+        return [self.driver._make_vpnservice_dict(vpnservice)
+                for vpnservice in vpnservices]
+
+    def update_status(self, context, status):
+        """Update status of vpnservices."""
+        plugin = self.driver.service_plugin
+        plugin.update_status_by_agent(context, status)
+
+
+class IPsecVpnAgentApi(proxy.RpcProxy):
+    """Agent RPC API for IPsecVPNAgent."""
+
+    RPC_API_VERSION = BASE_IPSEC_VERSION
+
+    def _agent_notification(self, context, method, router_id,
+                            version=None):
+        """Notify update for the agent.
+
+        This method will find where is the router, and
+        dispatch notification for the agent.
+        """
+        adminContext = context.is_admin and context or context.elevated()
+        plugin = manager.NeutronManager.get_plugin()
+        if not version:
+            version = self.RPC_API_VERSION
+        l3_agents = plugin.get_l3_agents_hosting_routers(
+            adminContext, [router_id],
+            admin_state_up=True,
+            active=True)
+        for l3_agent in l3_agents:
+            LOG.debug(_('Notify agent at %(topic)s.%(host)s the message '
+                        '%(method)s'),
+                      {'topic': topics.IPSEC_AGENT_TOPIC,
+                       'host': l3_agent.host,
+                       'method': method})
+            self.cast(
+                context, self.make_msg(method),
+                version=version,
+                topic='%s.%s' % (topics.IPSEC_AGENT_TOPIC, l3_agent.host))
+
+    def vpnservice_updated(self, context, router_id):
+        """Send update event of vpnservices."""
+        method = 'vpnservice_updated'
+        self._agent_notification(context, method, router_id)
+
+
+class IPsecVPNDriver(service_drivers.VpnDriver):
+    """VPN Service Driver class for IPsec."""
+
+    def __init__(self, service_plugin):
+        self.callbacks = IPsecVpnDriverCallBack(self)
+        self.service_plugin = service_plugin
+        self.conn = rpc.create_connection(new=True)
+        self.conn.create_consumer(
+            topics.IPSEC_DRIVER_TOPIC,
+            self.callbacks.create_rpc_dispatcher(),
+            fanout=False)
+        self.conn.consume_in_thread()
+        self.agent_rpc = IPsecVpnAgentApi(
+            topics.IPSEC_AGENT_TOPIC, BASE_IPSEC_VERSION)
+
+    @property
+    def service_type(self):
+        return IPSEC
+
+    def create_ipsec_site_connection(self, context, ipsec_site_connection):
+        vpnservice = self.service_plugin._get_vpnservice(
+            context, ipsec_site_connection['vpnservice_id'])
+        self.agent_rpc.vpnservice_updated(context, vpnservice['router_id'])
+
+    def update_ipsec_site_connection(
+        self, context, old_ipsec_site_connection, ipsec_site_connection):
+        vpnservice = self.service_plugin._get_vpnservice(
+            context, ipsec_site_connection['vpnservice_id'])
+        self.agent_rpc.vpnservice_updated(context, vpnservice['router_id'])
+
+    def delete_ipsec_site_connection(self, context, ipsec_site_connection):
+        vpnservice = self.service_plugin._get_vpnservice(
+            context, ipsec_site_connection['vpnservice_id'])
+        self.agent_rpc.vpnservice_updated(context, vpnservice['router_id'])
+
+    def create_ikepolicy(self, context, ikepolicy):
+        pass
+
+    def delete_ikepolicy(self, context, ikepolicy):
+        pass
+
+    def update_ikepolicy(self, context, old_ikepolicy, ikepolicy):
+        pass
+
+    def create_ipsecpolicy(self, context, ipsecpolicy):
+        pass
+
+    def delete_ipsecpolicy(self, context, ipsecpolicy):
+        pass
+
+    def update_ipsecpolicy(self, context, old_ipsec_policy, ipsecpolicy):
+        pass
+
+    def create_vpnservice(self, context, vpnservice):
+        pass
+
+    def update_vpnservice(self, context, old_vpnservice, vpnservice):
+        self.agent_rpc.vpnservice_updated(context, vpnservice['router_id'])
+
+    def delete_vpnservice(self, context, vpnservice):
+        self.agent_rpc.vpnservice_updated(context, vpnservice['router_id'])
+
+    def _make_vpnservice_dict(self, vpnservice):
+        """Convert vpnservice information for vpn agent.
+
+        also converting parameter name for vpn agent driver
+        """
+        vpnservice_dict = dict(vpnservice)
+        vpnservice_dict['ipsec_site_connections'] = []
+        vpnservice_dict['subnet'] = dict(
+            vpnservice.subnet)
+        vpnservice_dict['external_ip'] = vpnservice.router.gw_port[
+            'fixed_ips'][0]['ip_address']
+        for ipsec_site_connection in vpnservice.ipsec_site_connections:
+            ipsec_site_connection_dict = dict(ipsec_site_connection)
+            try:
+                netaddr.IPAddress(ipsec_site_connection['peer_id'])
+            except netaddr.core.AddrFormatError:
+                ipsec_site_connection['peer_id'] = (
+                    '@' + ipsec_site_connection['peer_id'])
+            ipsec_site_connection_dict['ikepolicy'] = dict(
+                ipsec_site_connection.ikepolicy)
+            ipsec_site_connection_dict['ipsecpolicy'] = dict(
+                ipsec_site_connection.ipsecpolicy)
+            vpnservice_dict['ipsec_site_connections'].append(
+                ipsec_site_connection_dict)
+            peer_cidrs = [
+                peer_cidr.cidr
+                for peer_cidr in ipsec_site_connection.peer_cidrs]
+            ipsec_site_connection_dict['peer_cidrs'] = peer_cidrs
+        return vpnservice_dict
index 0445e50a19e2a6fb8bd1ac4ee34368ba9481e3cd..4697bef4d947ebef48c2f04f663dfe075ae9391a 100644 (file)
@@ -23,7 +23,8 @@ class MetaPluginV2DBTestCase(test_plugin.NeutronDbPluginV2TestCase):
     _plugin_name = ('neutron.plugins.metaplugin.'
                     'meta_neutron_plugin.MetaPluginV2')
 
-    def setUp(self, plugin=None, ext_mgr=None):
+    def setUp(self, plugin=None, ext_mgr=None,
+              service_plugins=None):
         # NOTE(salv-orlando): The plugin keyword argument is ignored,
         # as this class will always invoke super with self._plugin_name.
         # These keyword parameters ensure setUp methods always have the
@@ -31,7 +32,8 @@ class MetaPluginV2DBTestCase(test_plugin.NeutronDbPluginV2TestCase):
         setup_metaplugin_conf()
         ext_mgr = ext_mgr or test_l3_plugin.L3TestExtensionManager()
         super(MetaPluginV2DBTestCase, self).setUp(
-            plugin=self._plugin_name, ext_mgr=ext_mgr)
+            plugin=self._plugin_name, ext_mgr=ext_mgr,
+            service_plugins=service_plugins)
 
 
 class TestMetaBasicGet(test_plugin.TestBasicGet,
index f2dc40a6edd3d8557ae34de6520044dc848f0f7d..044f86b91e5063fe6ae65b6712da4d87976e0a6b 100644 (file)
@@ -86,7 +86,10 @@ class NiciraPluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase):
                 '', kwargs['tenant_id'])
         return network_req.get_response(self.api)
 
-    def setUp(self, plugin=None, ext_mgr=None):
+    def setUp(self,
+              plugin=PLUGIN_NAME,
+              ext_mgr=None,
+              service_plugins=None):
         test_lib.test_config['config_files'] = [get_fake_conf('nvp.ini.test')]
         # mock nvp api client
         self.fc = fake_nvpapiclient.FakeClient(STUBS_PATH)
diff --git a/neutron/tests/unit/services/vpn/device_drivers/__init__.py b/neutron/tests/unit/services/vpn/device_drivers/__init__.py
new file mode 100644 (file)
index 0000000..9b27a75
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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.
diff --git a/neutron/tests/unit/services/vpn/device_drivers/test_ipsec.py b/neutron/tests/unit/services/vpn/device_drivers/test_ipsec.py
new file mode 100644 (file)
index 0000000..a06d6f9
--- /dev/null
@@ -0,0 +1,154 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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 mock
+
+from neutron.openstack.common import uuidutils
+from neutron.plugins.common import constants
+from neutron.services.vpn.device_drivers import ipsec as ipsec_driver
+from neutron.tests import base
+
+_uuid = uuidutils.generate_uuid
+FAKE_HOST = 'fake_host'
+FAKE_ROUTER_ID = _uuid()
+FAKE_VPN_SERVICE = {
+    'id': _uuid(),
+    'router_id': FAKE_ROUTER_ID,
+    'admin_state_up': True,
+    'status': constants.PENDING_CREATE,
+    'subnet': {'cidr': '10.0.0.0/24'},
+    'ipsec_site_connections': [
+        {'peer_cidrs': ['20.0.0.0/24',
+                        '30.0.0.0/24']},
+        {'peer_cidrs': ['40.0.0.0/24',
+                        '50.0.0.0/24']}]
+}
+
+
+class TestIPsecDeviceDriver(base.BaseTestCase):
+    def setUp(self, driver=ipsec_driver.OpenSwanDriver):
+        super(TestIPsecDeviceDriver, self).setUp()
+        self.addCleanup(mock.patch.stopall)
+
+        for klass in [
+            'os.makedirs',
+            'os.path.isdir',
+            'neutron.agent.linux.utils.replace_file',
+            'neutron.openstack.common.rpc.create_connection',
+            'neutron.services.vpn.device_drivers.ipsec.'
+                'OpenSwanProcess._gen_config_content',
+            'shutil.rmtree',
+        ]:
+            mock.patch(klass).start()
+        self.execute = mock.patch(
+            'neutron.agent.linux.utils.execute').start()
+        self.agent = mock.Mock()
+        self.driver = driver(
+            self.agent,
+            FAKE_HOST)
+        self.driver.agent_rpc = mock.Mock()
+
+    def test_vpnservice_updated(self):
+        with mock.patch.object(self.driver, 'sync') as sync:
+            context = mock.Mock()
+            self.driver.vpnservice_updated(context)
+            sync.assert_called_once_with(context, [])
+
+    def test_create_router(self):
+        process_id = _uuid()
+        process = mock.Mock()
+        process.vpnservice = FAKE_VPN_SERVICE
+        self.driver.processes = {
+            process_id: process}
+        self.driver.create_router(process_id)
+        process.enable.assert_called_once_with()
+
+    def test_destroy_router(self):
+        process_id = _uuid()
+        process = mock.Mock()
+        process.vpnservice = FAKE_VPN_SERVICE
+        self.driver.processes = {
+            process_id: process}
+        self.driver.destroy_router(process_id)
+        process.disable.assert_called_once_with()
+        self.assertNotIn(process_id, self.driver.processes)
+
+    def test_sync_added(self):
+        self.driver.agent_rpc.get_vpn_services_on_host.return_value = [
+            FAKE_VPN_SERVICE]
+        context = mock.Mock()
+        process = mock.Mock()
+        process.vpnservice = FAKE_VPN_SERVICE
+        process.connection_status = {}
+        process.status = constants.ACTIVE
+        process.updated_pending_status = True
+        self.driver.process_status_cache = {}
+        self.driver.processes = {
+            FAKE_ROUTER_ID: process}
+        self.driver.sync(context, [])
+        self.agent.assert_has_calls([
+            mock.call.add_nat_rule(
+                FAKE_ROUTER_ID,
+                'POSTROUTING',
+                '-s 10.0.0.0/24 -d 20.0.0.0/24 -m policy '
+                '--dir out --pol ipsec -j ACCEPT ',
+                top=True),
+            mock.call.add_nat_rule(
+                FAKE_ROUTER_ID,
+                'POSTROUTING',
+                '-s 10.0.0.0/24 -d 30.0.0.0/24 -m policy '
+                '--dir out --pol ipsec -j ACCEPT ',
+                top=True),
+            mock.call.add_nat_rule(
+                FAKE_ROUTER_ID,
+                'POSTROUTING',
+                '-s 10.0.0.0/24 -d 40.0.0.0/24 -m policy '
+                '--dir out --pol ipsec -j ACCEPT ',
+                top=True),
+            mock.call.add_nat_rule(
+                FAKE_ROUTER_ID,
+                'POSTROUTING',
+                '-s 10.0.0.0/24 -d 50.0.0.0/24 -m policy '
+                '--dir out --pol ipsec -j ACCEPT ',
+                top=True),
+            mock.call.iptables_apply(FAKE_ROUTER_ID)
+        ])
+        process.update.assert_called_once_with()
+        self.driver.agent_rpc.update_status.assert_called_once_with(
+            context,
+            [{'status': 'ACTIVE',
+             'ipsec_site_connections': {},
+             'updated_pending_status': True,
+             'id': FAKE_VPN_SERVICE['id']}])
+
+    def test_sync_removed(self):
+        self.driver.agent_rpc.get_vpn_services_on_host.return_value = []
+        context = mock.Mock()
+        process_id = _uuid()
+        process = mock.Mock()
+        process.vpnservice = FAKE_VPN_SERVICE
+        self.driver.processes = {
+            process_id: process}
+        self.driver.sync(context, [])
+        process.disable.assert_called_once_with()
+        self.assertNotIn(process_id, self.driver.processes)
+
+    def test_sync_removed_router(self):
+        self.driver.agent_rpc.get_vpn_services_on_host.return_value = []
+        context = mock.Mock()
+        process_id = _uuid()
+        self.driver.sync(context, [{'id': process_id}])
+        self.assertNotIn(process_id, self.driver.processes)
diff --git a/neutron/tests/unit/services/vpn/service_drivers/__init__.py b/neutron/tests/unit/services/vpn/service_drivers/__init__.py
new file mode 100644 (file)
index 0000000..9b27a75
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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.
diff --git a/neutron/tests/unit/services/vpn/service_drivers/test_ipsec.py b/neutron/tests/unit/services/vpn/service_drivers/test_ipsec.py
new file mode 100644 (file)
index 0000000..863d29b
--- /dev/null
@@ -0,0 +1,86 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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 mock
+
+from neutron import context
+from neutron.openstack.common import uuidutils
+from neutron.services.vpn.service_drivers import ipsec as ipsec_driver
+from neutron.tests import base
+
+_uuid = uuidutils.generate_uuid
+
+FAKE_VPN_CONNECTION = {
+    'vpnservice_id': _uuid()
+}
+FAKE_VPN_SERVICE = {
+    'router_id': _uuid()
+}
+FAKE_HOST = 'fake_host'
+
+
+class TestIPsecDriver(base.BaseTestCase):
+    def setUp(self):
+        super(TestIPsecDriver, self).setUp()
+        self.addCleanup(mock.patch.stopall)
+        mock.patch('neutron.openstack.common.rpc.create_connection').start()
+
+        l3_agent = mock.Mock()
+        l3_agent.host = FAKE_HOST
+        plugin = mock.Mock()
+        plugin.get_l3_agents_hosting_routers.return_value = [l3_agent]
+        plugin_p = mock.patch('neutron.manager.NeutronManager.get_plugin')
+        get_plugin = plugin_p.start()
+        get_plugin.return_value = plugin
+
+        service_plugin = mock.Mock()
+        service_plugin._get_vpnservice.return_value = {
+            'router_id': _uuid()
+        }
+        self.driver = ipsec_driver.IPsecVPNDriver(service_plugin)
+
+    def _test_update(self, func, args):
+        ctxt = context.Context('', 'somebody')
+        with mock.patch.object(self.driver.agent_rpc, 'cast') as cast:
+            func(ctxt, *args)
+            cast.assert_called_once_with(
+                ctxt,
+                {'args': {},
+                 'namespace': None,
+                 'method': 'vpnservice_updated'},
+                version='1.0',
+                topic='ipsec_agent.fake_host')
+
+    def test_create_ipsec_site_connection(self):
+        self._test_update(self.driver.create_ipsec_site_connection,
+                          [FAKE_VPN_CONNECTION])
+
+    def test_update_ipsec_site_connection(self):
+        self._test_update(self.driver.update_ipsec_site_connection,
+                          [FAKE_VPN_CONNECTION, FAKE_VPN_CONNECTION])
+
+    def test_delete_ipsec_site_connection(self):
+        self._test_update(self.driver.delete_ipsec_site_connection,
+                          [FAKE_VPN_CONNECTION])
+
+    def test_update_vpnservice(self):
+        self._test_update(self.driver.update_vpnservice,
+                          [FAKE_VPN_SERVICE, FAKE_VPN_SERVICE])
+
+    def test_delete_vpnservice(self):
+        self._test_update(self.driver.delete_vpnservice,
+                          [FAKE_VPN_SERVICE])
diff --git a/neutron/tests/unit/services/vpn/test_vpn_agent.py b/neutron/tests/unit/services/vpn/test_vpn_agent.py
new file mode 100644 (file)
index 0000000..623bfcc
--- /dev/null
@@ -0,0 +1,191 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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 mock
+from oslo.config import cfg
+
+from neutron.agent.common import config as agent_config
+from neutron.agent import l3_agent
+from neutron.agent.linux import interface
+from neutron.common import config as base_config
+from neutron.openstack.common import uuidutils
+from neutron.services.vpn import agent
+from neutron.services.vpn import device_drivers
+from neutron.tests import base
+
+_uuid = uuidutils.generate_uuid
+NOOP_DEVICE_CLASS = 'NoopDeviceDriver'
+NOOP_DEVICE = ('neutron.tests.unit.services.'
+               'vpn.test_vpn_agent.%s' % NOOP_DEVICE_CLASS)
+
+
+class NoopDeviceDriver(device_drivers.DeviceDriver):
+    def sync(self, context, processes):
+        pass
+
+    def create_router(self, process_id):
+        pass
+
+    def destroy_router(self, process_id):
+        pass
+
+
+class TestVPNAgent(base.BaseTestCase):
+    def setUp(self):
+        super(TestVPNAgent, self).setUp()
+        self.addCleanup(mock.patch.stopall)
+        self.conf = cfg.CONF
+        self.conf.register_opts(base_config.core_opts)
+        self.conf.register_opts(l3_agent.L3NATAgent.OPTS)
+        self.conf.register_opts(interface.OPTS)
+        agent_config.register_agent_state_opts_helper(self.conf)
+        agent_config.register_root_helper(self.conf)
+
+        self.conf.set_override('interface_driver',
+                               'neutron.agent.linux.interface.NullDriver')
+        self.conf.set_override(
+            'vpn_device_driver',
+            [NOOP_DEVICE],
+            'vpnagent')
+
+        for clazz in [
+            'neutron.agent.linux.ip_lib.device_exists',
+            'neutron.agent.linux.ip_lib.IPWrapper',
+            'neutron.agent.linux.interface.NullDriver',
+            'neutron.agent.linux.utils.execute'
+        ]:
+            mock.patch(clazz).start()
+
+        l3pluginApi_cls = mock.patch(
+            'neutron.agent.l3_agent.L3PluginApi').start()
+        self.plugin_api = mock.Mock()
+        l3pluginApi_cls.return_value = self.plugin_api
+
+        self.fake_host = 'fake_host'
+        self.agent = agent.VPNAgent(self.fake_host)
+
+    def test_setup_drivers(self):
+        self.assertEqual(1, len(self.agent.devices))
+        device = self.agent.devices[0]
+        self.assertEqual(
+            NOOP_DEVICE_CLASS,
+            device.__class__.__name__
+        )
+
+    def test_get_namespace(self):
+        router_id = _uuid()
+        ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
+                                 self.conf.use_namespaces, None)
+        self.agent.router_info = {router_id: ri}
+        namespace = self.agent.get_namespace(router_id)
+        self.assertTrue(namespace.endswith(router_id))
+        self.assertFalse(self.agent.get_namespace('fake_id'))
+
+    def test_add_nat_rule(self):
+        router_id = _uuid()
+        ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
+                                 self.conf.use_namespaces, None)
+        iptables = mock.Mock()
+        ri.iptables_manager.ipv4['nat'] = iptables
+        self.agent.router_info = {router_id: ri}
+        self.agent.add_nat_rule(router_id, 'fake_chain', 'fake_rule', True)
+        iptables.add_rule.assert_called_once_with(
+            'fake_chain', 'fake_rule', top=True)
+
+    def test_add_nat_rule_with_no_router(self):
+        self.agent.router_info = {}
+        #Should do nothing
+        self.agent.add_nat_rule(
+            'fake_router_id',
+            'fake_chain',
+            'fake_rule',
+            True)
+
+    def test_remove_rule(self):
+        router_id = _uuid()
+        ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
+                                 self.conf.use_namespaces, None)
+        iptables = mock.Mock()
+        ri.iptables_manager.ipv4['nat'] = iptables
+        self.agent.router_info = {router_id: ri}
+        self.agent.remove_nat_rule(router_id, 'fake_chain', 'fake_rule')
+        iptables.remove_rule.assert_called_once_with(
+            'fake_chain', 'fake_rule')
+
+    def test_remove_rule_with_no_router(self):
+        self.agent.router_info = {}
+        #Should do nothing
+        self.agent.remove_nat_rule(
+            'fake_router_id',
+            'fake_chain',
+            'fake_rule')
+
+    def test_iptables_apply(self):
+        router_id = _uuid()
+        ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
+                                 self.conf.use_namespaces, None)
+        iptables = mock.Mock()
+        ri.iptables_manager = iptables
+        self.agent.router_info = {router_id: ri}
+        self.agent.iptables_apply(router_id)
+        iptables.apply.assert_called_once_with()
+
+    def test_iptables_apply_with_no_router(self):
+        #Should do nothing
+        self.agent.router_info = {}
+        self.agent.iptables_apply('fake_router_id')
+
+    def test_router_added(self):
+        mock.patch(
+            'neutron.agent.linux.iptables_manager.IptablesManager').start()
+        router_id = _uuid()
+        router = {'id': router_id}
+        device = mock.Mock()
+        self.agent.devices = [device]
+        self.agent._router_added(router_id, router)
+        device.create_router.assert_called_once_with(router_id)
+
+    def test_router_removed(self):
+        self.plugin_api.get_external_network_id.return_value = None
+        mock.patch(
+            'neutron.agent.linux.iptables_manager.IptablesManager').start()
+        router_id = _uuid()
+        ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
+                                 self.conf.use_namespaces, None)
+        ri.router = {
+            'id': _uuid(),
+            'admin_state_up': True,
+            'routes': [],
+            'external_gateway_info': {}}
+        device = mock.Mock()
+        self.agent.router_info = {router_id: ri}
+        self.agent.devices = [device]
+        self.agent._router_removed(router_id)
+        device.destroy_router.assert_called_once_with(router_id)
+
+    def test_process_routers(self):
+        self.plugin_api.get_external_network_id.return_value = None
+        routers = [
+            {'id': _uuid(),
+             'admin_state_up': True,
+             'routes': [],
+             'external_gateway_info': {}}]
+
+        device = mock.Mock()
+        self.agent.devices = [device]
+        self.agent._process_routers(routers, False)
+        device.sync.assert_called_once_with(mock.ANY, routers)
diff --git a/neutron/tests/unit/services/vpn/test_vpnaas_driver_plugin.py b/neutron/tests/unit/services/vpn/test_vpnaas_driver_plugin.py
new file mode 100644 (file)
index 0000000..8c25d7e
--- /dev/null
@@ -0,0 +1,156 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT I3, 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 contextlib
+
+import mock
+
+from neutron.common import constants
+from neutron import context
+from neutron import manager
+from neutron.plugins.common import constants as p_constants
+from neutron.services.vpn.service_drivers import ipsec as ipsec_driver
+from neutron.tests.unit.db.vpn import test_db_vpnaas
+from neutron.tests.unit.openvswitch import test_agent_scheduler
+from neutron.tests.unit import test_agent_ext_plugin
+
+FAKE_HOST = test_agent_ext_plugin.L3_HOSTA
+VPN_DRIVER_CLASS = 'neutron.services.vpn.plugin.VPNDriverPlugin'
+
+
+class TestVPNDriverPlugin(test_db_vpnaas.TestVpnaas,
+                          test_agent_scheduler.AgentSchedulerTestMixIn,
+                          test_agent_ext_plugin.AgentDBTestMixIn):
+    def setUp(self):
+        self.addCleanup(mock.patch.stopall)
+        self.adminContext = context.get_admin_context()
+        driver_cls_p = mock.patch(
+            'neutron.services.vpn.'
+            'service_drivers.ipsec.IPsecVPNDriver')
+        driver_cls = driver_cls_p.start()
+        self.driver = mock.Mock()
+        self.driver.service_type = ipsec_driver.IPSEC
+        driver_cls.return_value = self.driver
+        super(TestVPNDriverPlugin, self).setUp(
+            vpnaas_plugin=VPN_DRIVER_CLASS)
+
+    def test_create_ipsec_site_connection(self, **extras):
+        super(TestVPNDriverPlugin, self).test_create_ipsec_site_connection()
+        self.driver.create_ipsec_site_connection.assert_called_once_with(
+            mock.ANY, mock.ANY)
+        self.driver.delete_ipsec_site_connection.assert_called_once_with(
+            mock.ANY, mock.ANY)
+
+    def test_delete_vpnservice(self, **extras):
+        super(TestVPNDriverPlugin, self).test_delete_vpnservice()
+        self.driver.delete_vpnservice.assert_called_once_with(
+            mock.ANY, mock.ANY)
+
+    @contextlib.contextmanager
+    def vpnservice_set(self):
+        """Test case to create a ipsec_site_connection."""
+        vpnservice_name = "vpn1"
+        ipsec_site_connection_name = "ipsec_site_connection"
+        ikename = "ikepolicy1"
+        ipsecname = "ipsecpolicy1"
+        description = "my-vpn-connection"
+        keys = {'name': vpnservice_name,
+                'description': "my-vpn-connection",
+                'peer_address': '192.168.1.10',
+                'peer_id': '192.168.1.10',
+                'peer_cidrs': ['192.168.2.0/24', '192.168.3.0/24'],
+                'initiator': 'bi-directional',
+                'mtu': 1500,
+                'dpd_action': 'hold',
+                'dpd_interval': 40,
+                'dpd_timeout': 120,
+                'tenant_id': self._tenant_id,
+                'psk': 'abcd',
+                'status': 'PENDING_CREATE',
+                'admin_state_up': True}
+        with self.ikepolicy(name=ikename) as ikepolicy:
+            with self.ipsecpolicy(name=ipsecname) as ipsecpolicy:
+                with self.subnet() as subnet:
+                    with self.router() as router:
+                        plugin = manager.NeutronManager.get_plugin()
+                        agent = {'host': FAKE_HOST,
+                                 'agent_type': constants.AGENT_TYPE_L3,
+                                 'binary': 'fake-binary',
+                                 'topic': 'fake-topic'}
+                        plugin.create_or_update_agent(self.adminContext, agent)
+                        plugin.schedule_router(
+                            self.adminContext, router['router']['id'])
+                        with self.vpnservice(name=vpnservice_name,
+                                             subnet=subnet,
+                                             router=router) as vpnservice1:
+                            keys['ikepolicy_id'] = ikepolicy['ikepolicy']['id']
+                            keys['ipsecpolicy_id'] = (
+                                ipsecpolicy['ipsecpolicy']['id']
+                            )
+                            keys['vpnservice_id'] = (
+                                vpnservice1['vpnservice']['id']
+                            )
+                            with self.ipsec_site_connection(
+                                self.fmt,
+                                ipsec_site_connection_name,
+                                keys['peer_address'],
+                                keys['peer_id'],
+                                keys['peer_cidrs'],
+                                keys['mtu'],
+                                keys['psk'],
+                                keys['initiator'],
+                                keys['dpd_action'],
+                                keys['dpd_interval'],
+                                keys['dpd_timeout'],
+                                vpnservice1,
+                                ikepolicy,
+                                ipsecpolicy,
+                                keys['admin_state_up'],
+                                description=description,
+                            ):
+                                yield vpnservice1['vpnservice']
+
+    def test_get_agent_hosting_vpn_services(self):
+        with self.vpnservice_set():
+            service_plugin = manager.NeutronManager.get_service_plugins()[
+                p_constants.VPN]
+            vpnservices = service_plugin._get_agent_hosting_vpn_services(
+                self.adminContext, FAKE_HOST)
+            vpnservices = vpnservices.all()
+            self.assertEqual(1, len(vpnservices))
+            vpnservice_db = vpnservices[0]
+            self.assertEqual(1, len(vpnservice_db.ipsec_site_connections))
+            ipsec_site_connection = vpnservice_db.ipsec_site_connections[0]
+            self.assertIsNotNone(
+                ipsec_site_connection['ikepolicy'])
+            self.assertIsNotNone(
+                ipsec_site_connection['ipsecpolicy'])
+
+    def test_update_status(self):
+        with self.vpnservice_set() as vpnservice:
+            self._register_agent_states()
+            service_plugin = manager.NeutronManager.get_service_plugins()[
+                p_constants.VPN]
+            service_plugin.update_status_by_agent(
+                self.adminContext,
+                [{'status': 'ACTIVE',
+                  'ipsec_site_connections': {},
+                  'updated_pending_status': True,
+                  'id': vpnservice['id']}])
+            vpnservices = service_plugin._get_agent_hosting_vpn_services(
+                self.adminContext, FAKE_HOST)
+            vpnservice_db = vpnservices[0]
+            self.assertEqual(p_constants.ACTIVE, vpnservice_db['status'])
index c2412c8794cbcde41dacebbae33e33a01c3599e1..3c950e0b02350cdb5d111417f76ed65d6a775dcc 100644 (file)
@@ -481,14 +481,16 @@ class L3NatTestCaseMixin(object):
 class L3NatTestCaseBase(L3NatTestCaseMixin,
                         test_db_plugin.NeutronDbPluginV2TestCase):
 
-    def setUp(self, plugin=None, ext_mgr=None):
+    def setUp(self, plugin=None, ext_mgr=None,
+              service_plugins=None):
         test_config['plugin_name_v2'] = (
             'neutron.tests.unit.test_l3_plugin.TestL3NatPlugin')
         # for these tests we need to enable overlapping ips
         cfg.CONF.set_default('allow_overlapping_ips', True)
         ext_mgr = ext_mgr or L3TestExtensionManager()
         super(L3NatTestCaseBase, self).setUp(
-            plugin=plugin, ext_mgr=ext_mgr)
+            plugin=plugin, ext_mgr=ext_mgr,
+            service_plugins=service_plugins)
 
         # Set to None to reload the drivers
         notifier_api._drivers = None
index 24cbc549df0f61c9d7c636377bff7916630c9645..b96b9e2b321452a31020957430f18f9bb5b1c035 100644 (file)
@@ -13,6 +13,7 @@ httplib2
 requests>=1.1
 iso8601>=0.1.4
 jsonrpclib
+Jinja2
 kombu>=2.4.8
 netaddr
 python-neutronclient>=2.2.3,<3
index e5f73eb5c65534e02beb574632251d0fc75ae441..ad9a9fd6dab76da0424f5cec13b58ac80cabce2f 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -91,6 +91,7 @@ console_scripts =
     neutron-usage-audit = neutron.cmd.usage_audit:main
     quantum-check-nvp-config = neutron.plugins.nicira.check_nvp_config:main
     quantum-db-manage = neutron.db.migration.cli:main
+    neutron-vpn-agent = neutron.services.vpn.agent:main
     quantum-debug = neutron.debug.shell:main
     quantum-dhcp-agent = neutron.agent.dhcp_agent:main
     quantum-hyperv-agent = neutron.plugins.hyperv.agent.hyperv_neutron_agent:main