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:
parent
c4a52721fe
commit
704d67a064
13
etc/vpn_agent.ini
Normal file
13
etc/vpn_agent.ini
Normal 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
|
@ -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']
|
||||
|
148
neutron/services/vpn/agent.py
Normal file
148
neutron/services/vpn/agent.py
Normal 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')
|
16
neutron/services/vpn/common/__init__.py
Normal file
16
neutron/services/vpn/common/__init__.py
Normal 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.
|
20
neutron/services/vpn/common/topics.py
Normal file
20
neutron/services/vpn/common/topics.py
Normal 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'
|
36
neutron/services/vpn/device_drivers/__init__.py
Normal file
36
neutron/services/vpn/device_drivers/__init__.py
Normal 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
|
687
neutron/services/vpn/device_drivers/ipsec.py
Normal file
687
neutron/services/vpn/device_drivers/ipsec.py
Normal 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)
|
@ -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 %}
|
@ -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 %}
|
@ -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)
|
||||
|
39
neutron/services/vpn/service_drivers/__init__.py
Normal file
39
neutron/services/vpn/service_drivers/__init__.py
Normal 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
|
189
neutron/services/vpn/service_drivers/ipsec.py
Normal file
189
neutron/services/vpn/service_drivers/ipsec.py
Normal 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
|
16
neutron/tests/unit/services/vpn/device_drivers/__init__.py
Normal file
16
neutron/tests/unit/services/vpn/device_drivers/__init__.py
Normal 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.
|
154
neutron/tests/unit/services/vpn/device_drivers/test_ipsec.py
Normal file
154
neutron/tests/unit/services/vpn/device_drivers/test_ipsec.py
Normal 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)
|
16
neutron/tests/unit/services/vpn/service_drivers/__init__.py
Normal file
16
neutron/tests/unit/services/vpn/service_drivers/__init__.py
Normal 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.
|
@ -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])
|
191
neutron/tests/unit/services/vpn/test_vpn_agent.py
Normal file
191
neutron/tests/unit/services/vpn/test_vpn_agent.py
Normal 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)
|
156
neutron/tests/unit/services/vpn/test_vpnaas_driver_plugin.py
Normal file
156
neutron/tests/unit/services/vpn/test_vpnaas_driver_plugin.py
Normal 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'])
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user