diff --git a/TESTING.rst b/TESTING.rst index 60fdeff1c72..de300bf635c 100644 --- a/TESTING.rst +++ b/TESTING.rst @@ -58,7 +58,8 @@ that broad categorization, here are a few more characteristic: such as an agent using no mocks. * Integration tests - Run against a running cloud, often target the API level, but also 'scenarios' or 'user stories'. You may find such tests under - tests/api, tests/fullstack and in the Tempest and Rally projects. + tests/tempest/api, tests/tempest/scenario, tests/fullstack, and in the + Tempest and Rally projects. Tests in the Neutron tree are typically organized by the testing infrastructure used, and not by the scope of the test. For example, many tests under the @@ -324,6 +325,17 @@ Tests for other resources should be contributed to the Neutron repository. Scenario tests should be similarly split up between Tempest and Neutron according to the API they're targeting. +Scenario Tests +~~~~~~~~~~~~~~ + +Scenario tests (neutron/tests/tempest/scenario), like API tests, use the +Tempest test infrastructure and have the same requirements. Guidelines for +writing a good scenario test may be found at the Tempest developer guide: +http://docs.openstack.org/developer/tempest/field_guide/scenario.html + +Scenario tests, like API tests, are split between the Tempest and Neutron +repositories according to the Neutron API the test is targeting. + Development Process ------------------- @@ -510,10 +522,10 @@ Logging from the test infrastructure itself is placed in: Fullstack test suite assumes 240.0.0.0/4 (Class E) range in root namespace of the test machine is available for its usage. -API Tests -+++++++++ +API & Scenario Tests +++++++++++++++++++++ -To run the api tests, deploy Tempest and Neutron with DevStack and +To run the api or scenario tests, deploy Tempest and Neutron with DevStack and then run the following command, from the tempest directory: :: tox -e all-plugin diff --git a/neutron/agent/securitygroups_rpc.py b/neutron/agent/securitygroups_rpc.py index 8c5c1d932f4..7818cdfcb10 100644 --- a/neutron/agent/securitygroups_rpc.py +++ b/neutron/agent/securitygroups_rpc.py @@ -310,7 +310,7 @@ class SecurityGroupAgentRpc(object): self.refresh_firewall(updated_devices) -# TODO(armax): for bw compat with external dependencies; to be dropped in M. +# TODO(armax): For bw compat with external dependencies; to be dropped in M. SG_RPC_VERSION = ( securitygroups_rpc.SecurityGroupAgentRpcApiMixin.SG_RPC_VERSION ) diff --git a/neutron/tests/tempest/api/clients.py b/neutron/tests/tempest/api/clients.py index d9879b042e0..8b51c6ef8f6 100644 --- a/neutron/tests/tempest/api/clients.py +++ b/neutron/tests/tempest/api/clients.py @@ -13,24 +13,21 @@ # License for the specific language governing permissions and limitations # under the License. +from tempest.lib.services.compute import keypairs_client +from tempest.lib.services.compute import servers_client from tempest import manager -from tempest.services.identity.v2.json.tenants_client import \ - TenantsClient +from tempest.services.identity.v2.json import tenants_client from neutron.tests.tempest import config -from neutron.tests.tempest.services.network.json.network_client import \ - NetworkClientJSON - +from neutron.tests.tempest.services.network.json import network_client CONF = config.CONF class Manager(manager.Manager): - """ Top level manager for OpenStack tempest clients """ - default_params = { 'disable_ssl_certificate_validation': CONF.identity.disable_ssl_certificate_validation, @@ -51,7 +48,7 @@ class Manager(manager.Manager): self._set_identity_clients() - self.network_client = NetworkClientJSON( + self.network_client = network_client.NetworkClientJSON( self.auth_provider, CONF.network.catalog_type, CONF.network.region or CONF.identity.region, @@ -60,6 +57,23 @@ class Manager(manager.Manager): build_timeout=CONF.network.build_timeout, **self.default_params) + params = { + 'service': CONF.compute.catalog_type, + 'region': CONF.compute.region or CONF.identity.region, + 'endpoint_type': CONF.compute.endpoint_type, + 'build_interval': CONF.compute.build_interval, + 'build_timeout': CONF.compute.build_timeout + } + params.update(self.default_params) + + self.servers_client = servers_client.ServersClient( + self.auth_provider, + enable_instance_password=CONF.compute_feature_enabled + .enable_instance_password, + **params) + self.keypairs_client = keypairs_client.KeyPairsClient( + self.auth_provider, **params) + def _set_identity_clients(self): params = { 'service': CONF.identity.catalog_type, @@ -69,5 +83,5 @@ class Manager(manager.Manager): params_v2_admin = params.copy() params_v2_admin['endpoint_type'] = CONF.identity.v2_admin_endpoint_type # Client uses admin endpoint type of Keystone API v2 - self.tenants_client = TenantsClient(self.auth_provider, - **params_v2_admin) + self.tenants_client = tenants_client.TenantsClient(self.auth_provider, + **params_v2_admin) diff --git a/neutron/tests/tempest/scenario/base.py b/neutron/tests/tempest/scenario/base.py new file mode 100644 index 00000000000..04b591a58d9 --- /dev/null +++ b/neutron/tests/tempest/scenario/base.py @@ -0,0 +1,113 @@ +# Copyright 2016 Red Hat, Inc. +# 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 oslo_log import log as logging + +from tempest.common import waiters +from tempest.lib.common import ssh +from tempest.lib.common.utils import data_utils + +from neutron.tests.tempest.api import base as base_api +from neutron.tests.tempest import config +from neutron.tests.tempest.scenario import constants + +CONF = config.CONF +LOG = logging.getLogger(__name__) + + +class BaseTempestTestCase(base_api.BaseNetworkTest): + @classmethod + def resource_setup(cls): + super(BaseTempestTestCase, cls).resource_setup() + + cls.servers = [] + cls.keypairs = [] + + @classmethod + def resource_cleanup(cls): + for server in cls.servers: + cls.manager.servers_client.delete_server(server) + waiters.wait_for_server_termination(cls.manager.servers_client, + server) + + for keypair in cls.keypairs: + cls.manager.keypairs_client.delete_keypair( + keypair_name=keypair['name']) + + super(BaseTempestTestCase, cls).resource_cleanup() + + @classmethod + def create_server(cls, flavor_ref, image_ref, key_name, networks, + name=None): + name = name or data_utils.rand_name('server-test') + server = cls.manager.servers_client.create_server( + name=name, flavorRef=flavor_ref, + imageRef=image_ref, + key_name=key_name, + networks=networks) + cls.servers.append(server['server']['id']) + return server + + @classmethod + def create_keypair(cls, client=None): + client = client or cls.manager.keypairs_client + name = data_utils.rand_name('keypair-test') + body = client.create_keypair(name=name) + cls.keypairs.append(body['keypair']) + return body['keypair'] + + @classmethod + def create_loginable_secgroup_rule(cls, secgroup_id=None): + client = cls.manager.network_client + if not secgroup_id: + sgs = client.list_security_groups()['security_groups'] + for sg in sgs: + if sg['name'] == constants.DEFAULT_SECURITY_GROUP: + secgroup_id = sg['id'] + break + + # This rule is intended to permit inbound ssh + # traffic from all sources, so no group_id is provided. + # Setting a group_id would only permit traffic from ports + # belonging to the same security group. + ruleset = {'protocol': 'tcp', + 'port_range_min': 22, + 'port_range_max': 22, + 'remote_ip_prefix': '0.0.0.0/0'} + rules = [client.create_security_group_rule( + direction='ingress', security_group_id=secgroup_id, + **ruleset)['security_group_rule']] + return rules + + @classmethod + def create_router_and_interface(cls, subnet_id): + router = cls.create_router( + data_utils.rand_name('router'), admin_state_up=True, + external_network_id=CONF.network.public_network_id) + cls.create_router_interface(router['id'], subnet_id) + cls.routers.append(router) + return router + + @classmethod + def create_and_associate_floatingip(cls, port_id): + fip = cls.manager.network_client.create_floatingip( + CONF.network.public_network_id, + port_id=port_id)['floatingip'] + cls.floating_ips.append(fip) + return fip + + @classmethod + def check_connectivity(cls, host, ssh_user, ssh_key=None): + ssh_client = ssh.Client(host, ssh_user, pkey=ssh_key) + ssh_client.test_connection_auth() diff --git a/neutron/tests/tempest/scenario/constants.py b/neutron/tests/tempest/scenario/constants.py new file mode 100644 index 00000000000..bb1cab31bb1 --- /dev/null +++ b/neutron/tests/tempest/scenario/constants.py @@ -0,0 +1,16 @@ +# Copyright 2016 Red Hat, Inc. +# +# 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. + +SERVER_STATUS_ACTIVE = 'ACTIVE' +DEFAULT_SECURITY_GROUP = 'default' diff --git a/neutron/tests/tempest/scenario/test_basic.py b/neutron/tests/tempest/scenario/test_basic.py new file mode 100644 index 00000000000..8fa6aeaa527 --- /dev/null +++ b/neutron/tests/tempest/scenario/test_basic.py @@ -0,0 +1,54 @@ +# Copyright 2016 Red Hat, Inc. +# 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 oslo_log import log as logging +from tempest.common import waiters + +from neutron.tests.tempest import config +from neutron.tests.tempest.scenario import base +from neutron.tests.tempest.scenario import constants + +CONF = config.CONF +LOG = logging.getLogger(__name__) + + +class NetworkBasicTest(base.BaseTempestTestCase): + credentials = ['primary'] + force_tenant_isolation = False + + # Default to ipv4. + _ip_version = 4 + + def test_basic_instance(self): + network = self.create_network() + subnet = self.create_subnet(network) + + self.create_router_and_interface(subnet['id']) + keypair = self.create_keypair() + self.create_loginable_secgroup_rule() + server = self.create_server( + flavor_ref=CONF.compute.flavor_ref, + image_ref=CONF.compute.image_ref, + key_name=keypair['name'], + networks=[{'uuid': network['id']}]) + waiters.wait_for_server_status(self.manager.servers_client, + server['server']['id'], + constants.SERVER_STATUS_ACTIVE) + port = self.client.list_ports(network_id=network['id'], + device_id=server[ + 'server']['id'])['ports'][0] + fip = self.create_and_associate_floatingip(port['id']) + self.check_connectivity(fip['floating_ip_address'], + CONF.validation.image_ssh_user, + keypair['private_key']) diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py index 4a89ad50653..70cc207d627 100644 --- a/neutron/tests/tempest/services/network/json/network_client.py +++ b/neutron/tests/tempest/services/network/json/network_client.py @@ -639,3 +639,57 @@ class NetworkClientJSON(service_client.RestClient): self.expected_success(200, resp.status) body = jsonutils.loads(body) return service_client.ResponseBody(resp, body) + + def create_security_group_rule(self, direction, security_group_id, + **kwargs): + post_body = {'security_group_rule': kwargs} + post_body['security_group_rule']['direction'] = direction + post_body['security_group_rule'][ + 'security_group_id'] = security_group_id + body = jsonutils.dumps(post_body) + uri = '%s/security-group-rules' % self.uri_prefix + resp, body = self.post(uri, body) + self.expected_success(201, resp.status) + body = jsonutils.loads(body) + return service_client.ResponseBody(resp, body) + + def list_security_groups(self, **kwargs): + post_body = {'security_groups': kwargs} + body = jsonutils.dumps(post_body) + uri = '%s/security-groups' % self.uri_prefix + if kwargs: + uri += '?' + urlparse.urlencode(kwargs, doseq=1) + resp, body = self.get(uri) + self.expected_success(200, resp.status) + body = jsonutils.loads(body) + return service_client.ResponseBody(resp, body) + + def delete_security_group(self, security_group_id): + uri = '%s/security-groups/%s' % ( + self.uri_prefix, security_group_id) + resp, body = self.delete(uri) + self.expected_success(204, resp.status) + return service_client.ResponseBody(resp, body) + + def list_ports(self, **kwargs): + post_body = {'ports': kwargs} + body = jsonutils.dumps(post_body) + uri = '%s/ports' % self.uri_prefix + if kwargs: + uri += '?' + urlparse.urlencode(kwargs, doseq=1) + resp, body = self.get(uri) + self.expected_success(200, resp.status) + body = jsonutils.loads(body) + return service_client.ResponseBody(resp, body) + + def create_floatingip(self, floating_network_id, **kwargs): + post_body = {'floatingip': { + 'floating_network_id': floating_network_id}} + if kwargs: + post_body['floatingip'].update(kwargs) + body = jsonutils.dumps(post_body) + uri = '%s/floatingips' % self.uri_prefix + resp, body = self.post(uri, body) + self.expected_success(201, resp.status) + body = jsonutils.loads(body) + return service_client.ResponseBody(resp, body)