From bba99f29ad8500770bd838366c96ae371787d98f Mon Sep 17 00:00:00 2001 From: Nachi Ueno Date: Wed, 5 Jun 2013 16:24:38 -0700 Subject: [PATCH] Reference driver implementation (IPsec) for VPNaaS 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 --- etc/neutron/rootwrap.d/vpnaas.filters | 13 + etc/vpn_agent.ini | 13 + neutron/agent/l3_agent.py | 4 +- neutron/db/vpn/vpn_db.py | 71 +- neutron/extensions/vpnaas.py | 7 +- neutron/plugins/common/constants.py | 1 + neutron/plugins/common/utils.py | 7 + neutron/services/vpn/agent.py | 148 ++++ neutron/services/vpn/common/__init__.py | 16 + neutron/services/vpn/common/topics.py | 20 + .../services/vpn/device_drivers/__init__.py | 36 + neutron/services/vpn/device_drivers/ipsec.py | 687 ++++++++++++++++++ .../template/openswan/ipsec.conf.template | 64 ++ .../template/openswan/ipsec.secret.template | 3 + neutron/services/vpn/plugin.py | 58 ++ .../services/vpn/service_drivers/__init__.py | 39 + neutron/services/vpn/service_drivers/ipsec.py | 189 +++++ neutron/tests/unit/metaplugin/test_basic.py | 6 +- .../tests/unit/nicira/test_nicira_plugin.py | 5 +- .../services/vpn/device_drivers/__init__.py | 16 + .../services/vpn/device_drivers/test_ipsec.py | 154 ++++ .../services/vpn/service_drivers/__init__.py | 16 + .../vpn/service_drivers/test_ipsec.py | 86 +++ .../tests/unit/services/vpn/test_vpn_agent.py | 191 +++++ .../services/vpn/test_vpnaas_driver_plugin.py | 156 ++++ neutron/tests/unit/test_l3_plugin.py | 6 +- requirements.txt | 1 + setup.cfg | 1 + 28 files changed, 1986 insertions(+), 28 deletions(-) create mode 100644 etc/neutron/rootwrap.d/vpnaas.filters create mode 100644 etc/vpn_agent.ini create mode 100644 neutron/services/vpn/agent.py create mode 100644 neutron/services/vpn/common/__init__.py create mode 100644 neutron/services/vpn/common/topics.py create mode 100644 neutron/services/vpn/device_drivers/__init__.py create mode 100644 neutron/services/vpn/device_drivers/ipsec.py create mode 100644 neutron/services/vpn/device_drivers/template/openswan/ipsec.conf.template create mode 100644 neutron/services/vpn/device_drivers/template/openswan/ipsec.secret.template create mode 100644 neutron/services/vpn/service_drivers/__init__.py create mode 100644 neutron/services/vpn/service_drivers/ipsec.py create mode 100644 neutron/tests/unit/services/vpn/device_drivers/__init__.py create mode 100644 neutron/tests/unit/services/vpn/device_drivers/test_ipsec.py create mode 100644 neutron/tests/unit/services/vpn/service_drivers/__init__.py create mode 100644 neutron/tests/unit/services/vpn/service_drivers/test_ipsec.py create mode 100644 neutron/tests/unit/services/vpn/test_vpn_agent.py create mode 100644 neutron/tests/unit/services/vpn/test_vpnaas_driver_plugin.py diff --git a/etc/neutron/rootwrap.d/vpnaas.filters b/etc/neutron/rootwrap.d/vpnaas.filters new file mode 100644 index 00000000000..7848136b9f0 --- /dev/null +++ b/etc/neutron/rootwrap.d/vpnaas.filters @@ -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 index 00000000000..3f8f61b1842 --- /dev/null +++ b/etc/vpn_agent.ini @@ -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 diff --git a/neutron/agent/l3_agent.py b/neutron/agent/l3_agent.py index c53d930e014..1e9086dc62e 100644 --- a/neutron/agent/l3_agent.py +++ b/neutron/agent/l3_agent.py @@ -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() diff --git a/neutron/db/vpn/vpn_db.py b/neutron/db/vpn/vpn_db.py index 727ee85ccab..8061b9b2203 100644 --- a/neutron/db/vpn/vpn_db.py +++ b/neutron/db/vpn/vpn_db.py @@ -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'] diff --git a/neutron/extensions/vpnaas.py b/neutron/extensions/vpnaas.py index b02890d24e4..a1bbd9d5195 100644 --- a/neutron/extensions/vpnaas.py +++ b/neutron/extensions/vpnaas.py @@ -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'] diff --git a/neutron/plugins/common/constants.py b/neutron/plugins/common/constants.py index 7a7d36bee2e..688de5ddd64 100644 --- a/neutron/plugins/common/constants.py +++ b/neutron/plugins/common/constants.py @@ -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" diff --git a/neutron/plugins/common/utils.py b/neutron/plugins/common/utils.py index 0daffad22af..55b767f96e4 100644 --- a/neutron/plugins/common/utils.py +++ b/neutron/plugins/common/utils.py @@ -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 index 00000000000..ffb4d74b6e6 --- /dev/null +++ b/neutron/services/vpn/agent.py @@ -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 index 00000000000..9b27a7520f1 --- /dev/null +++ b/neutron/services/vpn/common/__init__.py @@ -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 index 00000000000..87df69ce802 --- /dev/null +++ b/neutron/services/vpn/common/topics.py @@ -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 index 00000000000..c15fcbfd9b4 --- /dev/null +++ b/neutron/services/vpn/device_drivers/__init__.py @@ -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 index 00000000000..1ede7543ac7 --- /dev/null +++ b/neutron/services/vpn/device_drivers/ipsec.py @@ -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 index 00000000000..180b4a19e5f --- /dev/null +++ b/neutron/services/vpn/device_drivers/template/openswan/ipsec.conf.template @@ -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 index 00000000000..8302e859fec --- /dev/null +++ b/neutron/services/vpn/device_drivers/template/openswan/ipsec.secret.template @@ -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 %} diff --git a/neutron/services/vpn/plugin.py b/neutron/services/vpn/plugin.py index 6ba7c093329..3d58354674e 100644 --- a/neutron/services/vpn/plugin.py +++ b/neutron/services/vpn/plugin.py @@ -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 index 00000000000..4996882e1c0 --- /dev/null +++ b/neutron/services/vpn/service_drivers/__init__.py @@ -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 index 00000000000..2fea0a0bec2 --- /dev/null +++ b/neutron/services/vpn/service_drivers/ipsec.py @@ -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 diff --git a/neutron/tests/unit/metaplugin/test_basic.py b/neutron/tests/unit/metaplugin/test_basic.py index 0445e50a19e..4697bef4d94 100644 --- a/neutron/tests/unit/metaplugin/test_basic.py +++ b/neutron/tests/unit/metaplugin/test_basic.py @@ -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, diff --git a/neutron/tests/unit/nicira/test_nicira_plugin.py b/neutron/tests/unit/nicira/test_nicira_plugin.py index f2dc40a6edd..044f86b91e5 100644 --- a/neutron/tests/unit/nicira/test_nicira_plugin.py +++ b/neutron/tests/unit/nicira/test_nicira_plugin.py @@ -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 index 00000000000..9b27a7520f1 --- /dev/null +++ b/neutron/tests/unit/services/vpn/device_drivers/__init__.py @@ -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 index 00000000000..a06d6f9f9dd --- /dev/null +++ b/neutron/tests/unit/services/vpn/device_drivers/test_ipsec.py @@ -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 index 00000000000..9b27a7520f1 --- /dev/null +++ b/neutron/tests/unit/services/vpn/service_drivers/__init__.py @@ -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 index 00000000000..863d29b8f65 --- /dev/null +++ b/neutron/tests/unit/services/vpn/service_drivers/test_ipsec.py @@ -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 index 00000000000..623bfcc97cb --- /dev/null +++ b/neutron/tests/unit/services/vpn/test_vpn_agent.py @@ -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 index 00000000000..8c25d7e4ccf --- /dev/null +++ b/neutron/tests/unit/services/vpn/test_vpnaas_driver_plugin.py @@ -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']) diff --git a/neutron/tests/unit/test_l3_plugin.py b/neutron/tests/unit/test_l3_plugin.py index c2412c8794c..3c950e0b023 100644 --- a/neutron/tests/unit/test_l3_plugin.py +++ b/neutron/tests/unit/test_l3_plugin.py @@ -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 diff --git a/requirements.txt b/requirements.txt index 24cbc549df0..b96b9e2b321 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.cfg b/setup.cfg index e5f73eb5c65..ad9a9fd6dab 100644 --- 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