diff options
-rw-r--r-- | src/ovs/ofctl.py | 31 | ||||
-rw-r--r-- | testcases/integration.py | 7 | ||||
-rw-r--r-- | testcases/testcase.py | 20 | ||||
-rw-r--r-- | tools/namespace.py | 178 | ||||
-rw-r--r-- | tools/veth.py | 118 | ||||
-rw-r--r-- | vswitches/ovs.py | 76 |
6 files changed, 427 insertions, 3 deletions
diff --git a/src/ovs/ofctl.py b/src/ovs/ofctl.py index 1ee48133..d7a2b320 100644 --- a/src/ovs/ofctl.py +++ b/src/ovs/ofctl.py @@ -349,6 +349,37 @@ class OFBridge(OFBase): self.logger.debug('dump flows') self.run_ofctl(['dump-flows', self.br_name], timeout=120) + def set_stp(self, enable=True): + """ + Set stp status + :param enable: Boolean to enable or disable stp + :return: None + """ + self.logger.debug( + 'Setting stp on bridge to %s', 'on' if enable else 'off') + self.run_vsctl( + ['set', 'Bridge', self.br_name, 'stp_enable={}'.format( + 'true' if enable else 'false')]) + + def set_rstp(self, enable=True): + """ + Set rstp status + :param enable: Boolean to enable or disable rstp + :return: None + """ + self.logger.debug( + 'Setting rstp on bridge to %s', 'on' if enable else 'off') + self.run_vsctl( + ['set', 'Bridge', self.br_name, 'rstp_enable={}'.format( + 'true' if enable else 'false')]) + + def bridge_info(self): + """ + Get bridge info + :return: Returns bridge info from list bridge command + """ + return self.run_vsctl(['list', 'bridge', self.br_name]) + # # helper functions # diff --git a/testcases/integration.py b/testcases/integration.py index fdd8b09c..ffde5822 100644 --- a/testcases/integration.py +++ b/testcases/integration.py @@ -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 diff --git a/testcases/testcase.py b/testcases/testcase.py index e5f8a14c..5f5c9358 100644 --- a/testcases/testcase.py +++ b/testcases/testcase.py @@ -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 index 00000000..e6bcd819 --- /dev/null +++ b/tools/namespace.py @@ -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 index 00000000..6418d11a --- /dev/null +++ b/tools/veth.py @@ -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]) diff --git a/vswitches/ovs.py b/vswitches/ovs.py index 555d7dec..115ab19b 100644 --- a/vswitches/ovs.py +++ b/vswitches/ovs.py @@ -16,18 +16,20 @@ """ import logging -import re import os -import time import pexpect +import re +import time + from conf import settings -from vswitches.vswitch import IVSwitch from src.ovs import OFBridge, flow_key, flow_match from tools import tasks +from vswitches.vswitch import IVSwitch _OVS_VAR_DIR = settings.getValue('OVS_VAR_DIR') _OVS_ETC_DIR = settings.getValue('OVS_ETC_DIR') + class IVSwitchOvs(IVSwitch, tasks.Process): """Open vSwitch base class implementation @@ -193,6 +195,50 @@ class IVSwitchOvs(IVSwitch, tasks.Process): cnt = 0 return cnt + def disable_stp(self, switch_name): + """ + Disable stp protocol on the bridge + :param switch_name: bridge to disable stp + :return: None + """ + bridge = self._bridges[switch_name] + bridge.set_stp(False) + self._logger.info('Sleeping for 50 secs to allow stp to stop.') + time.sleep(50) # needs time to disable + + def enable_stp(self, switch_name): + """ + Enable stp protocol on the bridge + :param switch_name: bridge to enable stp + :return: None + """ + bridge = self._bridges[switch_name] + bridge.set_stp(True) + self._logger.info('Sleeping for 50 secs to allow stp to start.') + time.sleep(50) # needs time to enable + + def disable_rstp(self, switch_name): + """ + Disable rstp on the bridge + :param switch_name: bridge to disable rstp + :return: None + """ + bridge = self._bridges[switch_name] + bridge.set_rstp(False) + self._logger.info('Sleeping for 15 secs to allow rstp to stop.') + time.sleep(15) # needs time to disable + + def enable_rstp(self, switch_name): + """ + Enable rstp on the bridge + :param switch_name: bridge to enable rstp + :return: None + """ + bridge = self._bridges[switch_name] + bridge.set_rstp(True) + self._logger.info('Sleeping for 15 secs to allow rstp to start.') + time.sleep(15) # needs time to enable + def kill(self, signal='-15', sleep=10): """Kill ``ovs-vswitchd`` and ``ovs-ovsdb`` instances if they are alive. @@ -354,3 +400,27 @@ class IVSwitchOvs(IVSwitch, tasks.Process): """ Validate call of flow dump """ return True + + def validate_disable_rstp(self, result, switch_name): + """ Validate rstp disable + """ + bridge = self._bridges[switch_name] + return 'rstp_enable : false' in ''.join(bridge.bridge_info()) + + def validate_enable_rstp(self, result, switch_name): + """ Validate rstp enable + """ + bridge = self._bridges[switch_name] + return 'rstp_enable : true' in ''.join(bridge.bridge_info()) + + def validate_disable_stp(self, result, switch_name): + """ Validate stp disable + """ + bridge = self._bridges[switch_name] + return 'stp_enable : false' in ''.join(bridge.bridge_info()) + + def validate_enable_stp(self, result, switch_name): + """ Validate stp enable + """ + bridge = self._bridges[switch_name] + return 'stp_enable : true' in ''.join(bridge.bridge_info()) |