diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample index 7920ab5622..9e93759d36 100644 --- a/etc/tempest.conf.sample +++ b/etc/tempest.conf.sample @@ -285,3 +285,26 @@ build_timeout = 120 # Status change wait interval build_interval = 1 + +[orchestration] +# Status change wait interval +build_interval = 1 + +# Status change wait timout. This may vary across environments as some some +# tests spawn full VMs, which could be slow if the test is already in a VM. +build_timeout = 300 + +# Whether or not Heat is expected to be available +heat_available = false + +# Instance type for tests. Needs to be big enough for a +# full OS plus the test workload +instance_type = m1.tiny + +# Name of heat-cfntools enabled image to use when launching test instances +# If not specified, tests that spawn instances will not run +#image_ref = ubuntu-vm-heat-cfntools + +# Name of existing keypair to launch servers with. The default is not to specify +# any key, which will generate a keypair for each test class +#keypair_name = heat_key diff --git a/tempest/clients.py b/tempest/clients.py index 9b2c1f5f23..f216c9fd34 100644 --- a/tempest/clients.py +++ b/tempest/clients.py @@ -86,6 +86,8 @@ from tempest.services.object_storage.container_client import ContainerClient from tempest.services.object_storage.object_client import ObjectClient from tempest.services.object_storage.object_client import \ ObjectClientCustomizedHeader +from tempest.services.orchestration.json.orchestration_client import \ + OrchestrationClient from tempest.services.volume.json.admin.volume_types_client import \ VolumeTypesClientJSON from tempest.services.volume.json.snapshots_client import SnapshotsClientJSON @@ -287,6 +289,7 @@ class Manager(object): self.image_client_v2 = ImageClientV2JSON(*client_args) self.container_client = ContainerClient(*client_args) self.object_client = ObjectClient(*client_args) + self.orchestration_client = OrchestrationClient(*client_args) self.ec2api_client = botoclients.APIClientEC2(*client_args) self.s3_client = botoclients.ObjectClientS3(*client_args) self.custom_object_client = ObjectClientCustomizedHeader(*client_args) @@ -337,3 +340,17 @@ class ComputeAdminManager(Manager): conf.compute_admin.password, conf.compute_admin.tenant_name, interface=interface) + + +class OrchestrationManager(Manager): + """ + Manager object that uses the admin credentials for its + so that heat templates can create users + """ + def __init__(self, interface='json'): + conf = config.TempestConfig() + base = super(OrchestrationManager, self) + base.__init__(conf.identity.admin_username, + conf.identity.admin_password, + conf.identity.admin_tenant_name, + interface=interface) diff --git a/tempest/config.py b/tempest/config.py index d43c5d79cd..f5f56a8787 100644 --- a/tempest/config.py +++ b/tempest/config.py @@ -351,6 +351,48 @@ def register_object_storage_opts(conf): for opt in ObjectStoreConfig: conf.register_opt(opt, group='object-storage') + +orchestration_group = cfg.OptGroup(name='orchestration', + title='Orchestration Service Options') + +OrchestrationGroup = [ + cfg.StrOpt('catalog_type', + default='orchestration', + help="Catalog type of the Orchestration service."), + cfg.BoolOpt('allow_tenant_isolation', + default=False, + help="Allows test cases to create/destroy tenants and " + "users. This option enables isolated test cases and " + "better parallel execution, but also requires that " + "OpenStack Identity API admin credentials are known."), + cfg.IntOpt('build_interval', + default=1, + help="Time in seconds between build status checks."), + cfg.IntOpt('build_timeout', + default=300, + help="Timeout in seconds to wait for a stack to build."), + cfg.BoolOpt('heat_available', + default=False, + help="Whether or not Heat is expected to be available"), + cfg.StrOpt('instance_type', + default='m1.tiny', + help="Instance type for tests. Needs to be big enough for a " + "full OS plus the test workload"), + cfg.StrOpt('image_ref', + default=None, + help="Name of heat-cfntools enabled image to use when " + "launching test instances."), + cfg.StrOpt('keypair_name', + default=None, + help="Name of existing keypair to launch servers with."), +] + + +def register_orchestration_opts(conf): + conf.register_group(orchestration_group) + for opt in OrchestrationGroup: + conf.register_opt(opt, group='orchestration') + boto_group = cfg.OptGroup(name='boto', title='EC2/S3 options') BotoConfig = [ @@ -485,6 +527,7 @@ class TempestConfig: register_network_opts(cfg.CONF) register_volume_opts(cfg.CONF) register_object_storage_opts(cfg.CONF) + register_orchestration_opts(cfg.CONF) register_boto_opts(cfg.CONF) register_compute_admin_opts(cfg.CONF) register_stress_opts(cfg.CONF) @@ -495,6 +538,7 @@ class TempestConfig: self.network = cfg.CONF.network self.volume = cfg.CONF.volume self.object_storage = cfg.CONF['object-storage'] + self.orchestration = cfg.CONF.orchestration self.boto = cfg.CONF.boto self.compute_admin = cfg.CONF['compute-admin'] self.stress = cfg.CONF.stress diff --git a/tempest/exceptions.py b/tempest/exceptions.py index 235a2e77e8..448fbdfee4 100644 --- a/tempest/exceptions.py +++ b/tempest/exceptions.py @@ -90,6 +90,11 @@ class SnapshotBuildErrorException(TempestException): message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status" +class StackBuildErrorException(TempestException): + message = ("Stack %(stack_identifier)s is in %(stack_status)s status " + "due to '%(stack_status_reason)s'") + + class BadRequest(RestClientException): message = "Bad request" diff --git a/tempest/services/orchestration/__init__.py b/tempest/services/orchestration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/services/orchestration/json/__init__.py b/tempest/services/orchestration/json/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/services/orchestration/json/orchestration_client.py b/tempest/services/orchestration/json/orchestration_client.py new file mode 100644 index 0000000000..81162dfdff --- /dev/null +++ b/tempest/services/orchestration/json/orchestration_client.py @@ -0,0 +1,99 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 IBM Corp. +# 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 json +import time +import urllib + +from tempest.common import rest_client +from tempest import exceptions + + +class OrchestrationClient(rest_client.RestClient): + + def __init__(self, config, username, password, auth_url, tenant_name=None): + super(OrchestrationClient, self).__init__(config, username, password, + auth_url, tenant_name) + self.service = self.config.orchestration.catalog_type + self.build_interval = self.config.orchestration.build_interval + self.build_timeout = self.config.orchestration.build_timeout + + def list_stacks(self, params=None): + """Lists all stacks for a user.""" + + uri = 'stacks' + if params: + uri += '?%s' % urllib.urlencode(params) + + resp, body = self.get(uri) + body = json.loads(body) + return resp, body + + def create_stack(self, name, disable_rollback=True, parameters={}, + timeout_mins=60, template=None, template_url=None): + post_body = { + "stack_name": name, + "disable_rollback": disable_rollback, + "parameters": parameters, + "timeout_mins": timeout_mins, + "template": "HeatTemplateFormatVersion: '2012-12-12'\n" + } + if template: + post_body['template'] = template + if template_url: + post_body['template_url'] = template_url + body = json.dumps(post_body) + uri = 'stacks' + resp, body = self.post(uri, headers=self.headers, body=body) + return resp, body + + def get_stack(self, stack_identifier): + """Returns the details of a single stack.""" + url = "stacks/%s" % stack_identifier + resp, body = self.get(url) + body = json.loads(body) + return resp, body['stack'] + + def delete_stack(self, stack_identifier): + """Deletes the specified Stack.""" + return self.delete("stacks/%s" % str(stack_identifier)) + + def wait_for_stack_status(self, stack_identifier, status, failure_status=( + 'CREATE_FAILED', + 'DELETE_FAILED', + 'UPDATE_FAILED', + 'ROLLBACK_FAILED')): + """Waits for a Volume to reach a given status.""" + stack_status = None + start = int(time.time()) + + while stack_status != status: + resp, body = self.get_stack(stack_identifier) + stack_name = body['stack_name'] + stack_status = body['stack_status'] + if stack_status in failure_status: + raise exceptions.StackBuildErrorException( + stack_identifier=stack_identifier, + stack_status=stack_status, + stack_status_reason=body['stack_status_reason']) + + if int(time.time()) - start >= self.build_timeout: + message = ('Stack %s failed to reach %s status within ' + 'the required time (%s s).' % + (stack_name, status, self.build_timeout)) + raise exceptions.TimeoutException(message) + time.sleep(self.build_interval)