From 4e0af25f22f356f797bebc524dd136e84b1a5d1f Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 08:43:22 -0500 Subject: [PATCH 01/18] Don't need to lazy load resources loaded from API Change-Id: Ibff7feabc8cba062bc9367c6755279b88a9a3c04 --- keystoneclient/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/keystoneclient/base.py b/keystoneclient/base.py index 500f65820..60a172cca 100644 --- a/keystoneclient/base.py +++ b/keystoneclient/base.py @@ -76,13 +76,13 @@ class Manager(object): def _get(self, url, response_key): resp, body = self.api.get(url) - return self.resource_class(self, body[response_key]) + return self.resource_class(self, body[response_key], loaded=True) def _create(self, url, body, response_key, return_raw=False): resp, body = self.api.post(url, body=body) if return_raw: return body[response_key] - return self.resource_class(self, body[response_key]) + return self.resource_class(self, body[response_key], loaded=True) def _delete(self, url): resp, body = self.api.delete(url) @@ -97,7 +97,7 @@ class Manager(object): % method) # PUT requests may not return a body if body: - return self.resource_class(self, body[response_key]) + return self.resource_class(self, body[response_key], loaded=True) class ManagerWithFind(Manager): From b91cdf492a50348e06a6db6e1ebb1f89065fe55b Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 11:06:54 -0500 Subject: [PATCH 02/18] Add support for HEAD and PATCH Change-Id: Ic874c49b791e9d2cb3d44b15511cbb467a551589 --- keystoneclient/base.py | 9 +++++++-- keystoneclient/client.py | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/keystoneclient/base.py b/keystoneclient/base.py index 60a172cca..85f9cd291 100644 --- a/keystoneclient/base.py +++ b/keystoneclient/base.py @@ -78,6 +78,10 @@ class Manager(object): resp, body = self.api.get(url) return self.resource_class(self, body[response_key], loaded=True) + def _head(self, url): + resp, body = self.api.head(url) + return resp.status == 204 + def _create(self, url, body, response_key, return_raw=False): resp, body = self.api.post(url, body=body) if return_raw: @@ -87,9 +91,10 @@ class Manager(object): def _delete(self, url): resp, body = self.api.delete(url) - def _update(self, url, body, response_key=None, method="PUT"): + def _update(self, url, body=None, response_key=None, method="PUT"): methods = {"PUT": self.api.put, - "POST": self.api.post} + "POST": self.api.post, + "PATCH": self.api.patch} try: resp, body = methods[method](url, body=body) except KeyError: diff --git a/keystoneclient/client.py b/keystoneclient/client.py index 11e64085b..2e472d5fc 100644 --- a/keystoneclient/client.py +++ b/keystoneclient/client.py @@ -176,11 +176,17 @@ class HTTPClient(httplib2.Http): def get(self, url, **kwargs): return self._cs_request(url, 'GET', **kwargs) + def head(self, url, **kwargs): + return self._cs_request(url, 'HEAD', **kwargs) + def post(self, url, **kwargs): return self._cs_request(url, 'POST', **kwargs) def put(self, url, **kwargs): return self._cs_request(url, 'PUT', **kwargs) + def patch(self, url, **kwargs): + return self._cs_request(url, 'PATCH', **kwargs) + def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) From 703c8b340ca95721906342795f93d9ba7d2bdaae Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 11:10:00 -0500 Subject: [PATCH 03/18] Add generic entity.delete() Change-Id: I00188326b6343a4eb4d1dd1b6a24e691ffd30415 --- keystoneclient/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/keystoneclient/base.py b/keystoneclient/base.py index 85f9cd291..8be422743 100644 --- a/keystoneclient/base.py +++ b/keystoneclient/base.py @@ -190,6 +190,9 @@ class Resource(object): if new: self._add_details(new._info) + def delete(self): + return self.manager.delete(self) + def __eq__(self, other): if not isinstance(other, self.__class__): return False From e25959724508b3ea74ed3f456e4eaae4f72609dd Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 11:10:40 -0500 Subject: [PATCH 04/18] Allow serialization impl to be overridden Change-Id: I0f955c78897d4212f06942e59a7018dbe5d28540 --- keystoneclient/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/keystoneclient/client.py b/keystoneclient/client.py index 2e472d5fc..020dfc1b0 100644 --- a/keystoneclient/client.py +++ b/keystoneclient/client.py @@ -107,6 +107,9 @@ class HTTPClient(httplib2.Http): if self.debug_log: _logger.debug("RESP: %s\nRESP BODY: %s\n", resp, body) + def serialize(self, entity): + return json.dumps(entity) + def request(self, url, method, **kwargs): """ Send an http request with the specified characteristics. @@ -119,7 +122,7 @@ class HTTPClient(httplib2.Http): request_kwargs['headers']['User-Agent'] = self.USER_AGENT if 'body' in kwargs: request_kwargs['headers']['Content-Type'] = 'application/json' - request_kwargs['body'] = json.dumps(kwargs['body']) + request_kwargs['body'] = self.serialize(kwargs['body']) self.http_log_req((url, method,), request_kwargs) resp, body = super(HTTPClient, self).request(url, From 315285e76ad520e89b5616503bdce8e061c77141 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 11:12:37 -0500 Subject: [PATCH 05/18] Manager for generic CRUD on v3 Change-Id: I15944f2e171e26b5209b55356edd1110b301310c --- keystoneclient/base.py | 111 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/keystoneclient/base.py b/keystoneclient/base.py index 8be422743..eadc12728 100644 --- a/keystoneclient/base.py +++ b/keystoneclient/base.py @@ -18,6 +18,8 @@ Base utilities to build API operation managers and objects on top of. """ +import urllib + from keystoneclient import exceptions @@ -144,6 +146,115 @@ class ManagerWithFind(Manager): return found +class CrudManager(Manager): + """Base manager class for manipulating Keystone entities. + + Children of this class are expected to define a `collection_key` and `key`. + + - `collection_key`: Usually a plural noun by convention (e.g. `entities`); + used to refer collections in both URL's (e.g. `/v3/entities`) and JSON + objects containing a list of member resources (e.g. `{'entities': [{}, + {}, {}]}`). + - `key`: Usually a singular noun by convention (e.g. `entity`); used to + refer to an individual member of the collection. + + """ + collection_key = None + key = None + + def build_url(self, base_url=None, **kwargs): + """Builds a resource URL for the given kwargs. + + Given an example collection where `collection_key = 'entities'` and + `key = 'entity'`, the following URL's could be generated. + + By default, the URL will represent a collection of entities, e.g.:: + + /entities + + If kwargs contains an `entity_id`, then the URL will represent a + specific member, e.g.:: + + /entities/{entity_id} + + If a `base_url` is provided, the generated URL will be appended to it. + + """ + url = base_url if base_url is not None else '' + + url += '/%s' % self.collection_key + + # do we have a specific entity? + entity_id = kwargs.get('%s_id' % self.key) + if entity_id is not None: + url += '/%s' % entity_id + + return url + + def _filter_kwargs(self, kwargs): + # drop null values + for key, ref in kwargs.copy().iteritems(): + if ref is None: + kwargs.pop(key) + else: + id_value = getid(ref) + if id_value != ref: + kwargs.pop(key) + kwargs['%s_id' % key] = id_value + return kwargs + + def create(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._create( + self.build_url(**kwargs), + {self.key: kwargs}, + self.key) + + def get(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._get( + self.build_url(**kwargs), + self.key) + + def head(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._head(self.build_url(**kwargs)) + + def list(self, base_url=None, **kwargs): + kwargs = self._filter_kwargs(kwargs) + + return self._list( + '%(base_url)s%(query)s' % { + 'base_url': self.build_url(base_url=base_url, **kwargs), + 'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '', + }, + self.collection_key) + + def put(self, base_url=None, **kwargs): + kwargs = self._filter_kwargs(kwargs) + + return self._update( + self.build_url(base_url=base_url, **kwargs), + method='PUT') + + def update(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + params = kwargs.copy() + params.pop('%s_id' % self.key) + + return self._update( + self.build_url(**kwargs), + {self.key: params}, + self.key, + method='PATCH') + + def delete(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + + return self._delete( + self.build_url(**kwargs)) + + class Resource(object): """ A resource represents a particular instance of an object (tenant, user, From 2ca00dc61796c51457a3db5acf997c3852fac2c4 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 11:20:16 -0500 Subject: [PATCH 06/18] v3 Client & test utils Change-Id: I6cafaad053b7fa1ca31f4f5aed1f86aa97c4e87e --- keystoneclient/v3/__init__.py | 1 + keystoneclient/v3/client.py | 68 ++++++++++ tests/v3/__init__.py | 0 tests/v3/utils.py | 225 ++++++++++++++++++++++++++++++++++ 4 files changed, 294 insertions(+) create mode 100644 keystoneclient/v3/__init__.py create mode 100644 keystoneclient/v3/client.py create mode 100644 tests/v3/__init__.py create mode 100644 tests/v3/utils.py diff --git a/keystoneclient/v3/__init__.py b/keystoneclient/v3/__init__.py new file mode 100644 index 000000000..feb2536b8 --- /dev/null +++ b/keystoneclient/v3/__init__.py @@ -0,0 +1 @@ +from keystoneclient.v3.client import Client diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py new file mode 100644 index 000000000..7e99dd524 --- /dev/null +++ b/keystoneclient/v3/client.py @@ -0,0 +1,68 @@ +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import json +import logging + +from keystoneclient.v2_0 import client + + +_logger = logging.getLogger(__name__) + + +class Client(client.Client): + """Client for the OpenStack Identity API v3. + + :param string username: Username for authentication. (optional) + :param string password: Password for authentication. (optional) + :param string token: Token for authentication. (optional) + :param string tenant_name: Tenant id. (optional) + :param string tenant_id: Tenant name. (optional) + :param string auth_url: Keystone service endpoint for authorization. + :param string region_name: Name of a region to select when choosing an + endpoint from the service catalog. + :param string endpoint: A user-supplied endpoint URL for the keystone + service. Lazy-authentication is possible for API + service calls if endpoint is set at + instantiation.(optional) + :param integer timeout: Allows customization of the timeout for client + http requests. (optional) + + Example:: + + >>> from keystoneclient.v3 import client + >>> keystone = client.Client(username=USER, + password=PASS, + tenant_name=TENANT_NAME, + auth_url=KEYSTONE_URL) + >>> keystone.tenants.list() + ... + >>> user = keystone.users.get(USER_ID) + >>> user.delete() + + """ + + def __init__(self, endpoint=None, **kwargs): + """ Initialize a new client for the Keystone v2.0 API. """ + super(Client, self).__init__(endpoint=endpoint, **kwargs) + + # NOTE(gabriel): If we have a pre-defined endpoint then we can + # get away with lazy auth. Otherwise auth immediately. + if endpoint: + self.management_url = endpoint + else: + self.authenticate() + + def serialize(self, entity): + return json.dumps(entity, sort_keys=True) diff --git a/tests/v3/__init__.py b/tests/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/v3/utils.py b/tests/v3/utils.py new file mode 100644 index 000000000..ee71c1115 --- /dev/null +++ b/tests/v3/utils.py @@ -0,0 +1,225 @@ +import json +import uuid +import time +import urlparse + +import httplib2 +import mox +import unittest2 as unittest + +from keystoneclient.v3 import client + + +def parameterize(ref): + """Rewrites attributes to match the kwarg naming convention in client. + + >>> paramterize({'project_id': 0}) + {'project': 0} + + """ + params = ref.copy() + for key in ref: + if key[-3:] == '_id': + params.setdefault(key[:-3], params.pop(key)) + return params + + +class TestCase(unittest.TestCase): + TEST_TENANT_NAME = 'aTenant' + TEST_TOKEN = 'aToken' + TEST_USER = 'test' + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') + + def setUp(self): + super(TestCase, self).setUp() + self.mox = mox.Mox() + self._original_time = time.time + time.time = lambda: 1234 + httplib2.Http.request = self.mox.CreateMockAnything() + self.client = client.Client(username=self.TEST_USER, + token=self.TEST_TOKEN, + tenant_name=self.TEST_TENANT_NAME, + auth_url=self.TEST_URL, + endpoint=self.TEST_URL) + + def tearDown(self): + time.time = self._original_time + super(TestCase, self).tearDown() + self.mox.UnsetStubs() + self.mox.VerifyAll() + + +class UnauthenticatedTestCase(unittest.TestCase): + """ Class used as base for unauthenticated calls """ + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') + + def setUp(self): + super(UnauthenticatedTestCase, self).setUp() + self.mox = mox.Mox() + self._original_time = time.time + time.time = lambda: 1234 + httplib2.Http.request = self.mox.CreateMockAnything() + + def tearDown(self): + time.time = self._original_time + super(UnauthenticatedTestCase, self).tearDown() + self.mox.UnsetStubs() + self.mox.VerifyAll() + + +class CrudTests(object): + key = None + collection_key = None + model = None + manager = None + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + return kwargs + + def additionalSetUp(self): + self.headers = { + 'GET': { + 'X-Auth-Token': 'aToken', + 'User-Agent': 'python-keystoneclient', + } + } + + self.headers['DELETE'] = self.headers['GET'].copy() + self.headers['POST'] = self.headers['GET'].copy() + self.headers['POST']['Content-Type'] = 'application/json' + self.headers['PATCH'] = self.headers['POST'].copy() + + def serialize(self, entity): + if isinstance(entity, dict): + return json.dumps({self.key: entity}, sort_keys=True) + if isinstance(entity, list): + return json.dumps({self.collection_key: entity}, sort_keys=True) + raise NotImplementedError('Are you sure you want to serialize that?') + + def test_create(self): + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 201, + 'body': self.serialize(ref), + }) + + method = 'POST' + req_ref = ref.copy() + req_ref.pop('id') + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/%s' % self.collection_key), + method, + body=self.serialize(req_ref), + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned = self.manager.create(**parameterize(req_ref)) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + def test_get(self): + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref), + }) + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/%s/%s' % (self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned = self.manager.get(ref['id']) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + def test_list(self): + ref_list = [self.new_ref(), self.new_ref()] + + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref_list), + }) + + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/%s' % self.collection_key), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned_list = self.manager.list() + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] + + def test_update(self): + ref = self.new_ref() + req_ref = ref.copy() + del req_ref['id'] + + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref), + }) + + method = 'PATCH' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/%s/%s' % (self.collection_key, ref['id'])), + method, + body=self.serialize(req_ref), + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned = self.manager.update(ref['id'], **parameterize(req_ref)) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + def test_delete(self): + ref = self.new_ref() + method = 'DELETE' + resp = httplib2.Response({ + 'status': 204, + 'body': '', + }) + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/%s/%s' % (self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.delete(ref['id']) From a0ff56fea0b7dfcb21fdd5e0f81c5e937f8c2324 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 11:22:30 -0500 Subject: [PATCH 07/18] v3 Service CRUD Change-Id: I5594ce92e99241f95775773233f09170a8a371b1 --- keystoneclient/v3/client.py | 3 ++ keystoneclient/v3/services.py | 57 +++++++++++++++++++++++++++++++++++ tests/v3/test_services.py | 20 ++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 keystoneclient/v3/services.py create mode 100644 tests/v3/test_services.py diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py index 7e99dd524..46751574c 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -16,6 +16,7 @@ import json import logging from keystoneclient.v2_0 import client +from keystoneclient.v3 import services _logger = logging.getLogger(__name__) @@ -57,6 +58,8 @@ class Client(client.Client): """ Initialize a new client for the Keystone v2.0 API. """ super(Client, self).__init__(endpoint=endpoint, **kwargs) + self.services = services.ServiceManager(self) + # NOTE(gabriel): If we have a pre-defined endpoint then we can # get away with lazy auth. Otherwise auth immediately. if endpoint: diff --git a/keystoneclient/v3/services.py b/keystoneclient/v3/services.py new file mode 100644 index 000000000..900543bfb --- /dev/null +++ b/keystoneclient/v3/services.py @@ -0,0 +1,57 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Service(base.Resource): + """Represents an Identity service. + + Attributes: + * id: a uuid that identifies the service + * name: user-facing name of the service (e.g. Keystone) + * type: 'compute', 'identity', etc + + """ + pass + + +class ServiceManager(base.CrudManager): + """Manager class for manipulating Identity services.""" + resource_class = Service + collection_key = 'services' + key = 'service' + + def create(self, name, type, **kwargs): + return super(ServiceManager, self).create( + name=name, + type=type, + **kwargs) + + def get(self, service): + return super(ServiceManager, self).get( + service_id=base.getid(service)) + + def update(self, service, name=None, type=None, **kwargs): + return super(ServiceManager, self).update( + service_id=base.getid(service), + name=name, + type=type, + **kwargs) + + def delete(self, service): + return super(ServiceManager, self).delete( + service_id=base.getid(service)) diff --git a/tests/v3/test_services.py b/tests/v3/test_services.py new file mode 100644 index 000000000..bc8a50365 --- /dev/null +++ b/tests/v3/test_services.py @@ -0,0 +1,20 @@ +import uuid + +from keystoneclient.v3 import services +from tests.v3 import utils + + +class ServiceTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(ServiceTests, self).setUp() + self.additionalSetUp() + self.key = 'service' + self.collection_key = 'services' + self.model = services.Service + self.manager = self.client.services + + def new_ref(self, **kwargs): + kwargs = super(ServiceTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + return kwargs From 2af709ceb6cf20f37f79baed1d93a14874b49a5b Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 11:40:25 -0500 Subject: [PATCH 08/18] v3 Endpoint CRUD Change-Id: Iff60668a80f8a6679a691a8f256652d7814f2785 --- keystoneclient/v3/client.py | 2 + keystoneclient/v3/endpoints.py | 80 ++++++++++++++++++++++++++++++++++ tests/v3/test_endpoints.py | 77 ++++++++++++++++++++++++++++++++ tests/v3/utils.py | 24 +++++----- 4 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 keystoneclient/v3/endpoints.py create mode 100644 tests/v3/test_endpoints.py diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py index 46751574c..37774202b 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -16,6 +16,7 @@ import json import logging from keystoneclient.v2_0 import client +from keystoneclient.v3 import endpoints from keystoneclient.v3 import services @@ -58,6 +59,7 @@ class Client(client.Client): """ Initialize a new client for the Keystone v2.0 API. """ super(Client, self).__init__(endpoint=endpoint, **kwargs) + self.endpoints = endpoints.EndpointManager(self) self.services = services.ServiceManager(self) # NOTE(gabriel): If we have a pre-defined endpoint then we can diff --git a/keystoneclient/v3/endpoints.py b/keystoneclient/v3/endpoints.py new file mode 100644 index 000000000..ff4a678b0 --- /dev/null +++ b/keystoneclient/v3/endpoints.py @@ -0,0 +1,80 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +VALID_INTERFACES = ['public', 'admin', 'internal'] + + +class Endpoint(base.Resource): + """Represents an Identity endpoint. + + Attributes: + * id: a uuid that identifies the endpoint + * interface: 'public', 'admin' or 'internal' network interface + * region: geographic location of the endpoint + * service_id: service to which the endpoint belongs + * url: fully qualified service endpoint + + """ + pass + + +class EndpointManager(base.CrudManager): + """Manager class for manipulating Identity endpoints.""" + resource_class = Endpoint + collection_key = 'endpoints' + key = 'endpoint' + + def _validate_interface(self, interface): + if interface is not None and interface not in VALID_INTERFACES: + msg = '"interface" must be one of: %s' + msg = msg % ', '.join(VALID_INTERFACES) + raise Exception(msg) + + def create(self, service, url, name=None, interface=None, region=None): + self._validate_interface(interface) + return super(EndpointManager, self).create( + service_id=base.getid(service), + interface=interface, + url=url, + region=region) + + def get(self, endpoint): + return super(EndpointManager, self).get( + endpoint_id=base.getid(endpoint)) + + def list(self, service=None, name=None, interface=None, region=None): + self._validate_interface(interface) + return super(EndpointManager, self).list( + service_id=base.getid(service), + interface=interface, + region=region) + + def update(self, endpoint, service=None, url=None, name=None, + interface=None, region=None): + self._validate_interface(interface) + return super(EndpointManager, self).update( + endpoint_id=base.getid(endpoint), + service_id=base.getid(service), + interface=interface, + url=url, + region=region) + + def delete(self, endpoint): + return super(EndpointManager, self).delete( + endpoint_id=base.getid(endpoint)) diff --git a/tests/v3/test_endpoints.py b/tests/v3/test_endpoints.py new file mode 100644 index 000000000..f0a6e4266 --- /dev/null +++ b/tests/v3/test_endpoints.py @@ -0,0 +1,77 @@ +import uuid + +from keystoneclient.v3 import endpoints +from tests.v3 import utils + + +class EndpointTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(EndpointTests, self).setUp() + self.additionalSetUp() + self.key = 'endpoint' + self.collection_key = 'endpoints' + self.model = endpoints.Endpoint + self.manager = self.client.endpoints + + def new_ref(self, **kwargs): + kwargs = super(EndpointTests, self).new_ref(**kwargs) + kwargs.setdefault('interface', 'public') + kwargs.setdefault('region', uuid.uuid4().hex) + kwargs.setdefault('service_id', uuid.uuid4().hex) + kwargs.setdefault('url', uuid.uuid4().hex) + return kwargs + + def test_create_public_interface(self): + ref = self.new_ref(interface='public') + self.test_create(ref) + + def test_create_admin_interface(self): + ref = self.new_ref(interface='admin') + self.test_create(ref) + + def test_create_internal_interface(self): + ref = self.new_ref(interface='internal') + self.test_create(ref) + + def test_create_invalid_interface(self): + ref = self.new_ref(interface=uuid.uuid4().hex) + with self.assertRaises(Exception): + self.manager.create(**utils.parameterize(ref)) + + def test_update_public_interface(self): + ref = self.new_ref(interface='public') + self.test_update(ref) + + def test_update_admin_interface(self): + ref = self.new_ref(interface='admin') + self.test_update(ref) + + def test_update_internal_interface(self): + ref = self.new_ref(interface='internal') + self.test_update(ref) + + def test_update_invalid_interface(self): + ref = self.new_ref(interface=uuid.uuid4().hex) + with self.assertRaises(Exception): + self.manager.update(**utils.parameterize(ref)) + + def test_list_public_interface(self): + interface = 'public' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_admin_interface(self): + interface = 'admin' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_internal_interface(self): + interface = 'admin' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_invalid_interface(self): + interface = uuid.uuid4().hex + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + with self.assertRaises(Exception): + self.manager.list(expected_path=expected_path, interface=interface) diff --git a/tests/v3/utils.py b/tests/v3/utils.py index ee71c1115..af8d68bbe 100644 --- a/tests/v3/utils.py +++ b/tests/v3/utils.py @@ -103,8 +103,8 @@ class CrudTests(object): return json.dumps({self.collection_key: entity}, sort_keys=True) raise NotImplementedError('Are you sure you want to serialize that?') - def test_create(self): - ref = self.new_ref() + def test_create(self, ref=None): + ref = ref or self.new_ref() resp = httplib2.Response({ 'status': 201, 'body': self.serialize(ref), @@ -131,8 +131,8 @@ class CrudTests(object): ref[attr], 'Expected different %s' % attr) - def test_get(self): - ref = self.new_ref() + def test_get(self, ref=None): + ref = ref or self.new_ref() resp = httplib2.Response({ 'status': 200, 'body': self.serialize(ref), @@ -155,8 +155,8 @@ class CrudTests(object): ref[attr], 'Expected different %s' % attr) - def test_list(self): - ref_list = [self.new_ref(), self.new_ref()] + def test_list(self, ref_list=None, expected_path=None, **filter_kwargs): + ref_list = ref_list or [self.new_ref(), self.new_ref()] resp = httplib2.Response({ 'status': 200, @@ -167,18 +167,18 @@ class CrudTests(object): httplib2.Http.request( urlparse.urljoin( self.TEST_URL, - 'v3/%s' % self.collection_key), + expected_path or 'v3/%s' % self.collection_key), method, headers=self.headers[method]) \ .AndReturn((resp, resp['body'])) self.mox.ReplayAll() - returned_list = self.manager.list() + returned_list = self.manager.list(**filter_kwargs) self.assertTrue(len(returned_list)) [self.assertTrue(isinstance(r, self.model)) for r in returned_list] - def test_update(self): - ref = self.new_ref() + def test_update(self, ref=None): + ref = ref or self.new_ref() req_ref = ref.copy() del req_ref['id'] @@ -206,8 +206,8 @@ class CrudTests(object): ref[attr], 'Expected different %s' % attr) - def test_delete(self): - ref = self.new_ref() + def test_delete(self, ref=None): + ref = ref or self.new_ref() method = 'DELETE' resp = httplib2.Response({ 'status': 204, From 0534c02351c225412354215a1653aa755d92a6e5 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 11:44:05 -0500 Subject: [PATCH 09/18] v3 Policy CRUD Change-Id: Iaecf3427877dd1751bb546c24413bc759938922c --- keystoneclient/v3/client.py | 2 + keystoneclient/v3/policies.py | 77 +++++++++++++++++++++++++++++++++++ tests/v3/test_policies.py | 21 ++++++++++ 3 files changed, 100 insertions(+) create mode 100644 keystoneclient/v3/policies.py create mode 100644 tests/v3/test_policies.py diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py index 37774202b..f012cea53 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -17,6 +17,7 @@ import logging from keystoneclient.v2_0 import client from keystoneclient.v3 import endpoints +from keystoneclient.v3 import policies from keystoneclient.v3 import services @@ -60,6 +61,7 @@ class Client(client.Client): super(Client, self).__init__(endpoint=endpoint, **kwargs) self.endpoints = endpoints.EndpointManager(self) + self.policies = policies.PolicyManager(self) self.services = services.ServiceManager(self) # NOTE(gabriel): If we have a pre-defined endpoint then we can diff --git a/keystoneclient/v3/policies.py b/keystoneclient/v3/policies.py new file mode 100644 index 000000000..6f3f6a872 --- /dev/null +++ b/keystoneclient/v3/policies.py @@ -0,0 +1,77 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Policy(base.Resource): + """Represents an Identity policy. + + Attributes: + * id: a uuid that identifies the policy + * endpoint_id: references the endpoint the policy applies to + * blob: a policy document (blob) + * type: the mime type of the policy blob + + """ + def update(self, endpoint=None, blob=None, type=None): + kwargs = { + 'endpoint_id': (base.getid(endpoint) + if endpoint is not None + else self.endpoint_id), + 'blob': blob if blob is not None else self.blob, + 'type': type if type is not None else self.type, + } + + try: + retval = self.manager.update(self.id, **kwargs) + self = retval + except Exception: + retval = None + + return retval + + +class PolicyManager(base.CrudManager): + """Manager class for manipulating Identity policies.""" + resource_class = Policy + collection_key = 'policies' + key = 'policy' + + def create(self, endpoint, blob, type='application/json'): + return super(PolicyManager, self).create( + endpoint_id=base.getid(endpoint), + blob=blob, + type=type) + + def get(self, policy): + return super(PolicyManager, self).get( + policy_id=base.getid(policy)) + + def list(self, endpoint=None): + return super(PolicyManager, self).list( + endpoint_id=base.getid(endpoint)) + + def update(self, entity, endpoint=None, blob=None, type=None): + return super(PolicyManager, self).update( + policy_id=base.getid(entity), + endpoint_id=base.getid(endpoint), + blob=blob, + type=type) + + def delete(self, policy): + return super(PolicyManager, self).delete( + policy_id=base.getid(policy)) diff --git a/tests/v3/test_policies.py b/tests/v3/test_policies.py new file mode 100644 index 000000000..fd3c74ee7 --- /dev/null +++ b/tests/v3/test_policies.py @@ -0,0 +1,21 @@ +import uuid + +from keystoneclient.v3 import policies +from tests.v3 import utils + + +class PolicyTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(PolicyTests, self).setUp() + self.additionalSetUp() + self.key = 'policy' + self.collection_key = 'policies' + self.model = policies.Policy + self.manager = self.client.policies + + def new_ref(self, **kwargs): + kwargs = super(PolicyTests, self).new_ref(**kwargs) + kwargs.setdefault('endpoint_id', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('blob', uuid.uuid4().hex) + return kwargs From ac3beb3671cdccf9a5285e8b2296e950b1a8be9b Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 12:32:01 -0500 Subject: [PATCH 10/18] v3 Domain CRUD Change-Id: I830055dc3bd079715403029a85890c40b687f632 --- keystoneclient/v3/client.py | 2 ++ keystoneclient/v3/domains.py | 55 ++++++++++++++++++++++++++++++++++++ tests/v3/test_domains.py | 20 +++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 keystoneclient/v3/domains.py create mode 100644 tests/v3/test_domains.py diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py index f012cea53..3117c2a28 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -17,6 +17,7 @@ import logging from keystoneclient.v2_0 import client from keystoneclient.v3 import endpoints +from keystoneclient.v3 import domains from keystoneclient.v3 import policies from keystoneclient.v3 import services @@ -61,6 +62,7 @@ class Client(client.Client): super(Client, self).__init__(endpoint=endpoint, **kwargs) self.endpoints = endpoints.EndpointManager(self) + self.domains = domains.DomainManager(self) self.policies = policies.PolicyManager(self) self.services = services.ServiceManager(self) diff --git a/keystoneclient/v3/domains.py b/keystoneclient/v3/domains.py new file mode 100644 index 000000000..2d27db225 --- /dev/null +++ b/keystoneclient/v3/domains.py @@ -0,0 +1,55 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Domain(base.Resource): + """Represents an Identity domain. + + Attributes: + * id: a uuid that identifies the domain + + """ + pass + + +class DomainManager(base.CrudManager): + """Manager class for manipulating Identity domains.""" + resource_class = Domain + collection_key = 'domains' + key = 'domain' + + def create(self, name, description=None, enabled=True): + return super(DomainManager, self).create( + name=name, + description=description, + enabled=enabled) + + def get(self, domain): + return super(DomainManager, self).get( + domain_id=base.getid(domain)) + + def update(self, domain, name=None, description=None, enabled=None): + return super(DomainManager, self).update( + domain_id=base.getid(domain), + name=name, + description=description, + enabled=enabled) + + def delete(self, domain): + return super(DomainManager, self).delete( + domain_id=base.getid(domain)) diff --git a/tests/v3/test_domains.py b/tests/v3/test_domains.py new file mode 100644 index 000000000..8cf0ea075 --- /dev/null +++ b/tests/v3/test_domains.py @@ -0,0 +1,20 @@ +import uuid + +from keystoneclient.v3 import domains +from tests.v3 import utils + + +class DomainTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(DomainTests, self).setUp() + self.additionalSetUp() + self.key = 'domain' + self.collection_key = 'domains' + self.model = domains.Domain + self.manager = self.client.domains + + def new_ref(self, **kwargs): + kwargs = super(DomainTests, self).new_ref(**kwargs) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs From f885c0d09a6464e0d94d1c295a605877d711f88a Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 15:34:56 -0500 Subject: [PATCH 11/18] v3 Role CRUD Change-Id: Iacb6e56ef60537b7cd3a4fbe3db1f0db1604fdc2 --- keystoneclient/v3/client.py | 2 ++ keystoneclient/v3/roles.py | 52 +++++++++++++++++++++++++++++++++++++ tests/v3/test_roles.py | 19 ++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 keystoneclient/v3/roles.py create mode 100644 tests/v3/test_roles.py diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py index 3117c2a28..f2ea9b59e 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -19,6 +19,7 @@ from keystoneclient.v2_0 import client from keystoneclient.v3 import endpoints from keystoneclient.v3 import domains from keystoneclient.v3 import policies +from keystoneclient.v3 import roles from keystoneclient.v3 import services @@ -64,6 +65,7 @@ class Client(client.Client): self.endpoints = endpoints.EndpointManager(self) self.domains = domains.DomainManager(self) self.policies = policies.PolicyManager(self) + self.roles = roles.RoleManager(self) self.services = services.ServiceManager(self) # NOTE(gabriel): If we have a pre-defined endpoint then we can diff --git a/keystoneclient/v3/roles.py b/keystoneclient/v3/roles.py new file mode 100644 index 000000000..0e0c180fb --- /dev/null +++ b/keystoneclient/v3/roles.py @@ -0,0 +1,52 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Role(base.Resource): + """Represents an Identity role. + + Attributes: + * id: a uuid that identifies the role + * name: user-facing identifier + + """ + pass + + +class RoleManager(base.CrudManager): + """Manager class for manipulating Identity roles.""" + resource_class = Role + collection_key = 'roles' + key = 'role' + + def create(self, name): + return super(RoleManager, self).create( + name=name) + + def get(self, role): + return super(RoleManager, self).get( + role_id=base.getid(role)) + + def update(self, role, name=None): + return super(RoleManager, self).update( + role_id=base.getid(role), + name=name) + + def delete(self, role): + return super(RoleManager, self).delete( + role_id=base.getid(role)) diff --git a/tests/v3/test_roles.py b/tests/v3/test_roles.py new file mode 100644 index 000000000..ef1f7ce39 --- /dev/null +++ b/tests/v3/test_roles.py @@ -0,0 +1,19 @@ +import uuid + +from keystoneclient.v3 import roles +from tests.v3 import utils + + +class RoleTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(RoleTests, self).setUp() + self.additionalSetUp() + self.key = 'role' + self.collection_key = 'roles' + self.model = roles.Role + self.manager = self.client.roles + + def new_ref(self, **kwargs): + kwargs = super(RoleTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs From 577c78c9919478305154919e4011cc55469d7815 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 15:38:22 -0500 Subject: [PATCH 12/18] v3 Project CRUD Change-Id: I027dbba3a0573fde590295be5b31e3701d8c01f5 --- keystoneclient/v3/client.py | 2 + keystoneclient/v3/projects.py | 80 +++++++++++++++++++++++++++++++++++ tests/v3/test_projects.py | 21 +++++++++ 3 files changed, 103 insertions(+) create mode 100644 keystoneclient/v3/projects.py create mode 100644 tests/v3/test_projects.py diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py index f2ea9b59e..1aac8d4ce 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -19,6 +19,7 @@ from keystoneclient.v2_0 import client from keystoneclient.v3 import endpoints from keystoneclient.v3 import domains from keystoneclient.v3 import policies +from keystoneclient.v3 import projects from keystoneclient.v3 import roles from keystoneclient.v3 import services @@ -65,6 +66,7 @@ class Client(client.Client): self.endpoints = endpoints.EndpointManager(self) self.domains = domains.DomainManager(self) self.policies = policies.PolicyManager(self) + self.projects = projects.ProjectManager(self) self.roles = roles.RoleManager(self) self.services = services.ServiceManager(self) diff --git a/keystoneclient/v3/projects.py b/keystoneclient/v3/projects.py new file mode 100644 index 000000000..a5ebac5ee --- /dev/null +++ b/keystoneclient/v3/projects.py @@ -0,0 +1,80 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Project(base.Resource): + """Represents an Identity project. + + Attributes: + * id: a uuid that identifies the project + * name: project name + * description: project description + * enabled: boolean to indicate if project is enabled + + """ + def update(self, name=None, description=None, enabled=None): + kwargs = { + 'name': name if name is not None else self.name, + 'description': (description + if description is not None + else self.description), + 'enabled': enabled if enabled is not None else self.enabled, + } + + try: + retval = self.manager.update(self.id, **kwargs) + self = retval + except Exception: + retval = None + + return retval + + +class ProjectManager(base.CrudManager): + """Manager class for manipulating Identity projects.""" + resource_class = Project + collection_key = 'projects' + key = 'project' + + def create(self, name, domain, description=None, enabled=True): + return super(ProjectManager, self).create( + domain_id=base.getid(domain), + name=name, + description=description, + enabled=enabled) + + def list(self, domain=None): + return super(ProjectManager, self).list( + domain_id=base.getid(domain)) + + def get(self, project): + return super(ProjectManager, self).get( + project_id=base.getid(project)) + + def update(self, project, name=None, domain=None, description=None, + enabled=None): + return super(ProjectManager, self).update( + project_id=base.getid(project), + domain_id=base.getid(domain), + name=name, + description=description, + enabled=enabled) + + def delete(self, project): + return super(ProjectManager, self).delete( + project_id=base.getid(project)) diff --git a/tests/v3/test_projects.py b/tests/v3/test_projects.py new file mode 100644 index 000000000..73ce207ef --- /dev/null +++ b/tests/v3/test_projects.py @@ -0,0 +1,21 @@ +import uuid + +from keystoneclient.v3 import projects +from tests.v3 import utils + + +class ProjectTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(ProjectTests, self).setUp() + self.additionalSetUp() + self.key = 'project' + self.collection_key = 'projects' + self.model = projects.Project + self.manager = self.client.projects + + def new_ref(self, **kwargs): + kwargs = super(ProjectTests, self).new_ref(**kwargs) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs From 298b1c4903a87b2063772b66150af1fa344a95d4 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 15:40:37 -0500 Subject: [PATCH 13/18] v3 User CRUD Change-Id: Ieea3c474ce3795e2c97e399988228cdb2715f2ef --- keystoneclient/v3/client.py | 2 ++ keystoneclient/v3/users.py | 70 +++++++++++++++++++++++++++++++++++++ tests/v3/test_users.py | 23 ++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 keystoneclient/v3/users.py create mode 100644 tests/v3/test_users.py diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py index 1aac8d4ce..75b32630a 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -22,6 +22,7 @@ from keystoneclient.v3 import policies from keystoneclient.v3 import projects from keystoneclient.v3 import roles from keystoneclient.v3 import services +from keystoneclient.v3 import users _logger = logging.getLogger(__name__) @@ -69,6 +70,7 @@ class Client(client.Client): self.projects = projects.ProjectManager(self) self.roles = roles.RoleManager(self) self.services = services.ServiceManager(self) + self.users = users.UserManager(self) # NOTE(gabriel): If we have a pre-defined endpoint then we can # get away with lazy auth. Otherwise auth immediately. diff --git a/keystoneclient/v3/users.py b/keystoneclient/v3/users.py new file mode 100644 index 000000000..a8c1c4b38 --- /dev/null +++ b/keystoneclient/v3/users.py @@ -0,0 +1,70 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class User(base.Resource): + """Represents an Identity user. + + Attributes: + * id: a uuid that identifies the user + + """ + pass + + +class UserManager(base.CrudManager): + """Manager class for manipulating Identity users.""" + resource_class = User + collection_key = 'users' + key = 'user' + + def create(self, name, domain=None, project=None, password=None, + email=None, description=None, enabled=True): + return super(UserManager, self).create( + name=name, + domain_id=base.getid(domain), + project_id=base.getid(project), + password=password, + email=email, + description=description, + enabled=enabled) + + def list(self, project=None, domain=None): + return super(UserManager, self).list( + domain_id=base.getid(domain), + project_id=base.getid(project)) + + def get(self, user): + return super(UserManager, self).get( + user_id=base.getid(user)) + + def update(self, user, name=None, domain=None, project=None, password=None, + email=None, description=None, enabled=None): + return super(UserManager, self).update( + user_id=base.getid(user), + name=name, + domain_id=base.getid(domain), + project_id=base.getid(project), + password=password, + email=email, + description=description, + enabled=enabled) + + def delete(self, user): + return super(UserManager, self).delete( + user_id=base.getid(user)) diff --git a/tests/v3/test_users.py b/tests/v3/test_users.py new file mode 100644 index 000000000..ee9f9d875 --- /dev/null +++ b/tests/v3/test_users.py @@ -0,0 +1,23 @@ +import uuid + +from keystoneclient.v3 import users +from tests.v3 import utils + + +class UserTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(UserTests, self).setUp() + self.additionalSetUp() + self.key = 'user' + self.collection_key = 'users' + self.model = users.User + self.manager = self.client.users + + def new_ref(self, **kwargs): + kwargs = super(UserTests, self).new_ref(**kwargs) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('project_id', uuid.uuid4().hex) + return kwargs From 6c84d7bf8567bfa3024cd38f80c01ed84768a89c Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 15:42:38 -0500 Subject: [PATCH 14/18] v3 Credential CRUD Change-Id: I646ff7db3ccf827f912ebdb78fdf8d765d52c26c --- keystoneclient/v3/client.py | 2 ++ keystoneclient/v3/credentials.py | 57 ++++++++++++++++++++++++++++++++ tests/v3/test_credentials.py | 22 ++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 keystoneclient/v3/credentials.py create mode 100644 tests/v3/test_credentials.py diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py index 75b32630a..51672a738 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -16,6 +16,7 @@ import json import logging from keystoneclient.v2_0 import client +from keystoneclient.v3 import credentials from keystoneclient.v3 import endpoints from keystoneclient.v3 import domains from keystoneclient.v3 import policies @@ -64,6 +65,7 @@ class Client(client.Client): """ Initialize a new client for the Keystone v2.0 API. """ super(Client, self).__init__(endpoint=endpoint, **kwargs) + self.credentials = credentials.CredentialManager(self) self.endpoints = endpoints.EndpointManager(self) self.domains = domains.DomainManager(self) self.policies = policies.PolicyManager(self) diff --git a/keystoneclient/v3/credentials.py b/keystoneclient/v3/credentials.py new file mode 100644 index 000000000..264c367e7 --- /dev/null +++ b/keystoneclient/v3/credentials.py @@ -0,0 +1,57 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Credential(base.Resource): + """Represents an Identity credential. + + Attributes: + * id: a uuid that identifies the credential + + """ + pass + + +class CredentialManager(base.CrudManager): + """Manager class for manipulating Identity credentials.""" + resource_class = Credential + collection_key = 'credentials' + key = 'credential' + + def create(self, user, type, data, project=None): + return super(CredentialManager, self).create( + user_id=base.getid(user), + type=type, + data=data, + project_id=base.getid(project)) + + def get(self, credential): + return super(CredentialManager, self).get( + credential_id=base.getid(credential)) + + def update(self, credential, user, type=None, data=None, project=None): + return super(CredentialManager, self).update( + credential_id=base.getid(credential), + user_id=base.getid(user), + type=type, + data=data, + project_id=base.getid(project)) + + def delete(self, credential): + return super(CredentialManager, self).delete( + credential_id=base.getid(credential)) diff --git a/tests/v3/test_credentials.py b/tests/v3/test_credentials.py new file mode 100644 index 000000000..180f680dd --- /dev/null +++ b/tests/v3/test_credentials.py @@ -0,0 +1,22 @@ +import uuid + +from keystoneclient.v3 import credentials +from tests.v3 import utils + + +class CredentialTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(CredentialTests, self).setUp() + self.additionalSetUp() + self.key = 'credential' + self.collection_key = 'credentials' + self.model = credentials.Credential + self.manager = self.client.credentials + + def new_ref(self, **kwargs): + kwargs = super(CredentialTests, self).new_ref(**kwargs) + kwargs.setdefault('data', uuid.uuid4().hex) + kwargs.setdefault('project_id', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('user_id', uuid.uuid4().hex) + return kwargs From dd84cdc340c782f53c79ede7ec1e196828605f96 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Sep 2012 15:43:47 -0500 Subject: [PATCH 15/18] v3 List projects for a user Change-Id: I4d3dfebb0bbe3799c05b9bc39fe2454ccf300873 --- keystoneclient/v3/projects.py | 4 ++- tests/v3/test_projects.py | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/keystoneclient/v3/projects.py b/keystoneclient/v3/projects.py index a5ebac5ee..bcbc4fd57 100644 --- a/keystoneclient/v3/projects.py +++ b/keystoneclient/v3/projects.py @@ -58,8 +58,10 @@ class ProjectManager(base.CrudManager): description=description, enabled=enabled) - def list(self, domain=None): + def list(self, domain=None, user=None): + base_url = '/users/%s' % base.getid(user) if user else None return super(ProjectManager, self).list( + base_url=base_url, domain_id=base.getid(domain)) def get(self, project): diff --git a/tests/v3/test_projects.py b/tests/v3/test_projects.py index 73ce207ef..8a4ef4090 100644 --- a/tests/v3/test_projects.py +++ b/tests/v3/test_projects.py @@ -1,3 +1,5 @@ +import httplib2 +import urlparse import uuid from keystoneclient.v3 import projects @@ -19,3 +21,49 @@ class ProjectTests(utils.TestCase, utils.CrudTests): kwargs.setdefault('enabled', True) kwargs.setdefault('name', uuid.uuid4().hex) return kwargs + + def test_list_projects_for_user(self): + ref_list = [self.new_ref(), self.new_ref()] + + user_id = uuid.uuid4().hex + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref_list), + }) + + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/users/%s/%s' % (user_id, self.collection_key)), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned_list = self.manager.list(user=user_id) + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] + + def test_list_projects_for_domain(self): + ref_list = [self.new_ref(), self.new_ref()] + + domain_id = uuid.uuid4().hex + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref_list), + }) + + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/%s?domain_id=%s' % (self.collection_key, domain_id)), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned_list = self.manager.list(domain=domain_id) + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] From 0ee514703094e3410d71b0078bd3d27db4d790dc Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Wed, 12 Sep 2012 17:34:09 -0500 Subject: [PATCH 16/18] Fixed httplib2 mocking (bug 1050091, bug 1050097) - 204 No Content should be mocked with empty response bodies - Content-Type headers should not be mocked with empty response bodies - httplib2 would never return None as a response body - The Identity API never expects a req/resp body with a string value of "null" Change-Id: Ie22e8e5288573268165ed06049978195955f8ca6 --- keystoneclient/base.py | 5 ++++- tests/v2_0/test_ec2.py | 2 +- tests/v2_0/test_endpoints.py | 2 +- tests/v2_0/test_roles.py | 32 +++++++++++++++----------------- tests/v2_0/test_tenants.py | 32 +++++++++++++++----------------- tests/v2_0/test_tokens.py | 2 +- tests/v2_0/test_users.py | 2 +- 7 files changed, 38 insertions(+), 39 deletions(-) diff --git a/keystoneclient/base.py b/keystoneclient/base.py index eadc12728..77e6108fa 100644 --- a/keystoneclient/base.py +++ b/keystoneclient/base.py @@ -98,7 +98,10 @@ class Manager(object): "POST": self.api.post, "PATCH": self.api.patch} try: - resp, body = methods[method](url, body=body) + if body is not None: + resp, body = methods[method](url, body=body) + else: + resp, body = methods[method](url) except KeyError: raise exceptions.ClientException("Invalid update method: %s" % method) diff --git a/tests/v2_0/test_ec2.py b/tests/v2_0/test_ec2.py index 7a1529143..96378e68a 100644 --- a/tests/v2_0/test_ec2.py +++ b/tests/v2_0/test_ec2.py @@ -139,7 +139,7 @@ class EC2Tests(utils.TestCase): user_id = 'usr' access = 'access' resp = httplib2.Response({ - "status": 200, + "status": 204, "body": "", }) diff --git a/tests/v2_0/test_endpoints.py b/tests/v2_0/test_endpoints.py index 7bd813dd8..34406ed60 100644 --- a/tests/v2_0/test_endpoints.py +++ b/tests/v2_0/test_endpoints.py @@ -83,7 +83,7 @@ class EndpointTests(utils.TestCase): def test_delete(self): resp = httplib2.Response({ - "status": 200, + "status": 204, "body": "", }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, diff --git a/tests/v2_0/test_roles.py b/tests/v2_0/test_roles.py index 0d14d0f0b..6b46af4ab 100644 --- a/tests/v2_0/test_roles.py +++ b/tests/v2_0/test_roles.py @@ -66,7 +66,7 @@ class RoleTests(utils.TestCase): def test_delete(self): resp = httplib2.Response({ - "status": 200, + "status": 204, "body": "", }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, @@ -147,62 +147,60 @@ class RoleTests(utils.TestCase): def test_add_user_role(self): resp = httplib2.Response({ - "status": 200, - "body": json.dumps({}), + "status": 204, + "body": '', }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/users/foo/roles/OS-KSADM/barrr'), 'PUT', - body='null', - headers=self.TEST_POST_HEADERS) \ - .AndReturn((resp, None)) + headers=self.TEST_REQUEST_HEADERS) \ + .AndReturn((resp, resp['body'])) self.mox.ReplayAll() self.client.roles.add_user_role('foo', 'barrr') def test_add_user_role_tenant(self): resp = httplib2.Response({ - "status": 200, - "body": json.dumps({}), + "status": 204, + "body": '', }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), 'PUT', - body='null', - headers=self.TEST_POST_HEADERS) \ - .AndReturn((resp, None)) + headers=self.TEST_REQUEST_HEADERS) \ + .AndReturn((resp, resp['body'])) self.mox.ReplayAll() self.client.roles.add_user_role('foo', 'barrr', '4') def test_remove_user_role(self): resp = httplib2.Response({ - "status": 200, - "body": json.dumps({}), + "status": 204, + "body": '', }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/users/foo/roles/OS-KSADM/barrr'), 'DELETE', headers=self.TEST_REQUEST_HEADERS) \ - .AndReturn((resp, None)) + .AndReturn((resp, resp['body'])) self.mox.ReplayAll() self.client.roles.remove_user_role('foo', 'barrr') def test_remove_user_role_tenant(self): resp = httplib2.Response({ - "status": 200, - "body": json.dumps({}), + "status": 204, + "body": '', }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), 'DELETE', headers=self.TEST_REQUEST_HEADERS) \ - .AndReturn((resp, None)) + .AndReturn((resp, resp['body'])) self.mox.ReplayAll() self.client.roles.remove_user_role('foo', 'barrr', '4') diff --git a/tests/v2_0/test_tenants.py b/tests/v2_0/test_tenants.py index bb07f66c0..cbfca4136 100644 --- a/tests/v2_0/test_tenants.py +++ b/tests/v2_0/test_tenants.py @@ -83,7 +83,7 @@ class TenantTests(utils.TestCase): def test_delete(self): resp = httplib2.Response({ - "status": 200, + "status": 204, "body": "", }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, @@ -220,31 +220,30 @@ class TenantTests(utils.TestCase): def test_add_user(self): resp = httplib2.Response({ - "status": 200, - "body": json.dumps({}), + "status": 204, + "body": '', }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), 'PUT', - body='null', - headers=self.TEST_POST_HEADERS) \ - .AndReturn((resp, None)) + headers=self.TEST_REQUEST_HEADERS) \ + .AndReturn((resp, resp['body'])) self.mox.ReplayAll() self.client.tenants.add_user('4', 'foo', 'barrr') def test_remove_user(self): resp = httplib2.Response({ - "status": 200, - "body": json.dumps({}), + "status": 204, + "body": '', }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), 'DELETE', headers=self.TEST_REQUEST_HEADERS) \ - .AndReturn((resp, None)) + .AndReturn((resp, resp['body'])) self.mox.ReplayAll() self.client.tenants.remove_user('4', 'foo', 'barrr') @@ -259,16 +258,15 @@ class TenantTests(utils.TestCase): }, } resp = httplib2.Response({ - "status": 200, - "body": json.dumps({}), + "status": 204, + "body": '', }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), 'PUT', - body='null', - headers=self.TEST_POST_HEADERS) \ - .AndReturn((resp, None)) + headers=self.TEST_REQUEST_HEADERS) \ + .AndReturn((resp, resp['body'])) self.mox.ReplayAll() # make tenant object with manager @@ -287,15 +285,15 @@ class TenantTests(utils.TestCase): }, } resp = httplib2.Response({ - "status": 200, - "body": json.dumps({}), + "status": 204, + "body": '', }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/tenants/4/users/foo/roles/OS-KSADM/barrr'), 'DELETE', headers=self.TEST_REQUEST_HEADERS) \ - .AndReturn((resp, None)) + .AndReturn((resp, resp['body'])) self.mox.ReplayAll() # make tenant object with manager diff --git a/tests/v2_0/test_tokens.py b/tests/v2_0/test_tokens.py index 4bd5ae2a6..7b55fc09c 100644 --- a/tests/v2_0/test_tokens.py +++ b/tests/v2_0/test_tokens.py @@ -18,7 +18,7 @@ class TokenTests(utils.TestCase): def test_delete(self): resp = httplib2.Response({ - "status": 200, + "status": 204, "body": ""}) req = httplib2.Http.request( diff --git a/tests/v2_0/test_users.py b/tests/v2_0/test_users.py index e9c9b4705..702aed78a 100644 --- a/tests/v2_0/test_users.py +++ b/tests/v2_0/test_users.py @@ -82,7 +82,7 @@ class UserTests(utils.TestCase): def test_delete(self): resp = httplib2.Response({ - "status": 200, + "status": 204, "body": "", }) httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/users/1'), From 46360085ebcbea61a04ab001e7d9597d0ed0b746 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 13 Sep 2012 07:55:26 -0500 Subject: [PATCH 17/18] v3 Domain/Project role grants Change-Id: Idbe0702b42603d6f9f133c9f1855ea9b4f222066 --- keystoneclient/exceptions.py | 4 + keystoneclient/v3/roles.py | 58 +++++++++ tests/v3/test_roles.py | 233 +++++++++++++++++++++++++++++++++++ tests/v3/utils.py | 2 + 4 files changed, 297 insertions(+) diff --git a/keystoneclient/exceptions.py b/keystoneclient/exceptions.py index d01f3d290..06e6c57dd 100644 --- a/keystoneclient/exceptions.py +++ b/keystoneclient/exceptions.py @@ -9,6 +9,10 @@ class CommandError(Exception): pass +class ValidationError(Exception): + pass + + class AuthorizationFailure(Exception): pass diff --git a/keystoneclient/v3/roles.py b/keystoneclient/v3/roles.py index 0e0c180fb..690602575 100644 --- a/keystoneclient/v3/roles.py +++ b/keystoneclient/v3/roles.py @@ -15,6 +15,7 @@ # under the License. from keystoneclient import base +from keystoneclient import exceptions class Role(base.Resource): @@ -34,6 +35,23 @@ class RoleManager(base.CrudManager): collection_key = 'roles' key = 'role' + def _role_grants_base_url(self, user, domain, project): + params = {'user_id': base.getid(user)} + + if domain: + params['domain_id'] = base.getid(domain) + base_url = '/domains/%(domain_id)s/users/%(user_id)s' + elif project: + params['project_id'] = base.getid(project) + base_url = '/projects/%(project_id)s/users/%(user_id)s' + + return base_url % params + + def _require_domain_or_project(self, domain, project): + if (domain and project) or (not domain and not project): + msg = 'Specify either a domain or project, not both' + raise exceptions.ValidationError(msg) + def create(self, name): return super(RoleManager, self).create( name=name) @@ -42,6 +60,22 @@ class RoleManager(base.CrudManager): return super(RoleManager, self).get( role_id=base.getid(role)) + def list(self, user=None, domain=None, project=None): + """Lists roles and role grants. + + If no arguments are provided, all roles in the system will be listed. + + If a user is specified, you must also specify either a domain or + project to list role grants on that pair. + """ + + if user: + self._require_domain_or_project(domain, project) + return super(RoleManager, self).list( + base_url=self._role_grants_base_url(user, domain, project)) + + return super(RoleManager, self).list() + def update(self, role, name=None): return super(RoleManager, self).update( role_id=base.getid(role), @@ -50,3 +84,27 @@ class RoleManager(base.CrudManager): def delete(self, role): return super(RoleManager, self).delete( role_id=base.getid(role)) + + def grant(self, role, user, domain=None, project=None): + """Grants a role to a user on either a domain or project.""" + self._require_domain_or_project(domain, project) + + return super(RoleManager, self).put( + base_url=self._role_grants_base_url(user, domain, project), + role_id=base.getid(role)) + + def check(self, role, user, domain=None, project=None): + """Grants a role to a user on either a domain or project.""" + self._require_domain_or_project(domain, project) + + return super(RoleManager, self).head( + base_url=self._role_grants_base_url(user, domain, project), + role_id=base.getid(role)) + + def revoke(self, role, user, domain=None, project=None): + """Revokes a role from a user on either a domain or project.""" + self._require_domain_or_project(domain, project) + + return super(RoleManager, self).delete( + base_url=self._role_grants_base_url(user, domain, project), + role_id=base.getid(role)) diff --git a/tests/v3/test_roles.py b/tests/v3/test_roles.py index ef1f7ce39..e3fe35314 100644 --- a/tests/v3/test_roles.py +++ b/tests/v3/test_roles.py @@ -1,5 +1,8 @@ +import httplib2 +import urlparse import uuid +from keystoneclient import exceptions from keystoneclient.v3 import roles from tests.v3 import utils @@ -17,3 +20,233 @@ class RoleTests(utils.TestCase, utils.CrudTests): kwargs = super(RoleTests, self).new_ref(**kwargs) kwargs.setdefault('name', uuid.uuid4().hex) return kwargs + + def test_domain_role_grant(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 201, + 'body': '', + }) + + method = 'PUT' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/domains/%s/users/%s/%s/%s' % ( + domain_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.grant(role=ref['id'], domain=domain_id, user=user_id) + + def test_domain_role_list(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref_list), + }) + + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/domains/%s/users/%s/%s' % ( + domain_id, user_id, self.collection_key)), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.list(domain=domain_id, user=user_id) + + def test_domain_role_check(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 200, + 'body': '', + }) + + method = 'HEAD' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/domains/%s/users/%s/%s/%s' % ( + domain_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.check(role=ref['id'], domain=domain_id, user=user_id) + + def test_domain_role_revoke(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 204, + 'body': '', + }) + + method = 'DELETE' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/domains/%s/users/%s/%s/%s' % ( + domain_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.revoke(role=ref['id'], domain=domain_id, user=user_id) + + def test_project_role_grant(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 201, + 'body': '', + }) + + method = 'PUT' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/projects/%s/users/%s/%s/%s' % ( + project_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.grant(role=ref['id'], project=project_id, user=user_id) + + def test_project_role_list(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref_list), + }) + + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/projects/%s/users/%s/%s' % ( + project_id, user_id, self.collection_key)), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.list(project=project_id, user=user_id) + + def test_project_role_check(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 200, + 'body': '', + }) + + method = 'HEAD' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/projects/%s/users/%s/%s/%s' % ( + project_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.check(role=ref['id'], project=project_id, user=user_id) + + def test_project_role_revoke(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 204, + 'body': '', + }) + + method = 'DELETE' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/projects/%s/users/%s/%s/%s' % ( + project_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.revoke(role=ref['id'], project=project_id, user=user_id) + + def test_domain_project_role_grant_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.grant, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_list_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + + self.assertRaises( + exceptions.ValidationError, + self.manager.list, + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_check_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.check, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_revoke_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.revoke, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) diff --git a/tests/v3/utils.py b/tests/v3/utils.py index af8d68bbe..d45a07cba 100644 --- a/tests/v3/utils.py +++ b/tests/v3/utils.py @@ -91,7 +91,9 @@ class CrudTests(object): } } + self.headers['HEAD'] = self.headers['GET'].copy() self.headers['DELETE'] = self.headers['GET'].copy() + self.headers['PUT'] = self.headers['GET'].copy() self.headers['POST'] = self.headers['GET'].copy() self.headers['POST']['Content-Type'] = 'application/json' self.headers['PATCH'] = self.headers['POST'].copy() From 62c55bc2f0f13630db6d9962ae0edf468f495276 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Wed, 10 Oct 2012 07:38:27 +0000 Subject: [PATCH 18/18] Enable/disable services/endpoints (bug 1048662) Change-Id: I664b7f7dfa281a4481bfc37eab0948a901f1c0c5 --- keystoneclient/v3/endpoints.py | 18 ++++++++++++------ keystoneclient/v3/services.py | 7 +++++-- tests/v3/test_endpoints.py | 1 + tests/v3/test_services.py | 1 + 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/keystoneclient/v3/endpoints.py b/keystoneclient/v3/endpoints.py index ff4a678b0..c13313d83 100644 --- a/keystoneclient/v3/endpoints.py +++ b/keystoneclient/v3/endpoints.py @@ -29,6 +29,7 @@ class Endpoint(base.Resource): * region: geographic location of the endpoint * service_id: service to which the endpoint belongs * url: fully qualified service endpoint + * enabled: determines whether the endpoint appears in the catalog """ pass @@ -46,34 +47,39 @@ class EndpointManager(base.CrudManager): msg = msg % ', '.join(VALID_INTERFACES) raise Exception(msg) - def create(self, service, url, name=None, interface=None, region=None): + def create(self, service, url, name=None, interface=None, region=None, + enabled=True): self._validate_interface(interface) return super(EndpointManager, self).create( service_id=base.getid(service), interface=interface, url=url, - region=region) + region=region, + enabled=enabled) def get(self, endpoint): return super(EndpointManager, self).get( endpoint_id=base.getid(endpoint)) - def list(self, service=None, name=None, interface=None, region=None): + def list(self, service=None, name=None, interface=None, region=None, + enabled=None): self._validate_interface(interface) return super(EndpointManager, self).list( service_id=base.getid(service), interface=interface, - region=region) + region=region, + enabled=enabled) def update(self, endpoint, service=None, url=None, name=None, - interface=None, region=None): + interface=None, region=None, enabled=None): self._validate_interface(interface) return super(EndpointManager, self).update( endpoint_id=base.getid(endpoint), service_id=base.getid(service), interface=interface, url=url, - region=region) + region=region, + enabled=enabled) def delete(self, endpoint): return super(EndpointManager, self).delete( diff --git a/keystoneclient/v3/services.py b/keystoneclient/v3/services.py index 900543bfb..d2134d43d 100644 --- a/keystoneclient/v3/services.py +++ b/keystoneclient/v3/services.py @@ -24,6 +24,7 @@ class Service(base.Resource): * id: a uuid that identifies the service * name: user-facing name of the service (e.g. Keystone) * type: 'compute', 'identity', etc + * enabled: determines whether the service appears in the catalog """ pass @@ -35,21 +36,23 @@ class ServiceManager(base.CrudManager): collection_key = 'services' key = 'service' - def create(self, name, type, **kwargs): + def create(self, name, type, enabled=True, **kwargs): return super(ServiceManager, self).create( name=name, type=type, + enabled=enabled, **kwargs) def get(self, service): return super(ServiceManager, self).get( service_id=base.getid(service)) - def update(self, service, name=None, type=None, **kwargs): + def update(self, service, name=None, type=None, enabled=None, **kwargs): return super(ServiceManager, self).update( service_id=base.getid(service), name=name, type=type, + enabled=enabled, **kwargs) def delete(self, service): diff --git a/tests/v3/test_endpoints.py b/tests/v3/test_endpoints.py index f0a6e4266..d428b9148 100644 --- a/tests/v3/test_endpoints.py +++ b/tests/v3/test_endpoints.py @@ -19,6 +19,7 @@ class EndpointTests(utils.TestCase, utils.CrudTests): kwargs.setdefault('region', uuid.uuid4().hex) kwargs.setdefault('service_id', uuid.uuid4().hex) kwargs.setdefault('url', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) return kwargs def test_create_public_interface(self): diff --git a/tests/v3/test_services.py b/tests/v3/test_services.py index bc8a50365..545e84e1a 100644 --- a/tests/v3/test_services.py +++ b/tests/v3/test_services.py @@ -17,4 +17,5 @@ class ServiceTests(utils.TestCase, utils.CrudTests): kwargs = super(ServiceTests, self).new_ref(**kwargs) kwargs.setdefault('name', uuid.uuid4().hex) kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) return kwargs