From 094d2d5f01914fbebd4e16cc2c41390a72b5874d Mon Sep 17 00:00:00 2001 From: Cedric Brandily Date: Tue, 14 Feb 2017 13:36:02 +0100 Subject: [PATCH] Define in_namespace contextmanager This change defines the contextmanager in_namespace[1]. It moves current process in a network namespace (in __enter__) and moves it back in its original network namespace (in _exit__) or kills current process if __exit__ fails in order to ensure following commands will be executed in the correct network namespace. This change is an enabler to the Netlink solution to clean conntrack entries. [1] neutron_fwaas.privileged.utils Partial-Bug: #1664294 Change-Id: I587257db8e1fce56a95f0db3dc4e0752751fdd81 --- .../privileged/tests/functional/utils.py | 36 +++++++ neutron_fwaas/privileged/utils.py | 79 +++++++++++++++ .../tests/functional/privileged/test_utils.py | 40 ++++++++ .../tests/unit/privileged/__init__.py | 0 .../tests/unit/privileged/test_utils.py | 97 +++++++++++++++++++ 5 files changed, 252 insertions(+) create mode 100644 neutron_fwaas/privileged/tests/functional/utils.py create mode 100644 neutron_fwaas/privileged/utils.py create mode 100644 neutron_fwaas/tests/functional/privileged/test_utils.py create mode 100644 neutron_fwaas/tests/unit/privileged/__init__.py create mode 100644 neutron_fwaas/tests/unit/privileged/test_utils.py diff --git a/neutron_fwaas/privileged/tests/functional/utils.py b/neutron_fwaas/privileged/tests/functional/utils.py new file mode 100644 index 000000000..ea3a872f1 --- /dev/null +++ b/neutron_fwaas/privileged/tests/functional/utils.py @@ -0,0 +1,36 @@ +# Copyright (c) 2017 Thales Services SAS +# 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 os +import re + +from neutron_fwaas import privileged +from neutron_fwaas.privileged import utils + + +def get_my_netns_inode(): + link = os.readlink(utils.PROCESS_NETNS) + + # NOTE(cby): link respects the format "net:[]" + return int(re.match('net:\[(\d+)\]', link).group(1)) + + +@privileged.default.entrypoint +def get_in_namespace_netns_inodes(namespace): + before = get_my_netns_inode() + with utils.in_namespace(namespace): + inside = get_my_netns_inode() + after = get_my_netns_inode() + return before, inside, after diff --git a/neutron_fwaas/privileged/utils.py b/neutron_fwaas/privileged/utils.py new file mode 100644 index 000000000..153d0942b --- /dev/null +++ b/neutron_fwaas/privileged/utils.py @@ -0,0 +1,79 @@ +# Copyright (c) 2017 Thales Services SAS +# 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 ctypes +import os + +from oslo_log import log as logging +from pyroute2 import netns as pynetns +import six + +from neutron_fwaas._i18n import _ + + +PROCESS_NETNS = '/proc/self/ns/net' + +LOG = logging.getLogger(__name__) + + +class BackInNamespaceExit(SystemExit): + """Raised if we fail to moved back process in its original namespace.""" + + +def setns(netns): + """Mimic future pyroute2 setns.""" + if isinstance(netns, six.string_types): + return pynetns.setns(netns) + + # NOTE(cby): netns is a netns fd + libc = ctypes.CDLL('libc.so.6', use_errno=True) + error = libc.syscall(pynetns.__NR_setns, netns, pynetns.CLONE_NEWNET) + if error: + raise OSError(ctypes.get_errno(), 'failed to open netns', netns) + return netns + + +@contextlib.contextmanager +def in_namespace(namespace): + """Move current process in a specific namespace. + + This contextmanager moves current process in a specific namespace and + ensures to move it back in original namespace or kills it if we fail to + move back in original namespace. + """ + if not namespace: + yield + return + + org_netns_fd = os.open(PROCESS_NETNS, os.O_RDONLY) + try: + new_netns_fd = setns(namespace) + try: + try: + yield + finally: + try: + # NOTE(cby): this code is not executed only if we fail to + # move in target namespace + setns(org_netns_fd) + except Exception as e: + msg = _('Failed to move back in original netns: %s') % e + LOG.critical(msg) + raise BackInNamespaceExit(msg) + finally: + os.close(new_netns_fd) + finally: + os.close(org_netns_fd) diff --git a/neutron_fwaas/tests/functional/privileged/test_utils.py b/neutron_fwaas/tests/functional/privileged/test_utils.py new file mode 100644 index 000000000..5110da918 --- /dev/null +++ b/neutron_fwaas/tests/functional/privileged/test_utils.py @@ -0,0 +1,40 @@ +# Copyright (c) 2017 Thales Services SAS +# 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 os + +from neutron.tests.common import net_helpers +from neutron.tests.functional import base + +from neutron_fwaas.privileged.tests.functional import utils + + +def get_netns_inode(namespace): + return os.stat('/var/run/netns/%s' % namespace).st_ino + + +class InNamespaceTest(base.BaseSudoTestCase): + + def test_in_namespace(self): + namespace = self.useFixture(net_helpers.NamespaceFixture()).name + expected = get_netns_inode(namespace) + before, observed, after = utils.get_in_namespace_netns_inodes( + namespace) + self.assertEqual(expected, observed) + self.assertEqual(before, after) + + def test_in_no_namespace(self): + inodes = utils.get_in_namespace_netns_inodes(None) + self.assertEqual(1, len(set(inodes))) diff --git a/neutron_fwaas/tests/unit/privileged/__init__.py b/neutron_fwaas/tests/unit/privileged/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/privileged/test_utils.py b/neutron_fwaas/tests/unit/privileged/test_utils.py new file mode 100644 index 000000000..d6e8f351e --- /dev/null +++ b/neutron_fwaas/tests/unit/privileged/test_utils.py @@ -0,0 +1,97 @@ +# Copyright (c) 2017 Thales Services SAS +# 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 +import testtools + +from neutron_fwaas.privileged import utils +from neutron_fwaas.tests import base + + +class InNamespaceTest(base.BaseTestCase): + ORG_NETNS_FD = 124 + NEW_NETNS_FD = 421 + NEW_NETNS = 'newns' + + def setUp(self): + super(InNamespaceTest, self).setUp() + + # NOTE(cby): we should unmock os.open/close as early as possible + # because there are used in cleanups + open_patch = mock.patch('os.open', return_value=self.ORG_NETNS_FD) + self.open_mock = open_patch.start() + self.addCleanup(open_patch.stop) + + close_patch = mock.patch('os.close') + self.close_mock = close_patch.start() + self.addCleanup(close_patch.stop) + + self.setns_mock = mock.patch.object( + utils, 'setns', side_effect=self.fake_setns + ).start() + + def fake_setns(self, setns): + if setns is self.ORG_NETNS_FD: + return self.ORG_NETNS_FD + elif setns is self.NEW_NETNS: + return self.NEW_NETNS_FD + else: + self.fail('invalid netns name') + + def test_in_namespace(self): + with utils.in_namespace(self.NEW_NETNS): + self.setns_mock.assert_called_once_with(self.NEW_NETNS) + + setns_calls = [mock.call(self.NEW_NETNS), + mock.call(self.ORG_NETNS_FD)] + close_calls = [mock.call(self.NEW_NETNS_FD), + mock.call(self.ORG_NETNS_FD)] + self.setns_mock.assert_has_calls(setns_calls) + self.close_mock.assert_has_calls(close_calls) + + def test_in_no_namespace(self): + for namespace in ('', None): + with utils.in_namespace(namespace): + pass + self.setns_mock.assert_not_called() + self.close_mock.assert_not_called() + + def test_in_namespace_failed(self): + with testtools.ExpectedException(ValueError): + with utils.in_namespace(self.NEW_NETNS): + self.setns_mock.assert_called_once_with(self.NEW_NETNS) + raise ValueError + + setns_calls = [mock.call(self.NEW_NETNS), + mock.call(self.ORG_NETNS_FD)] + close_calls = [mock.call(self.NEW_NETNS_FD), + mock.call(self.ORG_NETNS_FD)] + self.setns_mock.assert_has_calls(setns_calls) + self.close_mock.assert_has_calls(close_calls) + + def test_in_namespace_enter_failed(self): + self.setns_mock.side_effect = ValueError + with testtools.ExpectedException(ValueError): + with utils.in_namespace(self.NEW_NETNS): + self.fail('It should fail before we reach this code') + + self.setns_mock.assert_called_once_with(self.NEW_NETNS) + self.close_mock.assert_called_once_with(self.ORG_NETNS_FD) + + def test_in_namespace_exit_failed(self): + self.setns_mock.side_effect = [self.NEW_NETNS_FD, ValueError] + with testtools.ExpectedException(utils.BackInNamespaceExit): + with utils.in_namespace(self.NEW_NETNS): + pass