From 0cf7671b0a990d0e0583a14bea22c53c4152d2ad Mon Sep 17 00:00:00 2001 From: Zhang Hua Date: Fri, 23 Jan 2015 09:55:53 +0800 Subject: [PATCH] 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 neutron-netns-wrapper --mount_paths \ =/etc:/var/lib/neutron/vpnaas//etc, \ /var/run:/var/lib/neutron/vpnaas//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 --- etc/neutron/rootwrap.d/vpnaas.filters | 2 + .../services/vpn/common/netns_wrapper.py | 157 ++++++++++++++++++ .../unit/services/vpn/common/__init__.py | 0 .../vpn/common/test_agent_netns_wrapper.py | 84 ++++++++++ setup.cfg | 1 + 5 files changed, 244 insertions(+) create mode 100644 neutron_vpnaas/services/vpn/common/netns_wrapper.py create mode 100644 neutron_vpnaas/tests/unit/services/vpn/common/__init__.py create mode 100644 neutron_vpnaas/tests/unit/services/vpn/common/test_agent_netns_wrapper.py diff --git a/etc/neutron/rootwrap.d/vpnaas.filters b/etc/neutron/rootwrap.d/vpnaas.filters index 7848136b9..0a12d81f0 100644 --- a/etc/neutron/rootwrap.d/vpnaas.filters +++ b/etc/neutron/rootwrap.d/vpnaas.filters @@ -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 diff --git a/neutron_vpnaas/services/vpn/common/netns_wrapper.py b/neutron_vpnaas/services/vpn/common/netns_wrapper.py new file mode 100644 index 000000000..98a2c71ff --- /dev/null +++ b/neutron_vpnaas/services/vpn/common/netns_wrapper.py @@ -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() diff --git a/neutron_vpnaas/tests/unit/services/vpn/common/__init__.py b/neutron_vpnaas/tests/unit/services/vpn/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_vpnaas/tests/unit/services/vpn/common/test_agent_netns_wrapper.py b/neutron_vpnaas/tests/unit/services/vpn/common/test_agent_netns_wrapper.py new file mode 100644 index 000000000..01e62a741 --- /dev/null +++ b/neutron_vpnaas/tests/unit/services/vpn/common/test_agent_netns_wrapper.py @@ -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) diff --git a/setup.cfg b/setup.cfg index 424d5f0ed..08ca66c5b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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