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
This commit is contained in:
parent
b97825874b
commit
094d2d5f01
36
neutron_fwaas/privileged/tests/functional/utils.py
Normal file
36
neutron_fwaas/privileged/tests/functional/utils.py
Normal file
@ -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:[<inode>]"
|
||||
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
|
79
neutron_fwaas/privileged/utils.py
Normal file
79
neutron_fwaas/privileged/utils.py
Normal file
@ -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)
|
40
neutron_fwaas/tests/functional/privileged/test_utils.py
Normal file
40
neutron_fwaas/tests/functional/privileged/test_utils.py
Normal file
@ -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)))
|
0
neutron_fwaas/tests/unit/privileged/__init__.py
Normal file
0
neutron_fwaas/tests/unit/privileged/__init__.py
Normal file
97
neutron_fwaas/tests/unit/privileged/test_utils.py
Normal file
97
neutron_fwaas/tests/unit/privileged/test_utils.py
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user