Merge "Use `username' in ImagesConfig"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..68c771a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,176 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
diff --git a/README.rst b/README.rst
index 11ed58f..0ea5229 100644
--- a/README.rst
+++ b/README.rst
@@ -33,8 +33,8 @@
devstack uploaded and set the image_ref value in the [environment]
section in the tempest.conf to that image UUID.
- In addition, the ``tempest/tools/conf_from_devstack`` script can also be
- used to generate a tempest.conf based on your localrc file.
+ In addition, the ``<devstack-repo>/tools/configure_tempest.sh`` script can
+ also be used to generate a tempest.conf based on your devstack's rc files.
Tempest is not tied to any single test runner, but Nose been the most commonly
used tool. After setting up your configuration file, you can execute
diff --git a/stress/README.rst b/stress/README.rst
index 1667e31..d935289 100644
--- a/stress/README.rst
+++ b/stress/README.rst
@@ -17,7 +17,7 @@
controlling rate of fire and stuff like that.
This test framework is designed to stress test a Nova cluster. Hence,
-you must have a working Nova cluster.
+you must have a working Nova cluster with rate limiting turned off.
Environment
------------
@@ -34,12 +34,16 @@
controller=<hostname for calling nova-manage>
max_instances=<limit on instances that will be created>
+Also, make sure to set
+
+log_level=CRITICAL
+
+so that the API client does not log failed calls which are expected while
+running stress tests.
+
The stress test needs the top-level tempest directory to be on PYTHONPATH
if you are not using nosetests to run.
-For real stress, you need to remove "ratelimit" from the pipeline in
-api-paste.ini.
-
Running the sample test
-----------------------
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index c8af2ca..e9741bf 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -30,21 +30,49 @@
class RestClient(object):
- def __init__(self, config, user, password, auth_url, service,
- tenant_name=None):
+ def __init__(self, config, user, password, auth_url, tenant_name=None):
self.log = logging.getLogger(__name__)
self.log.setLevel(getattr(logging, config.compute.log_level))
self.config = config
- if self.config.identity.strategy == 'keystone':
- self.token, self.base_url = self.keystone_auth(user,
- password,
- auth_url,
- service,
- tenant_name)
+ self.user = user
+ self.password = password
+ self.auth_url = auth_url
+ self.tenant_name = tenant_name
+
+ self.service = None
+ self.token = None
+ self.base_url = None
+ self.config = config
+ self.region = 0
+ self.endpoint_url = 'publicURL'
+ self.strategy = self.config.identity.strategy
+ self.headers = {'Content-Type': 'application/json',
+ 'Accept': 'application/json'}
+
+ def _set_auth(self):
+ """
+ Sets the token and base_url used in requests based on the strategy type
+ """
+
+ if self.strategy == 'keystone':
+ self.token, self.base_url = self.keystone_auth(self.user,
+ self.password,
+ self.auth_url,
+ self.service,
+ self.tenant_name)
else:
- self.token, self.base_url = self.basic_auth(user,
- password,
- auth_url)
+ self.token, self.base_url = self.basic_auth(self.user,
+ self.password,
+ self.auth_url)
+
+ def clear_auth(self):
+ """
+ Can be called to clear the token and base_url so that the next request
+ will fetch a new token and base_url
+ """
+
+ self.token = None
+ self.base_url = None
def basic_auth(self, user, password, auth_url):
"""
@@ -93,7 +121,7 @@
mgmt_url = None
for ep in auth_data['serviceCatalog']:
if ep["type"] == service:
- mgmt_url = ep['endpoints'][0]['publicURL']
+ mgmt_url = ep['endpoints'][self.region][self.endpoint_url]
# See LP#920817. The tenantId is *supposed*
# to be returned for each endpoint accorsing to the
# Keystone spec. But... it isn't, so we have to parse
@@ -135,6 +163,9 @@
def request(self, method, url, headers=None, body=None, depth=0):
"""A simple HTTP request interface."""
+ if (self.token is None) or (self.base_url is None):
+ self._set_auth()
+
self.http_obj = httplib2.Http()
if headers == None:
headers = {}
diff --git a/tempest/services/nova/json/extensions_client.py b/tempest/services/nova/json/extensions_client.py
index 284a655..5627afc 100644
--- a/tempest/services/nova/json/extensions_client.py
+++ b/tempest/services/nova/json/extensions_client.py
@@ -1,18 +1,21 @@
-from tempest.common import rest_client
+from tempest.common.rest_client import RestClient
import json
-class ExtensionsClient(object):
+class ExtensionsClient(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- self.config = config
- catalog_type = self.config.compute.catalog_type
- self.client = rest_client.RestClient(config, username, password,
- auth_url, catalog_type,
- tenant_name)
+ super(ExtensionsClient, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
def list_extensions(self):
url = 'extensions'
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body
+
+ def is_enabled(self, extension):
+ _, extensions = self.list_extensions()
+ exts = extensions['extensions']
+ return any([e for e in exts if e['name'] == extension])
diff --git a/tempest/services/nova/json/flavors_client.py b/tempest/services/nova/json/flavors_client.py
index 7d612d7..84fa9ff 100644
--- a/tempest/services/nova/json/flavors_client.py
+++ b/tempest/services/nova/json/flavors_client.py
@@ -1,15 +1,13 @@
-from tempest.common import rest_client
+from tempest.common.rest_client import RestClient
import json
-class FlavorsClient(object):
+class FlavorsClient(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- self.config = config
- catalog_type = self.config.compute.catalog_type
- self.client = rest_client.RestClient(config, username, password,
- auth_url, catalog_type,
- tenant_name)
+ super(FlavorsClient, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
def list_flavors(self, params=None):
url = 'flavors'
@@ -20,7 +18,7 @@
url = "flavors?" + "".join(param_list)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body['flavors']
@@ -33,11 +31,11 @@
url = "flavors/detail?" + "".join(param_list)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body['flavors']
def get_flavor_details(self, flavor_id):
- resp, body = self.client.get("flavors/%s" % str(flavor_id))
+ resp, body = self.get("flavors/%s" % str(flavor_id))
body = json.loads(body)
return resp, body['flavor']
diff --git a/tempest/services/nova/json/floating_ips_client.py b/tempest/services/nova/json/floating_ips_client.py
index 3ab8f28..5b5b538 100644
--- a/tempest/services/nova/json/floating_ips_client.py
+++ b/tempest/services/nova/json/floating_ips_client.py
@@ -1,17 +1,13 @@
-from tempest.common import rest_client
+from tempest.common.rest_client import RestClient
from tempest import exceptions
import json
-class FloatingIPsClient(object):
+class FloatingIPsClient(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- self.config = config
- catalog_type = self.config.compute.catalog_type
- self.client = rest_client.RestClient(config, username, password,
- auth_url, catalog_type,
- tenant_name)
- self.headers = {'Content-Type': 'application/json',
- 'Accept': 'application/json'}
+ super(FloatingIPsClient, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
def list_floating_ips(self, params=None):
"""Returns a list of all floating IPs filtered by any parameters"""
@@ -21,14 +17,14 @@
for param, value in params.iteritems():
param_list.append("%s=%s&" % (param, value))
url += '?' + ' '.join(param_list)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body['floating_ips']
def get_floating_ip_details(self, floating_ip_id):
"""Get the details of a floating IP"""
url = "os-floating-ips/%s" % str(floating_ip_id)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
if resp.status == 404:
raise exceptions.NotFound(body)
@@ -37,14 +33,14 @@
def create_floating_ip(self):
"""Allocate a floating IP to the project"""
url = 'os-floating-ips'
- resp, body = self.client.post(url, None, None)
+ resp, body = self.post(url, None, None)
body = json.loads(body)
return resp, body['floating_ip']
def delete_floating_ip(self, floating_ip_id):
"""Deletes the provided floating IP from the project"""
url = "os-floating-ips/%s" % str(floating_ip_id)
- resp, body = self.client.delete(url)
+ resp, body = self.delete(url)
return resp, body
def associate_floating_ip_to_server(self, floating_ip, server_id):
@@ -57,7 +53,7 @@
}
post_body = json.dumps(post_body)
- resp, body = self.client.post(url, post_body, self.headers)
+ resp, body = self.post(url, post_body, self.headers)
return resp, body
def disassociate_floating_ip_from_server(self, floating_ip, server_id):
@@ -70,5 +66,5 @@
}
post_body = json.dumps(post_body)
- resp, body = self.client.post(url, post_body, self.headers)
+ resp, body = self.post(url, post_body, self.headers)
return resp, body
diff --git a/tempest/services/nova/json/images_client.py b/tempest/services/nova/json/images_client.py
index 5bf10ef..c9a0370 100644
--- a/tempest/services/nova/json/images_client.py
+++ b/tempest/services/nova/json/images_client.py
@@ -1,22 +1,17 @@
-from tempest.common import rest_client
+from tempest.common.rest_client import RestClient
from tempest import exceptions
import json
import time
-class ImagesClient(object):
+class ImagesClient(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- self.config = config
- catalog_type = self.config.compute.catalog_type
- self.client = rest_client.RestClient(config, username, password,
- auth_url, catalog_type,
- tenant_name)
-
+ super(ImagesClient, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
self.build_interval = self.config.compute.build_interval
self.build_timeout = self.config.compute.build_timeout
- self.headers = {'Content-Type': 'application/json',
- 'Accept': 'application/json'}
def create_image(self, server_id, name, meta=None):
"""Creates an image of the original server"""
@@ -31,7 +26,7 @@
post_body['createImage']['metadata'] = meta
post_body = json.dumps(post_body)
- resp, body = self.client.post('servers/%s/action' %
+ resp, body = self.post('servers/%s/action' %
str(server_id), post_body, self.headers)
return resp, body
@@ -45,7 +40,7 @@
url = "images?" + "".join(param_list)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body['images']
@@ -59,31 +54,31 @@
url = "images/detail?" + "".join(param_list)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body['images']
def get_image(self, image_id):
"""Returns the details of a single image"""
- resp, body = self.client.get("images/%s" % str(image_id))
+ resp, body = self.get("images/%s" % str(image_id))
body = json.loads(body)
return resp, body['image']
def delete_image(self, image_id):
"""Deletes the provided image"""
- return self.client.delete("images/%s" % str(image_id))
+ return self.delete("images/%s" % str(image_id))
def wait_for_image_resp_code(self, image_id, code):
"""
Waits until the HTTP response code for the request matches the
expected value
"""
- resp, body = self.client.get("images/%s" % str(image_id))
+ resp, body = self.get("images/%s" % str(image_id))
start = int(time.time())
while resp.status != code:
time.sleep(self.build_interval)
- resp, body = self.client.get("images/%s" % str(image_id))
+ resp, body = self.get("images/%s" % str(image_id))
if int(time.time()) - start >= self.build_timeout:
raise exceptions.BuildErrorException
@@ -105,14 +100,14 @@
def list_image_metadata(self, image_id):
"""Lists all metadata items for an image"""
- resp, body = self.client.get("images/%s/metadata" % str(image_id))
+ resp, body = self.get("images/%s/metadata" % str(image_id))
body = json.loads(body)
return resp, body['metadata']
def set_image_metadata(self, image_id, meta):
"""Sets the metadata for an image"""
post_body = json.dumps({'metadata': meta})
- resp, body = self.client.put('images/%s/metadata' %
+ resp, body = self.put('images/%s/metadata' %
str(image_id), post_body, self.headers)
body = json.loads(body)
return resp, body['metadata']
@@ -120,14 +115,14 @@
def update_image_metadata(self, image_id, meta):
"""Updates the metadata for an image"""
post_body = json.dumps({'metadata': meta})
- resp, body = self.client.post('images/%s/metadata' %
+ resp, body = self.post('images/%s/metadata' %
str(image_id), post_body, self.headers)
body = json.loads(body)
return resp, body['metadata']
def get_image_metadata_item(self, image_id, key):
"""Returns the value for a specific image metadata key"""
- resp, body = self.client.get("images/%s/metadata/%s" %
+ resp, body = self.get("images/%s/metadata/%s" %
(str(image_id), key))
body = json.loads(body)
return resp, body['meta']
@@ -135,7 +130,7 @@
def set_image_metadata_item(self, image_id, key, meta):
"""Sets the value for a specific image metadata key"""
post_body = json.dumps({'meta': meta})
- resp, body = self.client.put('images/%s/metadata/%s' %
+ resp, body = self.put('images/%s/metadata/%s' %
(str(image_id), key), post_body,
self.headers)
body = json.loads(body)
@@ -143,6 +138,6 @@
def delete_image_metadata_item(self, image_id, key):
"""Deletes a single image metadata key/value pair"""
- resp, body = self.client.delete("images/%s/metadata/%s" %
+ resp, body = self.delete("images/%s/metadata/%s" %
(str(image_id), key))
return resp, body
diff --git a/tempest/services/nova/json/keypairs_client.py b/tempest/services/nova/json/keypairs_client.py
index a0bef15..4e72a01 100644
--- a/tempest/services/nova/json/keypairs_client.py
+++ b/tempest/services/nova/json/keypairs_client.py
@@ -1,20 +1,16 @@
-from tempest.common import rest_client
+from tempest.common.rest_client import RestClient
import json
-class KeyPairsClient(object):
+class KeyPairsClient(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- self.config = config
- catalog_type = self.config.compute.catalog_type
- self.client = rest_client.RestClient(config, username, password,
- auth_url, catalog_type,
- tenant_name)
- self.headers = {'Content-Type': 'application/json',
- 'Accept': 'application/json'}
+ super(KeyPairsClient, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
def list_keypairs(self):
- resp, body = self.client.get("os-keypairs")
+ resp, body = self.get("os-keypairs")
body = json.loads(body)
#Each returned keypair is embedded within an unnecessary 'keypair'
#element which is a deviation from other resources like floating-ips,
@@ -24,7 +20,7 @@
return resp, body['keypairs']
def get_keypair(self, key_name):
- resp, body = self.client.get("os-keypairs/%s" % str(key_name))
+ resp, body = self.get("os-keypairs/%s" % str(key_name))
body = json.loads(body)
return resp, body['keypair']
@@ -33,10 +29,10 @@
if pub_key:
post_body['keypair']['public_key'] = pub_key
post_body = json.dumps(post_body)
- resp, body = self.client.post("os-keypairs",
+ resp, body = self.post("os-keypairs",
headers=self.headers, body=post_body)
body = json.loads(body)
return resp, body['keypair']
def delete_keypair(self, key_name):
- return self.client.delete("os-keypairs/%s" % str(key_name))
+ return self.delete("os-keypairs/%s" % str(key_name))
diff --git a/tempest/services/nova/json/limits_client.py b/tempest/services/nova/json/limits_client.py
index 6f5f57a..163b685 100644
--- a/tempest/services/nova/json/limits_client.py
+++ b/tempest/services/nova/json/limits_client.py
@@ -1,18 +1,16 @@
import json
-from tempest.common import rest_client
+from tempest.common.rest_client import RestClient
-class LimitsClient(object):
+class LimitsClient(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- self.config = config
- catalog_type = self.config.compute.catalog_type
- self.client = rest_client.RestClient(config, username, password,
- auth_url, catalog_type,
- tenant_name)
+ super(LimitsClient, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
def get_limits(self):
- resp, body = self.client.get("limits")
+ resp, body = self.get("limits")
body = json.loads(body)
return resp, body['limits']
diff --git a/tempest/services/nova/json/security_groups_client.py b/tempest/services/nova/json/security_groups_client.py
index 29223ea..6fa1a8c 100644
--- a/tempest/services/nova/json/security_groups_client.py
+++ b/tempest/services/nova/json/security_groups_client.py
@@ -1,17 +1,13 @@
-from tempest.common import rest_client
+from tempest.common.rest_client import RestClient
import json
-class SecurityGroupsClient(object):
+class SecurityGroupsClient(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- self.config = config
- catalog_type = self.config.compute.catalog_type
- self.client = rest_client.RestClient(config, username, password,
- auth_url, catalog_type,
- tenant_name)
- self.headers = {'Content-Type': 'application/json',
- 'Accept': 'application/json'}
+ super(SecurityGroupsClient, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
def list_security_groups(self, params=None):
"""List all security groups for a user"""
@@ -24,14 +20,14 @@
url += '?' + ' '.join(param_list)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body['security_groups']
def get_security_group(self, security_group_id):
"""Get the details of a Security Group"""
url = "os-security-groups/%s" % str(security_group_id)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body['security_group']
@@ -46,14 +42,14 @@
'description': description,
}
post_body = json.dumps({'security_group': post_body})
- resp, body = self.client.post('os-security-groups',
+ resp, body = self.post('os-security-groups',
post_body, self.headers)
body = json.loads(body)
return resp, body['security_group']
def delete_security_group(self, security_group_id):
"""Deletes the provided Security Group"""
- return self.client.delete('os-security-groups/%s'
+ return self.delete('os-security-groups/%s'
% str(security_group_id))
def create_security_group_rule(self, parent_group_id, ip_proto, from_port,
@@ -78,11 +74,11 @@
}
post_body = json.dumps({'security_group_rule': post_body})
url = 'os-security-group-rules'
- resp, body = self.client.post(url, post_body, self.headers)
+ resp, body = self.post(url, post_body, self.headers)
body = json.loads(body)
return resp, body['security_group_rule']
def delete_security_group_rule(self, group_rule_id):
"""Deletes the provided Security Group rule"""
- return self.client.delete('os-security-group-rules/%s'
+ return self.delete('os-security-group-rules/%s'
% str(group_rule_id))
diff --git a/tempest/services/nova/json/servers_client.py b/tempest/services/nova/json/servers_client.py
index 35e5782..08ac6df 100644
--- a/tempest/services/nova/json/servers_client.py
+++ b/tempest/services/nova/json/servers_client.py
@@ -1,22 +1,17 @@
from tempest import exceptions
-from tempest.common import rest_client
+from tempest.common.rest_client import RestClient
import json
import time
-class ServersClient(object):
+class ServersClient(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- self.config = config
- catalog_type = self.config.compute.catalog_type
- self.client = rest_client.RestClient(config, username, password,
- auth_url, catalog_type,
- tenant_name)
-
+ super(ServersClient, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
self.build_interval = self.config.compute.build_interval
self.build_timeout = self.config.compute.build_timeout
- self.headers = {'Content-Type': 'application/json',
- 'Accept': 'application/json'}
def create_server(self, name, image_ref, flavor_ref, **kwargs):
"""
@@ -26,7 +21,7 @@
flavor_ref (Required): The flavor used to build the server.
Following optional keyword arguments are accepted:
adminPass: Sets the initial root password.
- metadata: A dictionary of values to be used as metadata.
+ meta: A dictionary of values to be used as metadata.
personality: A list of dictionaries for files to be injected into
the server.
security_groups: A list of security group dicts.
@@ -37,26 +32,30 @@
accessIPv6: The IPv6 access address for the server.
min_count: Count of minimum number of instances to launch.
max_count: Count of maximum number of instances to launch.
+ disk_config: Determines if user or admin controls disk configuration.
"""
post_body = {
'name': name,
'imageRef': image_ref,
- 'flavorRef': flavor_ref,
- 'metadata': kwargs.get('meta'),
- 'personality': kwargs.get('personality'),
- 'adminPass': kwargs.get('adminPass'),
- 'security_groups': kwargs.get('security_groups'),
- 'networks': kwargs.get('networks'),
- 'user_data': kwargs.get('user_data'),
- 'availability_zone': kwargs.get('availability_zone'),
- 'accessIPv4': kwargs.get('accessIPv4'),
- 'accessIPv6': kwargs.get('accessIPv6'),
- 'min_count': kwargs.get('min_count'),
- 'max_count': kwargs.get('max_count'),
+ 'flavorRef': flavor_ref
}
+ for option in ['personality', 'adminPass',
+ 'security_groups', 'networks', 'user_data',
+ 'availability_zone', 'accessIPv4', 'accessIPv6',
+ 'min_count', 'max_count', ('metadata', 'meta'),
+ ('OS-DCF:diskConfig', 'disk_config')]:
+ if isinstance(option, tuple):
+ post_param = option[0]
+ key = option[1]
+ else:
+ post_param = option
+ key = option
+ value = kwargs.get(key)
+ if value != None:
+ post_body[post_param] = value
post_body = json.dumps({'server': post_body})
- resp, body = self.client.post('servers', post_body, self.headers)
+ resp, body = self.post('servers', post_body, self.headers)
body = json.loads(body)
return resp, body['server']
@@ -87,20 +86,20 @@
post_body['accessIPv6'] = accessIPv6
post_body = json.dumps({'server': post_body})
- resp, body = self.client.put("servers/%s" % str(server_id),
+ resp, body = self.put("servers/%s" % str(server_id),
post_body, self.headers)
body = json.loads(body)
return resp, body['server']
def get_server(self, server_id):
"""Returns the details of an existing server"""
- resp, body = self.client.get("servers/%s" % str(server_id))
+ resp, body = self.get("servers/%s" % str(server_id))
body = json.loads(body)
return resp, body['server']
def delete_server(self, server_id):
"""Deletes the given server"""
- return self.client.delete("servers/%s" % str(server_id))
+ return self.delete("servers/%s" % str(server_id))
def list_servers(self, params=None):
"""Lists all servers for a user"""
@@ -113,7 +112,7 @@
url = "servers?" + "".join(param_list)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body
@@ -128,7 +127,7 @@
url = "servers/detail?" + "".join(param_list)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body
@@ -157,13 +156,13 @@
def list_addresses(self, server_id):
"""Lists all addresses for a server"""
- resp, body = self.client.get("servers/%s/ips" % str(server_id))
+ resp, body = self.get("servers/%s/ips" % str(server_id))
body = json.loads(body)
return resp, body['addresses']
def list_addresses_by_network(self, server_id, network_id):
"""Lists all addresses of a specific network type for a server"""
- resp, body = self.client.get("servers/%s/ips/%s" %
+ resp, body = self.get("servers/%s/ips/%s" %
(str(server_id), network_id))
body = json.loads(body)
return resp, body
@@ -177,7 +176,7 @@
}
post_body = json.dumps(post_body)
- return self.client.post('servers/%s/action' % str(server_id),
+ return self.post('servers/%s/action' % str(server_id),
post_body, self.headers)
def reboot(self, server_id, reboot_type):
@@ -189,11 +188,11 @@
}
post_body = json.dumps(post_body)
- return self.client.post('servers/%s/action' % str(server_id),
+ return self.post('servers/%s/action' % str(server_id),
post_body, self.headers)
def rebuild(self, server_id, image_ref, name=None, meta=None,
- personality=None, adminPass=None):
+ personality=None, adminPass=None, disk_config=None):
"""Rebuilds a server with a new image"""
post_body = {
'imageRef': image_ref,
@@ -211,14 +210,17 @@
if personality != None:
post_body['personality'] = personality
+ if disk_config != None:
+ post_body['OS-DCF:diskConfig'] = disk_config
+
post_body = json.dumps({'rebuild': post_body})
- resp, body = self.client.post('servers/%s/action' %
+ resp, body = self.post('servers/%s/action' %
str(server_id), post_body,
self.headers)
body = json.loads(body)
return resp, body['server']
- def resize(self, server_id, flavor_ref):
+ def resize(self, server_id, flavor_ref, disk_config=None):
"""Changes the flavor of a server."""
post_body = {
'resize': {
@@ -226,8 +228,11 @@
}
}
+ if disk_config != None:
+ post_body['resize']['OS-DCF:diskConfig'] = disk_config
+
post_body = json.dumps(post_body)
- resp, body = self.client.post('servers/%s/action' %
+ resp, body = self.post('servers/%s/action' %
str(server_id), post_body, self.headers)
return resp, body
@@ -238,7 +243,7 @@
}
post_body = json.dumps(post_body)
- resp, body = self.client.post('servers/%s/action' %
+ resp, body = self.post('servers/%s/action' %
str(server_id), post_body, self.headers)
return resp, body
@@ -249,7 +254,7 @@
}
post_body = json.dumps(post_body)
- resp, body = self.client.post('servers/%s/action' %
+ resp, body = self.post('servers/%s/action' %
str(server_id), post_body, self.headers)
return resp, body
@@ -262,44 +267,44 @@
}
post_body = json.dumps(post_body)
- resp, body = self.client.post('servers/%s/action' %
+ resp, body = self.post('servers/%s/action' %
str(server_id), post_body, self.headers)
return resp, body
def list_server_metadata(self, server_id):
- resp, body = self.client.get("servers/%s/metadata" % str(server_id))
+ resp, body = self.get("servers/%s/metadata" % str(server_id))
body = json.loads(body)
return resp, body['metadata']
def set_server_metadata(self, server_id, meta):
post_body = json.dumps({'metadata': meta})
- resp, body = self.client.put('servers/%s/metadata' %
+ resp, body = self.put('servers/%s/metadata' %
str(server_id), post_body, self.headers)
body = json.loads(body)
return resp, body['metadata']
def update_server_metadata(self, server_id, meta):
post_body = json.dumps({'metadata': meta})
- resp, body = self.client.post('servers/%s/metadata' %
+ resp, body = self.post('servers/%s/metadata' %
str(server_id), post_body, self.headers)
body = json.loads(body)
return resp, body['metadata']
def get_server_metadata_item(self, server_id, key):
- resp, body = self.client.get("servers/%s/metadata/%s" %
+ resp, body = self.get("servers/%s/metadata/%s" %
(str(server_id), key))
body = json.loads(body)
return resp, body['meta']
def set_server_metadata_item(self, server_id, key, meta):
post_body = json.dumps({'meta': meta})
- resp, body = self.client.put('servers/%s/metadata/%s' %
+ resp, body = self.put('servers/%s/metadata/%s' %
(str(server_id), key),
post_body, self.headers)
body = json.loads(body)
return resp, body['meta']
def delete_server_metadata_item(self, server_id, key):
- resp, body = self.client.delete("servers/%s/metadata/%s" %
+ resp, body = self.delete("servers/%s/metadata/%s" %
(str(server_id), key))
return resp, body
diff --git a/tempest/services/nova/json/volumes_client.py b/tempest/services/nova/json/volumes_client.py
index c935621..ce212ff 100644
--- a/tempest/services/nova/json/volumes_client.py
+++ b/tempest/services/nova/json/volumes_client.py
@@ -1,20 +1,17 @@
from tempest import exceptions
-from tempest.common import rest_client
+from tempest.common.rest_client import RestClient
import json
import time
-class VolumesClient(object):
+class VolumesClient(RestClient):
- def __init__(self, config, username, key, auth_url, tenant_name=None):
- self.config = config
- catalog_type = self.config.compute.catalog_type
- self.client = rest_client.RestClient(config, username, key, auth_url,
- catalog_type, tenant_name)
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(VolumesClient, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
self.build_interval = self.config.compute.build_interval
self.build_timeout = self.config.compute.build_timeout
- self.headers = {'Content-Type': 'application/json',
- 'Accept': 'application/json'}
def list_volumes(self, params=None):
"""List all the volumes created"""
@@ -26,7 +23,7 @@
url += '?' + ' '.join(param_list)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body['volumes']
@@ -40,14 +37,14 @@
url = '?' + ' '.join(param_list)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body['volumes']
def get_volume(self, volume_id):
"""Returns the details of a single volume"""
url = "os-volumes/%s" % str(volume_id)
- resp, body = self.client.get(url)
+ resp, body = self.get(url)
body = json.loads(body)
return resp, body['volume']
@@ -66,13 +63,13 @@
}
post_body = json.dumps({'volume': post_body})
- resp, body = self.client.post('os-volumes', post_body, self.headers)
+ resp, body = self.post('os-volumes', post_body, self.headers)
body = json.loads(body)
return resp, body['volume']
def delete_volume(self, volume_id):
"""Deletes the Specified Volume"""
- return self.client.delete("os-volumes/%s" % str(volume_id))
+ return self.delete("os-volumes/%s" % str(volume_id))
def wait_for_volume_status(self, volume_id, status):
"""Waits for a Volume to reach a given status"""
diff --git a/tempest/tests/compute/test_disk_config.py b/tempest/tests/compute/test_disk_config.py
new file mode 100644
index 0000000..fca83be
--- /dev/null
+++ b/tempest/tests/compute/test_disk_config.py
@@ -0,0 +1,180 @@
+from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
+from tempest import openstack
+import tempest.config
+from tempest.tests import utils
+import unittest2 as unittest
+from nose.plugins.attrib import attr
+
+
+class TestServerDiskConfig(unittest.TestCase):
+
+ resize_available = tempest.config.TempestConfig().compute.resize_available
+
+ @classmethod
+ def setUpClass(cls):
+ cls.os = openstack.Manager()
+ cls.client = cls.os.servers_client
+ extensions_client = cls.os.extensions_client
+ cls.config = cls.os.config
+ cls.image_ref = cls.config.compute.image_ref
+ cls.image_ref_alt = cls.config.compute.image_ref_alt
+ cls.flavor_ref = cls.config.compute.flavor_ref
+ cls.flavor_ref_alt = cls.config.compute.flavor_ref_alt
+ cls.disk_config = extensions_client.is_enabled('DiskConfig')
+
+ @attr(type='positive')
+ @utils.skip_unless_attr('disk_config', 'Disk config extension not enabled')
+ def test_create_server_with_manual_disk_config(self):
+ """A server should be created with manual disk config"""
+ name = rand_name('server')
+ resp, server = self.client.create_server(name,
+ self.image_ref,
+ self.flavor_ref,
+ disk_config='MANUAL')
+
+ #Wait for the server to become active
+ self.client.wait_for_server_status(server['id'], 'ACTIVE')
+
+ #Verify the specified attributes are set correctly
+ resp, server = self.client.get_server(server['id'])
+ self.assertEqual('MANUAL', server['OS-DCF:diskConfig'])
+
+ #Delete the server
+ resp, body = self.client.delete_server(server['id'])
+
+ @attr(type='positive')
+ @utils.skip_unless_attr('disk_config', 'Disk config extension not enabled')
+ def test_create_server_with_auto_disk_config(self):
+ """A server should be created with auto disk config"""
+ name = rand_name('server')
+ resp, server = self.client.create_server(name,
+ self.image_ref,
+ self.flavor_ref,
+ disk_config='AUTO')
+
+ #Wait for the server to become active
+ self.client.wait_for_server_status(server['id'], 'ACTIVE')
+
+ #Verify the specified attributes are set correctly
+ resp, server = self.client.get_server(server['id'])
+ self.assertEqual('AUTO', server['OS-DCF:diskConfig'])
+
+ #Delete the server
+ resp, body = self.client.delete_server(server['id'])
+
+ @attr(type='positive')
+ @utils.skip_unless_attr('disk_config', 'Disk config extension not enabled')
+ def test_rebuild_server_with_manual_disk_config(self):
+ """A server should be rebuilt using the manual disk config option"""
+ name = rand_name('server')
+ resp, server = self.client.create_server(name,
+ self.image_ref,
+ self.flavor_ref,
+ disk_config='AUTO')
+
+ #Wait for the server to become active
+ self.client.wait_for_server_status(server['id'], 'ACTIVE')
+
+ #Verify the specified attributes are set correctly
+ resp, server = self.client.get_server(server['id'])
+ self.assertEqual('AUTO', server['OS-DCF:diskConfig'])
+
+ resp, server = self.client.rebuild(server['id'],
+ self.image_ref_alt,
+ disk_config='MANUAL')
+
+ #Wait for the server to become active
+ self.client.wait_for_server_status(server['id'], 'ACTIVE')
+
+ #Verify the specified attributes are set correctly
+ resp, server = self.client.get_server(server['id'])
+ self.assertEqual('MANUAL', server['OS-DCF:diskConfig'])
+
+ #Delete the server
+ resp, body = self.client.delete_server(server['id'])
+
+ @attr(type='positive')
+ @utils.skip_unless_attr('disk_config', 'Disk config extension not enabled')
+ def test_rebuild_server_with_auto_disk_config(self):
+ """A server should be rebuilt using the auto disk config option"""
+ name = rand_name('server')
+ resp, server = self.client.create_server(name,
+ self.image_ref,
+ self.flavor_ref,
+ disk_config='MANUAL')
+
+ #Wait for the server to become active
+ self.client.wait_for_server_status(server['id'], 'ACTIVE')
+
+ #Verify the specified attributes are set correctly
+ resp, server = self.client.get_server(server['id'])
+ self.assertEqual('MANUAL', server['OS-DCF:diskConfig'])
+
+ resp, server = self.client.rebuild(server['id'],
+ self.image_ref_alt,
+ disk_config='AUTO')
+
+ #Wait for the server to become active
+ self.client.wait_for_server_status(server['id'], 'ACTIVE')
+
+ #Verify the specified attributes are set correctly
+ resp, server = self.client.get_server(server['id'])
+ self.assertEqual('AUTO', server['OS-DCF:diskConfig'])
+
+ #Delete the server
+ resp, body = self.client.delete_server(server['id'])
+
+ @attr(type='positive')
+ @utils.skip_unless_attr('disk_config', 'Disk config extension not enabled')
+ @unittest.skipIf(not resize_available, 'Resize not available.')
+ def test_resize_server_from_manual_to_auto(self):
+ """A server should be resized from manual to auto disk config"""
+ name = rand_name('server')
+ resp, server = self.client.create_server(name,
+ self.image_ref,
+ self.flavor_ref,
+ disk_config='MANUAL')
+
+ #Wait for the server to become active
+ self.client.wait_for_server_status(server['id'], 'ACTIVE')
+
+ #Resize with auto option
+ self.client.resize(server['id'], self.flavor_ref_alt,
+ disk_config='AUTO')
+ self.client.wait_for_server_status(server['id'], 'VERIFY_RESIZE')
+ self.client.confirm_resize(server['id'])
+ self.client.wait_for_server_status(server['id'], 'ACTIVE')
+
+ resp, server = self.client.get_server(server['id'])
+ self.assertEqual('AUTO', server['OS-DCF:diskConfig'])
+
+ #Delete the server
+ resp, body = self.client.delete_server(server['id'])
+
+ @attr(type='positive')
+ @utils.skip_unless_attr('disk_config', 'Disk config extension not enabled')
+ @unittest.skipIf(not resize_available, 'Resize not available.')
+ def test_resize_server_from_auto_to_manual(self):
+ """A server should be resized from auto to manual disk config"""
+ name = rand_name('server')
+ resp, server = self.client.create_server(name,
+ self.image_ref,
+ self.flavor_ref,
+ disk_config='AUTO')
+
+ #Wait for the server to become active
+ self.client.wait_for_server_status(server['id'], 'ACTIVE')
+
+ #Resize with manual option
+ self.client.resize(server['id'], self.flavor_ref_alt,
+ disk_config='MANUAL')
+ self.client.wait_for_server_status(server['id'], 'VERIFY_RESIZE')
+ self.client.confirm_resize(server['id'])
+ self.client.wait_for_server_status(server['id'], 'ACTIVE')
+
+ resp, server = self.client.get_server(server['id'])
+ self.assertEqual('MANUAL', server['OS-DCF:diskConfig'])
+
+ #Delete the server
+ resp, body = self.client.delete_server(server['id'])
diff --git a/tempest/tests/compute/test_server_addresses.py b/tempest/tests/compute/test_server_addresses.py
index 4f4bfb3..6658ccc 100644
--- a/tempest/tests/compute/test_server_addresses.py
+++ b/tempest/tests/compute/test_server_addresses.py
@@ -59,9 +59,12 @@
# We do not know the exact network configuration, but an instance
# should at least have a single public or private address
- self.assertTrue('public' in addresses and len(addresses['public']) > 0
- or 'private' in addresses
- and len(addresses['private']) > 0)
+ self.assertGreaterEqual(len(addresses), 1)
+ for network_name, network_addresses in addresses.iteritems():
+ self.assertGreaterEqual(len(network_addresses), 1)
+ for address in network_addresses:
+ self.assertTrue(address['addr'])
+ self.assertTrue(address['version'])
@attr(type='smoke', category='server-addresses')
def test_list_server_addresses_by_network(self):
diff --git a/tempest/tests/test_floating_ips_actions.py b/tempest/tests/test_floating_ips_actions.py
index 1c0331c..9d7ed6c 100644
--- a/tempest/tests/test_floating_ips_actions.py
+++ b/tempest/tests/test_floating_ips_actions.py
@@ -24,7 +24,6 @@
cls.servers_client.wait_for_server_status(server['id'], 'ACTIVE')
cls.server_id = server['id']
resp, body = cls.servers_client.get_server(server['id'])
- cls.fixed_ip_addr = body['addresses']['private'][0]['addr']
#Floating IP creation
resp, body = cls.client.create_floating_ip()
cls.floating_ip_id = body['id']
diff --git a/tempest/tests/test_keypairs.py b/tempest/tests/test_keypairs.py
index baedbb6..40d00dc 100644
--- a/tempest/tests/test_keypairs.py
+++ b/tempest/tests/test_keypairs.py
@@ -2,7 +2,6 @@
import unittest2 as unittest
from tempest import openstack
from tempest.common.utils.data_utils import rand_name
-import tempest.config
from tempest import exceptions
@@ -12,7 +11,6 @@
def setUpClass(cls):
cls.os = openstack.Manager()
cls.client = cls.os.keypairs_client
- cls.config = cls.os.config
@attr(type='smoke')
def test_keypairs_create_list_delete(self):
@@ -110,6 +108,30 @@
self.fail('nonexistent key')
@attr(type='negative')
+ def test_create_keypair_with_empty_public_key(self):
+ """Keypair should not be created with an empty public key"""
+ k_name = rand_name("keypair-")
+ pub_key = ' '
+ try:
+ resp, _ = self.client.create_keypair(k_name, pub_key)
+ except exceptions.BadRequest:
+ pass
+ else:
+ self.fail('Expected BadRequest for empty public key')
+
+ @attr(type='negative')
+ def test_create_keypair_when_public_key_bits_exceeds_maximum(self):
+ """Keypair should not be created when public key bits are too long"""
+ k_name = rand_name("keypair-")
+ pub_key = 'ssh-rsa ' + 'A' * 2048 + ' openstack@ubuntu'
+ try:
+ resp, _ = self.client.create_keypair(k_name, pub_key)
+ except exceptions.BadRequest:
+ pass
+ else:
+ self.fail('Expected BadRequest for too long public key')
+
+ @attr(type='negative')
def test_create_keypair_with_duplicate_name(self):
"""Keypairs with duplicate names should not be created"""
k_name = rand_name('keypair-')
@@ -146,3 +168,14 @@
pass
else:
self.fail('too long')
+
+ @attr(type='negative')
+ def test_create_keypair_invalid_name(self):
+ """Keypairs with name being an invalid name should not be created"""
+ k_name = 'key_/.\@:'
+ try:
+ resp, _ = self.client.create_keypair(k_name)
+ except exceptions.BadRequest:
+ pass
+ else:
+ self.fail('invalid name')
diff --git a/tempest/tests/test_volumes_list.py b/tempest/tests/test_volumes_list.py
index 4a4c7d1..01ce394 100644
--- a/tempest/tests/test_volumes_list.py
+++ b/tempest/tests/test_volumes_list.py
@@ -25,7 +25,8 @@
cls.volume_list.append(volume)
cls.volume_id_list.append(volume['id'])
- def teardown(cls):
+ @classmethod
+ def tearDownClass(cls):
#Delete the created Volumes
for i in range(3):
resp, _ = cls.client.delete_volume(cls.volume_id_list[i])
diff --git a/tempest/tools/conf_from_devstack b/tempest/tools/conf_from_devstack
deleted file mode 100755
index 81df659..0000000
--- a/tempest/tools/conf_from_devstack
+++ /dev/null
@@ -1,188 +0,0 @@
-#!/usr/bin/env python
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2012 OpenStack, LLC
-# 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.
-
-"""
-Simple script that analyzes a devstack environment and constructs
-a Tempest configuration file for the devstack environment.
-"""
-
-import optparse
-import os
-import subprocess
-import sys
-
-SUCCESS = 0
-FAILURE = 1
-
-
-def execute(cmd, raise_error=True):
- """
- Executes a command in a subprocess. Returns a tuple
- of (exitcode, out, err), where out is the string output
- from stdout and err is the string output from stderr when
- executing the command.
-
- :param cmd: Command string to execute
- :param raise_error: If returncode is not 0 (success), then
- raise a RuntimeError? Default: True)
- """
-
- process = subprocess.Popen(cmd,
- shell=True,
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- result = process.communicate()
- (out, err) = result
- exitcode = process.returncode
- if process.returncode != 0 and raise_error:
- msg = "Command %(cmd)s did not succeed. Returned an exit "\
- "code of %(exitcode)d."\
- "\n\nSTDOUT: %(out)s"\
- "\n\nSTDERR: %(err)s" % locals()
- raise RuntimeError(msg)
- return exitcode, out, err
-
-
-def add_options(parser):
- """
- Adds CLI options to a supplied option parser
-
- :param parser: `optparse.OptionParser`
- """
- parser.add_option('-o', '--outfile', metavar="PATH",
- help=("File to save generated config to. Default: "
- "prints config to stdout."))
- parser.add_option('-v', '--verbose', default=False, action="store_true",
- help="Print more verbose output")
- parser.add_option('-D', '--devstack-dir', metavar="PATH",
- default=os.getcwd(),
- help="Directory to find devstack. Default: $PWD")
-
-
-def get_devstack_localrc(options):
- """
- Finds the localrc file in the devstack directory and returns a dict
- representing the key/value pairs in the localrc file.
- """
- localrc_path = os.path.join(os.path.abspath(options.devstack_dir), 'localrc')
- if not os.path.exists(localrc_path):
- raise RuntimeError("Failed to find localrc file in devstack dir %s" %
- options.devstack_dir)
-
- if options.verbose:
- print "Reading localrc settings from %s" % localrc_path
-
- try:
- settings = dict([line.split('=') for line in
- open(localrc_path, 'r').readlines()
- if not line.startswith('#')])
- return settings
- except (TypeError, ValueError) as e:
- raise RuntimeError("Failed to read settings from localrc file %s. "
- "Got error: %s" % (localrc_path, e))
-
-
-def main():
- oparser = optparse.OptionParser()
- add_options(oparser)
-
- options, args = oparser.parse_args()
-
- localrc = get_devstack_localrc(options)
-
- conf_settings = {
- 'service_host': localrc.get('HOST_IP', '127.0.0.1'),
- 'service_port': 5000, # Make this configurable when devstack does
- 'identity_api_version': 'v2.0', # Ditto
- 'user': localrc.get('USERNAME', 'admin'),
- 'password': localrc.get('ADMIN_PASSWORD', 'password')
- }
-
- # We need to determine the UUID of the base image, so we
- # query the Glance endpoint for a list of images...
- cmd = "glance index -A %s" % localrc['SERVICE_TOKEN']
- retcode, out, err = execute(cmd)
-
- if retcode != 0:
- raise RuntimeError("Unable to get list of images from Glance. "
- "Got error: %s" % err)
-
- image_lines = out.split('\n')[2:]
- for line in image_lines:
- if 'ami' in line:
- conf_settings['base_image_uuid'] = line.split()[0]
- break
-
- if 'base_image_uuid' not in conf_settings:
- raise RuntimeError("Unable to find any AMI images in glance index")
-
- if options.verbose:
- print "Found base image with UUID %s" % conf_settings['base_image_uuid']
-
- tempest_conf = """[identity]
-host=%(service_host)s
-port=%(service_port)s
-api_version=%(identity_api_version)s
-path=tokens
-user=%(user)s
-password=%(password)s
-tenant_name=%(user)s
-strategy=keystone
-
-[image]
-host = %(service_host)s
-port = 9292
-username = %(user)s
-password = %(password)s
-tenant = %(user)s
-auth_url = http://127.0.0.1:5000/v2.0/tokens/
-strategy = keystone
-service_token = servicetoken
-
-[compute]
-image_ref=%(base_image_uuid)s
-image_ref_alt=%(base_image_uuid)s
-flavor_ref=1
-flavor_ref_alt=2
-create_image_enabled=true
-resize_available=false
-ssh_timeout=300
-build_interval=10
-build_timeout=600""" % conf_settings
-
- if options.outfile:
- outfile_path = os.path.abspath(options.outfile)
- if os.path.exists(outfile_path):
- confirm = raw_input("Output file already exists. Overwrite? [y/N]")
- if confirm != 'Y':
- print "Exiting"
- return SUCCESS
- with open(outfile_path, 'wb') as outfile:
- outfile.write(tempest_conf)
- if options.verbose:
- print "Wrote tempest config to %s" % outfile_path
- else:
- print tempest_conf
-
-
-if __name__ == '__main__':
- try:
- sys.exit(main())
- except RuntimeError, e:
- sys.exit("ERROR: %s" % e)