Extensions API
1. Added an extensions-api package to Identity. 2. Moved Identity base and constant models to a common package. 2. Added extensions-api responses model and client. 3. Added a data folder for extensions-api tests. 4. Tested extensions-api extension responses model and client. 5. Fixed pep8 issues as per reviewer comments. 6. Cherry-pick commit: 9c3806200a310d75f2643cdb57631a608c85a9ef. 7. Added extensions-api admin parameter to extensions-api client. 8. Added and tested list roles to extensions-api client. 9. Made a small fix (import of Roles). 10. Renamed sections and properties in Identity config.py. 11. Renamed client to ExtensionsAPI_Client for consistency. 12. Fixed all reviewer comments. 13. Added a create role functionality to extensions-api client. 14. Added create role test and refactored extensions-api client test. 15. Added and tested delete role functionality to extensions-api client. 16. Fixed reviewer comments - line continuation issues. 17. Fixed reviewer comments - refactored extensions-api client and its test. 18. Removed a section and property (extensions-api-admin) from Identity config.py. 19. Added an admin extensions class to the constants model, to use it across API clients. 20. Refactored extensions-api client and its test. 21. Rebased changes and uploaded a new patchset. Change-Id: I4f0691b5dfd2302c3414b3c8d4b881b1624063e6
This commit is contained in:
parent
745e1c86cd
commit
604e52f427
@ -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):
|
||||
|
@ -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.
|
||||
"""
|
||||
|
||||
|
@ -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'
|
||||
|
15
cloudcafe/identity/v2_0/extensions_api/__init__.py
Normal file
15
cloudcafe/identity/v2_0/extensions_api/__init__.py
Normal file
@ -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.
|
||||
"""
|
103
cloudcafe/identity/v2_0/extensions_api/client.py
Normal file
103
cloudcafe/identity/v2_0/extensions_api/client.py
Normal file
@ -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
|
15
cloudcafe/identity/v2_0/extensions_api/models/__init__.py
Normal file
15
cloudcafe/identity/v2_0/extensions_api/models/__init__.py
Normal file
@ -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.
|
||||
"""
|
@ -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.
|
||||
"""
|
@ -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
|
@ -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
|
||||
# <ENVIRONMENT>.<FORMAT>.config
|
||||
# ======================================================
|
||||
|
||||
[token_api]
|
||||
[tokens_api]
|
||||
serialize_format=json
|
||||
deserialize_format=json
|
||||
version=v2.0
|
||||
|
@ -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."}]}}
|
@ -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"}}
|
@ -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)
|
@ -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"
|
||||
)
|
||||
|
2
unittest.json.config
Normal file
2
unittest.json.config
Normal file
@ -0,0 +1,2 @@
|
||||
[CCTNG_ENGINE]
|
||||
use_verbose_logging = false
|
Loading…
x
Reference in New Issue
Block a user