diff --git a/cloudcafe/identity/config.py b/cloudcafe/identity/config.py index df7bc1ce..1f2cf4a1 100644 --- a/cloudcafe/identity/config.py +++ b/cloudcafe/identity/config.py @@ -18,7 +18,7 @@ from cloudcafe.common.models.configuration import ConfigSectionInterface class IdentityTokenConfig(ConfigSectionInterface): - SECTION_NAME = 'token_api' + SECTION_NAME = 'tokens_api' @property def serialize_format(self): diff --git a/cloudcafe/identity/v2_0/common/models/__init__.py b/cloudcafe/identity/v2_0/common/models/__init__.py index dd8b1e4e..59ab77fa 100644 --- a/cloudcafe/identity/v2_0/common/models/__init__.py +++ b/cloudcafe/identity/v2_0/common/models/__init__.py @@ -13,4 +13,3 @@ 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/cloudcafe/identity/v2_0/common/models/constants.py b/cloudcafe/identity/v2_0/common/models/constants.py index 83b34568..e5e0e454 100644 --- a/cloudcafe/identity/v2_0/common/models/constants.py +++ b/cloudcafe/identity/v2_0/common/models/constants.py @@ -32,3 +32,7 @@ class V2_0Constants(object): XML_NS_RAX_KSGRP = \ 'http://docs.rackspace.com/identity/api/ext/RAX-KSGRP/v1.0' XML_NS_ATOM = 'http://www.w3.org/2005/Atom' + + +class AdminExtensions(object): + OS_KS_ADM = 'OS-KSADM' diff --git a/cloudcafe/identity/v2_0/extensions_api/__init__.py b/cloudcafe/identity/v2_0/extensions_api/__init__.py new file mode 100644 index 00000000..59ab77fa --- /dev/null +++ b/cloudcafe/identity/v2_0/extensions_api/__init__.py @@ -0,0 +1,15 @@ +""" +Copyright 2013 Rackspace + +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/cloudcafe/identity/v2_0/extensions_api/client.py b/cloudcafe/identity/v2_0/extensions_api/client.py new file mode 100644 index 00000000..6e6c5d01 --- /dev/null +++ b/cloudcafe/identity/v2_0/extensions_api/client.py @@ -0,0 +1,103 @@ +""" +Copyright 2013 Rackspace + +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 cafe.engine.clients.rest import AutoMarshallingRestClient +from cloudcafe.identity.v2_0.common.models.constants import AdminExtensions +from cloudcafe.identity.v2_0.extensions_api.models.responses.extensions \ + import Extensions +from cloudcafe.identity.v2_0.tenants_api.models.responses.role import \ + Role, Roles + +_version = 'v2.0' +_admin_extensions = AdminExtensions.OS_KS_ADM + + +class ExtensionsAPI_Client(AutoMarshallingRestClient): + def __init__(self, url=None, auth_token=None, + serialized_format=None, deserialized_format=None): + """ + @param url: Base URL for the compute service + @type url: String + @param auth_token: Auth token to be used for all requests + @type auth_token: String + @param serialized_format: Format for serializing requests + @type serialized_format: String + @param deserialized_format: Format for de-serializing responses + @type deserialized_format: String + """ + super(ExtensionsAPI_Client, self).__init__( + serialized_format, deserialized_format) + self.base_url = '{0}/{1}'.format(url, _version) + self.default_headers['Content-Type'] = 'application/{0}'.format( + serialized_format) + self.default_headers['Accept'] = 'application/{0}'.format( + serialized_format) + self.default_headers['X-Auth-Token'] = auth_token + + def list_extensions(self, requestslib_kwargs=None): + """ + @summary: Lists all the extensions. Maps to /extensions + @return: response + @rtype: Response + """ + url = '{0}/extensions'.format(self.base_url) + response = self.request('GET', url, + response_entity_type=Extensions, + requestslib_kwargs=requestslib_kwargs) + return response + + def list_roles(self, requestslib_kwargs=None): + """ + @summary: List all roles. + @return: response + @rtype: Response + """ + url = '{0}/{1}/roles'.format(self.base_url, _admin_extensions) + response = self.request('GET', url, + response_entity_type=Roles, + requestslib_kwargs=requestslib_kwargs) + return response + + def create_role(self, name=None, requestslib_kwargs=None): + """ + @summary: Create a role. + @return: response + @rtype: Response + @param name: the role name + @type name: String + """ + url = '{0}/{1}/roles'.format(self.base_url, _admin_extensions) + role_request_object = Role(name=name) + response = self.request('POST', url, + response_entity_type=Role, + request_entity=role_request_object, + requestslib_kwargs=requestslib_kwargs) + return response + + def delete_role(self, role_id, requestslib_kwargs=None): + """ + @summary: Delete a role. + @return: response + @rtype: Response + @param role_id: the role id + @type role_id: String + """ + url = '{0}/{1}/roles/{2}'.format(self.base_url, + _admin_extensions, + role_id) + response = self.request('DELETE', url, + requestslib_kwargs=requestslib_kwargs) + return response diff --git a/cloudcafe/identity/v2_0/extensions_api/models/__init__.py b/cloudcafe/identity/v2_0/extensions_api/models/__init__.py new file mode 100644 index 00000000..59ab77fa --- /dev/null +++ b/cloudcafe/identity/v2_0/extensions_api/models/__init__.py @@ -0,0 +1,15 @@ +""" +Copyright 2013 Rackspace + +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/cloudcafe/identity/v2_0/extensions_api/models/responses/__init__.py b/cloudcafe/identity/v2_0/extensions_api/models/responses/__init__.py new file mode 100644 index 00000000..59ab77fa --- /dev/null +++ b/cloudcafe/identity/v2_0/extensions_api/models/responses/__init__.py @@ -0,0 +1,15 @@ +""" +Copyright 2013 Rackspace + +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/cloudcafe/identity/v2_0/extensions_api/models/responses/extensions.py b/cloudcafe/identity/v2_0/extensions_api/models/responses/extensions.py new file mode 100644 index 00000000..b93bc94f --- /dev/null +++ b/cloudcafe/identity/v2_0/extensions_api/models/responses/extensions.py @@ -0,0 +1,122 @@ +""" +Copyright 2013 Rackspace + +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 +from cloudcafe.identity.v2_0.common.models.base import \ + BaseIdentityModel, BaseIdentityListModel + + +class Extensions(BaseIdentityModel): + def __init__(self, values=None): + """ + Models a extensions object returned by keystone + """ + super(Extensions, self).__init__() + self.values = values + + @classmethod + def _dict_to_obj(cls, json_dict): + extensions = Extensions() + extensions.values = Values._list_to_obj( + json_dict.get('extensions')) + + return extensions + + @classmethod + def _json_to_obj(cls, serialized_str): + json_dict = json.loads(serialized_str) + return cls._dict_to_obj(json_dict.get('extensions')) + + +class Values(BaseIdentityListModel): + def __init__(self, values=None): + """ + Models a list of values returned by keystone + """ + super(Values, self).__init__() + self.extend(values or []) + + @classmethod + def _list_to_obj(self, value_dict_list): + values = Values() + for value_dict in value_dict_list: + value = Value._dict_to_obj(value_dict) + values.append(value) + + return values + + +class Value(BaseIdentityModel): + def __init__(self, updated=None, name=None, links=None, namespace=None, + alias=None, description=None): + """ + Models a value object returned by keystone + """ + super(Value, self).__init__() + self.updated = updated + self.name = name + self.links = links + self.namespace = namespace + self.alias = alias + self.description = description + + @classmethod + def _dict_to_obj(cls, json_dict): + value = Value(updated=json_dict.get('updated'), + name=json_dict.get('name'), + namespace=json_dict.get('namespace'), + alias=json_dict.get('alias'), + description=json_dict.get('description'), + links=(Links._list_to_obj(json_dict.get('links')))) + + return value + + +class Links(BaseIdentityListModel): + def __init__(self, links=None): + """ + Models a list of links returned by keystone + """ + super(Links, self).__init__() + self.extend(links or []) + + @classmethod + def _list_to_obj(self, link_dict_list): + links = Links() + for link_dict in link_dict_list: + link = Link._dict_to_obj(link_dict) + links.append(link) + + return links + + +class Link(BaseIdentityModel): + def __init__(self, href=None, type_=None, rel=None): + """ + Models a link object returned by keystone + """ + super(Link, self).__init__() + self.href = href + self.type_ = type_ + self.rel = rel + + @classmethod + def _dict_to_obj(cls, json_dict): + link = Link(href=json_dict.get('href'), + type_=json_dict.get('type'), + rel=json_dict.get('rel')) + + return link diff --git a/configs/identity/reference.json.config b/configs/identity/reference.json.config index a4c2a0ba..12162763 100644 --- a/configs/identity/reference.json.config +++ b/configs/identity/reference.json.config @@ -1,17 +1,17 @@ # ====================================================== # reference.json.config # ------------------------------------------------------ -# This configuration is specifically a reference +# This configuration is specifically a reference # implementation for a configuration file. # You must create a proper configuration file and supply # the correct values for your Environment(s) # -# For multiple environments it is suggested that you +# For multiple environments it is suggested that you # generate specific configurations and name the files # ..config # ====================================================== -[token_api] +[tokens_api] serialize_format=json deserialize_format=json version=v2.0 diff --git a/metatests/cloudcafe/identity/v2_0/extensions_api/data/extensions.json b/metatests/cloudcafe/identity/v2_0/extensions_api/data/extensions.json new file mode 100644 index 00000000..9698ecd9 --- /dev/null +++ b/metatests/cloudcafe/identity/v2_0/extensions_api/data/extensions.json @@ -0,0 +1 @@ +{"extensions": {"values": [{"updated": "2011-08-19T13:25:27-06:00", "name": "Openstack Keystone Admin", "links": [{"href": "https://github.com/openstack/identity-api", "type": "text/html", "rel": "describedby"}], "namespace": "http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0", "alias": "OS-KSADM", "description": "Openstack extensions to Keystone v2.0 API enabling Admin Operations."}]}} \ No newline at end of file diff --git a/metatests/cloudcafe/identity/v2_0/extensions_api/extensions_client_test.py b/metatests/cloudcafe/identity/v2_0/extensions_api/extensions_client_test.py new file mode 100644 index 00000000..bca420be --- /dev/null +++ b/metatests/cloudcafe/identity/v2_0/extensions_api/extensions_client_test.py @@ -0,0 +1,80 @@ +from unittest import TestCase +from httpretty import HTTPretty +from cloudcafe.identity.v2_0.extensions_api.client import ExtensionsAPI_Client + +IDENTITY_ENDPOINT_URL = "http://localhost:35357" + + +class ExtensionsClientTest(TestCase): + def setUp(self): + self.url = IDENTITY_ENDPOINT_URL + self.serialized_format = "json" + self.deserialized_format = "json" + self.auth_token = "AUTH_TOKEN" + self.admin_extensions = "OS-KSADM" + + self.extensions_api_client = ExtensionsAPI_Client( + url=self.url, + auth_token=self.auth_token, + serialized_format=self.serialized_format, + deserialized_format=self.deserialized_format) + + self.role_id = "1" + + HTTPretty.enable() + + def test_list_extensions(self): + url = "{0}/v2.0/extensions".format(self.url) + HTTPretty.register_uri(HTTPretty.GET, url, + body=self._build_expected_body_response()) + actual_response = self.extensions_api_client.list_extensions() + self._build_assertions(actual_response, url) + + def test_list_roles(self): + url = "{0}/v2.0/{1}/roles".format(self.url, self.admin_extensions) + HTTPretty.register_uri(HTTPretty.GET, url, + body=self._build_create_role_response()) + actual_response = self.extensions_api_client.list_roles() + self._build_assertions(actual_response, url) + + def test_create_role(self): + url = "{0}/v2.0/{1}/roles".format(self.url, self.admin_extensions) + HTTPretty.register_uri( + HTTPretty.POST, url, + body=self._build_create_role_response()) + actual_response = self.extensions_api_client.create_role() + self._build_assertions(actual_response, url) + + def test_delete_role(self): + url = "{0}/v2.0/{1}/roles/{2}".format(self.url, self.admin_extensions, + self.role_id) + HTTPretty.register_uri(HTTPretty.DELETE, url) + actual_response = self.extensions_api_client.delete_role( + role_id=self.role_id) + self._build_assertions(actual_response, url) + + def _build_assertions(self, actual_response, url): + assert HTTPretty.last_request.headers['Content-Type'] == \ + 'application/{0}'.format(self.serialized_format) + assert HTTPretty.last_request.headers['Accept'] == \ + 'application/{0}'.format(self.deserialized_format) + assert HTTPretty.last_request.headers[ + 'X-Auth-Token'] == self.auth_token + assert 200 == actual_response.status_code + assert url == actual_response.url + + def _build_expected_body_response(self): + return {"extensions": [{"values": [ + {"updated": "2011-08-19T13:25:27-06:00", + "name": "Openstack Keystone Admin", + "links": {"href": "https://github.com/openstack/identity-api", + "type": "text/html", "rel": "describedby"}, + "namespace": "http://docs.openstack" + ".org/identity/api/ext/OS-KSADM/v1.0", + "alias": "OS-KSADM", + "description": "Openstack extensions to Keystone " + "v2.0 API enabling Admin Operations."}]}]} + + def _build_create_role_response(self): + return {"role": {"id": "25dfade062ca486ebdb4e00246c40441", + "name": "response-test-221460"}} diff --git a/metatests/cloudcafe/identity/v2_0/extensions_api/models/responses/extensions_test.py b/metatests/cloudcafe/identity/v2_0/extensions_api/models/responses/extensions_test.py new file mode 100644 index 00000000..4326a6d8 --- /dev/null +++ b/metatests/cloudcafe/identity/v2_0/extensions_api/models/responses/extensions_test.py @@ -0,0 +1,51 @@ +import json +from unittest import TestCase +import os +from cloudcafe.identity.v2_0.extensions_api.models.responses.extensions \ + import Extensions, Value, Values, Link, Links + + +class ExtensionsTest(TestCase): + def setUp(self): + self.extensions_json_dict = open(os.path.join(os.path.dirname( + __file__), "../../data/extensions.json")).read() + + self.extensions_dict = json.loads(self.extensions_json_dict).get( + 'extensions') + self.values = self.extensions_dict.get('values') + self.links = self.values[0].get('links') + self.dict_for_link = self.links[0] + self.href = self.dict_for_link.get('href') + self.type = self.dict_for_link.get('type') + self.rel = self.dict_for_link.get('rel') + + self.updated_date = self.values[0].get('updated') + self.name = self.values[0].get('name') + self.name_space = self.values[0].get('namespace') + self.alias = self.values[0].get('alias') + self.description = self.values[0].get('description') + + self.expected_link = Link(href=self.href, type_=self.type, + rel=self.rel) + self.expected_links = Links(links=[self.expected_link]) + + self.expected_value = Value(updated=self.updated_date, + name=self.name, + links=[self.expected_link], + namespace=self.name_space, + alias=self.alias, + description=self.description) + self.expected_values = Values(values=[self.expected_value]) + + self.expected_extensions = Extensions(values=self.expected_values) + self.dict_for_extensions = {'extensions': self.values} + + def test_dict_to_obj(self): + assert self.expected_extensions == Extensions._dict_to_obj( + self.dict_for_extensions) + assert self.expected_link == Link._dict_to_obj(self.dict_for_link) + assert self.expected_value == Value._dict_to_obj(self.values[0]) + + def test_list_to_obj(self): + assert self.expected_values == Values._list_to_obj(self.values) + assert self.expected_links == Links._list_to_obj(self.links) diff --git a/metatests/conftest.py b/metatests/conftest.py index 268b8886..9ed18c5c 100644 --- a/metatests/conftest.py +++ b/metatests/conftest.py @@ -6,3 +6,8 @@ os.environ["CCTNG_CONFIG_FILE"] = os.path.join( "unittest.json.config" ) os.environ["MOCK"] = 'True' + +os.environ["OSTNG_CONFIG_FILE"] = os.path.join( + os.path.dirname(__file__), + "unittest.json.config" +) diff --git a/unittest.json.config b/unittest.json.config new file mode 100644 index 00000000..8966247f --- /dev/null +++ b/unittest.json.config @@ -0,0 +1,2 @@ +[CCTNG_ENGINE] +use_verbose_logging = false