diff --git a/tempest/api/orchestration/base.py b/tempest/api/orchestration/base.py index 745dd87bb6..2a72c95d33 100644 --- a/tempest/api/orchestration/base.py +++ b/tempest/api/orchestration/base.py @@ -89,8 +89,8 @@ class BaseOrchestrationTest(tempest.test.BaseTestCase): pass @classmethod - def _create_keypair(cls, namestart='keypair-heat-'): - kp_name = rand_name(namestart) + def _create_keypair(cls, name_start='keypair-heat-'): + kp_name = rand_name(name_start) resp, body = cls.keypairs_client.create_keypair(kp_name) cls.keypairs.append(kp_name) return body diff --git a/tempest/api/orchestration/stacks/test_neutron_resources.py b/tempest/api/orchestration/stacks/test_neutron_resources.py new file mode 100644 index 0000000000..c934020582 --- /dev/null +++ b/tempest/api/orchestration/stacks/test_neutron_resources.py @@ -0,0 +1,211 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 logging + +from tempest.api.orchestration import base +from tempest import clients +from tempest.common.utils.data_utils import rand_name +from tempest.test import attr + + +LOG = logging.getLogger(__name__) + + +class NeutronResourcesTestJSON(base.BaseOrchestrationTest): + _interface = 'json' + + template = """ +HeatTemplateFormatVersion: '2012-12-12' +Description: | + Template which creates single EC2 instance +Parameters: + KeyName: + Type: String + InstanceType: + Type: String + ImageId: + Type: String + ExternalRouterId: + Type: String +Resources: + Network: + Type: OS::Quantum::Net + Properties: {name: NewNetwork} + Subnet: + Type: OS::Quantum::Subnet + Properties: + network_id: {Ref: Network} + name: NewSubnet + ip_version: 4 + cidr: 10.0.3.0/24 + dns_nameservers: ["8.8.8.8"] + allocation_pools: + - {end: 10.0.3.150, start: 10.0.3.20} + RouterInterface: + Type: OS::Quantum::RouterInterface + Properties: + router_id: {Ref: ExternalRouterId} + subnet_id: {Ref: Subnet} + Server: + Type: AWS::EC2::Instance + Metadata: + Name: SmokeServer + Properties: + ImageId: {Ref: ImageId} + InstanceType: {Ref: InstanceType} + KeyName: {Ref: KeyName} + SubnetId: {Ref: Subnet} + UserData: + Fn::Base64: + Fn::Join: + - '' + - - '#!/bin/bash -v + + ' + - /opt/aws/bin/cfn-signal -e 0 -r "SmokeServer created" ' + - {Ref: WaitHandle} + - ''' + + ' + WaitHandle: + Type: AWS::CloudFormation::WaitConditionHandle + WaitCondition: + Type: AWS::CloudFormation::WaitCondition + DependsOn: Server + Properties: + Handle: {Ref: WaitHandle} + Timeout: '600' +""" + + @classmethod + def setUpClass(cls): + super(NeutronResourcesTestJSON, cls).setUpClass() + if not cls.orchestration_cfg.image_ref: + raise cls.skipException("No image available to test") + cls.client = cls.orchestration_client + os = clients.Manager() + cls.network_cfg = os.config.network + if not cls.config.service_available.neutron: + raise cls.skipException("Neutron support is required") + cls.network_client = os.network_client + cls.stack_name = rand_name('heat') + cls.keypair_name = (cls.orchestration_cfg.keypair_name or + cls._create_keypair()['name']) + cls.external_router_id = cls._get_external_router_id() + + # create the stack + cls.stack_identifier = cls.create_stack( + cls.stack_name, + cls.template, + parameters={ + 'KeyName': cls.keypair_name, + 'InstanceType': cls.orchestration_cfg.instance_type, + 'ImageId': cls.orchestration_cfg.image_ref, + 'ExternalRouterId': cls.external_router_id + }) + cls.stack_id = cls.stack_identifier.split('/')[1] + cls.client.wait_for_stack_status(cls.stack_id, 'CREATE_COMPLETE') + _, resources = cls.client.list_resources(cls.stack_identifier) + cls.test_resources = {} + for resource in resources: + cls.test_resources[resource['logical_resource_id']] = resource + + @classmethod + def _get_external_router_id(cls): + resp, body = cls.network_client.list_ports() + ports = body['ports'] + router_ports = filter(lambda port: port['device_owner'] == + 'network:router_interface', ports) + return router_ports[0]['device_id'] + + @attr(type='slow') + def test_created_resources(self): + """Verifies created neutron resources.""" + resources = [('Network', 'OS::Quantum::Net'), + ('Subnet', 'OS::Quantum::Subnet'), + ('RouterInterface', 'OS::Quantum::RouterInterface'), + ('Server', 'AWS::EC2::Instance')] + for resource_name, resource_type in resources: + resource = self.test_resources.get(resource_name, None) + self.assertIsInstance(resource, dict) + self.assertEqual(resource_name, resource['logical_resource_id']) + self.assertEqual(resource_type, resource['resource_type']) + self.assertEqual('CREATE_COMPLETE', resource['resource_status']) + + @attr(type='slow') + def test_created_network(self): + """Verifies created netowrk.""" + network_id = self.test_resources.get('Network')['physical_resource_id'] + resp, body = self.network_client.show_network(network_id) + self.assertEqual('200', resp['status']) + network = body['network'] + self.assertIsInstance(network, dict) + self.assertEqual(network_id, network['id']) + self.assertEqual('NewNetwork', network['name']) + + @attr(type='slow') + def test_created_subnet(self): + """Verifies created subnet.""" + subnet_id = self.test_resources.get('Subnet')['physical_resource_id'] + resp, body = self.network_client.show_subnet(subnet_id) + self.assertEqual('200', resp['status']) + subnet = body['subnet'] + network_id = self.test_resources.get('Network')['physical_resource_id'] + self.assertEqual(subnet_id, subnet['id']) + self.assertEqual(network_id, subnet['network_id']) + self.assertEqual('NewSubnet', subnet['name']) + self.assertEqual('8.8.8.8', subnet['dns_nameservers'][0]) + self.assertEqual('10.0.3.20', subnet['allocation_pools'][0]['start']) + self.assertEqual('10.0.3.150', subnet['allocation_pools'][0]['end']) + self.assertEqual(4, subnet['ip_version']) + self.assertEqual('10.0.3.0/24', subnet['cidr']) + + @attr(type='slow') + def test_created_router_interface(self): + """Verifies created router interface.""" + network_id = self.test_resources.get('Network')['physical_resource_id'] + subnet_id = self.test_resources.get('Subnet')['physical_resource_id'] + resp, body = self.network_client.list_ports() + self.assertEqual('200', resp['status']) + ports = body['ports'] + router_ports = filter(lambda port: port['device_id'] == + self.external_router_id, ports) + created_network_ports = filter(lambda port: port['network_id'] == + network_id, router_ports) + self.assertEqual(1, len(created_network_ports)) + router_interface = created_network_ports[0] + fixed_ips = router_interface['fixed_ips'] + subnet_fixed_ips = filter(lambda port: port['subnet_id'] == + subnet_id, fixed_ips) + self.assertEqual(1, len(subnet_fixed_ips)) + router_interface_ip = subnet_fixed_ips[0]['ip_address'] + self.assertEqual('10.0.3.1', router_interface_ip) + + @attr(type='slow') + def test_created_server(self): + """Verifies created sever.""" + server_id = self.test_resources.get('Server')['physical_resource_id'] + resp, server = self.servers_client.get_server(server_id) + self.assertEqual('200', resp['status']) + self.assertEqual(self.keypair_name, server['key_name']) + self.assertEqual('ACTIVE', server['status']) + network = server['addresses']['NewNetwork'][0] + self.assertEqual(4, network['version']) + ip_addr_prefix = network['addr'][:7] + ip_addr_suffix = int(network['addr'].split('.')[3]) + self.assertEqual('10.0.3.', ip_addr_prefix) + self.assertTrue(ip_addr_suffix >= 20) + self.assertTrue(ip_addr_suffix <= 150) diff --git a/tempest/api/orchestration/stacks/test_non_empty_stack.py b/tempest/api/orchestration/stacks/test_non_empty_stack.py new file mode 100644 index 0000000000..defb910bf6 --- /dev/null +++ b/tempest/api/orchestration/stacks/test_non_empty_stack.py @@ -0,0 +1,169 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 logging + +from tempest.api.orchestration import base +from tempest.common.utils.data_utils import rand_name +from tempest.test import attr + + +LOG = logging.getLogger(__name__) + + +class StacksTestJSON(base.BaseOrchestrationTest): + _interface = 'json' + + template = """ +HeatTemplateFormatVersion: '2012-12-12' +Description: | + Template which creates single EC2 instance +Parameters: + KeyName: + Type: String + InstanceType: + Type: String + ImageId: + Type: String +Resources: + SmokeServer: + Type: AWS::EC2::Instance + Metadata: + Name: SmokeServer + Properties: + ImageId: {Ref: ImageId} + InstanceType: {Ref: InstanceType} + KeyName: {Ref: KeyName} + UserData: + Fn::Base64: + Fn::Join: + - '' + - - '#!/bin/bash -v + + ' + - /opt/aws/bin/cfn-signal -e 0 -r "SmokeServer created" ' + - {Ref: WaitHandle} + - ''' + + ' + WaitHandle: + Type: AWS::CloudFormation::WaitConditionHandle + WaitCondition: + Type: AWS::CloudFormation::WaitCondition + DependsOn: SmokeServer + Properties: + Handle: {Ref: WaitHandle} + Timeout: '600' +""" + + @classmethod + def setUpClass(cls): + super(StacksTestJSON, cls).setUpClass() + if not cls.orchestration_cfg.image_ref: + raise cls.skipException("No image available to test") + cls.client = cls.orchestration_client + cls.stack_name = rand_name('heat') + keypair_name = (cls.orchestration_cfg.keypair_name or + cls._create_keypair()['name']) + + # create the stack + cls.stack_identifier = cls.create_stack( + cls.stack_name, + cls.template, + parameters={ + 'KeyName': keypair_name, + 'InstanceType': cls.orchestration_cfg.instance_type, + 'ImageId': cls.orchestration_cfg.image_ref + }) + cls.stack_id = cls.stack_identifier.split('/')[1] + cls.resource_name = 'SmokeServer' + cls.resource_type = 'AWS::EC2::Instance' + cls.client.wait_for_stack_status(cls.stack_id, 'CREATE_COMPLETE') + + @attr(type='slow') + def test_stack_list(self): + """Created stack should be on the list of existing stacks.""" + resp, stacks = self.client.list_stacks() + self.assertEqual('200', resp['status']) + self.assertIsInstance(stacks, list) + stacks_names = map(lambda stack: stack['stack_name'], stacks) + self.assertIn(self.stack_name, stacks_names) + + @attr(type='slow') + def test_stack_show(self): + """Getting details about created stack should be possible.""" + resp, stack = self.client.get_stack(self.stack_name) + self.assertEqual('200', resp['status']) + self.assertIsInstance(stack, dict) + self.assertEqual(self.stack_name, stack['stack_name']) + self.assertEqual(self.stack_id, stack['id']) + + @attr(type='slow') + def test_list_resources(self): + """Getting list of created resources for the stack should be possible. + """ + resp, resources = self.client.list_resources(self.stack_identifier) + self.assertEqual('200', resp['status']) + self.assertIsInstance(resources, list) + resources_names = map(lambda resource: resource['logical_resource_id'], + resources) + self.assertIn(self.resource_name, resources_names) + resources_types = map(lambda resource: resource['resource_type'], + resources) + self.assertIn(self.resource_type, resources_types) + + @attr(type='slow') + def test_show_resource(self): + """Getting details about created resource should be possible.""" + resp, resource = self.client.get_resource(self.stack_identifier, + self.resource_name) + self.assertIsInstance(resource, dict) + self.assertEqual(self.resource_name, resource['logical_resource_id']) + self.assertEqual(self.resource_type, resource['resource_type']) + + @attr(type='slow') + def test_resource_metadata(self): + """Getting metadata for created resource should be possible.""" + resp, metadata = self.client.show_resource_metadata( + self.stack_identifier, + self.resource_name) + self.assertEqual('200', resp['status']) + self.assertIsInstance(metadata, dict) + self.assertEqual(self.resource_name, metadata.get('Name', None)) + + @attr(type='slow') + def test_list_events(self): + """Getting list of created events for the stack should be possible.""" + resp, events = self.client.list_events(self.stack_identifier) + self.assertEqual('200', resp['status']) + self.assertIsInstance(events, list) + resource_statuses = map(lambda event: event['resource_status'], events) + self.assertIn('CREATE_IN_PROGRESS', resource_statuses) + self.assertIn('CREATE_COMPLETE', resource_statuses) + + @attr(type='slow') + def test_show_event(self): + """Getting details about existing event should be possible.""" + resp, events = self.client.list_resource_events(self.stack_identifier, + self.resource_name) + self.assertNotEqual([], events) + events.sort(key=lambda event: event['event_time']) + event_id = events[0]['id'] + resp, event = self.client.show_event(self.stack_identifier, + self.resource_name, event_id) + self.assertEqual('200', resp['status']) + self.assertEqual('CREATE_IN_PROGRESS', event['resource_status']) + self.assertEqual('state changed', event['resource_status_reason']) + self.assertEqual(self.resource_name, event['logical_resource_id']) + self.assertIsInstance(event, dict) diff --git a/tempest/api/orchestration/stacks/test_stacks.py b/tempest/api/orchestration/stacks/test_stacks.py index f1f1f7e5d2..4bda5ab41e 100644 --- a/tempest/api/orchestration/stacks/test_stacks.py +++ b/tempest/api/orchestration/stacks/test_stacks.py @@ -33,8 +33,7 @@ class StacksTestJSON(base.BaseOrchestrationTest): @attr(type='smoke') def test_stack_list_responds(self): - resp, body = self.client.list_stacks() - stacks = body['stacks'] + resp, stacks = self.client.list_stacks() self.assertEqual('200', resp['status']) self.assertIsInstance(stacks, list) @@ -42,9 +41,6 @@ class StacksTestJSON(base.BaseOrchestrationTest): def test_stack_crud_no_resources(self): stack_name = rand_name('heat') - # count how many stacks to start with - resp, body = self.client.list_stacks() - # create the stack stack_identifier = self.create_stack( stack_name, self.empty_template) @@ -54,21 +50,21 @@ class StacksTestJSON(base.BaseOrchestrationTest): self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE') # check for stack in list - resp, body = self.client.list_stacks() - list_ids = list([stack['id'] for stack in body['stacks']]) + resp, stacks = self.client.list_stacks() + list_ids = list([stack['id'] for stack in stacks]) self.assertIn(stack_id, list_ids) # fetch the stack - resp, body = self.client.get_stack(stack_identifier) - self.assertEqual('CREATE_COMPLETE', body['stack_status']) + resp, stack = self.client.get_stack(stack_identifier) + self.assertEqual('CREATE_COMPLETE', stack['stack_status']) # fetch the stack by name - resp, body = self.client.get_stack(stack_name) - self.assertEqual('CREATE_COMPLETE', body['stack_status']) + resp, stack = self.client.get_stack(stack_name) + self.assertEqual('CREATE_COMPLETE', stack['stack_status']) # fetch the stack by id - resp, body = self.client.get_stack(stack_id) - self.assertEqual('CREATE_COMPLETE', body['stack_status']) + resp, stack = self.client.get_stack(stack_id) + self.assertEqual('CREATE_COMPLETE', stack['stack_status']) # delete the stack resp = self.client.delete_stack(stack_identifier) diff --git a/tempest/api/orchestration/stacks/test_templates.py b/tempest/api/orchestration/stacks/test_templates.py new file mode 100644 index 0000000000..6a7c541ac5 --- /dev/null +++ b/tempest/api/orchestration/stacks/test_templates.py @@ -0,0 +1,86 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 logging + +from tempest.api.orchestration import base +from tempest.common.utils.data_utils import rand_name +from tempest import exceptions +from tempest.test import attr + + +LOG = logging.getLogger(__name__) + + +class TemplateYAMLTestJSON(base.BaseOrchestrationTest): + _interface = 'json' + + template = """ +HeatTemplateFormatVersion: '2012-12-12' +Description: | + Template which creates only a new user +Resources: + CfnUser: + Type: AWS::IAM::User +""" + + invalid_template_url = 'http://www.example.com/template.yaml' + + @classmethod + def setUpClass(cls): + super(TemplateYAMLTestJSON, cls).setUpClass() + cls.client = cls.orchestration_client + cls.stack_name = rand_name('heat') + cls.stack_identifier = cls.create_stack(cls.stack_name, cls.template) + cls.client.wait_for_stack_status(cls.stack_identifier, + 'CREATE_COMPLETE') + cls.stack_id = cls.stack_identifier.split('/')[1] + cls.parameters = {} + + @attr(type='gate') + def test_show_template(self): + """Getting template used to create the stack.""" + resp, template = self.client.show_template(self.stack_identifier) + self.assertEqual('200', resp['status']) + + @attr(type='gate') + def test_validate_template(self): + """Validating template passing it content.""" + resp, parameters = self.client.validate_template(self.template, + self.parameters) + self.assertEqual('200', resp['status']) + + @attr(type=['gate', 'negative']) + def test_validate_template_url(self): + """Validating template passing url to it.""" + self.assertRaises(exceptions.BadRequest, + self.client.validate_template_url, + template_url=self.invalid_template_url, + parameters=self.parameters) + + +class TemplateAWSTestJSON(TemplateYAMLTestJSON): + template = """ +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template which creates only a new user", + "Resources" : { + "CfnUser" : { + "Type" : "AWS::IAM::User" + } + } +} +""" + + invalid_template_url = 'http://www.example.com/template.template' diff --git a/tempest/services/orchestration/json/orchestration_client.py b/tempest/services/orchestration/json/orchestration_client.py index ef88eed1de..e896e0d9f8 100644 --- a/tempest/services/orchestration/json/orchestration_client.py +++ b/tempest/services/orchestration/json/orchestration_client.py @@ -42,7 +42,7 @@ class OrchestrationClient(rest_client.RestClient): resp, body = self.get(uri) body = json.loads(body) - return resp, body + return resp, body['stacks'] def create_stack(self, name, disable_rollback=True, parameters={}, timeout_mins=60, template=None, template_url=None): @@ -176,3 +176,64 @@ class OrchestrationClient(rest_client.RestClient): (stack_name, status, self.build_timeout)) raise exceptions.TimeoutException(message) time.sleep(self.build_interval) + + def show_resource_metadata(self, stack_identifier, resource_name): + """Returns the resource's metadata.""" + url = ('stacks/{stack_identifier}/resources/{resource_name}' + '/metadata'.format(**locals())) + resp, body = self.get(url) + body = json.loads(body) + return resp, body['metadata'] + + def list_events(self, stack_identifier): + """Returns list of all events for a stack.""" + url = 'stacks/{stack_identifier}/events'.format(**locals()) + resp, body = self.get(url) + body = json.loads(body) + return resp, body['events'] + + def list_resource_events(self, stack_identifier, resource_name): + """Returns list of all events for a resource from stack.""" + url = ('stacks/{stack_identifier}/resources/{resource_name}' + '/events'.format(**locals())) + resp, body = self.get(url) + body = json.loads(body) + return resp, body['events'] + + def show_event(self, stack_identifier, resource_name, event_id): + """Returns the details of a single stack's event.""" + url = ('stacks/{stack_identifier}/resources/{resource_name}/events' + '/{event_id}'.format(**locals())) + resp, body = self.get(url) + body = json.loads(body) + return resp, body['event'] + + def show_template(self, stack_identifier): + """Returns the template for the stack.""" + url = ('stacks/{stack_identifier}/template'.format(**locals())) + resp, body = self.get(url) + body = json.loads(body) + return resp, body + + def _validate_template(self, post_body): + """Returns the validation request result.""" + post_body = json.dumps(post_body) + resp, body = self.post('validate', post_body, self.headers) + body = json.loads(body) + return resp, body + + def validate_template(self, template, parameters={}): + """Returns the validation result for a template with parameters.""" + post_body = { + 'template': template, + 'parameters': parameters, + } + return self._validate_template(post_body) + + def validate_template_url(self, template_url, parameters={}): + """Returns the validation result for a template with parameters.""" + post_body = { + 'template_url': template_url, + 'parameters': parameters, + } + return self._validate_template(post_body)