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
This commit is contained in:
Nachi Ueno 2013-06-05 16:24:38 -07:00
parent c4a52721fe
commit 704d67a064
20 changed files with 1945 additions and 20 deletions

13
etc/vpn_agent.ini Normal file
View File

@ -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

View 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']

View File

@ -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')

View File

@ -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.

View File

@ -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'

View File

@ -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

View File

@ -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)

View File

@ -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 %}

View File

@ -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 %}

View 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)

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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.

View File

@ -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])

View File

@ -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)

View File

@ -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'])

View 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

View File

@ -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