diff --git a/.cookiecutter.json b/.cookiecutter.json index 6c61e83..6d42188 100644 --- a/.cookiecutter.json +++ b/.cookiecutter.json @@ -10,5 +10,5 @@ "project_name": "openswitch node for topology_docker", "short_description": "A Topology OpenSwitch Node for topology_docker.", "year": "2016", - "version": "0.1.0" + "version": "1.1.5" } diff --git a/MANIFEST.in b/MANIFEST.in index 6d25501..5e30912 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include README.rst LICENSE include requirements.txt +include lib/topology_docker_openswitch/openswitch_setup exclude test/* global-exclude __pycache__ *.py[co] diff --git a/README.rst b/README.rst index c49510a..0aa19eb 100644 --- a/README.rst +++ b/README.rst @@ -4,29 +4,211 @@ openswitch node for topology_docker A Topology OpenSwitch Node for topology_docker. +Changelog +========= -Documentation -============= - https://github.com/HPENetworking/topology_docker_openswitch/tree/master/doc +1.1.5 (2018-03-06) +------------------ +- Merge pull request #31 from saenzpa/package_fix. [Pablo Saenz] + fix: dev: Add openswitch_setup script to package. -License -======= -:: +1.1.4 (2017-06-01) +------------------ +- Merge pull request #30 from saenzpa/dev_log_removal. [Diego Hurtado] - Copyright (C) 2016 Hewlett Packard Enterprise Development LP + Remove switch container volume mount dev/log +- Remove switch container volume mount dev/log. [Sergio Barahona] - 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 + This mount was used to gather logs from containers and log them in the + execution machine, it was an old practice from first topology days and + is not required anymore. + + The removal is mainly because there are some vtysh commands that extract + data from local log files, so the logs need to be on the container instead + of the execution machine. + + +1.1.3 (2017-02-21) +------------------ +- Merge branch 'herjosep-p4_string_ifnames' [Diego Antonio Hurtado + Pimentel] + + +1.1.1 (2017-02-12) +------------------ +- Merge pull request #28 from saenzpa/no_rest. [Diego Hurtado] + + No rest +- Revert "fix: dev: Refactoring to remove race conditions." [Diego + Antonio Hurtado Pimentel] + + This reverts commit 53bf52b8693adfb3a2a5b664a30186b6a881b63c. +- Merge pull request #27 from saenzpa/P4refactor. [Pablo Saenz] + + fix: dev: Refactoring to remove race conditions. +- Merge pull request #26 from saenzpa/remove_rest. [Diego Hurtado] + + fix: dev: Parametrize setup script path. + + +1.1.0 (2017-01-19) +------------------ +- Merge pull request #25 from saenzpa/operator_prompt. [Diego Hurtado] + + Operator prompt + + +1.0.1 (2016-12-06) +------------------ + +Fix +~~~ +- Documenting exit from vtysh. [Diego Antonio Hurtado Pimentel] + + +1.0.0 (2016-11-23) +------------------ +- Merge pull request #24 from saenzpa/no_echo. [Diego Hurtado] + + fix: dev: Removing shell echo. +- Merge pull request #23 from saenzpa/environment_fix. [Pablo Saenz] + + fix: dev: Passing 'container' env variable. +- Merge pull request #22 from saenzpa/hpe_sync_with_logs. [Diego + Hurtado] + + chg: dev: Setting environment container to docker. +- Merge pull request #21 from saenzpa/script_mod. [Pablo Saenz] + + fix: dev: Increasing config timeout. +- Merge pull request #20 from saenzpa/register_shell. [Diego Hurtado] + + chg: dev: Using _register_shell. +- Merge pull request #19 from saenzpa/coredumps. [Pablo Saenz] + + chg: dev: Collecting docker coredumps on teardown and adding checks for p4 simulator +- Merge pull request #17 from saenzpa/p4switch. [Pablo Saenz] + + chg: dev: adding support for p4simulator images + + +0.1.0 (2016-08-12) +------------------ + +Changes +~~~~~~~ +- The binds attribute can now be injected and extended by users. [Carlos + Miguel Jenkins Perez] + +Fix +~~~ +- Fixed bug in initial prompt matching causing bash based shells to + timeout. [Carlos Miguel Jenkins Perez] + +Other +~~~~~ +- Merge pull request #16 from saenzpa/restd_start. [Diego Hurtado] + + Restd start +- Fixing restd validation status and service start. [fonsecamau] +- Merge pull request #15 from saenzpa/file_exist_fix. [Pablo Saenz] + + fix: dev: Adding handler for existing files. +- Merge pull request #14 from saenzpa/bringup_checks. [Pablo Saenz] + + chg: dev: Modifying boot time checks +- Merge pull request #13 from saenzpa/improved_logging. [Diego Hurtado] + + chg: dev: Improving logging. +- Merge pull request #11 from saenzpa/ops_switchd_active_timeout. [Pablo + Saenz] + + chg: dev: Increase ops-switchd active timeout +- Merge pull request #9 from saenzpa/switchd_active. [Pablo Saenz] + + chg: dev: Add check for ops-switchd to be active +- Merge pull request #8 from baraserg/master. [Pablo Saenz] + + Use safe method of querying dictionary +- Merge pull request #11 from baraserg/master. [Diego Hurtado] + + fix: dev: Change static path to shared_dir attribute +- Merge pull request #7 from saenzpa/log_messages. [Pablo Saenz] + + Log messages +- Merge pull request #6 from baraserg/master. [Pablo Saenz] + + Merge log plugin +- Merge pull request #5 from saenzpa/master_sync. [Pablo Saenz] + + Master sync +- Merge pull request #4 from HPENetworking/master. [Pablo Saenz] + + pulling from master +- Merge pull request #9 from fonsecamau/master. [Carlos Jenkins] + + chg: dev: Adding/modifying logging feature on process bring-up +- Merge pull request #8 from fonsecamau/master. [Carlos Jenkins] + + new: dev: Adding more logging and exception handling +- Merge pull request #24 from HPENetworking/new_shell_api_migration. + [David Diaz Barquero] + + chg: dev: Migrated all nodes shells to new Topology shell API. +- Merge pull request #23 from HPENetworking/new_binds_attribute. [Carlos + Jenkins] + + chg: usr: The binds attribute can now be injected and extended by users. +- Merge pull request #20 from HPENetworking/ddompe-patch-1. [Diego + Hurtado] + + Improvements during initialization +- Fix bugs during initialization. [Diego Dompe] + + - Handle support for sync the port readiness with the newer openswitch images + - Delay waiting for the cur_cfg, and handle the case where the cfg is not ready yet better. +- Merge pull request #19 from agustin-meneses-fuentes/master. [Carlos + Jenkins] + + fix: dev: Add bonding_masters to ip link set exceptions +- Merge pull request #11 from walintonc/master. [Carlos Jenkins] + + new: usr: Add support to specifying the hostname for a node. +- Add support to specifying hostname for create_container. [Walinton + Cambronero] + + - This allows that nodes can specify the hostname of choice + - In the openswitch node, the default hostname is 'switch' + - Clarify that tag must be specified in image param +- Merge pull request #2 from fonsecamau/fix_cut_output. [Carlos Jenkins] + + fix: dev: Make vtysh shell regular expression for prompt more specific. +- Merge pull request #19 from hpe-networking/fix_cut_output. [Carlos + Miguel Jenkins Perez] + + fix: dev: Output gets confused with switch prompt +- Merge pull request #17 from hpe-networking/ops_oobm. [Carlos Miguel + Jenkins Perez] + + chg: dev: Avoid moving new oobm interface to swns namespace +- Merge pull request #15 from hpe-networking/after_autopull. [David Diaz + Barquero] + + Refactored code, fixed minor issues and code quality. +- Merge pull request #8 from hpe-networking/docker_tmp. [David Diaz + Barquero] + + Mapping port to port labels for openswitch in topology +- Merge pull request #4 from hpe-networking/send_command_to_docker_exec. + [David Diaz Barquero] + + chg: dev: Refactored all send_commands to docker_exec to avoid using pexpect. +- Merge pull request #3 from hpe-networking/dockerfiles. [Carlos Miguel + Jenkins Perez] + + new: dev: Add docker file for toxin node - 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. diff --git a/doc/conf.py b/doc/conf.py index dfa0bdd..26de76a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -53,7 +53,7 @@ master_doc = 'index' # General information about the project. -project = 'openswitch node for topology_docker' +project = 'OpenSwitch node for Topology Docker' copyright = '2016, Hewlett Packard Enterprise Development LP' author = 'Hewlett Packard Enterprise Development LP' @@ -310,6 +310,7 @@ def setup(app): app.add_stylesheet('styles/custom.css') + # autoapi configuration autoapi_modules = { 'topology_docker_openswitch': None diff --git a/doc/developer.rst b/doc/developer.rst deleted file mode 100644 index d1f544d..0000000 --- a/doc/developer.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. toctree:: - -.. highlight:: sh - -=============== -Developer Guide -=============== - - -Setup Development Environment -============================= - -#. Install ``pip`` and ``tox``: - - :: - - sudo apt-get install python-pip - sudo pip install tox - -#. Configure git pre-commit hook: - - :: - - sudo pip install flake8 pep8-naming - flake8 --install-hook - git config flake8.strict true - - -Building Documentation -====================== - -:: - - tox -e doc - -Output will be available at ``.tox/doc/tmp/html``. It is recommended to install -the ``webdev`` package: - -:: - - sudo pip install webdev - -So a development web server can serve any location like this: - -:: - - $ webdev .tox/doc/tmp/html - - -Running Test Suite -================== - -:: - - tox -e py27,py34 diff --git a/doc/index.rst b/doc/index.rst index 73f113f..16de4e9 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,29 +1,22 @@ .. toctree:: :hidden: - developer + user topology_docker_openswitch/topology_docker_openswitch =================================== -openswitch node for topology_docker +OpenSwitch node for Topology Docker =================================== -A Topology OpenSwitch Node for topology_docker. +An OpenSwitch Node for Topology Docker Documentation ============= -- :doc:`Developer Guide. ` +- :doc:`User Guide. ` - :doc:`Internal Documentation Reference. ` - -Development -=========== - -- `Project repository. `_ - - License ======= diff --git a/doc/user.rst b/doc/user.rst new file mode 100644 index 0000000..f3ab46b --- /dev/null +++ b/doc/user.rst @@ -0,0 +1,132 @@ +.. toctree:: + +.. highlight:: sh + +========== +User Guide +========== + +This is an OpenSwitch node for Topology Docker. + +Shells +====== + +This node has several shells, their details are explained below. + +bash +.... + +This shell points to the ``bash`` shell of the node. Its prompt is set to this +value: + +:: + @~~==::BASH_PROMPT::==~~@ + +This shell has its echo disabled also, so that commands typed in it will not +show up in the console of the user. + + +bash_swns +......... + +This shell has all the attributes of the ``bash`` shell but all commands are +executed in the ``swns`` network namespace. + +vsctl +..... + +This shell has all the attributes of the ``bash`` shell but all commands are +prefixed with ``ovs-vsctl``. + +vtysh +..... + +This shell behaves differently depending on the availability of the ``vtysh`` +``set prompt`` command. + +If it is available, the shell will have its echo disabled and its prompt set to +this value: + +:: + + X@~~==::VTYSH_PROMPT::==~~@X + +Note that this is only the prompt that will be set, contexts will still be +appearing after it, for that reason you may want to use this regular +expression if you want to match with any context that has this forced prompt: + +:: + + r'(\r\n)?X@~~==::VTYSH_PROMPT::==~~@X(\([-\w\s]+\))?[#>] ' + +If ``set prompt`` is not available, the echo will not be disabled and the +prompt will remain in its standard value. + +Be aware that in order for the node to detect the ``Segmentation fault`` error +message, the ``vytsh`` shell is started with ``stdbuf -oL vtysh``. + +Before the node is destroyed at the end of its life, this shell will be exited +by sending the ``end`` and ``exit`` commands. + +The Booting Process +=================== + +The node copies a Python script in the container that performs the following +actions: + +#. Waits 30 seconds for ``/var/run/netns/swns``. +#. Waits 30 seconds for ``/etc/openswitch/hwdesc``. +#. Creates interfaces. +#. Waits 30 seconds for ``/var/run/openvswitch/db.sock``. +#. Waits 30 seconds for ``cur_hw``. +#. Waits 30 seconds for ``cur_cfg``. +#. Waits 30 seconds for ``/var/run/openvswitch/ops-switchd.pid``. +#. Waits 30 seconds for the hostname to be set to ``switch``. + +For the case of ``cur_hw`` and ``cur_cfg``, their value is taken from a query +sent to ``/var/run/openswitch/db.sock``. This query has this format: + +:: + + 'X': { + 'method': 'transact', + 'params': [ + 'OpenSwitch', + { + 'op': 'select', + 'table': 'System', + 'where': [], + 'columns': ['X'] + } + ], + 'id': id(db_sock) + } + +The value of ``1`` is looked for in: + +:: + + response['result'][0]['rows'][0][X] == 1 + +``X`` is a placeholder for ``cur_hw`` and ``cur_cfg``. + +If any of the previous waits times out, an exception of this kind will be +raised: + +:: + + The image did not boot correctly, ... + +These errors are caused by a faulty image, the framework is just reporting +them. This errors happen *before* the very first line of test case is executed. + +This node will create interfaces and will move them to ``swns`` or to +``emulns`` if the image is using the P4 simulator. Any failure in the process +of creating interfaces will be reported like this: + +:: + + Failed to map ports with port labels... + +Depending on the error, the failing command or other information will be +displayed after that message. diff --git a/lib/topology_docker_openswitch/__init__.py b/lib/topology_docker_openswitch/__init__.py index 796ad72..864965e 100644 --- a/lib/topology_docker_openswitch/__init__.py +++ b/lib/topology_docker_openswitch/__init__.py @@ -24,4 +24,4 @@ __author__ = 'Hewlett Packard Enterprise Development LP' __email__ = 'hpe-networking@lists.hp.com' -__version__ = '0.1.0' +__version__ = '1.1.5' diff --git a/lib/topology_docker_openswitch/openswitch.py b/lib/topology_docker_openswitch/openswitch.py index c3c4deb..7db30af 100644 --- a/lib/topology_docker_openswitch/openswitch.py +++ b/lib/topology_docker_openswitch/openswitch.py @@ -24,310 +24,281 @@ from __future__ import unicode_literals, absolute_import from __future__ import print_function, division +from abc import ABCMeta, abstractmethod from json import loads -from subprocess import check_call +from subprocess import check_output, CalledProcessError +from platform import system, linux_distribution +from logging import StreamHandler, getLogger, INFO, Formatter +from sys import stdout +from os.path import join, dirname, normpath, abspath -from topology_docker.node import DockerNode -from topology_docker.shell import DockerShell, DockerBashShell - - -SETUP_SCRIPT = """\ -import logging -from sys import argv -from time import sleep -from os.path import exists, split -from json import dumps, loads -from shlex import split as shsplit -from subprocess import check_call, check_output -from socket import AF_UNIX, SOCK_STREAM, socket, gethostname - -import yaml - -config_timeout = 300 -swns_netns = '/var/run/netns/swns' -hwdesc_dir = '/etc/openswitch/hwdesc' -db_sock = '/var/run/openvswitch/db.sock' -switchd_pid = '/var/run/openvswitch/ops-switchd.pid' -query = { - 'method': 'transact', - 'params': [ - 'OpenSwitch', - { - 'op': 'select', - 'table': 'System', - 'where': [], - 'columns': ['cur_hw'] - } - ], - 'id': id(db_sock) -} -sock = None - - -def create_interfaces(): - # Read ports from hardware description - with open('{}/ports.yaml'.format(hwdesc_dir), 'r') as fd: - ports_hwdesc = yaml.load(fd) - hwports = [str(p['name']) for p in ports_hwdesc['ports']] - - # Get list of already created ports - not_in_swns = check_output(shsplit( - 'ls /sys/class/net/' - )).split() - in_swns = check_output(shsplit( - 'ip netns exec swns ls /sys/class/net/' - )).split() - - create_cmd_tpl = 'ip tuntap add dev {hwport} mode tap' - netns_cmd_tpl = 'ip link set {hwport} netns swns' - rename_int = 'ip link set {portlbl} name {hwport}' - - # Save port mapping information - mapping_ports = {} - - # Map the port with the labels - for portlbl in not_in_swns: - if portlbl in ['lo', 'oobm', 'bonding_masters']: - continue - hwport = hwports.pop(0) - mapping_ports[portlbl] = hwport - logging.info( - ' - Port {portlbl} moved to swns netns as {hwport}.'.format( - **locals() - ) - ) - try: - check_call(shsplit(rename_int.format(**locals()))) - check_call(shsplit(netns_cmd_tpl.format(hwport=hwport))) - except: - raise Exception('Failed to map ports with port labels') - - # Writting mapping to file - shared_dir_tmp = split(__file__)[0] - with open('{}/port_mapping.json'.format(shared_dir_tmp), 'w') as json_file: - json_file.write(dumps(mapping_ports)) - - for hwport in hwports: - if hwport in in_swns: - logging.info(' - Port {} already present.'.format(hwport)) - continue - - logging.info(' - Port {} created.'.format(hwport)) - try: - check_call(shsplit(create_cmd_tpl.format(hwport=hwport))) - except: - raise Exception('Failed to create tuntap') - - try: - check_call(shsplit(netns_cmd_tpl.format(hwport=hwport))) - except: - raise Exception('Failed to move port to swns netns') - check_call(shsplit('touch /tmp/ops-virt-ports-ready')) - logging.info(' - Ports readiness notified to the image') - -def cur_cfg_is_set(): - global sock - if sock is None: - sock = socket(AF_UNIX, SOCK_STREAM) - sock.connect(db_sock) - sock.send(dumps(query)) - response = loads(sock.recv(4096)) - try: - return response['result'][0]['rows'][0]['cur_hw'] == 1 - except IndexError: - return 0 - -def main(): - - if '-d' in argv: - logging.basicConfig(level=logging.DEBUG) - - logging.info('Waiting for swns netns...') - for i in range(0, config_timeout): - if not exists(swns_netns): - sleep(0.1) - else: - break - else: - raise Exception('Timed out while waiting for swns.') - - logging.info('Waiting for hwdesc directory...') - for i in range(0, config_timeout): - if not exists(hwdesc_dir): - sleep(0.1) - else: - break - else: - raise Exception('Timed out while waiting for hwdesc directory.') - - logging.info('Creating interfaces...') - create_interfaces() - - logging.info('Waiting for DB socket...') - for i in range(0, config_timeout): - if not exists(db_sock): - sleep(0.1) - else: - break - else: - raise Exception('Timed out while waiting for DB socket.') - - logging.info('Waiting for switchd pid...') - for i in range(0, config_timeout): - if not exists(switchd_pid): - sleep(0.1) - else: - break - else: - raise Exception('Timed out while waiting for switchd pid.') - - logging.info('Wait for final hostname...') - for i in range(0, config_timeout): - if gethostname() != 'switch': - sleep(0.1) - else: - break - else: - raise Exception('Timed out while waiting for final hostname.') - - logging.info('Waiting for cur_cfg...') - for i in range(0, config_timeout): - if not cur_cfg_is_set(): - sleep(0.1) - else: - break - else: - raise Exception('Timed out while waiting for cur_cfg.') - -if __name__ == '__main__': - main() -""" +from six import add_metaclass +from topology_openswitch.openswitch import OpenSwitchBase -PROCESS_LOG = """ -#!/bin/bash -ovs-vsctl list Daemon >> /tmp/logs -echo "Coredump -->" >> /tmp/logs -coredumpctl gdb >> /tmp/logs -echo "All the running processes:" >> /tmp/logs -ps -aef >> /tmp/logs +from topology_docker.node import DockerNode +from topology_docker.shell import DockerBashShell + +from .shell import OpenSwitchVtyshShell + +# When a failure happens during boot time, logs and other information is +# collected to help with the debugging. The path of this collection is to be +# stored here at module level to be able to import it in the pytest teardown +# hook later. Non-failing containers will append their log paths here also. +LOG_PATHS = [] +LOG = getLogger(__name__) +LOG_HDLR = StreamHandler(stream=stdout) +LOG_HDLR.setFormatter(Formatter('%(asctime)s %(message)s')) +LOG_HDLR.setLevel(INFO) +LOG.addHandler(LOG_HDLR) +LOG.setLevel(INFO) + + +def log_commands( + commands, location, function, escape=True, + prefix=None, suffix=None, **kwargs +): + if prefix is None: + prefix = '' + if suffix is None: + suffix = '' + + for command in commands: + log_path = ' >> {} 2>&1'.format(location) + args = [ + r'{prefix}echo \"Output of:' + r' {command}{log_path}\"{log_path}{suffix}'.format( + prefix=prefix, command=command, + log_path=log_path, suffix=suffix + ), + r'{}{}{}{}'.format( + prefix, command, log_path, suffix + ), + r'{}echo \"\"{}{}'.format(prefix, log_path, suffix) + ] -systemctl status >> /tmp/systemctl -systemctl --state=failed --all >> /tmp/systemctl + for arg in args: + try: + if not escape: + arg = arg.replace('\\', '') + function(arg, **kwargs) -ovsdb-client dump >> /tmp/ovsdb_dump -""" + except CalledProcessError as error: + LOG.warning( + '{} failed with error {}.'.format( + command, error.returncode + ) + ) -class OpenSwitchNode(DockerNode): +@add_metaclass(ABCMeta) +class DockerOpenSwitch(OpenSwitchBase, DockerNode): """ Custom OpenSwitch node for the Topology Docker platform engine. - This custom node loads an OpenSwitch image and has vtysh as default shell (in addition to bash). - See :class:`topology_docker.node.DockerNode`. """ + # FIXME: document shared_dir_mount + _class_openswitch_attributes = {'shared_dir_mount': ''} + + @abstractmethod def __init__( self, identifier, image='topology/ops:latest', binds=None, + environment={'container': 'docker'}, **kwargs): # Add binded directories container_binds = [ - '/dev/log:/dev/log', - '/sys/fs/cgroup:/sys/fs/cgroup:ro' + '/sys/fs/cgroup:/sys/fs/cgroup' ] if binds is not None: container_binds.append(binds) - super(OpenSwitchNode, self).__init__( + super(DockerOpenSwitch, self).__init__( identifier, image=image, command='/sbin/init', binds=';'.join(container_binds), hostname='switch', + network_mode='bridge', environment={'container': 'docker'}, **kwargs ) + # FIXME: Remove this attribute to merge with version > 1.6.0 + self._shared_dir_mount = '/tmp' + # Add vtysh (default) shell - # FIXME: Create a subclass to handle better the particularities of - # vtysh, like prompt setup etc. - self._shells['vtysh'] = DockerShell( - self.container_id, 'vtysh', '(^|\n)switch(\([\-a-zA-Z0-9]*\))?#' - ) + # This shell is started as a bash shell but it changes itself to a + # vtysh one afterwards. This is necessary because this shell must be + # started from a bash one that has echo disabled to avoid wrong + # matching with some command output and by setting an unique prompt + # with the set prompt vtysh command + self._register_shell('vtysh', OpenSwitchVtyshShell(self.container_id)) # Add bash shells - initial_prompt = '(^|\n).*[#$] ' - self._shells['bash'] = DockerBashShell( - self.container_id, 'bash', - initial_prompt=initial_prompt + initial_prompt = '(^|\n).*[#$] ' + self._register_shell( + 'bash', + DockerBashShell( + self.container_id, 'bash', + initial_prompt=initial_prompt + ) ) - self._shells['bash_swns'] = DockerBashShell( - self.container_id, 'ip netns exec swns bash', - initial_prompt=initial_prompt + self._register_shell( + 'bash_swns', + DockerBashShell( + self.container_id, 'ip netns exec swns bash', + initial_prompt=initial_prompt + ) ) - self._shells['vsctl'] = DockerBashShell( - self.container_id, 'bash', - initial_prompt=initial_prompt, - prefix='ovs-vsctl ', timeout=60 + self._register_shell( + 'vsctl', + DockerBashShell( + self.container_id, 'bash', + initial_prompt=initial_prompt, + prefix='ovs-vsctl ', timeout=60 + ) ) - def notify_post_build(self): + def notify_post_build(self, script_path=None): """ Get notified that the post build stage of the topology build was reached. + :param script_path: + string with the path of the setup script to be used + See :meth:`DockerNode.notify_post_build` for more information. """ - super(OpenSwitchNode, self).notify_post_build() - self._setup_system() + super(DockerOpenSwitch, self).notify_post_build() + self._setup_system(script_path) - def _setup_system(self): + def _setup_system(self, script_path=None): """ Setup the OpenSwitch image for testing. #. Wait for daemons to converge. #. Assign an interface to each port label. #. Create remaining interfaces. + :param script_path: + string with the path of the setup script to be used """ - # Write the log gathering script - process_log = '{}/process_log.sh'.format(self.shared_dir) - with open(process_log, "w") as fd: - fd.write(PROCESS_LOG) - check_call('chmod 755 {}/process_log.sh'.format(self.shared_dir), - shell=True) - # Write and execute setup script + openswitch_setup_path = script_path or join( + dirname(normpath(abspath(__file__))), 'openswitch_setup' + ) + + with open(openswitch_setup_path) as openswitch_setup_file: + openswitch_setup = openswitch_setup_file.read() + setup_script = '{}/openswitch_setup.py'.format(self.shared_dir) with open(setup_script, 'w') as fd: - fd.write(SETUP_SCRIPT) + fd.write(openswitch_setup) try: - self._docker_exec('python {}/openswitch_setup.py -d' - .format(self.shared_dir_mount)) + self._docker_exec( + 'python {}/openswitch_setup.py -d'.format( + self.shared_dir_mount + ) + ) except Exception as e: - check_call('touch {}/logs'.format(self.shared_dir), shell=True) - check_call('chmod 766 {}/logs'.format(self.shared_dir), - shell=True) - self._docker_exec('/bin/bash {}/process_log.sh' - .format(self.shared_dir_mount)) - check_call( - 'tail -n 2000 /var/log/syslog > {}/syslog'.format( - self.shared_dir - ), shell=True) - check_call( - 'docker ps -a >> {}/logs'.format(self.shared_dir), + global FAIL_LOG_PATH + lines_to_dump = 100 + + platforms_log_location = { + 'Ubuntu': 'cat /var/log/upstart/docker.log', + 'CentOS Linux': 'grep docker /var/log/daemon.log', + 'debian': 'journalctl -u docker.service', + # FIXME: find the right values for the next dictionary keys: + # 'boot2docker': 'cat /var/log/docker.log', + # 'debian': 'cat /var/log/daemon.log', + # 'fedora': 'journalctl -u docker.service', + # 'red hat': 'grep docker /var/log/messages', + # 'opensuse': 'journalctl -u docker.service' + } + + # Here, we find the command to dump the last "lines_to_dump" lines + # of the docker log file in the logs. The location of the docker + # log file depends on the Linux distribution. These locations are + # defined the in "platforms_log_location" dictionary. + + operating_system = system() + + if operating_system != 'Linux': + LOG.warning( + 'Operating system is not Linux but {}.'.format( + operating_system + ) + ) + return + + linux_distro = linux_distribution()[0] + + if linux_distro not in platforms_log_location.keys(): + LOG.warning( + 'Unknown Linux distribution {}.'.format( + linux_distro + ) + ) + + docker_log_command = '{} | tail -n {}'.format( + platforms_log_location[linux_distro], lines_to_dump + ) + + container_commands = [ + 'ovs-vsctl list Daemon', + 'coredumpctl gdb', + 'ps -aef', + 'systemctl status', + 'systemctl --state=failed --all', + 'ovsdb-client dump', + 'systemctl status switchd -n 10000 -l', + 'cat /var/log/messages' + ] + + execution_machine_commands = [ + 'tail -n 2000 /var/log/syslog', + 'docker ps -a', + docker_log_command + ] + + log_commands( + container_commands, + '{}/container_logs'.format(self.shared_dir_mount), + self._docker_exec, + prefix=r'sh -c "', + suffix=r'"' + ) + log_commands( + execution_machine_commands, + '{}/execution_machine_logs'.format(self.shared_dir), + check_output, + escape=False, shell=True ) - check_call('cat {}/logs'.format(self.shared_dir), shell=True) + LOG_PATHS.append(self.shared_dir) + raise e + # Add virtual type + + vtysh = self.get_shell('vtysh') + + vtysh.send_command('show version', silent=True) + if 'genericx86-64' in vtysh.get_response(silent=True): + self.product_name = 'genericx86-64' + else: + self.product_name = 'genericx86-p4' + # Read back port mapping port_mapping = '{}/port_mapping.json'.format(self.shared_dir) with open(port_mapping, 'r') as fd: mappings = loads(fd.read()) + LOG_PATHS.append(self.shared_dir) + if hasattr(self, 'ports'): self.ports.update(mappings) return @@ -348,5 +319,27 @@ def set_port_state(self, portlbl, state): command = '{prefix} ip link set dev {iface} {state}'.format(**locals()) self._docker_exec(command) + def stop(self): + """ + Exit all vtysh shells. + + See :meth:`DockerNode.stop` for more information. + """ + + for shell in self._shells.values(): + if isinstance(shell, OpenSwitchVtyshShell): + shell._exit() + + super(DockerOpenSwitch, self).stop() + + +class OpenSwitch(DockerOpenSwitch): + """ + FIXME: document this + """ + + def __init__(self, *args, **kwargs): + super(OpenSwitch, self).__init__(*args, **kwargs) + -__all__ = ['OpenSwitchNode'] +__all__ = ['DockerOpenSwitch', 'OpenSwitch'] diff --git a/lib/topology_docker_openswitch/openswitch_setup b/lib/topology_docker_openswitch/openswitch_setup new file mode 100644 index 0000000..4e4579c --- /dev/null +++ b/lib/topology_docker_openswitch/openswitch_setup @@ -0,0 +1,282 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +This script prepares an OpenSwitch image to run as a Topology node. It is +copied as openswitch_setup.py in the docker container shared folder and then +executed with python /path/to/the/script/openswitch_setup.py -d. +""" + +from logging import info, DEBUG, basicConfig +from sys import argv +from time import sleep +from os.path import exists, split +from json import dumps, loads +from shlex import split as shsplit +from subprocess import check_call, check_output, call, CalledProcessError +from socket import AF_UNIX, SOCK_STREAM, socket, gethostname +from re import findall, MULTILINE +from yaml import load + +config_timeout = 1200 +ops_switchd_active_timeout = 60 +swns_netns = '/var/run/netns/swns' +emulns_netns = '/var/run/netns/emulns' +hwdesc_dir = '/etc/openswitch/hwdesc' +db_sock = '/var/run/openvswitch/db.sock' +switchd_pid = '/var/run/openvswitch/ops-switchd.pid' +sock = None + + +def create_interfaces(): + # Read ports from hardware description + with open('{}/ports.yaml'.format(hwdesc_dir), 'r') as fd: + ports_hwdesc = load(fd) + hwports = [str(p['name']) for p in ports_hwdesc['ports']] + + netns = check_output("ls /var/run/netns", shell=True) + + # Get list of already created ports + not_in_netns = check_output(shsplit( + 'ls /sys/class/net/' + )).split() + + if "emulns" not in netns: + in_netns = check_output(shsplit( + 'ip netns exec swns ls /sys/class/net/' + )).split() + else: + in_netns = check_output(shsplit( + 'ip netns exec emulns ls /sys/class/net/' + )).split() + + info('Not in swns/emulns: {not_in_netns} '.format(**locals())) + info('In swns/emulns {in_netns} '.format(**locals())) + + create_cmd_tpl = 'ip tuntap add dev {hwport} mode tap' + netns_cmd_tpl_swns = 'ip link set {hwport} netns swns' + netns_fp_cmd_tpl_swns = 'ip link set {hwport} netns swns' + netns_cmd_tpl_emulns = ( + 'ip netns exec swns ip link set {hwport} netns emulns' + ) + netns_fp_cmd_tpl_emulns = 'ip link set {hwport} netns emulns' + rename_int = 'ip link set {portlbl} name {hwport}' + ns_exec = 'ip netns exec emulns ' + + # Save port mapping information + mapping_ports = {} + + # Map the port with the labels + for portlbl in not_in_netns: + info('Port {portlbl} found'.format(**locals())) + + if portlbl in ['lo', 'oobm', 'eth0', 'bonding_masters']: + continue + + hwport = hwports.pop(0) + mapping_ports[portlbl] = hwport + + info( + 'Port {portlbl} moved to swns/emulns netns as {hwport}.' + .format(**locals()) + ) + + try: + check_call(shsplit(rename_int.format(**locals()))) + + if 'emulns' not in netns: + check_call( + shsplit(netns_fp_cmd_tpl_swns.format(hwport=hwport)) + ) + else: + check_call( + shsplit(netns_fp_cmd_tpl_emulns.format(hwport=hwport)) + ) + check_call( + '{ns_exec} ip link set dev {hwport} up'.format(**locals()), + shell=True + ) + + for i in range(0, config_timeout): + link_state = check_output( + '{ns_exec} ip link show {hwport}'.format(**locals()), + shell=True + ) + if "UP" in link_state: + break + else: + sleep(0.1) + else: + raise Exception('emulns interface did not came up.') + + out = check_output( + '{ns_exec} echo port_add {hwport} ' + ' {port} | {ns_exec} ' + '/usr/bin/bm_tools/runtime_CLI.py --json ' + '/usr/share/ovs_p4_plugin/switch_bmv2.json ' + '--thrift-port 10001'.format( + ns_exec=ns_exec, hwport=hwport, + port=str(int(len(mapping_ports.keys())) - 1) + ), + shell=True + ) + + info('BM port creation: {}'.format(out)) + + regex = ( + r'\s*Control utility for runtime P4 table' + r' manipulation\s*\nRuntimeCmd:\s*\nRuntimeCmd:\s*$' + ) + + if findall(regex, out, MULTILINE) is None: + raise Exception( + 'Control utility for runtime P4 table failed.' + ) + + except CalledProcessError as error: + raise Exception( + 'Failed to map ports with port labels, {} failed with this ' + 'error: {}'.format(error.cmd, error.output) + ) + + except Exception as error: + raise Exception( + 'Failed to map ports with port labels: {}'.format( + error.message + ) + ) + + # Writting mapping to file + shared_dir_tmp = split(__file__)[0] + + with open('{}/port_mapping.json'.format(shared_dir_tmp), 'w') as json_file: + json_file.write(dumps(mapping_ports)) + + for hwport in hwports: + if hwport in in_netns: + info('Port {} already present.'.format(hwport)) + continue + + info('Port {} created.'.format(hwport)) + try: + if 'emulns' not in netns: + check_call(shsplit(create_cmd_tpl.format(hwport=hwport))) + except: + raise Exception('Failed to create tuntap') + + try: + if 'emulns' not in netns: + check_call(shsplit(netns_cmd_tpl_swns.format(hwport=hwport))) + except: + raise Exception('Failed to move port to swns/emulns netns.') + + check_call(shsplit('touch /tmp/ops-virt-ports-ready')) + info('Port readiness notified to the image.') + + +def cur_is_set(cur_key): + queries = { + 'cur_hw': { + 'method': 'transact', + 'params': [ + 'OpenSwitch', + { + 'op': 'select', + 'table': 'System', + 'where': [], + 'columns': ['cur_hw'] + } + ], + 'id': id(db_sock) + }, + 'cur_cfg': { + 'method': 'transact', + 'params': [ + 'OpenSwitch', + { + 'op': 'select', + 'table': 'System', + 'where': [], + 'columns': ['cur_cfg'] + } + ], + 'id': id(db_sock) + } + } + + global sock + if sock is None: + sock = socket(AF_UNIX, SOCK_STREAM) + sock.connect(db_sock) + sock.send(dumps(queries[cur_key])) + response = loads(sock.recv(4096)) + + try: + return response['result'][0]['rows'][0][cur_key] == 1 + except IndexError: + return 0 + + +def ops_switchd_is_active(): + is_active = call(["systemctl", "is-active", "switchd.service"]) + return is_active == 0 + + +def main(): + + if '-d' in argv: + basicConfig(level=DEBUG) + + def wait_check(function, wait_name, wait_error, *args): + info('Waiting for {}'.format(wait_name)) + + for i in range(0, config_timeout): + if not function(*args): + sleep(0.1) + else: + break + else: + raise Exception( + 'The image did not boot correctly, ' + '{} after waiting {} seconds.'.format( + wait_error, int(0.1 * config_timeout) + ) + ) + + wait_check( + exists, swns_netns, '{} was not present'.format(swns_netns), swns_netns + ) + wait_check( + exists, hwdesc_dir, '{} was not present'.format(hwdesc_dir), hwdesc_dir + ) + + info('Creating interfaces') + create_interfaces() + + wait_check( + exists, db_sock, '{} was not present'.format(db_sock), db_sock + ) + wait_check( + cur_is_set, 'cur_hw to be set to 1', 'cur_hw is not set to 1', + 'cur_hw' + ) + wait_check( + cur_is_set, 'cur_cfg to be set to 1', 'cur_cfg is not set to 1', + 'cur_cfg' + ) + wait_check( + exists, switchd_pid, '{} was not present'.format(switchd_pid), + switchd_pid + ) + wait_check( + ops_switchd_is_active, 'ops-switchd to be active', + 'ops-switchd was not active' + ) + wait_check( + lambda: gethostname() == 'switch', 'final hostname', + 'hostname was not set' + ) + + +if __name__ == '__main__': + main() diff --git a/lib/topology_docker_openswitch/plugin/plugin.py b/lib/topology_docker_openswitch/plugin/plugin.py deleted file mode 100644 index 3637709..0000000 --- a/lib/topology_docker_openswitch/plugin/plugin.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2015-2016 Hewlett Packard Enterprise Development LP -# -# 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. - -from os.path import exists, basename, splitext -from os import makedirs -from shutil import copytree, Error -from logging import warning - - -def pytest_runtest_teardown(item): - """ - pytest hook to get the name of the test executed, it creates a folder with - the name, then copies the folders defined in the shared_dir_mount attribute - of each openswitch container, additionally the /var/log/messages of the - container is copied to the same folder. - """ - if 'topology' in item.funcargs: - topology = item.funcargs['topology'] - if topology.engine == 'docker': - logs_path = '/var/log/messages' - for node in topology.nodes: - node_obj = topology.get(node) - if node_obj.metadata['type'] == 'openswitch': - shared_dir = node_obj.shared_dir - try: - node_obj.send_command( - 'cat {} > {}/var_messages.log'.format( - logs_path, node_obj.shared_dir_mount - ), - shell='bash', - silent=True - ) - except Error: - warning( - 'Unable to get {} from container'.format(logs_path) - ) - test_suite = splitext(basename(item.parent.name))[0] - path_name = '/tmp/{}_{}_{}'.format( - test_suite, item.name, str(id(item)) - ) - if not exists(path_name): - makedirs(path_name) - try: - copytree( - shared_dir, '{}/{}'.format( - path_name, - basename(shared_dir) - ) - ) - except Error as err: - errors = err.args[0] - for error in errors: - src, dest, msg = error - warning( - 'Unable to copy file {}, Error {}'.format( - src, msg - ) - ) diff --git a/lib/topology_docker_openswitch/plugin/__init__.py b/lib/topology_docker_openswitch/pytest/__init__.py similarity index 100% rename from lib/topology_docker_openswitch/plugin/__init__.py rename to lib/topology_docker_openswitch/pytest/__init__.py diff --git a/lib/topology_docker_openswitch/pytest/plugin.py b/lib/topology_docker_openswitch/pytest/plugin.py new file mode 100644 index 0000000..c55354d --- /dev/null +++ b/lib/topology_docker_openswitch/pytest/plugin.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015-2016 Hewlett Packard Enterprise Development LP +# +# 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. + +from os.path import exists, basename, splitext, join +from shutil import copytree, Error, rmtree +from logging import warning +from datetime import datetime + +from topology_docker_openswitch.openswitch import log_commands + + +def pytest_runtest_teardown(item): + """ + Pytest hook to get node information after the test executed. + + This creates a folder with the name of the test case, copies the folders + defined in the shared_dir_mount attribute of each openswitch container + and the /var/log/messages file inside. + + FIXME: document the item argument + """ + test_suite = splitext(basename(item.parent.name))[0] + + from pytest import config + + topology_log_dir = config.getoption('--topology-log-dir') + + if not topology_log_dir: + return + else: + path_name = join( + topology_log_dir, + '{}_{}_{}'.format( + test_suite, + item.name, + datetime.now().strftime('%Y_%m_%d_%H_%M_%S') + ) + ) + + # Being extra-prudent here + if exists(path_name): + rmtree(path_name) + + if 'topology' not in item.funcargs: + from topology_docker_openswitch.openswitch import LOG_PATHS + + for log_path in LOG_PATHS: + try: + destination = join(path_name, basename(log_path)) + try: + rmtree(destination) + except: + pass + copytree(log_path, destination) + rmtree(path_name) + except Error as err: + errors = err.args[0] + for error in errors: + src, dest, msg = error + warning( + 'Unable to copy file {}, Error {}'.format( + src, msg + ) + ) + return + + topology = item.funcargs['topology'] + + if topology.engine != 'docker': + return + + logs_path = '/var/log/messages' + + for node in topology.nodes: + node_obj = topology.get(node) + + if node_obj.metadata.get('type', None) != 'openswitch': + return + + shared_dir = node_obj.shared_dir + + try: + commands = ['cat {}'.format(logs_path)] + log_commands( + commands, join(node_obj.shared_dir_mount, 'container_logs'), + node_obj._docker_exec, prefix=r'sh -c "', suffix=r'"' + ) + except: + warning( + 'Unable to get {} from node {}.'.format( + logs_path, node_obj.identifier + ) + ) + + bash_shell = node_obj.get_shell('bash') + + try: + core_path = '/var/diagnostics/coredump' + + bash_shell.send_command( + 'ls -1 {}/core* 2>/dev/null'.format(core_path), silent=True + ) + + core_files = bash_shell.get_response(silent=True).splitlines() + + for core_file in core_files: + bash_shell.send_command( + 'cp {core_file} /tmp'.format(**locals()), + silent=True + ) + except: + warning( + 'Unable to get coredumps from node {}.'.format( + node_obj.identifier + ) + ) + + try: + copytree(shared_dir, join(path_name, basename(shared_dir))) + rmtree(shared_dir) + except Error as err: + errors = err.args[0] + for error in errors: + src, dest, msg = error + warning( + 'Unable to copy file {}, Error {}'.format( + src, msg + ) + ) diff --git a/lib/topology_docker_openswitch/shell.py b/lib/topology_docker_openswitch/shell.py new file mode 100644 index 0000000..ed68cb8 --- /dev/null +++ b/lib/topology_docker_openswitch/shell.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015-2016 Hewlett Packard Enterprise Development LP +# +# 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. + +""" +OpenSwitch shell module +""" + +from __future__ import unicode_literals, absolute_import +from __future__ import print_function, division + +from topology_docker.shell import DockerShell + +from topology_openswitch.vtysh import ( + BASH_FORCED_PROMPT, + VTYSH_FORCED_PROMPT, + VTYSH_STANDARD_PROMPT, + VtyshShellMixin +) + + +class OpenSwitchVtyshShell(DockerShell, VtyshShellMixin): + """ + OpenSwitch ``vtysh`` shell + + This shell handles the particularities of the ``vtysh`` shell of an + OpenSwitch node. It is actually a shell that connects first to ``bash`` and + from the ``bash`` shell it then opens a ``vtysh`` shell. + + The actual process that this shell follows depends on the image of the + OpenSwitch node. Newer images support the ``vtysh`` ``set prompt`` command, + older images do not. This command allows the user to change the vtysh + prompt to any value without other side effects (like the hostname command + has). + + #. A connection to the ``bash`` shell of the node is done. + #. The ``bash`` prompt is set to ``@~~==::BASH_PROMPT::==~~@``. + #. A ``vtysh`` shell is opened with ``stdbuf -oL vtysh``. + #. The ``vtysh`` ``set prompt X@~~==::VTYSH_PROMPT::==~~@X`` command is + executed to set the ``vtysh`` forced prompt. + + If the next prompt received matches the ``vtysh`` forced prompt, this + process is followed: + + #. The ``vtysh`` shell is exited back to the ``bash`` shell by sending + ``exit``. + #. The echo of the ``bash`` shell is disabled with ``stty -echo``. This + will also disable the echo of the ``vtysh`` shell that will be started + from the ``bash`` shell. + #. A ``vtysh`` shell will be started with ``stdbuf -oL vtysh``. + #. The ``vtysh`` ``set prompt X@~~==::VTYSH_PROMPT::==~~@X`` command is + executed. + #. The shell prompt is set to the forced ``vtysh`` prompt. + #. In this case, the shell will not try to remove the echo of the ``vtysh`` + commands because they should not appear since the echo is disabled. + + If the next prompt received does not match the ``vtysh`` forced prompt, + this process is followed: + + #. The shell is configured to try to remove the echo of the ``vtysh`` + commands by looking for them in the command output. + #. The shell prompt is set to the standard ``vtysh`` prompt. + + Once the container is to be destroyed in the normal clean up of nodes, the + ``vtysh`` shell is exited to the ``bash`` one by sending the ``end`` + command followed by the ``exit`` command. + + :param str container: identifier of the container that holds this shell + """ + + def __init__(self, container): + # The parameter try_filter_echo is disabled by default here to handle + # images that support the vtysh "set prompt" command and will have its + # echo disabled since it extends from DockeBashShell. For other + # situations where this is not supported, the self._try_filter_echo + # attribute is disabled afterwards by altering it directly. + # The prompt value passed here is the one that will match with an + # OpenSwitch bash shell initial prompt. + super(OpenSwitchVtyshShell, self).__init__( + container, 'bash', '(^|\n).*[#$] ', try_filter_echo=False + ) + + def _setup_shell(self, connection=None): + """ + Get the shell ready to handle ``vtysh`` particularities. + + These particularities are the handling of segmentation fault errors + and forced or standard ``vtysh`` prompts. + + See :meth:`PExpectShell._setup_shell` for more information. + """ + + spawn = self._get_connection(connection) + # Since user, password or initial_command are not being used, this is + # the first expect done in the connection. The value of self._prompt at + # this moment is the initial prompt of an OpenSwitch bash shell prompt. + spawn.expect(self._prompt) + + # The bash prompt is set to a forced value for vtysh shells that + # support prompt setting and for the ones that do not. + spawn.sendline('export PS1={}'.format(BASH_FORCED_PROMPT)) + spawn.expect(BASH_FORCED_PROMPT) + + def join_prompt(prompt): + return '{}|{}'.format(BASH_FORCED_PROMPT, prompt) + + if self._determine_set_prompt(): + # If this image supports "set prompt", then exit back to bash to + # set the bash shell without echo. + spawn.sendline('exit') + spawn.expect(BASH_FORCED_PROMPT) + + # This disables the echo in the bash and in the subsequent vtysh + # shell too. + spawn.sendline('stty -echo') + spawn.expect(BASH_FORCED_PROMPT) + + # Go into the vtysh shell again. Exiting vtysh after calling "set + # prompt" successfully disables the vtysh shell prompt to its + # standard value, so it is necessary to call it again. + self._determine_set_prompt() + + # From now on the shell _prompt attribute is set to the defined + # vtysh forced prompt. + self._prompt = '|'.join([BASH_FORCED_PROMPT, VTYSH_FORCED_PROMPT]) + + else: + # If the image does not support "set prompt", then enable the + # filtering of echo by setting the corresponding attribute to True. + # WARNING: Using a private attribute here. + self._try_filter_echo = True + + # From now on the shell _prompt attribute is set to the defined + # vtysh standard prompt. + self._prompt = '|'.join( + [BASH_FORCED_PROMPT, VTYSH_STANDARD_PROMPT] + ) + + # This sendline is used here just because a _setup_shell must end in an + # send/sendline call since it is followed by a call to expect in the + # connect method. + spawn.sendline('') + + def send_command( + self, command, matches=None, newline=True, timeout=None, + connection=None, silent=False + ): + # This parent method performs the connection to the shell and the set + # up of a bash prompt to an unique value. + match_index = super(OpenSwitchVtyshShell, self).send_command( + command, matches=matches, newline=newline, timeout=timeout, + connection=connection, silent=silent + ) + + # This will raise a proper exception if a crash has been found. + self._handle_crash(connection) + + return match_index + + +__all__ = ['OpenSwitchVtyshShell'] diff --git a/requirements.dev.txt b/requirements.dev.txt index ec7568a..0edfcf8 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -9,5 +9,5 @@ autoapi topology_lib_ping topology_lib_ip --e git+https://github.com/HPENetworking/topology_docker.git@master#egg=topology_docker --e git+https://git.openswitch.net/openswitch/ops-topology-lib-vtysh@8c6af11bf84813324a6518ccda101712297cd357#egg=topology_lib_vtysh +-e git+https://github.com/saenzpa/topology_docker.git@master#egg=topology_docker +-e git+https://github.com/HPENetworking/topology_openswitch.git@master#egg=topology_openswitch diff --git a/requirements.txt b/requirements.txt index 2a437bf..ffe2fce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ six -topology_docker>=1.5.0 diff --git a/setup.py b/setup.py index fa2a23e..3e6afc1 100755 --- a/setup.py +++ b/setup.py @@ -62,6 +62,7 @@ def find_requirements(filename): version=find_version('lib/topology_docker_openswitch/__init__.py'), package_dir={'': 'lib'}, packages=find_packages('lib'), + include_package_data=True, # Dependencies install_requires=find_requirements('requirements.txt'), @@ -93,10 +94,12 @@ def find_requirements(filename): # Entry points entry_points={ - 'pytest11': ['topology_docker_openswitch ' - '= topology_docker_openswitch.plugin.plugin'], + 'pytest11': [ + 'topology_docker_openswitch ' + '= topology_docker_openswitch.pytest.plugin' + ], 'topology_docker_node_10': [ - 'openswitch = topology_docker_openswitch.openswitch:OpenSwitchNode' + 'openswitch = topology_docker_openswitch.openswitch:OpenSwitch' ] } ) diff --git a/test/helpers.py b/test/helpers.py deleted file mode 100644 index 5a14227..0000000 --- a/test/helpers.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2015-2016 Hewlett Packard Enterprise Development LP -# -# 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. - -""" -Testing helpers for OpenSwitch support node for the Topology framework. -""" - -from __future__ import unicode_literals, absolute_import -from __future__ import print_function, division - -from time import sleep - - -def wait_until_interface_up(switch, portlbl, timeout=30, polling_frequency=1): - """ - Wait until the interface, as mapped by the given portlbl, is marked as up. - - :param switch: The switch node. - :param str portlbl: Port label that is mapped to the interfaces. - :param int timeout: Number of seconds to wait. - :param int polling_frequency: Frequency of the polling. - :return: None if interface is brought-up. If not, an assertion is raised. - """ - for i in range(timeout): - status = switch.libs.vtysh.show_interface(portlbl) - if status['interface_state'] == 'up': - break - sleep(polling_frequency) - else: - assert False, ( - 'Interface {}:{} never brought-up after ' - 'waiting for {} seconds'.format( - switch.identifier, portlbl, timeout - ) - ) - - -__all__ = ['wait_until_interface_up'] diff --git a/test/test_ping.py b/test/test_ping.py deleted file mode 100644 index a0cb604..0000000 --- a/test/test_ping.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2015-2016 Hewlett Packard Enterprise Development LP -# -# 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. - -""" -OpenSwitch Test for simple ping between nodes. -""" - -from __future__ import unicode_literals, absolute_import -from __future__ import print_function, division - -from time import sleep - -from .helpers import wait_until_interface_up - - -TOPOLOGY = """ -# +-------+ +-------+ -# | | +-------+ +-------+ | | -# | hs1 <-----> sw1 <-----> sw2 <-----> hs2 | -# | | +-------+ +-------+ | | -# +-------+ +-------+ - -# Nodes -[type=openswitch name="Switch 1"] sw1 -[type=openswitch name="Switch 2"] sw2 -[type=host name="Host 1"] hs1 -[type=host name="Host 2"] hs2 - -# Links -hs1:1 -- sw1:3 -sw1:4 -- sw2:3 -sw2:4 -- hs2:1 -""" - - -def test_ping(topology): - """ - Set network addresses and static routes between nodes and ping h2 from h1. - """ - sw1 = topology.get('sw1') - sw2 = topology.get('sw2') - hs1 = topology.get('hs1') - hs2 = topology.get('hs2') - - assert sw1 is not None - assert sw2 is not None - assert hs1 is not None - assert hs2 is not None - - # Configure IP and bring UP host 1 interfaces - hs1.libs.ip.interface('1', addr='10.0.10.1/24', up=True) - - # Configure IP and bring UP host 2 interfaces - hs2.libs.ip.interface('1', addr='10.0.30.1/24', up=True) - - # Configure IP and bring UP switch 1 interfaces - with sw1.libs.vtysh.ConfigInterface('3') as ctx: - ctx.ip_address('10.0.10.2/24') - ctx.no_shutdown() - - with sw1.libs.vtysh.ConfigInterface('4') as ctx: - ctx.ip_address('10.0.20.1/24') - ctx.no_shutdown() - - # Configure IP and bring UP switch 2 interfaces - with sw2.libs.vtysh.ConfigInterface('3') as ctx: - ctx.ip_address('10.0.20.2/24') - ctx.no_shutdown() - - with sw2.libs.vtysh.ConfigInterface('4') as ctx: - ctx.ip_address('10.0.30.2/24') - ctx.no_shutdown() - - # Wait until interfaces are up - for switch, portlbl in [(sw1, '3'), (sw1, '4'), (sw2, '3'), (sw2, '4')]: - wait_until_interface_up(switch, portlbl) - - # Set static routes in switches - sw1.libs.ip.add_route('10.0.30.0/24', '10.0.20.2', shell='bash_swns') - sw2.libs.ip.add_route('10.0.10.0/24', '10.0.20.1', shell='bash_swns') - - # Set gateway in hosts - hs1.libs.ip.add_route('default', '10.0.10.2') - hs2.libs.ip.add_route('default', '10.0.30.2') - - sleep(1) - ping = hs1.libs.ping.ping(1, '10.0.30.1') - assert ping['transmitted'] == ping['received'] == 1 diff --git a/test/test_port_labels.py b/test/test_port_labels.py deleted file mode 100644 index 3bfe301..0000000 --- a/test/test_port_labels.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2015-2016 Hewlett Packard Enterprise Development LP -# -# 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. - -""" -OpenSwitch Test for vlan related configurations. -""" - -from __future__ import unicode_literals, absolute_import -from __future__ import print_function, division - -from time import sleep - - -TOPOLOGY = """ -# +-------+ +-------+ -# | | +--------+ | | -# | hs1 <-----> ops1 <-----> hs2 | -# | | +--------+ | | -# +-------+ +-------+ - -# Nodes -[type=openswitch name="OpenSwitch 1"] ops1 -[type=host name="Host 1"] hs1 -[type=host name="Host 2"] hs2 - -# Links -hs1:if01 -- ops1:if01 -ops1:IF02 -- hs2:if01 -""" - - -def test_topology_nodes_openvswitch_port_labels(topology): - ops1 = topology.get('ops1') - hs1 = topology.get('hs1') - hs2 = topology.get('hs2') - - assert ops1 is not None - assert hs1 is not None - assert hs2 is not None - - ops1('configure terminal') - ops1('interface ' + str(ops1.ports['if01'])) - ops1('no shutdown') - ops1('end') - sleep(5) - result = ops1('show interface ' + str(ops1.ports['if01'])) - assert result != '% Unknown command.' diff --git a/test/test_topology_docker_openswitch.py b/test/test_topology_docker_openswitch.py new file mode 100644 index 0000000..b9d196d --- /dev/null +++ b/test/test_topology_docker_openswitch.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015-2016 Hewlett Packard Enterprise Development LP +# +# 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. + +""" +Test suite for module topology_docker_openswitch. +""" + +from __future__ import unicode_literals, absolute_import +from __future__ import print_function, division + +from topology import __version__ + + +def setup_module(module): + print('setup_module({})'.format(module.__name__)) + + +def teardown_module(module): + print('teardown_module({})'.format(module.__name__)) + + +def test_semantic_version(): + """ + Check that version follows the Semantic Versioning 2.0.0 specification. + + http://semver.org/ + """ + mayor, minor, rev = map(int, __version__.split('.')) + + assert mayor >= 0 + assert minor >= 0 + assert rev >= 0 diff --git a/test/test_vlan.py b/test/test_vlan.py deleted file mode 100644 index 3d7d149..0000000 --- a/test/test_vlan.py +++ /dev/null @@ -1,119 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2015-2016 Hewlett Packard Enterprise Development LP -# -# 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. - -""" -OpenSwitch Test for vlan related configurations. -""" - -from __future__ import unicode_literals, absolute_import -from __future__ import print_function, division - -from .helpers import wait_until_interface_up - - -TOPOLOGY = """ -# +-------+ +-------+ -# | | +--------+ | | -# | hs1 <-----> ops1 <-----> hs2 | -# | | +--------+ | | -# +-------+ +-------+ - -# Nodes -[type=openswitch name="OpenSwitch 1"] ops1 -[type=host name="Host 1"] hs1 -[type=host name="Host 2"] hs2 - -# Links -hs1:1 -- ops1:7 -ops1:8 -- hs2:1 -""" - - -def test_vlan(topology): - """ - Test that a vlan configuration is functional with a OpenSwitch switch. - - Build a topology of one switch and two hosts and connect the hosts to the - switch. Setup a VLAN for the ports connected to the hosts and ping from - host 1 to host 2. - """ - ops1 = topology.get('ops1') - hs1 = topology.get('hs1') - hs2 = topology.get('hs2') - - assert ops1 is not None - assert hs1 is not None - assert hs2 is not None - - p7 = ops1.ports['7'] - p8 = ops1.ports['8'] - - # Mark interfaces as enabled - # Note: It is possible that this test fails here with - # pexpect.exceptions.TIMEOUT. There not much we can do, OpenSwitch - # may have a race condition or something that makes this command to - # freeze or to take more than 60 seconds to complete. - iface_enabled = ops1( - 'set interface {p7} user_config:admin=up'.format(**locals()), - shell='vsctl' - ) - assert not iface_enabled - - iface_enabled = ops1( - 'set interface {p8} user_config:admin=up'.format(**locals()), - shell='vsctl' - ) - assert not iface_enabled - - # Configure interfaces - with ops1.libs.vtysh.ConfigInterface('7') as ctx: - ctx.no_routing() - ctx.no_shutdown() - - with ops1.libs.vtysh.ConfigInterface('8') as ctx: - ctx.no_routing() - ctx.no_shutdown() - - # Configure vlan and switch interfaces - with ops1.libs.vtysh.ConfigVlan('8') as ctx: - ctx.no_shutdown() - - with ops1.libs.vtysh.ConfigInterface('7') as ctx: - ctx.vlan_access('8') - - with ops1.libs.vtysh.ConfigInterface('8') as ctx: - ctx.vlan_access('8') - - # Wait until interfaces are up - for portlbl in ['7', '8']: - wait_until_interface_up(ops1, portlbl) - - # Assert vlan status - vlan_status = ops1.libs.vtysh.show_vlan('8').get('8') - assert vlan_status is not None - assert vlan_status['vlan_id'] == '8' - assert vlan_status['status'] == 'up' - assert vlan_status['reason'] == 'ok' - assert sorted(vlan_status['ports']) == [p7, p8] - - # Configure host interfaces - hs1.libs.ip.interface('1', addr='10.0.10.1/24', up=True) - hs2.libs.ip.interface('1', addr='10.0.10.2/24', up=True) - - # Test ping - ping = hs1.libs.ping.ping(1, '10.0.10.2') - assert ping['transmitted'] == ping['received'] == 1