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: 9c3806200a.
    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:
Charles Kimpolo 2013-06-18 12:20:21 +02:00
parent 745e1c86cd
commit 604e52f427
14 changed files with 417 additions and 5 deletions

View File

@ -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):

View File

@ -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.
"""

View File

@ -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'

View 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.
"""

View 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

View 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.
"""

View 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.
"""

View File

@ -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

View File

@ -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

View File

@ -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."}]}}

View File

@ -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"}}

View File

@ -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)

View File

@ -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
View File

@ -0,0 +1,2 @@
[CCTNG_ENGINE]
use_verbose_logging = false