VPN Scenario tests using Rally

This patch aims at creating Rally scale tests for VPNaaS.
It tests the full stack creation of VPNaaS -creating actual
networks/routers etc.,

(a)vpn_utils.py contains a set of standalone utility methods to
   create/delete networks, nova instances etc.,
(b)vpn_base.py holds the vpn specific functions to create, delete
   and verify vpn connectivity.
(c)test_vpn_connectivity.py holds the actual scenario.
(d)rally_config.yaml file is the input configuration file

To test this patch:
(1) Install devstack with rally plugin.
(2) Create the following folder structure.
    sudo mkdir /opt/rally
(3) Create a symlink to the plugins directory.
    cd /opt/rally
    sudo ln -s /opt/stack/neutron-vpnaas/rally-jobs/plugins
(4) Run the test
    rally task start <path-to-input-file>
    To see the debug logs,
    rally -vd taks start <path-to-input-file>

Note:
This patch also includes the commit ID cb5e1708 of vpn_utils.py

Change-Id: I28915baf8c4ff07b61e70a33ba63693d65fce80a
This commit is contained in:
Aishwarya Thangappa 2015-09-30 00:49:37 -07:00
parent 77e6a5e2e5
commit 0fc509ebcc
4 changed files with 870 additions and 707 deletions

View File

@ -0,0 +1,70 @@
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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 neutron.i18n import _LI
from rally.common import log as logging
from rally.task import scenario
from rally.task import types as types
import vpn_base
LOG = logging.getLogger(__name__)
class VpnBasicScenario(vpn_base.VpnBase):
"""Rally scenarios for VPNaaS"""
@types.set(image=types.ImageResourceType,
flavor=types.FlavorResourceType)
@scenario.configure()
def create_and_delete_vpn_connection(
self, **kwargs):
"""Basic VPN connectivity scenario.
1. Create 2 private networks, subnets and routers
2. Create public network, subnets and GW IPs on routers, if not present
3. Execute ip netns command and get the snat and qrouter namespaces
(assuming we use DVR)
4. Verify that there is a route between the router gateways by pinging
each other from their snat namespaces
5. Add security group rules for SSH and ICMP
6. Start a nova instance in each of the private networks
7. Create IKE and IPSEC policies
8. Create VPN service at each of the routers
9. Create IPSEC site connections at both endpoints
10. Verify that the ipsec-site-connection is ACTIVE (takes upto 30secs)
11. To verify the vpn connectivity, get into the first snat
namespace and start a tcpdump at the qg-xxxx interface
12. SSH into the second instance from the second qrouter namespace
and try to ping the first instance
14. Verify that the captured packets are encapsulated and encrypted.
15. Verify the connectivity in the reverse direction following the
steps 11 through 13
16. Submit a request to delete all the resources
"""
try:
self.setup()
self.create_networks_and_servers(**kwargs)
self.check_route()
self.ike_policy = self._create_ike_policy(**kwargs)
self.ipsec_policy = self._create_ipsec_policy(**kwargs)
self.create_vpn_services()
self.create_ipsec_site_connections(**kwargs)
self.assert_statuses(**kwargs)
self.assert_vpn_connectivity()
LOG.info(_LI("VPN CONNECTIVITY TEST PASSED!!"))
finally:
self.cleanup()

View File

@ -0,0 +1,405 @@
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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 concurrent.futures
import exceptions
import multiprocessing
import re
import time
from oslo_utils import uuidutils
from rally.common import log as logging
from rally.plugins.openstack import scenario as rally_base
from rally.task import atomic
import vpn_utils
LOG = logging.getLogger(__name__)
LOCK = multiprocessing.RLock()
MAX_RESOURCES = 2
class VpnBase(rally_base.OpenStackScenario):
def setup(self):
"""Creates and initializes data structures to hold various resources"""
self.snat_namespaces = []
self.qrouter_namespaces = []
self.rally_router_gw_ips = []
self.rally_routers = []
self.rally_networks = []
self.rally_subnets = []
self.rally_cidrs = []
self.ike_policy = None
self.ipsec_policy = None
self.vpn_services = []
self.ipsec_site_connections = []
self.servers = []
self.server_private_ips = []
self.suffixes = [uuidutils.generate_uuid(), uuidutils.generate_uuid()]
self.key_names = map(lambda x: "rally_keypair_" + x, self.suffixes)
self.key_file_paths = map(lambda x: '/tmp/' + x, self.key_names)
self.neutron_client = self.clients("neutron")
self.neutron_admin_client = self.admin_clients("neutron")
self.nova_client = self.clients("nova")
@atomic.action_timer("cleanup")
def cleanup(self):
"""Cleans up all the resources"""
vpn_utils.delete_servers(self.nova_client, self.servers)
vpn_utils.delete_hosts_from_knownhosts_file(self.server_private_ips)
vpn_utils.delete_key_files(self.key_file_paths)
self._delete_ipsec_site_connections()
self._delete_vpn_services()
self._delete_ipsec_policy()
self._delete_ike_policy()
vpn_utils.delete_network(
self.neutron_client, self.neutron_admin_client, self.rally_routers,
self.rally_networks, self.rally_subnets)
@atomic.action_timer("_create_ike_policy")
def _create_ike_policy(self, **kwargs):
"""Creates IKE policy
:return: IKE policy
"""
LOG.debug("CREATING IKE_POLICY")
ike_policy = self.neutron_client.create_ikepolicy({
"ikepolicy": {
"phase1_negotiation_mode":
kwargs.get("phase1_negotiation_mode", "main"),
"auth_algorithm": kwargs.get("auth_algorithm", "sha1"),
"encryption_algorithm":
kwargs.get("encryption_algorithm", "aes-128"),
"pfs": kwargs.get("pfs", "group5"),
"lifetime": {
"units": "seconds",
"value": kwargs.get("value", 7200)},
"ike_version": kwargs.get("ike_version", "v1"),
"name": "rally_ikepolicy"
}
})
return ike_policy
@atomic.action_timer("_create_ipsec_policy")
def _create_ipsec_policy(self, **kwargs):
"""Creates IPSEC policy
:return: IPSEC policy
"""
LOG.debug("CREATING IPSEC_POLICY")
ipsec_policy = self.neutron_client.create_ipsecpolicy({
"ipsecpolicy": {
"name": "rally_ipsecpolicy",
"transform_protocol": kwargs.get("transform_protocol", "esp"),
"auth_algorithm": kwargs.get("auth_algorithm", "sha1"),
"encapsulation_mode":
kwargs.get("encapsulation_mode", "tunnel"),
"encryption_algorithm":
kwargs.get("encryption_algorithm", "aes-128"),
"pfs": kwargs.get("pfs", "group5"),
"lifetime": {
"units": "seconds",
"value": kwargs.get("value", 7200)
}
}
})
return ipsec_policy
@atomic.action_timer("_create_vpn_service")
def _create_vpn_service(self, rally_subnet, rally_router, vpn_suffix=None):
"""Creates VPN service endpoints
:param rally_subnet: local subnet
:param rally_router: router endpoint
:param vpn_suffix: suffix name for vpn service
:return: VPN service
"""
LOG.debug("CREATING VPN_SERVICE")
vpn_service = self.neutron_client.create_vpnservice({
"vpnservice": {
"subnet_id": rally_subnet["subnet"]["id"],
"router_id": rally_router["router"]["id"],
"name": "rally_vpn_service_" + vpn_suffix,
"admin_state_up": True
}
})
return vpn_service
@atomic.action_timer("_create_ipsec_site_connection")
def _create_ipsec_site_connection(self, local_index, peer_index, **kwargs):
"""Creates IPSEC site connection
:param local_index: parameter to point to the local end-point
:param peer_index: parameter to point to the peer end-point
:return: IPSEC site connection
"""
LOG.debug("CREATING IPSEC_SITE_CONNECTION")
ipsec_site_conn = self.neutron_client.create_ipsec_site_connection({
"ipsec_site_connection": {
"psk": kwargs.get("secret", "secret"),
"initiator": "bi-directional",
"ipsecpolicy_id": self.ipsec_policy["ipsecpolicy"]["id"],
"admin_state_up": True,
"peer_cidrs": self.rally_cidrs[peer_index],
"mtu": kwargs.get("mtu", "1500"),
"ikepolicy_id": self.ike_policy["ikepolicy"]["id"],
"dpd": {
"action": "disabled",
"interval": 60,
"timeout": 240
},
"vpnservice_id":
self.vpn_services[local_index]["vpnservice"]["id"],
"peer_address": self.rally_router_gw_ips[peer_index],
"peer_id": self.rally_router_gw_ips[peer_index],
"name": "rally_ipsec_site_connection_" +
self.suffixes[local_index]
}
})
return ipsec_site_conn
def _get_resource(self, resource_tag, resource_id):
"""Gets the resource(vpn_service or ipsec_site_connection)
:param resource_tag: "vpnservice" or "ipsec_site_connection"
:param resource_id: id of the resource
:return: resource (vpn_service or ipsec_site_connection)
"""
if resource_tag == "vpnservice":
vpn_service = self.neutron_client.show_vpnservice(resource_id)
if vpn_service:
return vpn_service
elif resource_tag == 'ipsec_site_connection':
ipsec_site_conn = self.neutron_client.show_ipsec_site_connection(
resource_id)
if ipsec_site_conn:
return ipsec_site_conn
@atomic.action_timer("_wait_for_status_change")
def _wait_for_status_change(self, resource, resource_tag, final_status,
wait_timeout=60, check_interval=1):
"""Wait for resource's status change
Wait till the status of the resource changes to final state or till
the time exceeds the wait_timeout value.
:param resource: resource whose status has to be checked
:param final_status: desired final status of the resource
:param resource_tag: to identify the resource as vpnservice or
ipsec_site_connection
:param wait_timeout: timeout value in seconds
:param check_interval: time to sleep before each check for the status
change
:return: resource
"""
start_time = time.time()
while True:
resource = self._get_resource(resource_tag,
resource[resource_tag]['id'])
current_status = resource[resource_tag]['status']
if current_status == final_status:
return resource
time.sleep(check_interval)
if time.time() - start_time > wait_timeout:
raise exceptions.Exception(
"Timeout waiting for resource {} to change to {} status".
format(resource[resource_tag]['name'], final_status)
)
def _assert_statuses(self, ipsec_site_conn, vpn_service, **kwargs):
"""Assert statuses of vpn_service and ipsec_site_connection
:param ipsec_site_conn: ipsec_site_connection object
:param vpn_service: vpn_service object
"""
vpn_service = self._wait_for_status_change(
vpn_service,
resource_tag="vpnservice",
final_status="ACTIVE",
wait_timeout=kwargs.get("vpn_service_creation_timeout"),
check_interval=5)
LOG.debug("VPN SERVICE STATUS %s", vpn_service['vpnservice']['status'])
assert('ACTIVE' == vpn_service['vpnservice']['status']), (
"VPN_SERVICE IS NOT IN ACTIVE STATE")
ipsec_site_conn = self._wait_for_status_change(
ipsec_site_conn,
resource_tag="ipsec_site_connection",
final_status="ACTIVE",
wait_timeout=kwargs.get("ipsec_site_connection_creation_timeout"),
check_interval=5)
LOG.debug("IPSEC_SITE_CONNECTION STATUS: %s",
ipsec_site_conn['ipsec_site_connection']['status'])
assert('ACTIVE' ==
ipsec_site_conn['ipsec_site_connection']['status']), (
"THE INSTANCE IS NOT IN ACTIVE STATE")
@atomic.action_timer("_verify_vpn_connection")
def _verify_vpn_connection(self, local_index, peer_index):
"""Verifies the vpn connectivity between the endpoints
:param local_index: parameter to point to the local end-point
:param peer_index: parameter to point to the peer end-point
:return: True or False
"""
qg = vpn_utils.get_interfaces(self.snat_namespaces[peer_index])
if qg:
p = re.compile(r"qg-\w+-\w+")
m = p.search(qg)
if m:
qg_interface = m.group()
else:
qg_interface = None
if qg_interface:
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as e:
tcpdump_future = e.submit(vpn_utils.start_tcpdump,
self.snat_namespaces[peer_index],
qg_interface)
ssh_future = e.submit(vpn_utils.ssh_and_ping_server,
self.server_private_ips[local_index],
self.server_private_ips[peer_index],
self.qrouter_namespaces[local_index],
self.key_file_paths[local_index])
assert(ssh_future.result()), "SSH/Ping failed"
lines = tcpdump_future.result().split('\n')
for line in lines:
if 'ESP' in line:
return True
return False
@atomic.action_timer("_delete_ipsec_site_connection")
def _delete_ipsec_site_connections(self):
"""Deletes IPSEC site connections"""
if self.ipsec_site_connections:
for site_conn in self.ipsec_site_connections:
if "rally" in (site_conn['ipsec_site_connection']['name']):
LOG.debug("DELETING IPSEC_SITE_CONNECTION %s",
site_conn['ipsec_site_connection']['id'])
self.neutron_client.delete_ipsec_site_connection(
site_conn['ipsec_site_connection']['id'])
@atomic.action_timer("_delete_vpn_service")
def _delete_vpn_services(self):
"""Deletes VPN service endpoints"""
if self.vpn_services:
for vpn_service in self.vpn_services:
if "rally" in vpn_service['vpnservice']['name']:
LOG.debug("DELETING VPN_SERVICE %s",
vpn_service['vpnservice']['id'])
self.neutron_client.delete_vpnservice(
vpn_service['vpnservice']['id'])
@atomic.action_timer("_delete_ipsec_policy")
def _delete_ipsec_policy(self):
"""Deletes IPSEC policy
:param ipsec_policy: ipsec_policy object
:return:
"""
LOG.debug("DELETING IPSEC POLICY")
if (self.ipsec_policy and
"rally" in self.ipsec_policy['ipsecpolicy']['name']):
self.neutron_client.delete_ipsecpolicy(
self.ipsec_policy['ipsecpolicy']['id'])
@atomic.action_timer("_delete_ike_policy")
def _delete_ike_policy(self):
"""Deletes IKE policy
:param ike_policy: ike_policy object
:return:
"""
LOG.debug("DELETING IKE POLICY")
if (self.ike_policy and
"rally" in self.ike_policy['ikepolicy']['name']):
self.neutron_client.delete_ikepolicy(
self.ike_policy['ikepolicy']['id'])
def create_networks_and_servers(self, **kwargs):
with LOCK:
keypairs = []
for x in range(MAX_RESOURCES):
router, network, subnet, cidr = vpn_utils.create_network(
self.neutron_client, self. neutron_admin_client,
self.suffixes[x])
self.rally_cidrs.append(cidr)
self.rally_subnets.append(subnet)
self.rally_networks.append(network)
self.rally_routers.append(router)
self.rally_router_gw_ips.append(
router["router"]["external_gateway_info"]
["external_fixed_ips"][0]["ip_address"])
self.snat_namespaces.append(
vpn_utils.wait_for_namespace_creation(
"snat-", router, **kwargs))
self.qrouter_namespaces.append(
vpn_utils.wait_for_namespace_creation(
"qrouter-", router, **kwargs))
keypairs.append(vpn_utils.create_keypair(
self.nova_client, self.key_names[x],
self.key_file_paths[x]))
kwargs.update({
"nics":
[{"net-id": self.rally_networks[x]["network"]["id"]}],
"sec_group_suffix": self.suffixes[x],
"server_suffix": self.suffixes[x]
})
server = vpn_utils.create_nova_vm(
self.nova_client, keypairs[x], **kwargs)
self.server_private_ips.append(vpn_utils.get_server_ip(
self.nova_client, server.id, self.suffixes[x]))
self.servers.append(server)
def check_route(self):
LOG.debug("VERIFYING THAT THERE IS A ROUTE BETWEEN ROUTER "
"GATEWAYS")
for ns in self.snat_namespaces:
for ip in self.rally_router_gw_ips:
assert(True == vpn_utils.ping(ns, ip)), (
"PING FAILED FROM NAMESPACE " + ns + " TO IP "
+ ip)
def create_vpn_services(self):
with LOCK:
for x in range(MAX_RESOURCES):
self.vpn_services.append(self._create_vpn_service(
self.rally_subnets[x], self.rally_routers[x],
self.suffixes[x]))
def create_ipsec_site_connections(self, **kwargs):
with LOCK:
self.ipsec_site_connections = [
self._create_ipsec_site_connection(0, 1, **kwargs),
self._create_ipsec_site_connection(1, 0, **kwargs)
]
def assert_statuses(self, **kwargs):
LOG.debug("ASSERTING ACTIVE STATUSES FOR VPN-SERVICES AND "
"VPN-CONNECTIONS")
for x in range(MAX_RESOURCES):
self._assert_statuses(self.ipsec_site_connections[x],
self.vpn_services[x], **kwargs)
def assert_vpn_connectivity(self):
LOG.debug("VERIFY THE VPN CONNECTIVITY")
with LOCK:
assert(self._verify_vpn_connection(0, 1)), "VPN CONNECTION FAILED"
assert(self._verify_vpn_connection(1, 0)), "VPN CONNECTION FAILED"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
{% set image_name = image_name or "cirros-0.3.3-x86_64-disk" %}
---
VpnBasicScenario.create_and_delete_vpn_connection:
-
args:
flavor:
name: "m1.tiny"
image:
name: {{image_name}}
phase1_negotiation_mode: "main"
auth_algorithm: "sha1"
encryption_algorithm: "aes-128"
pfs: "group5"
value: 7200
ike_version: "v1"
transform_protocol: "esp"
encapsulation_mode: "tunnel"
mtu: 1500
secret: "secret"
nova_server_boot_timeout: 60 * 6
vpn_service_creation_timeout: 100
ipsec_site_connection_creation_timeout: 400
namespace_creation_timeout: 60
runner:
type: "constant"
times: 1
concurrency: 1
context:
users:
tenants: 1
users_per_tenant: 1
sla:
failure_rate:
max: 0