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