Namespace_veth: Add funtionality for network namespace, veth ports 29/16129/3
authorChristian Trautman <ctrautma@redhat.com>
Wed, 29 Jun 2016 15:22:41 +0000 (11:22 -0400)
committerChristian Trautman <ctrautma@redhat.com>
Thu, 7 Jul 2016 15:18:13 +0000 (11:18 -0400)
Adds functionality for network namespaces and veth/peer ports
to connect the namespaces. The namespaces are tracked outside
of default Linux namespaces to prevent possible disruption of
work outside of VSPerf where a user may create a namespace and
the cleanup code deletes it. The cleanup code only removes
namespaces and veth ports created inside the testcase.

JIRA: VSPERF-310

Change-Id: If8881cafb119f38f052403a1de497e9660187d2e
Signed-off-by: Christian Trautman <ctrautma@redhat.com>
testcases/integration.py
testcases/testcase.py
tools/namespace.py [new file with mode: 0644]
tools/veth.py [new file with mode: 0644]

index fdd8b09..ffde582 100644 (file)
@@ -22,10 +22,13 @@ import copy
 from testcases import TestCase
 from conf import settings as S
 from collections import OrderedDict
+from tools import namespace
+from tools import veth
 from core.loader import Loader
 
 CHECK_PREFIX = 'validate_'
 
+
 class IntegrationTestCase(TestCase):
     """IntegrationTestCase class
     """
@@ -115,6 +118,10 @@ class IntegrationTestCase(TestCase):
                                     step_ok = False
                                     if step[0] == 'vswitch':
                                         test_object = self._vswitch_ctl.get_vswitch()
+                                    elif step[0] == 'namespace':
+                                        test_object = namespace
+                                    elif step[0] == 'veth':
+                                        test_object = veth
                                     elif step[0] == 'trafficgen':
                                         test_object = self._traffic_ctl
                                         # in case of send_traffic method, ensure that specified
index e5f8a14..5f5c935 100644 (file)
@@ -234,6 +234,26 @@ class TestCase(object):
         # restore original settings
         S.load_from_dict(self._settings_original)
 
+        # cleanup any namespaces created
+        if os.path.isdir('/tmp/namespaces'):
+            import tools.namespace
+            namespace_list = os.listdir('/tmp/namespaces')
+            if len(namespace_list):
+                self._logger.info('Cleaning up namespaces')
+            for name in namespace_list:
+                tools.namespace.delete_namespace(name)
+            os.rmdir('/tmp/namespaces')
+        # cleanup any veth ports created
+        if os.path.isdir('/tmp/veth'):
+            import tools.veth
+            veth_list = os.listdir('/tmp/veth')
+            if len(veth_list):
+                self._logger.info('Cleaning up veth ports')
+            for eth in veth_list:
+                port1, port2 = eth.split('-')
+                tools.veth.del_veth_port(port1, port2)
+            os.rmdir('/tmp/veth')
+
     def run_report(self):
         """ Report test results
         """
diff --git a/tools/namespace.py b/tools/namespace.py
new file mode 100644 (file)
index 0000000..e6bcd81
--- /dev/null
@@ -0,0 +1,178 @@
+# Copyright 2016 Red Hat Corporation.
+#
+# 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.
+
+
+"""
+Network namespace emulation
+"""
+
+import logging
+import os
+
+from tools import tasks
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def add_ip_to_namespace_eth(port, name, ip_addr, cidr):
+    """
+    Assign port ip address in namespace
+    :param port: port to assign ip to
+    :param name: namespace where port resides
+    :param ip_addr: ip address in dot notation format
+    :param cidr: cidr as string
+    :return:
+    """
+    ip_string = '{}/{}'.format(ip_addr, cidr)
+    tasks.run_task(['sudo', 'ip', 'netns', 'exec', name,
+                    'ip', 'addr', 'add', ip_string, 'dev', port],
+                   _LOGGER, 'Assigning ip to port {}...'.format(port), False)
+
+
+def assign_port_to_namespace(port, name, port_up=False):
+    """
+    Assign NIC port to namespace
+    :param port: port name as string
+    :param name: namespace name as string
+    :param port_up: Boolean if the port should be brought up on assignment
+    :return: None
+    """
+    tasks.run_task(['sudo', 'ip', 'link', 'set',
+                    'netns', name, 'dev', port],
+                   _LOGGER, 'Assigning port {} to namespace {}...'.format(
+                       port, name), False)
+    if port_up:
+        tasks.run_task(['sudo', 'ip', 'netns', 'exec', name,
+                        'ip', 'link', 'set', port, 'up'],
+                       _LOGGER, 'Bringing up port {}...'.format(port), False)
+
+
+def create_namespace(name):
+    """
+    Create a linux namespace. Raises RuntimeError if namespace already exists
+    in the system.
+    :param name: name of the namespace to be created as string
+    :return: None
+    """
+    if name in get_system_namespace_list():
+        raise RuntimeError('Namespace already exists in system')
+
+    # touch some files in a tmp area so we can track them separately from
+    # the OS's internal namespace tracking. This allows us to track VSPerf
+    # created namespaces so they can be cleaned up if needed.
+    if not os.path.isdir('/tmp/namespaces'):
+        try:
+            os.mkdir('/tmp/namespaces')
+        except os.error:
+            # OK don't crash, but cleanup may be an issue...
+            _LOGGER.error('Unable to create namespace temp folder.')
+            _LOGGER.error(
+                'Namespaces will not be removed on test case completion')
+    if os.path.isdir('/tmp/namespaces'):
+        with open('/tmp/namespaces/{}'.format(name), 'a'):
+            os.utime('/tmp/namespaces/{}'.format(name), None)
+
+    tasks.run_task(['sudo', 'ip', 'netns', 'add', name], _LOGGER,
+                   'Creating namespace {}...'.format(name), False)
+    tasks.run_task(['sudo', 'ip', 'netns', 'exec', name,
+                    'ip', 'link', 'set', 'lo', 'up'], _LOGGER,
+                   'Enabling loopback interface...', False)
+
+
+def delete_namespace(name):
+    """
+    Delete linux network namespace
+    :param name: namespace to delete
+    :return: None
+    """
+    # delete the file if it exists in the temp area
+    if os.path.exists('/tmp/namespaces/{}'.format(name)):
+        os.remove('/tmp/namespaces/{}'.format(name))
+    tasks.run_task(['sudo', 'ip', 'netns', 'delete', name], _LOGGER,
+                   'Deleting namespace {}...'.format(name), False)
+
+
+def get_system_namespace_list():
+    """
+    Return tuple of strings for namespaces on the system
+    :return: tuple of namespaces as string
+    """
+    return tuple(os.listdir('/var/run/netns'))
+
+
+def get_vsperf_namespace_list():
+    """
+    Return a tuple of strings for namespaces created by vsperf testcase
+    :return: tuple of namespaces as string
+    """
+    if os.path.isdir('/tmp/namespaces'):
+        return tuple(os.listdir('/tmp/namespaces'))
+    else:
+        return []
+
+
+def reset_port_to_root(port, name):
+    """
+    Return the assigned port to the root namespace
+    :param port: port to return as string
+    :param name: namespace the port currently resides
+    :return: None
+    """
+    tasks.run_task(['sudo', 'ip', 'netns', 'exec', name,
+                    'ip', 'link', 'set', port, 'netns', '1'],
+                   _LOGGER, 'Assigning port {} to namespace {}...'.format(
+                       port, name), False)
+
+
+# pylint: disable=unused-argument
+# pylint: disable=invalid-name
+def validate_add_ip_to_namespace_eth(result, port, name, ip_addr, cidr):
+    """
+    Validation function for integration testcases
+    """
+    ip_string = '{}/{}'.format(ip_addr, cidr)
+    return ip_string in ''.join(tasks.run_task(
+        ['sudo', 'ip', 'netns', 'exec', name, 'ip', 'addr', 'show', port],
+        _LOGGER, 'Validating ip address in namespace...', False))
+
+
+def validate_assign_port_to_namespace(result, port, name, port_up=False):
+    """
+    Validation function for integration testcases
+    """
+    # this could be improved...its not 100% accurate
+    return port in ''.join(tasks.run_task(
+        ['sudo', 'ip', 'netns', 'exec', name, 'ip', 'addr'],
+        _LOGGER, 'Validating port in namespace...'))
+
+
+def validate_create_namespace(result, name):
+    """
+    Validation function for integration testcases
+    """
+    return name in get_system_namespace_list()
+
+
+def validate_delete_namespace(result, name):
+    """
+    Validation function for integration testcases
+    """
+    return name not in get_system_namespace_list()
+
+
+def validate_reset_port_to_root(result, port, name):
+    """
+    Validation function for integration testcases
+    """
+    return not validate_assign_port_to_namespace(result, port, name)
diff --git a/tools/veth.py b/tools/veth.py
new file mode 100644 (file)
index 0000000..6418d11
--- /dev/null
@@ -0,0 +1,118 @@
+# Copyright 2016 Red Hat Corporation.
+#
+# 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.
+
+"""
+veth port emulation
+"""
+
+import logging
+import os
+
+from tools import tasks
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def add_veth_port(port, peer_port):
+    """
+    Add a veth port
+    :param port:port name for the first port
+    :param peer_port: port name for the peer port
+    :return: None
+    """
+    # touch some files in a tmp area so we can track them. This allows us to
+    # track VSPerf created veth ports so they can be cleaned up if needed.
+    if not os.path.isdir('/tmp/veth'):
+        try:
+            os.mkdir('/tmp/veth')
+        except os.error:
+            # OK don't crash but cleanup may be an issue
+            _LOGGER.error('Unable to create veth temp folder.')
+            _LOGGER.error(
+                'Veth ports may not be removed on testcase completion')
+    if os.path.isdir('/tmp/veth'):
+        with open('/tmp/veth/{}-{}'.format(port, peer_port), 'a'):
+            os.utime('/tmp/veth/{}-{}'.format(port, peer_port), None)
+    tasks.run_task(['sudo', 'ip', 'link', 'add',
+                    port, 'type', 'veth', 'peer', 'name', peer_port],
+                   _LOGGER, 'Adding veth port {} with peer port {}...'.format(
+                       port, peer_port), False)
+
+
+def bring_up_eth_port(eth_port, namespace=None):
+    """
+    Bring up an eth port
+    :param eth_port: string of eth port to bring up
+    :param namespace: Namespace eth port it located if needed
+    :return: None
+    """
+    if namespace:
+        tasks.run_task(['sudo', 'ip', 'netns', 'exec', namespace,
+                        'ip', 'link', 'set', eth_port, 'up'],
+                       _LOGGER,
+                       'Bringing up port {} in namespace {}...'.format(
+                           eth_port, namespace), False)
+    else:
+        tasks.run_task(['sudo', 'ip', 'link', 'set', eth_port, 'up'],
+                       _LOGGER, 'Bringing up port...', False)
+
+
+def del_veth_port(port, peer_port):
+    """
+    Delete the veth ports, the peer will automatically be deleted on deletion
+    of the first port param
+    :param port: port name to delete
+    :param port: peer port name
+    :return: None
+    """
+    # delete the file if it exists in the temp area
+    if os.path.exists('/tmp/veth/{}-{}'.format(port, peer_port)):
+        os.remove('/tmp/veth/{}-{}'.format(port, peer_port))
+    tasks.run_task(['sudo', 'ip', 'link', 'del', port],
+                   _LOGGER, 'Deleting veth port {} with peer {}...'.format(
+                       port, peer_port), False)
+
+
+# pylint: disable=unused-argument
+def validate_add_veth_port(result, port, peer_port):
+    """
+    Validation function for integration testcases
+    """
+    devs = os.listdir('/sys/class/net')
+    return all([port in devs, peer_port in devs])
+
+
+def validate_bring_up_eth_port(result, eth_port, namespace=None):
+    """
+    Validation function for integration testcases
+    """
+    command = list()
+    if namespace:
+        command += ['ip', 'netns', 'exec', namespace]
+    command += ['cat', '/sys/class/net/{}/operstate'.format(eth_port)]
+    out = tasks.run_task(command, _LOGGER, 'Validating port up...', False)
+
+    # since different types of ports may report different status the best way
+    # we can do this for now is to just make sure it doesn't say down
+    if 'down' in out:
+        return False
+    return True
+
+
+def validate_del_veth_port(result, port, peer_port):
+    """
+    Validation function for integration testcases
+    """
+    devs = os.listdir('/sys/class/net')
+    return not any([port in devs, peer_port in devs])