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:
Cedric Brandily 2017-02-14 13:36:02 +01:00 committed by Ha Van Tu
parent b97825874b
commit 094d2d5f01
5 changed files with 252 additions and 0 deletions

View 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

View 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)

View 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)))

View 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