From 716bac8174a36fd883a19d82480450a817ecad52 Mon Sep 17 00:00:00 2001 From: jmehta Date: Tue, 9 Jun 2015 06:17:03 -0700 Subject: [PATCH] End to end scenario test case for l2gateway Validating entries in the ovsdb after creating l2gateway connection Logical Switch for network and tunnel key UCast MAC Remote for MAC address and VTEP mapping Physical Port Validating vni binding Physical Locator for hosts Test l2gateway connection Create a network Create a subnet Create a server Create an l2gateway Create an l2gateway connection Validate resources in ovsdb Test l2gateway multiple connections Create a network Create a subnet Create a server Create an l2gateway Create an l2gateway connection Validate resources in ovsdb Repeat for one more connection Test boot VM after connection create Create a network Create a subnet Create an l2gateway Create an l2gateway connection Create a server Validate resources in ovsdb Test create connection after deleting old Create a network Create a subnet Create a server Create an l2gateway Create an l2gateway connection Validate resources in ovsdb Delete the l2gateway connection Create a new l2gateway connection Validate resources in ovsdb Change-Id: Ia634336d3162cb91661ed2c6d21cb507f02a3d11 --- networking_l2gw/tests/scenario/__init__.py | 0 .../tests/scenario/ovsdb_connections.py | 153 +++++++ .../tests/scenario/test_l2gateways.py | 388 ++++++++++++++++++ networking_l2gw/tests/tempest/config.py | 20 +- .../tests/tempest/etc/tempest.conf.sample | 8 +- 5 files changed, 567 insertions(+), 2 deletions(-) create mode 100644 networking_l2gw/tests/scenario/__init__.py create mode 100644 networking_l2gw/tests/scenario/ovsdb_connections.py create mode 100644 networking_l2gw/tests/scenario/test_l2gateways.py diff --git a/networking_l2gw/tests/scenario/__init__.py b/networking_l2gw/tests/scenario/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/networking_l2gw/tests/scenario/ovsdb_connections.py b/networking_l2gw/tests/scenario/ovsdb_connections.py new file mode 100644 index 0000000..c585148 --- /dev/null +++ b/networking_l2gw/tests/scenario/ovsdb_connections.py @@ -0,0 +1,153 @@ +# Copyright 2015 OpenStack Foundation +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# 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 collections +import json +import logging +import random +import socket +import threading +import time + +logging.basicConfig(level=logging.DEBUG) + + +def default_echo_handler(message, ovsconn): + logging.debug("responding to echo") + ovsconn.send({"result": message.get("params", None), + "error": None, "id": message['id']}) + + +def default_message_handler(message, ovsconn): + ovsconn.responses.append(message) + + +class OVSDBConnection(threading.Thread): + """Connects to an ovsdb server that has manager set using + + ovs-vsctl set-manager ptcp:5000 + + clients can make calls and register a callback for results, callbacks + are linked based on the message ids. + + clients can also register methods which they are interested in by + providing a callback. + """ + + def __init__(self, IP, PORT, **handlers): + super(OVSDBConnection, self).__init__() + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect((IP, PORT)) + self.responses = [] + self.callbacks = {} + self.read_on = True + self.handlers = handlers or {"echo": default_echo_handler} + self.start() + + def send(self, message, callback=None): + if callback: + self.callbacks[message['id']] = callback + self.socket.send(json.dumps(message)) + + def response(self, id): + return [x for x in self.responses if x['id'] == id] + + def set_handler(self, method_name, handler): + self.handlers[method_name] = handler + + def _on_remote_message(self, message): + try: + json_m = json.loads(message, + object_pairs_hook=collections.OrderedDict) + handler_method = json_m.get('method', None) + if handler_method: + self.handlers.get(handler_method, default_message_handler)( + json_m, self) + elif json_m.get("result", None) and json_m['id'] in self.callbacks: + id = json_m['id'] + if not self.callbacks[id](json_m, self): + self.callbacks.pop(id) + + else: + default_message_handler(message, self) + except Exception as e: + logging.exception( + "exception [%s] in handling message [%s]", e.message, message) + + def __echo_response(message, self): + self.send({"result": message.get("params", None), + "error": None, "id": message['id']}) + + def run(self): + + chunks = [] + lc = rc = 0 + while self.read_on: + try: + response = self.socket.recv(4096) + if response: + response = response.decode('utf8') + message_mark = 0 + for i, c in enumerate(response): + if c == '{': + lc += 1 + elif c == '}': + rc += 1 + + if rc > lc: + raise Exception("json string not valid") + + elif lc == rc and lc is not 0: + chunks.append(response[message_mark:i + 1]) + message = "".join(chunks) + self._on_remote_message(message) + lc = rc = 0 + message_mark = i + 1 + chunks = [] + + chunks.append(response[message_mark:]) + except Exception: + # Pass to avoid EOF error + pass + + def stop(self, force=False): + self.read_on = False + if force: + self.socket.close() + + def select_table(self, table): + select_dict = {"op": "select", "table": table, "where": []} + op_id = str(random.getrandbits(128)) + params = ['hardware_vtep'] + params.append(select_dict) + query_select = {"method": "transact", + "params": params, + "id": op_id} + return query_select + + def find_row(self, net_id, count, resp_dec): + for i in range(count): + row = str(resp_dec['result'][0]['rows'][i]) + if net_id in row: + return row + + def get_response(self, OVSDB_IP, OVSDB_PORT, table): + query = self.select_table(table) + self.send(query) + time.sleep(2) + resp = self.responses + resp = str(resp[0]) + return resp diff --git a/networking_l2gw/tests/scenario/test_l2gateways.py b/networking_l2gw/tests/scenario/test_l2gateways.py new file mode 100644 index 0000000..38cd20a --- /dev/null +++ b/networking_l2gw/tests/scenario/test_l2gateways.py @@ -0,0 +1,388 @@ +# Copyright 2015 OpenStack Foundation +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# 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. + +from networking_l2gw.tests.api import base_l2gw +from networking_l2gw.tests.scenario import ovsdb_connections +from networking_l2gw.tests.tempest import config + +from neutron.i18n import _LI +from neutron.tests.api import base +from neutron.tests.tempest import exceptions +from neutron.tests.tempest import manager + +from oslo_log import log +from tempest_lib.common import rest_client +from tempest_lib.common.utils import data_utils +from tempest_lib.common.utils import misc as misc_utils +from tempest_lib import exceptions as lib_exc + +import json +import time + +CONF = config.CONF +LOG = log.getLogger(__name__) +OVSDB_IP = CONF.network.ovsdb_ip +OVSDB_PORT = CONF.network.ovsdb_port +OVSDB_SCHEMA_NAME = CONF.network.ovsdb_schema_name + + +class TestL2GatewayBasic(base.BaseAdminNetworkTest): + + """This test case tests the basic end to end functionality of l2-gateway + + and tests whether the appropriate entries are getting registered in + + the ovsdb. + """ + + @classmethod + def resource_setup(cls): + super(TestL2GatewayBasic, cls).resource_setup() + nova_creds = cls.isolated_creds.get_admin_creds() + cls.auth_provider = manager.get_auth_provider(nova_creds) + + def _create_server( + self, name=None, + network=None, wait_on_boot=True, wait_on_delete=True): + region = CONF.compute.region + image = CONF.compute.image_ref + flavor = CONF.compute.flavor_ref + rs_client = rest_client.RestClient( + self.auth_provider, 'compute', region) + data = {'server': { + 'name': name, + 'imageRef': image, + 'flavorRef': flavor, + 'max_count': 1, + 'min_count': 1, + 'networks': [{'uuid': network}]}} + data = json.dumps(data) + (resp, body,) = rs_client.post('/servers', data) + rs_client.expected_success(202, resp.status) + body = json.loads(body) + server_id = body['server']['id'] + self.wait_for_server_status(server_id, 'ACTIVE') + return server_id + + def _delete_server(self, server=None): + rs_client = rest_client.RestClient( + self.auth_provider, 'compute', 'RegionOne') + (resp, body, ) = rs_client.delete('servers/%s' % str(server)) + self.wait_for_server_termination(server) + rest_client.ResponseBody(resp, body) + + def wait_for_server_status(self, server_id, status, ready_wait=True, + extra_timeout=0, raise_on_error=True): + """Waits for a server to reach a given status.""" + build_timeout = CONF.compute.build_timeout + build_interval = CONF.boto.build_interval + + def _get_task_state(body): + return body.get('OS-EXT-STS:task_state', None) + rs = rest_client.RestClient(self.auth_provider, "compute", "RegionOne") + resp, body = rs.get("servers/%s" % str(server_id)) + body = json.loads(body) + old_status = server_status = body['server']['status'] + old_task_state = task_state = _get_task_state(body) + start_time = int(time.time()) + timeout = build_timeout + extra_timeout + while True: + if status == 'BUILD' and server_status != 'UNKNOWN': + return + if server_status == status: + if ready_wait: + if status == 'BUILD': + return + if str(task_state) == "None": + time.sleep(CONF.compute.ready_wait) + return + else: + return + time.sleep(build_interval) + resp, body = rs.get("servers/%s" % str(server_id)) + body = json.loads(body) + server_status = body['server']['status'] + task_state = _get_task_state(body) + if (server_status != old_status) or (task_state != old_task_state): + oldstatus = '/'.join((old_status, str(old_task_state))) + serverstatus = '/'.join((server_status, str(task_state))) + waitsec = (time.time() - start_time) + LOG.info( + _LI('State transtion %(oldstatus)s => %(serverstatus)s' + 'after %(waitsec)d second wait') % + {'oldstatus': oldstatus, 'serverstatus': serverstatus, + 'waitsec': waitsec} + ) + if (server_status == 'ERROR') and raise_on_error: + if 'fault' in body: + raise exceptions.BuildErrorException(body['fault'], + server_id=server_id) + else: + raise exceptions.BuildErrorException(server_id=server_id) + timed_out = int(time.time()) - start_time >= timeout + if timed_out: + expected_task_state = 'None' if ready_wait else 'n/a' + message = ('Server %(server_id)s failed to reach %(status)s ' + 'status and task state "%(expected_task_state)s" ' + 'within the required time (%(timeout)s s).' % + {'server_id': server_id, + 'status': status, + 'expected_task_state': expected_task_state, + 'timeout': timeout}) + message += ' Current status: %s.' % server_status + message += ' Current task state: %s.' % task_state + caller = misc_utils.find_test_caller() + if caller: + message = '(%s) %s' % (caller, message) + raise exceptions.TimeoutException(message) + old_status = server_status + old_task_state = task_state + + def wait_for_server_termination(self, server_id, ignore_error=False): + """Waits for server to reach termination.""" + build_interval = CONF.boto.build_interval + while True: + try: + rs = rest_client.RestClient( + self.auth_provider, 'compute', 'RegionOne') + (resp, body,) = rs.get('servers/%s' % str(server_id)) + body = json.loads(body) + except lib_exc.NotFound: + return + server_status = body['server']['status'] + if server_status == 'ERROR' and not ignore_error: + raise exceptions.BuildErrorException(server_id=server_id) + time.sleep(build_interval) + + def validate_ovsdb(self, seg_id, port, network_id_1, tunnel_key): + # Check Logical_Switch + objConnection = ovsdb_connections.OVSDBConnection(OVSDB_IP, OVSDB_PORT) + resp = objConnection.get_response( + OVSDB_IP, OVSDB_PORT, "Logical_Switch") + resp_dec = json.loads(resp) + count = resp.count('_uuid') + try: + self.assertIn(str(network_id_1), resp) + except Exception: + raise lib_exc.NotFound("Network not found in Logical Switch table") + row = objConnection.find_row(network_id_1, count, resp_dec) + try: + self.assertIn(str(tunnel_key), row) + except Exception: + raise lib_exc.NotFound( + "Tunnel key not found in Logical Switch table") + objConnection.stop("true") + + # Check Physical_Port + objConnection = ovsdb_connections.OVSDBConnection(OVSDB_IP, OVSDB_PORT) + resp = objConnection.get_response( + OVSDB_IP, OVSDB_PORT, "Physical_Port") + count = resp.count('_uuid') + try: + self.assertIn(str(seg_id[0]), resp) + except Exception: + raise lib_exc.NotFound( + "Segmentation ID not found in Physical Port table") + objConnection.stop("true") + + # Check Physical_Locator + objConnection = ovsdb_connections.OVSDBConnection(OVSDB_IP, OVSDB_PORT) + resp = objConnection.get_response( + OVSDB_IP, OVSDB_PORT, "Physical_Locator") + count = resp.count('_uuid') + port_str = str(port) + count_port = port_str.count('fixed_ips') + net_node_host = [] + compute_node_host = [] + # Extracting unique Network node host name and Compute host name + for i in range(count_port): + net_id = port['ports'][i]['network_id'] + device_owner = port['ports'][i]['device_owner'] + if net_id == network_id_1 and device_owner == 'network:dhcp': + if port['ports'][i]['binding:host_id'] not in net_node_host: + net_node_host.append(port['ports'][i]['binding:host_id']) + if port['ports'][i]['device_owner'] == 'compute:None': + if port['ports'][i]['binding:host_id'] not in net_node_host: + compute_node_host.append( + port['ports'][i]['binding:host_id']) + ip_SW = CONF.network.l2gw_switch_ip + host_and_ip = CONF.network.hosts + list_ = host_and_ip.split(', ') + host_ip_dict = {} + for i in list_: + sub_list = i.split(':') + host = sub_list[0] + ip = sub_list[1] + host_ip_dict.update({host: ip}) + for net_node in net_node_host: + ip_NN = host_ip_dict[net_node] + try: + self.assertIn(ip_NN, resp) + except Exception: + raise lib_exc.NotFound( + "Network Node IP not found in Physical Locator table") + for compute_node in compute_node_host: + ip_CN = host_ip_dict[compute_node] + try: + self.assertIn(ip_CN, resp) + except Exception: + raise lib_exc.NotFound( + "Compute Node IP not found in Physical Locator table") + try: + self.assertIn(ip_SW, resp) + except Exception: + raise lib_exc.NotFound( + "Switch IP not found in Physical Locator table") + objConnection.stop("true") + + # Check Ucast_macs_Remote + objConnection = ovsdb_connections.OVSDBConnection(OVSDB_IP, OVSDB_PORT) + resp = objConnection.get_response( + OVSDB_IP, OVSDB_PORT, "Ucast_Macs_Remote") + ip_mac_dict = {} + count_uuid = resp.count('_uuid') + resp_dec = json.loads(resp) + for i in range(count_port): + mac = port['ports'][i]['mac_address'] + ip = port['ports'][i]['fixed_ips'][0]['ip_address'] + ip_mac_dict.update({mac: ip}) + try: + for key, value in ip_mac_dict.iteritems(): + row = objConnection.find_row(key, count_uuid, resp_dec) + self.assertIn(value, row) + except Exception: + raise lib_exc.NotFound( + "MAC & its port not found in UCast MAC Remote table") + objConnection.stop("true") + + def _create_l2_gateway(self, name, devices): + body_l2gateway = self.admin_client.create_l2_gateway( + name=name, devices=devices) + self.addCleanup( + self.admin_client.delete_l2_gateway, + body_l2gateway['l2_gateway']['id']) + return body_l2gateway + + def _create_l2_gw_connection( + self, l2gw, net_id, seg_id=None, explicit=None): + l2gw_id = l2gw['l2_gateway']['id'] + if l2gw['l2_gateway']['devices'][0]['interfaces'][ + 0]['segmentation_id']: + resp_l2gwconn = self.admin_client.create_l2_gateway_connection( + network_id=net_id, l2_gateway_id=l2gw_id) + else: + resp_l2gwconn = self.admin_client.create_l2_gateway_connection( + network_id=net_id, + l2_gateway_id=l2gw_id, segmentation_id=seg_id) + if explicit: + # Connection deleted explicitly, thus addCleanup not called + pass + else: + self.addCleanup( + self.admin_client.delete_l2_gateway_connection, + resp_l2gwconn['l2_gateway_connection']['id']) + return resp_l2gwconn + + def _setup_network_and_server(self, cidr=None): + network = self.create_network() + self.addCleanup(self.client.delete_network, network['id']) + self.create_subnet(network=network, cidr=cidr) + name = data_utils.rand_name('server-smoke') + server_id = self._create_server(name, network=network['id']) + self.addCleanup(self._delete_server, server_id) + return network + + def test_l2gw_create_connection(self): + network = self._setup_network_and_server() + network_body = self.admin_client.show_network(network['id']) + gw_name = data_utils.rand_name('l2gw') + devices = base_l2gw.get_l2gw_body(CONF.network.l2gw_switch)['devices'] + l2_gw_body = self._create_l2_gateway(name=gw_name, devices=devices) + segmentation_id = l2_gw_body['l2_gateway']['devices'][0][ + 'interfaces'][0]['segmentation_id'] + self._create_l2_gw_connection(l2_gw_body, network['id']) + tunnel_key = network_body['network']['provider:segmentation_id'] + port = self.admin_client.list_ports() + self.validate_ovsdb(segmentation_id, port, network['id'], tunnel_key) + + def test_multiple_connections(self): + # Create first connection and validate + network = self._setup_network_and_server() + network_body = self.admin_client.show_network(network['id']) + gw_name = data_utils.rand_name('l2gw') + devices = base_l2gw.get_l2gw_body(CONF.network.l2gw_switch)['devices'] + l2_gw_body = self._create_l2_gateway(name=gw_name, devices=devices) + segmentation_id = l2_gw_body['l2_gateway']['devices'][0][ + 'interfaces'][0]['segmentation_id'] + self._create_l2_gw_connection(l2_gw_body, network['id']) + tunnel_key = network_body['network']['provider:segmentation_id'] + port = self.admin_client.list_ports() + self.validate_ovsdb(segmentation_id, port, network['id'], tunnel_key) + # Create second connection and validate + network_2 = self._setup_network_and_server() + network_body_2 = self.admin_client.show_network(network_2['id']) + gw_name_2 = data_utils.rand_name('l2gw') + devices_2 = base_l2gw.get_l2gw_body( + CONF.network.l2gw_switch_2)['devices'] + l2_gw_body_2 = self._create_l2_gateway( + name=gw_name_2, devices=devices_2) + segmentation_id_2 = l2_gw_body_2['l2_gateway']['devices'][0][ + 'interfaces'][0]['segmentation_id'] + self._create_l2_gw_connection(l2_gw_body_2, network_2['id']) + tunnel_key_2 = network_body_2['network']['provider:segmentation_id'] + port_2 = self.admin_client.list_ports() + self.validate_ovsdb( + segmentation_id_2, port_2, network_2['id'], tunnel_key_2) + + def test_boot_vm_after_create_connection(self): + network = self.create_network() + self.addCleanup(self.client.delete_network, network['id']) + self.create_subnet(network) + gw_name = data_utils.rand_name('l2gw') + devices = base_l2gw.get_l2gw_body(CONF.network.l2gw_switch)['devices'] + l2_gw_body = self._create_l2_gateway(name=gw_name, devices=devices) + segmentation_id = l2_gw_body['l2_gateway']['devices'][0][ + 'interfaces'][0]['segmentation_id'] + self._create_l2_gw_connection(l2_gw_body, network['id']) + name = data_utils.rand_name('server-smoke') + server_id = self._create_server(name, network=network['id']) + self.addCleanup(self._delete_server, server_id) + network_body = self.admin_client.show_network(network['id']) + segmentation_id = l2_gw_body['l2_gateway']['devices'][0][ + 'interfaces'][0]['segmentation_id'] + tunnel_key = network_body['network']['provider:segmentation_id'] + port = self.admin_client.list_ports() + self.validate_ovsdb(segmentation_id, port, network['id'], tunnel_key) + + def test_create_new_connection_after_deleting_old_one(self): + network = self._setup_network_and_server() + network_body = self.admin_client.show_network(network['id']) + gw_name = data_utils.rand_name('l2gw') + devices = base_l2gw.get_l2gw_body(CONF.network.l2gw_switch)['devices'] + l2_gw_body = self._create_l2_gateway(name=gw_name, devices=devices) + segmentation_id = l2_gw_body['l2_gateway']['devices'][0][ + 'interfaces'][0]['segmentation_id'] + # Create a connection and validate ovsdb + l2gw_connection = self._create_l2_gw_connection( + l2_gw_body, network['id'], explicit=True) + tunnel_key = network_body['network']['provider:segmentation_id'] + port = self.admin_client.list_ports() + self.validate_ovsdb(segmentation_id, port, network['id'], tunnel_key) + # Delete and create new connection and validate ovsdb + self.admin_client.delete_l2_gateway_connection( + l2gw_connection['l2_gateway_connection']['id']) + self._create_l2_gw_connection(l2_gw_body, network['id']) + self.validate_ovsdb(segmentation_id, port, network['id'], tunnel_key) diff --git a/networking_l2gw/tests/tempest/config.py b/networking_l2gw/tests/tempest/config.py index e7a0d66..a6ea301 100644 --- a/networking_l2gw/tests/tempest/config.py +++ b/networking_l2gw/tests/tempest/config.py @@ -403,7 +403,25 @@ network_group = cfg.OptGroup(name='network', NetworkGroup = [ cfg.StrOpt('l2gw_switch', default='', - help='Test'), + help='Switch name ,interface and vlan id information '), + cfg.StrOpt('l2gw_switch_2', + default='', + help='Switch name ,interface and vlan id information'), + cfg.StrOpt('hosts', + default='', + help='Network node and compute node host names and IPs'), + cfg.StrOpt('l2gw_switch_ip', + default='', + help='Switch IP'), + cfg.StrOpt('ovsdb_ip', + default='', + help='IP of ovsdb server'), + cfg.IntOpt('ovsdb_port', + default=6632, + help='Port of ovsdb server'), + cfg.StrOpt('ovsdb_schema_name', + default='', + help='schema name of ovsdb'), cfg.StrOpt('catalog_type', default='network', help='Catalog type of the Neutron service.'), diff --git a/networking_l2gw/tests/tempest/etc/tempest.conf.sample b/networking_l2gw/tests/tempest/etc/tempest.conf.sample index 2dfb478..beefd6c 100644 --- a/networking_l2gw/tests/tempest/etc/tempest.conf.sample +++ b/networking_l2gw/tests/tempest/etc/tempest.conf.sample @@ -1,6 +1,12 @@ -[l2_gateway] +[network] #List of switch device,interfaces name and segmentation_ids for l2gateway l2gw_switch = cell08-5930-01::FortyGigE1/0/1|100 +l2gw_switch_2 = cell21-5930-01::FortyGigE1/0/1|101 #l2gw_switch = cell08-5930-01::FortyGigE1/0/1|100#200;FortyGigE1/0/2|300 #l2gw_switch = cell08-5930-01::FortyGigE1/0/1|100#200,cell08-5930-02::FortyGigE1/0/2|300 +l2gw_switch_ip = switch_vtep_ip +hosts = cn_host_name:cn_ip, nn_host_name:nn_ip +ovsdb_ip = ovsdb_ip +ovsdb_port = 6632 +ovsdb_schema_name = hardware_vtep