summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_client.py5
-rw-r--r--tests/v1/test_shell.py2
-rw-r--r--tests/v2/__init__.py15
-rw-r--r--tests/v2/contrib/__init__.py0
-rw-r--r--tests/v2/contrib/test_list_extensions.py36
-rw-r--r--tests/v2/fakes.py317
-rw-r--r--tests/v2/test_auth.py390
-rw-r--r--tests/v2/test_quota_classes.py42
-rw-r--r--tests/v2/test_quotas.py47
-rw-r--r--tests/v2/test_shell.py159
-rw-r--r--tests/v2/test_types.py50
-rw-r--r--tests/v2/test_volumes.py87
12 files changed, 1149 insertions, 1 deletions
diff --git a/tests/test_client.py b/tests/test_client.py
index 77c7297..1b72971 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1,6 +1,7 @@
import cinderclient.client
import cinderclient.v1.client
+import cinderclient.v2.client
from tests import utils
@@ -10,6 +11,10 @@ class ClientTest(utils.TestCase):
output = cinderclient.client.get_client_class('1')
self.assertEqual(output, cinderclient.v1.client.Client)
+ def test_get_client_class_v2(self):
+ output = cinderclient.client.get_client_class('2')
+ self.assertEqual(output, cinderclient.v2.client.Client)
+
def test_get_client_class_unknown(self):
self.assertRaises(cinderclient.exceptions.UnsupportedVersion,
cinderclient.client.get_client_class, '0')
diff --git a/tests/v1/test_shell.py b/tests/v1/test_shell.py
index ad88918..8d28309 100644
--- a/tests/v1/test_shell.py
+++ b/tests/v1/test_shell.py
@@ -32,7 +32,7 @@ class ShellTest(utils.TestCase):
'CINDER_USERNAME': 'username',
'CINDER_PASSWORD': 'password',
'CINDER_PROJECT_ID': 'project_id',
- 'OS_COMPUTE_API_VERSION': '1.1',
+ 'OS_VOLUME_API_VERSION': '1.1',
'CINDER_URL': 'http://no.where',
}
diff --git a/tests/v2/__init__.py b/tests/v2/__init__.py
new file mode 100644
index 0000000..0cd9c14
--- /dev/null
+++ b/tests/v2/__init__.py
@@ -0,0 +1,15 @@
+# Copyright (c) 2013 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.
diff --git a/tests/v2/contrib/__init__.py b/tests/v2/contrib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/v2/contrib/__init__.py
diff --git a/tests/v2/contrib/test_list_extensions.py b/tests/v2/contrib/test_list_extensions.py
new file mode 100644
index 0000000..49b0e57
--- /dev/null
+++ b/tests/v2/contrib/test_list_extensions.py
@@ -0,0 +1,36 @@
+# Copyright (c) 2013 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.
+
+from cinderclient import extension
+from cinderclient.v2.contrib import list_extensions
+from tests import utils
+from tests.v1 import fakes
+
+
+extensions = [
+ extension.Extension(list_extensions.__name__.split(".")[-1],
+ list_extensions),
+]
+cs = fakes.FakeClient(extensions=extensions)
+
+
+class ListExtensionsTests(utils.TestCase):
+ def test_list_extensions(self):
+ all_exts = cs.list_extensions.show_all()
+ cs.assert_called('GET', '/extensions')
+ self.assertTrue(len(all_exts) > 0)
+ for r in all_exts:
+ self.assertTrue(len(r.summary) > 0)
diff --git a/tests/v2/fakes.py b/tests/v2/fakes.py
new file mode 100644
index 0000000..e04caf4
--- /dev/null
+++ b/tests/v2/fakes.py
@@ -0,0 +1,317 @@
+# Copyright 2013 OpenStack, LLC
+#
+# 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 urlparse
+
+from cinderclient import client as base_client
+from cinderclient.v2 import client
+from tests import fakes
+import tests.utils as utils
+
+
+def _stub_volume(**kwargs):
+ volume = {
+ 'id': '1234',
+ 'name': None,
+ 'description': None,
+ "attachments": [],
+ "bootable": "false",
+ "availability_zone": "cinder",
+ "created_at": "2012-08-27T00:00:00.000000",
+ "id": '00000000-0000-0000-0000-000000000000',
+ "metadata": {},
+ "size": 1,
+ "snapshot_id": None,
+ "status": "available",
+ "volume_type": "None",
+ "links": [
+ {
+ "href": "http://localhost/v2/fake/volumes/1234",
+ "rel": "self"
+ },
+ {
+ "href": "http://localhost/fake/volumes/1234",
+ "rel": "bookmark"
+ }
+ ],
+ }
+ volume.update(kwargs)
+ return volume
+
+
+def _stub_snapshot(**kwargs):
+ snapshot = {
+ "created_at": "2012-08-28T16:30:31.000000",
+ "display_description": None,
+ "display_name": None,
+ "id": '11111111-1111-1111-1111-111111111111',
+ "size": 1,
+ "status": "available",
+ "volume_id": '00000000-0000-0000-0000-000000000000',
+ }
+ snapshot.update(kwargs)
+ return snapshot
+
+
+class FakeClient(fakes.FakeClient, client.Client):
+
+ def __init__(self, *args, **kwargs):
+ client.Client.__init__(self, 'username', 'password',
+ 'project_id', 'auth_url',
+ extensions=kwargs.get('extensions'))
+ self.client = FakeHTTPClient(**kwargs)
+
+
+class FakeHTTPClient(base_client.HTTPClient):
+
+ def __init__(self, **kwargs):
+ self.username = 'username'
+ self.password = 'password'
+ self.auth_url = 'auth_url'
+ self.callstack = []
+
+ def _cs_request(self, url, method, **kwargs):
+ # Check that certain things are called correctly
+ if method in ['GET', 'DELETE']:
+ assert 'body' not in kwargs
+ elif method == 'PUT':
+ assert 'body' in kwargs
+
+ # Call the method
+ args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
+ kwargs.update(args)
+ munged_url = url.rsplit('?', 1)[0]
+ munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
+ munged_url = munged_url.replace('-', '_')
+
+ callback = "%s_%s" % (method.lower(), munged_url)
+
+ if not hasattr(self, callback):
+ raise AssertionError('Called unknown API method: %s %s, '
+ 'expected fakes method name: %s' %
+ (method, url, callback))
+
+ # Note the call
+ self.callstack.append((method, url, kwargs.get('body', None)))
+ status, headers, body = getattr(self, callback)(**kwargs)
+ r = utils.TestResponse({
+ "status_code": status,
+ "text": body,
+ "headers": headers,
+ })
+ return r, body
+
+ if hasattr(status, 'items'):
+ return utils.TestResponse(status), body
+ else:
+ return utils.TestResponse({"status": status}), body
+
+ #
+ # Snapshots
+ #
+
+ def get_snapshots_detail(self, **kw):
+ return (200, {}, {'snapshots': [
+ _stub_snapshot(),
+ ]})
+
+ def get_snapshots_1234(self, **kw):
+ return (200, {}, {'snapshot': _stub_snapshot(id='1234')})
+
+ def put_snapshots_1234(self, **kw):
+ snapshot = _stub_snapshot(id='1234')
+ snapshot.update(kw['body']['snapshot'])
+ return (200, {}, {'snapshot': snapshot})
+
+ #
+ # Volumes
+ #
+
+ def put_volumes_1234(self, **kw):
+ volume = _stub_volume(id='1234')
+ volume.update(kw['body']['volume'])
+ return (200, {}, {'volume': volume})
+
+ def get_volumes(self, **kw):
+ return (200, {}, {"volumes": [
+ {'id': 1234, 'name': 'sample-volume'},
+ {'id': 5678, 'name': 'sample-volume2'}
+ ]})
+
+ # TODO(jdg): This will need to change
+ # at the very least it's not complete
+ def get_volumes_detail(self, **kw):
+ return (200, {}, {"volumes": [
+ {'id': 1234,
+ 'name': 'sample-volume',
+ 'attachments': [{'server_id': 1234}]},
+ ]})
+
+ def get_volumes_1234(self, **kw):
+ r = {'volume': self.get_volumes_detail()[2]['volumes'][0]}
+ return (200, {}, r)
+
+ def post_volumes_1234_action(self, body, **kw):
+ _body = None
+ resp = 202
+ assert len(body.keys()) == 1
+ action = body.keys()[0]
+ if action == 'os-attach':
+ assert body[action].keys() == ['instance_uuid', 'mountpoint']
+ elif action == 'os-detach':
+ assert body[action] is None
+ elif action == 'os-reserve':
+ assert body[action] is None
+ elif action == 'os-unreserve':
+ assert body[action] is None
+ elif action == 'os-initialize_connection':
+ assert body[action].keys() == ['connector']
+ return (202, {}, {'connection_info': 'foos'})
+ elif action == 'os-terminate_connection':
+ assert body[action].keys() == ['connector']
+ elif action == 'os-begin_detaching':
+ assert body[action] is None
+ elif action == 'os-roll_detaching':
+ assert body[action] is None
+ else:
+ raise AssertionError("Unexpected server action: %s" % action)
+ return (resp, {}, _body)
+
+ def post_volumes(self, **kw):
+ return (202, {}, {'volume': {}})
+
+ def delete_volumes_1234(self, **kw):
+ return (202, {}, None)
+
+ #
+ # Quotas
+ #
+
+ def get_os_quota_sets_test(self, **kw):
+ return (200, {}, {'quota_set': {
+ 'tenant_id': 'test',
+ 'metadata_items': [],
+ 'volumes': 1,
+ 'gigabytes': 1}})
+
+ def get_os_quota_sets_test_defaults(self):
+ return (200, {}, {'quota_set': {
+ 'tenant_id': 'test',
+ 'metadata_items': [],
+ 'volumes': 1,
+ 'gigabytes': 1}})
+
+ def put_os_quota_sets_test(self, body, **kw):
+ assert body.keys() == ['quota_set']
+ fakes.assert_has_keys(body['quota_set'],
+ required=['tenant_id'])
+ return (200, {}, {'quota_set': {
+ 'tenant_id': 'test',
+ 'metadata_items': [],
+ 'volumes': 2,
+ 'gigabytes': 1}})
+
+ #
+ # Quota Classes
+ #
+
+ def get_os_quota_class_sets_test(self, **kw):
+ return (200, {}, {'quota_class_set': {
+ 'class_name': 'test',
+ 'metadata_items': [],
+ 'volumes': 1,
+ 'gigabytes': 1}})
+
+ def put_os_quota_class_sets_test(self, body, **kw):
+ assert body.keys() == ['quota_class_set']
+ fakes.assert_has_keys(body['quota_class_set'],
+ required=['class_name'])
+ return (200, {}, {'quota_class_set': {
+ 'class_name': 'test',
+ 'metadata_items': [],
+ 'volumes': 2,
+ 'gigabytes': 1}})
+
+ #
+ # VolumeTypes
+ #
+ def get_types(self, **kw):
+ return (200, {}, {
+ 'volume_types': [{'id': 1,
+ 'name': 'test-type-1',
+ 'extra_specs':{}},
+ {'id': 2,
+ 'name': 'test-type-2',
+ 'extra_specs':{}}]})
+
+ def get_types_1(self, **kw):
+ return (200, {}, {'volume_type': {'id': 1,
+ 'name': 'test-type-1',
+ 'extra_specs': {}}})
+
+ def post_types(self, body, **kw):
+ return (202, {}, {'volume_type': {'id': 3,
+ 'name': 'test-type-3',
+ 'extra_specs': {}}})
+
+ def post_types_1_extra_specs(self, body, **kw):
+ assert body.keys() == ['extra_specs']
+ return (200, {}, {'extra_specs': {'k': 'v'}})
+
+ def delete_types_1_extra_specs_k(self, **kw):
+ return(204, {}, None)
+
+ def delete_types_1(self, **kw):
+ return (202, {}, None)
+
+ #
+ # Set/Unset metadata
+ #
+ def delete_volumes_1234_metadata_test_key(self, **kw):
+ return (204, {}, None)
+
+ def delete_volumes_1234_metadata_key1(self, **kw):
+ return (204, {}, None)
+
+ def delete_volumes_1234_metadata_key2(self, **kw):
+ return (204, {}, None)
+
+ def post_volumes_1234_metadata(self, **kw):
+ return (204, {}, {'metadata': {'test_key': 'test_value'}})
+
+ #
+ # List all extensions
+ #
+ def get_extensions(self, **kw):
+ exts = [
+ {
+ "alias": "FAKE-1",
+ "description": "Fake extension number 1",
+ "links": [],
+ "name": "Fake1",
+ "namespace": ("http://docs.openstack.org/"
+ "/ext/fake1/api/v1.1"),
+ "updated": "2011-06-09T00:00:00+00:00"
+ },
+ {
+ "alias": "FAKE-2",
+ "description": "Fake extension number 2",
+ "links": [],
+ "name": "Fake2",
+ "namespace": ("http://docs.openstack.org/"
+ "/ext/fake1/api/v1.1"),
+ "updated": "2011-06-09T00:00:00+00:00"
+ },
+ ]
+ return (200, {}, {"extensions": exts, })
diff --git a/tests/v2/test_auth.py b/tests/v2/test_auth.py
new file mode 100644
index 0000000..b18a7a3
--- /dev/null
+++ b/tests/v2/test_auth.py
@@ -0,0 +1,390 @@
+# Copyright (c) 2013 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.
+
+import json
+
+import mock
+import requests
+
+from cinderclient import exceptions
+from cinderclient.v2 import client
+from tests import utils
+
+
+class AuthenticateAgainstKeystoneTests(utils.TestCase):
+ def test_authenticate_success(self):
+ cs = client.Client("username", "password", "project_id",
+ "auth_url/v2.0", service_type='compute')
+ resp = {
+ "access": {
+ "token": {
+ "expires": "12345",
+ "id": "FAKE_ID",
+ },
+ "serviceCatalog": [
+ {
+ "type": "compute",
+ "endpoints": [
+ {
+ "region": "RegionOne",
+ "adminURL": "http://localhost:8774/v2",
+ "internalURL": "http://localhost:8774/v2",
+ "publicURL": "http://localhost:8774/v2/",
+ },
+ ],
+ },
+ ],
+ },
+ }
+ auth_response = utils.TestResponse({
+ "status_code": 200,
+ "text": json.dumps(resp),
+ })
+
+ mock_request = mock.Mock(return_value=(auth_response))
+
+ @mock.patch.object(requests, "request", mock_request)
+ def test_auth_call():
+ cs.client.authenticate()
+ headers = {
+ 'User-Agent': cs.client.USER_AGENT,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ }
+ body = {
+ 'auth': {
+ 'passwordCredentials': {
+ 'username': cs.client.user,
+ 'password': cs.client.password,
+ },
+ 'tenantName': cs.client.projectid,
+ },
+ }
+
+ token_url = cs.client.auth_url + "/tokens"
+ mock_request.assert_called_with(
+ "POST",
+ token_url,
+ headers=headers,
+ data=json.dumps(body),
+ allow_redirects=True,
+ **self.TEST_REQUEST_BASE)
+
+ endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
+ public_url = endpoints[0]["publicURL"].rstrip('/')
+ self.assertEqual(cs.client.management_url, public_url)
+ token_id = resp["access"]["token"]["id"]
+ self.assertEqual(cs.client.auth_token, token_id)
+
+ test_auth_call()
+
+ def test_authenticate_tenant_id(self):
+ cs = client.Client("username", "password", auth_url="auth_url/v2.0",
+ tenant_id='tenant_id', service_type='compute')
+ resp = {
+ "access": {
+ "token": {
+ "expires": "12345",
+ "id": "FAKE_ID",
+ "tenant": {
+ "description": None,
+ "enabled": True,
+ "id": "tenant_id",
+ "name": "demo"
+ } # tenant associated with token
+ },
+ "serviceCatalog": [
+ {
+ "type": "compute",
+ "endpoints": [
+ {
+ "region": "RegionOne",
+ "adminURL": "http://localhost:8774/v2",
+ "internalURL": "http://localhost:8774/v2",
+ "publicURL": "http://localhost:8774/v2/",
+ },
+ ],
+ },
+ ],
+ },
+ }
+ auth_response = utils.TestResponse({
+ "status_code": 200,
+ "text": json.dumps(resp),
+ })
+
+ mock_request = mock.Mock(return_value=(auth_response))
+
+ @mock.patch.object(requests, "request", mock_request)
+ def test_auth_call():
+ cs.client.authenticate()
+ headers = {
+ 'User-Agent': cs.client.USER_AGENT,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ }
+ body = {
+ 'auth': {
+ 'passwordCredentials': {
+ 'username': cs.client.user,
+ 'password': cs.client.password,
+ },
+ 'tenantId': cs.client.tenant_id,
+ },
+ }
+
+ token_url = cs.client.auth_url + "/tokens"
+ mock_request.assert_called_with(
+ "POST",
+ token_url,
+ headers=headers,
+ data=json.dumps(body),
+ allow_redirects=True,
+ **self.TEST_REQUEST_BASE)
+
+ endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
+ public_url = endpoints[0]["publicURL"].rstrip('/')
+ self.assertEqual(cs.client.management_url, public_url)
+ token_id = resp["access"]["token"]["id"]
+ self.assertEqual(cs.client.auth_token, token_id)
+ tenant_id = resp["access"]["token"]["tenant"]["id"]
+ self.assertEqual(cs.client.tenant_id, tenant_id)
+
+ test_auth_call()
+
+ def test_authenticate_failure(self):
+ cs = client.Client("username", "password", "project_id",
+ "auth_url/v2.0")
+ resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}}
+ auth_response = utils.TestResponse({
+ "status_code": 401,
+ "text": json.dumps(resp),
+ })
+
+ mock_request = mock.Mock(return_value=(auth_response))
+
+ @mock.patch.object(requests, "request", mock_request)
+ def test_auth_call():
+ self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
+
+ test_auth_call()
+
+ def test_auth_redirect(self):
+ cs = client.Client("username", "password", "project_id",
+ "auth_url/v2", service_type='compute')
+ dict_correct_response = {
+ "access": {
+ "token": {
+ "expires": "12345",
+ "id": "FAKE_ID",
+ },
+ "serviceCatalog": [
+ {
+ "type": "compute",
+ "endpoints": [
+ {
+ "adminURL": "http://localhost:8774/v2",
+ "region": "RegionOne",
+ "internalURL": "http://localhost:8774/v2",
+ "publicURL": "http://localhost:8774/v2/",
+ },
+ ],
+ },
+ ],
+ },
+ }
+ correct_response = json.dumps(dict_correct_response)
+ dict_responses = [
+ {"headers": {'location':'http://127.0.0.1:5001'},
+ "status_code": 305,
+ "text": "Use proxy"},
+ # Configured on admin port, cinder redirects to v2.0 port.
+ # When trying to connect on it, keystone auth succeed by v1.0
+ # protocol (through headers) but tokens are being returned in
+ # body (looks like keystone bug). Leaved for compatibility.
+ {"headers": {},
+ "status_code": 200,
+ "text": correct_response},
+ {"headers": {},
+ "status_code": 200,
+ "text": correct_response}
+ ]
+
+ responses = [(utils.TestResponse(resp)) for resp in dict_responses]
+
+ def side_effect(*args, **kwargs):
+ return responses.pop(0)
+
+ mock_request = mock.Mock(side_effect=side_effect)
+
+ @mock.patch.object(requests, "request", mock_request)
+ def test_auth_call():
+ cs.client.authenticate()
+ headers = {
+ 'User-Agent': cs.client.USER_AGENT,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ }
+ body = {
+ 'auth': {
+ 'passwordCredentials': {
+ 'username': cs.client.user,
+ 'password': cs.client.password,
+ },
+ 'tenantName': cs.client.projectid,
+ },
+ }
+
+ token_url = cs.client.auth_url + "/tokens"
+ mock_request.assert_called_with(
+ "POST",
+ token_url,
+ headers=headers,
+ data=json.dumps(body),
+ allow_redirects=True,
+ **self.TEST_REQUEST_BASE)
+
+ resp = dict_correct_response
+ endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
+ public_url = endpoints[0]["publicURL"].rstrip('/')
+ self.assertEqual(cs.client.management_url, public_url)
+ token_id = resp["access"]["token"]["id"]
+ self.assertEqual(cs.client.auth_token, token_id)
+
+ test_auth_call()
+
+ def test_ambiguous_endpoints(self):
+ cs = client.Client("username", "password", "project_id",
+ "auth_url/v2.0", service_type='compute')
+ resp = {
+ "access": {
+ "token": {
+ "expires": "12345",
+ "id": "FAKE_ID",
+ },
+ "serviceCatalog": [
+ {
+ "adminURL": "http://localhost:8774/v2",
+ "type": "compute",
+ "name": "Compute CLoud",
+ "endpoints": [
+ {
+ "region": "RegionOne",
+ "internalURL": "http://localhost:8774/v2",
+ "publicURL": "http://localhost:8774/v2/",
+ },
+ ],
+ },
+ {
+ "adminURL": "http://localhost:8774/v2",
+ "type": "compute",
+ "name": "Hyper-compute Cloud",
+ "endpoints": [
+ {
+ "internalURL": "http://localhost:8774/v2",
+ "publicURL": "http://localhost:8774/v2/",
+ },
+ ],
+ },
+ ],
+ },
+ }
+ auth_response = utils.TestResponse({
+ "status_code": 200,
+ "text": json.dumps(resp),
+ })
+
+ mock_request = mock.Mock(return_value=(auth_response))
+
+ @mock.patch.object(requests, "request", mock_request)
+ def test_auth_call():
+ self.assertRaises(exceptions.AmbiguousEndpoints,
+ cs.client.authenticate)
+
+ test_auth_call()
+
+
+class AuthenticationTests(utils.TestCase):
+ def test_authenticate_success(self):
+ cs = client.Client("username", "password", "project_id", "auth_url")
+ management_url = 'https://localhost/v2.1/443470'
+ auth_response = utils.TestResponse({
+ 'status_code': 204,
+ 'headers': {
+ 'x-server-management-url': management_url,
+ 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
+ },
+ })
+ mock_request = mock.Mock(return_value=(auth_response))
+
+ @mock.patch.object(requests, "request", mock_request)
+ def test_auth_call():
+ cs.client.authenticate()
+ headers = {
+ 'Accept': 'application/json',
+ 'X-Auth-User': 'username',
+ 'X-Auth-Key': 'password',
+ 'X-Auth-Project-Id': 'project_id',
+ 'User-Agent': cs.client.USER_AGENT
+ }
+ mock_request.assert_called_with(
+ "GET",
+ cs.client.auth_url,
+ headers=headers,
+ **self.TEST_REQUEST_BASE)
+
+ self.assertEqual(cs.client.management_url,
+ auth_response.headers['x-server-management-url'])
+ self.assertEqual(cs.client.auth_token,
+ auth_response.headers['x-auth-token'])
+
+ test_auth_call()
+
+ def test_authenticate_failure(self):
+ cs = client.Client("username", "password", "project_id", "auth_url")
+ auth_response = utils.TestResponse({"status_code": 401})
+ mock_request = mock.Mock(return_value=(auth_response))
+
+ @mock.patch.object(requests, "request", mock_request)
+ def test_auth_call():
+ self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
+
+ test_auth_call()
+
+ def test_auth_automatic(self):
+ cs = client.Client("username", "password", "project_id", "auth_url")
+ http_client = cs.client
+ http_client.management_url = ''
+ mock_request = mock.Mock(return_value=(None, None))
+
+ @mock.patch.object(http_client, 'request', mock_request)
+ @mock.patch.object(http_client, 'authenticate')
+ def test_auth_call(m):
+ http_client.get('/')
+ m.assert_called()
+ mock_request.assert_called()
+
+ test_auth_call()
+
+ def test_auth_manual(self):
+ cs = client.Client("username", "password", "project_id", "auth_url")
+
+ @mock.patch.object(cs.client, 'authenticate')
+ def test_auth_call(m):
+ cs.authenticate()
+ m.assert_called()
+
+ test_auth_call()
diff --git a/tests/v2/test_quota_classes.py b/tests/v2/test_quota_classes.py
new file mode 100644
index 0000000..6be6cc0
--- /dev/null
+++ b/tests/v2/test_quota_classes.py
@@ -0,0 +1,42 @@
+# Copyright 2013 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.
+
+from tests import utils
+from tests.v2 import fakes
+
+
+cs = fakes.FakeClient()
+
+
+class QuotaClassSetsTest(utils.TestCase):
+
+ def test_class_quotas_get(self):
+ class_name = 'test'
+ cs.quota_classes.get(class_name)
+ cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name)
+
+ def test_update_quota(self):
+ q = cs.quota_classes.get('test')
+ q.update(volumes=2)
+ cs.assert_called('PUT', '/os-quota-class-sets/test')
+
+ def test_refresh_quota(self):
+ q = cs.quota_classes.get('test')
+ q2 = cs.quota_classes.get('test')
+ self.assertEqual(q.volumes, q2.volumes)
+ q2.volumes = 0
+ self.assertNotEqual(q.volumes, q2.volumes)
+ q2.get()
+ self.assertEqual(q.volumes, q2.volumes)
diff --git a/tests/v2/test_quotas.py b/tests/v2/test_quotas.py
new file mode 100644
index 0000000..71837a2
--- /dev/null
+++ b/tests/v2/test_quotas.py
@@ -0,0 +1,47 @@
+# Copyright 2013 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.
+
+from tests import utils
+from tests.v2 import fakes
+
+
+cs = fakes.FakeClient()
+
+
+class QuotaSetsTest(utils.TestCase):
+
+ def test_tenant_quotas_get(self):
+ tenant_id = 'test'
+ cs.quotas.get(tenant_id)
+ cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id)
+
+ def test_tenant_quotas_defaults(self):
+ tenant_id = 'test'
+ cs.quotas.defaults(tenant_id)
+ cs.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id)
+
+ def test_update_quota(self):
+ q = cs.quotas.get('test')
+ q.update(volumes=2)
+ cs.assert_called('PUT', '/os-quota-sets/test')
+
+ def test_refresh_quota(self):
+ q = cs.quotas.get('test')
+ q2 = cs.quotas.get('test')
+ self.assertEqual(q.volumes, q2.volumes)
+ q2.volumes = 0
+ self.assertNotEqual(q.volumes, q2.volumes)
+ q2.get()
+ self.assertEqual(q.volumes, q2.volumes)
diff --git a/tests/v2/test_shell.py b/tests/v2/test_shell.py
new file mode 100644
index 0000000..ce6646f
--- /dev/null
+++ b/tests/v2/test_shell.py
@@ -0,0 +1,159 @@
+# Copyright 2013 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.
+
+import fixtures
+
+from cinderclient import client
+from cinderclient import shell
+from tests import utils
+from tests.v2 import fakes
+
+
+class ShellTest(utils.TestCase):
+
+ FAKE_ENV = {
+ 'CINDER_USERNAME': 'username',
+ 'CINDER_PASSWORD': 'password',
+ 'CINDER_PROJECT_ID': 'project_id',
+ 'OS_VOLUME_API_VERSION': '2',
+ 'CINDER_URL': 'http://no.where',
+ }
+
+ # Patch os.environ to avoid required auth info.
+ def setUp(self):
+ """Run before each test."""
+ super(ShellTest, self).setUp()
+ for var in self.FAKE_ENV:
+ self.useFixture(fixtures.EnvironmentVariable(var,
+ self.FAKE_ENV[var]))
+
+ self.shell = shell.OpenStackCinderShell()
+
+ #HACK(bcwaldon): replace this when we start using stubs
+ self.old_get_client_class = client.get_client_class
+ client.get_client_class = lambda *_: fakes.FakeClient
+
+ def tearDown(self):
+ # For some method like test_image_meta_bad_action we are
+ # testing a SystemExit to be thrown and object self.shell has
+ # no time to get instantatiated which is OK in this case, so
+ # we make sure the method is there before launching it.
+ if hasattr(self.shell, 'cs'):
+ self.shell.cs.clear_callstack()
+
+ #HACK(bcwaldon): replace this when we start using stubs
+ client.get_client_class = self.old_get_client_class
+ super(ShellTest, self).tearDown()
+
+ def run_command(self, cmd):
+ self.shell.main(cmd.split())
+
+ def assert_called(self, method, url, body=None, **kwargs):
+ return self.shell.cs.assert_called(method, url, body, **kwargs)
+
+ def assert_called_anytime(self, method, url, body=None):
+ return self.shell.cs.assert_called_anytime(method, url, body)
+
+ def test_list(self):
+ self.run_command('list')
+ # NOTE(jdg): we default to detail currently
+ self.assert_called('GET', '/volumes/detail')
+
+ def test_list_filter_status(self):
+ self.run_command('list --status=available')
+ self.assert_called('GET', '/volumes/detail?status=available')
+
+ def test_list_filter_name(self):
+ self.run_command('list --name=1234')
+ self.assert_called('GET', '/volumes/detail?name=1234')
+
+ def test_list_all_tenants(self):
+ self.run_command('list --all-tenants=1')
+ self.assert_called('GET', '/volumes/detail?all_tenants=1')
+
+ def test_show(self):
+ self.run_command('show 1234')
+ self.assert_called('GET', '/volumes/1234')
+
+ def test_delete(self):
+ self.run_command('delete 1234')
+ self.assert_called('DELETE', '/volumes/1234')
+
+ def test_snapshot_list_filter_volume_id(self):
+ self.run_command('snapshot-list --volume-id=1234')
+ self.assert_called('GET', '/snapshots/detail?volume_id=1234')
+
+ def test_snapshot_list_filter_status_and_volume_id(self):
+ self.run_command('snapshot-list --status=available --volume-id=1234')
+ self.assert_called('GET', '/snapshots/detail?'
+ 'status=available&volume_id=1234')
+
+ def test_rename(self):
+ # basic rename with positional agruments
+ self.run_command('rename 1234 new-name')
+ expected = {'volume': {'name': 'new-name'}}
+ self.assert_called('PUT', '/volumes/1234', body=expected)
+ # change description only
+ self.run_command('rename 1234 --description=new-description')
+ expected = {'volume': {'description': 'new-description'}}
+ self.assert_called('PUT', '/volumes/1234', body=expected)
+ # rename and change description
+ self.run_command('rename 1234 new-name '
+ '--description=new-description')
+ expected = {'volume': {
+ 'name': 'new-name',
+ 'description': 'new-description',
+ }}
+ self.assert_called('PUT', '/volumes/1234', body=expected)
+ # noop, the only all will be the lookup
+ self.run_command('rename 1234')
+ self.assert_called('GET', '/volumes/1234')
+
+ def test_rename_snapshot(self):
+ # basic rename with positional agruments
+ self.run_command('snapshot-rename 1234 new-name')
+ expected = {'snapshot': {'name': 'new-name'}}
+ self.assert_called('PUT', '/snapshots/1234', body=expected)
+ # change description only
+ self.run_command('snapshot-rename 1234 '
+ '--description=new-description')
+ expected = {'snapshot': {'description': 'new-description'}}
+ self.assert_called('PUT', '/snapshots/1234', body=expected)
+ # snapshot-rename and change description
+ self.run_command('snapshot-rename 1234 new-name '
+ '--description=new-description')
+ expected = {'snapshot': {
+ 'name': 'new-name',
+ 'description': 'new-description',
+ }}
+ self.assert_called('PUT', '/snapshots/1234', body=expected)
+ # noop, the only all will be the lookup
+ self.run_command('snapshot-rename 1234')
+ self.assert_called('GET', '/snapshots/1234')
+
+ def test_set_metadata_set(self):
+ self.run_command('metadata 1234 set key1=val1 key2=val2')
+ self.assert_called('POST', '/volumes/1234/metadata',
+ {'metadata': {'key1': 'val1', 'key2': 'val2'}})
+
+ def test_set_metadata_delete_dict(self):
+ self.run_command('metadata 1234 unset key1=val1 key2=val2')
+ self.assert_called('DELETE', '/volumes/1234/metadata/key1')
+ self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
+
+ def test_set_metadata_delete_keys(self):
+ self.run_command('metadata 1234 unset key1 key2')
+ self.assert_called('DELETE', '/volumes/1234/metadata/key1')
+ self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
diff --git a/tests/v2/test_types.py b/tests/v2/test_types.py
new file mode 100644
index 0000000..f36b768
--- /dev/null
+++ b/tests/v2/test_types.py
@@ -0,0 +1,50 @@
+# Copyright (c) 2013 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.
+
+from cinderclient.v2 import volume_types
+from tests import utils
+from tests.v2 import fakes
+
+cs = fakes.FakeClient()
+
+
+class TypesTest(utils.TestCase):
+ def test_list_types(self):
+ tl = cs.volume_types.list()
+ cs.assert_called('GET', '/types')
+ for t in tl:
+ self.assertTrue(isinstance(t, volume_types.VolumeType))
+
+ def test_create(self):
+ t = cs.volume_types.create('test-type-3')
+ cs.assert_called('POST', '/types')
+ self.assertTrue(isinstance(t, volume_types.VolumeType))
+
+ def test_set_key(self):
+ t = cs.volume_types.get(1)
+ t.set_keys({'k': 'v'})
+ cs.assert_called('POST',
+ '/types/1/extra_specs',
+ {'extra_specs': {'k': 'v'}})
+
+ def test_unsset_keys(self):
+ t = cs.volume_types.get(1)
+ t.unset_keys(['k'])
+ cs.assert_called('DELETE', '/types/1/extra_specs/k')
+
+ def test_delete(self):
+ cs.volume_types.delete(1)
+ cs.assert_called('DELETE', '/types/1')
diff --git a/tests/v2/test_volumes.py b/tests/v2/test_volumes.py
new file mode 100644
index 0000000..85401af
--- /dev/null
+++ b/tests/v2/test_volumes.py
@@ -0,0 +1,87 @@
+# Copyright (c) 2013 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.
+
+from tests import utils
+from tests.v2 import fakes
+
+
+cs = fakes.FakeClient()
+
+
+class VolumesTest(utils.TestCase):
+
+ def test_delete_volume(self):
+ v = cs.volumes.list()[0]
+ v.delete()
+ cs.assert_called('DELETE', '/volumes/1234')
+ cs.volumes.delete('1234')
+ cs.assert_called('DELETE', '/volumes/1234')
+ cs.volumes.delete(v)
+ cs.assert_called('DELETE', '/volumes/1234')
+
+ def test_create_keypair(self):
+ cs.volumes.create(1)
+ cs.assert_called('POST', '/volumes')
+
+ def test_attach(self):
+ v = cs.volumes.get('1234')
+ cs.volumes.attach(v, 1, '/dev/vdc')
+ cs.assert_called('POST', '/volumes/1234/action')
+
+ def test_detach(self):
+ v = cs.volumes.get('1234')
+ cs.volumes.detach(v)
+ cs.assert_called('POST', '/volumes/1234/action')
+
+ def test_reserve(self):
+ v = cs.volumes.get('1234')
+ cs.volumes.reserve(v)
+ cs.assert_called('POST', '/volumes/1234/action')
+
+ def test_unreserve(self):
+ v = cs.volumes.get('1234')
+ cs.volumes.unreserve(v)
+ cs.assert_called('POST', '/volumes/1234/action')
+
+ def test_begin_detaching(self):
+ v = cs.volumes.get('1234')
+ cs.volumes.begin_detaching(v)
+ cs.assert_called('POST', '/volumes/1234/action')
+
+ def test_roll_detaching(self):
+ v = cs.volumes.get('1234')
+ cs.volumes.roll_detaching(v)
+ cs.assert_called('POST', '/volumes/1234/action')
+
+ def test_initialize_connection(self):
+ v = cs.volumes.get('1234')
+ cs.volumes.initialize_connection(v, {})
+ cs.assert_called('POST', '/volumes/1234/action')
+
+ def test_terminate_connection(self):
+ v = cs.volumes.get('1234')
+ cs.volumes.terminate_connection(v, {})
+ cs.assert_called('POST', '/volumes/1234/action')
+
+ def test_set_metadata(self):
+ cs.volumes.set_metadata(1234, {'k1': 'v2'})
+ cs.assert_called('POST', '/volumes/1234/metadata',
+ {'metadata': {'k1': 'v2'}})
+
+ def test_delete_metadata(self):
+ keys = ['key1']
+ cs.volumes.delete_metadata(1234, keys)
+ cs.assert_called('DELETE', '/volumes/1234/metadata/key1')