vpn namespace wrapper

strongSwan doesn't support namespace natively, this wrapper
will use "mount --bind" to simulate the ns like this:

sudo neutron-rootwrap /etc/neutron/rootwrap.conf ip netns \
exec <namespace-id> neutron-netns-wrapper --mount_paths \
=/etc:/var/lib/neutron/vpnaas/<xxxx-id>/etc, \
/var/run:/var/lib/neutron/vpnaas/<xxxx-id>/var/run \
--cmd=ipsec,status

Both sudoers and rootwrap.conf will not exist in the
directory /etc after bind-mount, thus we can't use
utils.execute(cmd, conf.root_helper) in
neutron/agent/linux/utils.py. so implement a function
execte(cmd) in this wrapper as an alternative. then we can
use root_helper to invoke this wrapper to make sure all
commands are still running as root as below code shows.
Finally, also need to check in wrapper if cmd matches
CommandFilter based on the same reason.
ip_wrapper = ip_lib.IPWrapper(root_helper, namespace)
    ip_wrapper.netns.execute(
    [NS_WRAPPER,
    '--mount_paths=/etc:%s/etc,/var/run:%s/var/run' % (
    self.config_dir, self.config_dir),
    '--cmd=%s' % ','.join(cmd)],
    check_exit_code=check_exit_code)

We are using check of net namespace (since linux 3.0),
instead of mount namespace (since Linux 3.8), as older
kernels do not support mount namespace. In addition,
mount --bind has been available since Linux 2.4. so we
don't need to worry kilo's minumum kernel requirement.

This patch is based on patchset67 of nachi's initial
vpnaas implementation, many thanks to nachi.

submit this wrapper as a separate review from [1].

[1] https://review.openstack.org/#/c/144391/

Partially-implements: blueprint ipsec-strongswan-driver

Change-Id: Icc80b9102acb87170f2d1cda06c848fa71bb1634
This commit is contained in:
Zhang Hua 2015-01-23 09:55:53 +08:00
parent dbf0b711c2
commit 0cf7671b0a
5 changed files with 244 additions and 0 deletions

View File

@ -11,3 +11,5 @@
ip: IpFilter, ip, root
ip_exec: IpNetnsExecFilter, ip, root
openswan: CommandFilter, ipsec, root
neutron_netns_wrapper: CommandFilter, neutron-vpn-netns-wrapper, root
neutron_netns_wrapper_local: CommandFilter, /usr/local/bin/neutron-vpn-netns-wrapper, root

View File

@ -0,0 +1,157 @@
# Copyright (c) 2015 OpenStack Foundation.
# 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 ConfigParser
import errno
import os
import sys
from eventlet.green import subprocess
from oslo.config import cfg
from oslo.rootwrap import wrapper
from neutron.common import config
from neutron.common import utils
from neutron.i18n import _LE, _LI
from neutron.openstack.common import log as logging
LOG = logging.getLogger(__name__)
def setup_conf():
cli_opts = [
cfg.DictOpt('mount_paths',
required=True,
help=_('Dict of paths to bind-mount (source:target) '
'prior to launch subprocess.')),
cfg.ListOpt(
'cmd',
required=True,
help=_('Command line to execute as a subprocess '
'provided as comma-separated list of arguments.')),
cfg.StrOpt('rootwrap_config', default='/etc/neutron/rootwrap.conf',
help=_('Rootwrap configuration file.')),
]
conf = cfg.CONF
conf.register_cli_opts(cli_opts)
return conf
def execute(cmd):
if not cmd:
return
cmd = map(str, cmd)
LOG.debug("Running command: %s", cmd)
env = os.environ.copy()
obj = utils.subprocess_popen(cmd, shell=False,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env)
_stdout, _stderr = obj.communicate()
LOG.debug('Command: %(cmd)s Exit code: %(returncode)s '
'Stdout: %(stdout)s Stderr: %(stderr)s',
{'cmd': cmd,
'returncode': obj.returncode,
'stdout': _stdout,
'stderr': _stderr})
obj.stdin.close()
return obj.returncode
def filter_command(command, rootwrap_config):
# Load rootwrap configuration
try:
rawconfig = ConfigParser.RawConfigParser()
rawconfig.read(rootwrap_config)
rw_config = wrapper.RootwrapConfig(rawconfig)
except ValueError as exc:
LOG.error(_LE('Incorrect value in %(config)s: %(exc)s'),
{'config': rootwrap_config, 'exc': exc.message})
sys.exit(errno.EINVAL)
except ConfigParser.Error:
LOG.error(_LE('Incorrect configuration file: %(config)s'),
{'config': rootwrap_config})
sys.exit(errno.EINVAL)
# Check if command matches any of the loaded filters
filters = wrapper.load_filters(rw_config.filters_path)
try:
wrapper.match_filter(filters, command, exec_dirs=rw_config.exec_dirs)
except wrapper.FilterMatchNotExecutable as exc:
LOG.error(_LE('Command %(command)s is not executable: '
'%(path)s (filter match = %(name)s)'),
{'command': command,
'path': exc.match.exec_path,
'name': exc.match.name})
sys.exit(errno.EINVAL)
except wrapper.NoFilterMatched:
LOG.error(_LE('Unauthorized command: %(cmd)s (no filter matched)'),
{'cmd': command})
sys.exit(errno.EPERM)
def execute_with_mount():
conf = setup_conf()
conf()
config.setup_logging()
if not conf.cmd:
LOG.error(_LE('No command provided, exiting'))
return errno.EINVAL
if not conf.mount_paths:
LOG.error(_LE('No mount path provided, exiting'))
return errno.EINVAL
# Both sudoers and rootwrap.conf will not exist in the directory /etc
# after bind-mount, so we can't use utils.execute(conf.cmd,
# conf.root_helper). That's why we have to check here if cmd matches
# CommandFilter
filter_command(conf.cmd, conf.rootwrap_config)
# Make sure the process is running in net namespace invoked by ip
# netns exec(/proc/[pid]/ns/net) which is since Linux 3.0,
# as we can't check mount namespace(/proc/[pid]/ns/mnt)
# which is since Linux 3.8. For more detail please refer the link
# http://man7.org/linux/man-pages/man7/namespaces.7.html
if os.path.samefile(os.path.join('/proc/1/ns/net'),
os.path.join('/proc', str(os.getpid()), 'ns/net')):
LOG.error(_LE('Cannot run without netns, exiting'))
return errno.EINVAL
for path, new_path in conf.mount_paths.iteritems():
if not os.path.isdir(new_path):
# Sometimes all directories are not ready
LOG.debug('%s is not directory', new_path)
continue
if os.path.isdir(path) and os.path.isabs(path):
return_code = execute(['mount', '--bind', new_path, path])
if return_code == 0:
LOG.info(_LI('%(new_path)s has been '
'bind-mounted in %(path)s'),
{'new_path': new_path, 'path': path})
else:
LOG.error(_LE('Failed to bind-mount '
'%(new_path)s in %(path)s'),
{'new_path': new_path, 'path': path})
return execute(conf.cmd)
def main():
sys.exit(execute_with_mount())
if __name__ == "__main__":
main()

View File

@ -0,0 +1,84 @@
# Copyright (c) 2015 OpenStack Foundation.
# 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.tests import base
from neutron_vpnaas.services.vpn.common import netns_wrapper as nswrap
class TestNetnsWrapper(base.BaseTestCase):
def setUp(self):
super(TestNetnsWrapper, self).setUp()
patch_methods = ['filter_command',
'execute',
'setup_conf']
for method in patch_methods:
self.patch_obj(nswrap, method)
patch_classes = ['neutron.common.config.setup_logging',
'os.path.isdir',
'os.path.samefile',
'sys.exit']
for cls in patch_classes:
self.patch_cls(cls)
self.filter_command.return_value = False
self.execute.return_value = 0
self.conf = mock.Mock()
self.conf.cmd = 'ls,-al'
self.conf.mount_paths = {'/foo': '/dir/foo',
'/var': '/dir/var'}
self.setup_conf.return_value = self.conf
self.conf.rootwrap_config = 'conf'
self.isdir.return_value = True
self.samefile.return_value = False
def patch_obj(self, obj, method):
_m = mock.patch.object(obj, method)
_mock = _m.start()
setattr(self, method, _mock)
def patch_cls(self, patch_class):
_m = mock.patch(patch_class)
mock_name = patch_class.split('.')[-1]
_mock = _m.start()
setattr(self, mock_name, _mock)
def test_netns_wrap_fail_without_netns(self):
self.samefile.return_value = True
return_val = nswrap.execute_with_mount()
self.assertTrue(return_val)
def test_netns_wrap(self):
self.conf.cmd = 'ls,-al'
return_val = nswrap.execute_with_mount()
exp_calls = [mock.call(['mount', '--bind', '/dir/foo', '/foo']),
mock.call(['mount', '--bind', '/dir/var', '/var']),
mock.call('ls,-al')]
self.execute.assert_has_calls(exp_calls, any_order=False)
self.assertFalse(return_val)
def test_netns_wrap_fail_without_cmd(self):
self.conf.cmd = None
return_val = nswrap.execute_with_mount()
self.assertFalse(self.execute.called)
self.assertTrue(return_val)
def test_netns_wrap_fail_without_mount_paths(self):
self.conf.mount_paths = None
return_val = nswrap.execute_with_mount()
self.assertFalse(self.execute.called)
self.assertTrue(return_val)

View File

@ -32,6 +32,7 @@ setup-hooks =
[entry_points]
console_scripts =
neutron-vpn-netns-wrapper = neutron_vpnaas.services.vpn.common.netns_wrapper:main
neutron-vpn-agent = neutron_vpnaas.services.vpn.agent:main
device_drivers =
neutron.services.vpn.device_drivers.ipsec.OpenSwanDriver = neutron_vpnaas.services.vpn.device_drivers.ipsec:OpenSwanDriver