Nova IP Associations Rackspace Extension for Shared IPs

- Adding a couple of HTTPResponseCodes to compute constants
- Adding the compute IP Associations request and response models and metatests
- Adding the compute IP Associations constants, client and composite

Change-Id: I43dbbf16192c31f9b95860ed5dddfd578c1a0006
This commit is contained in:
Leonardo Maycotte 2015-06-11 16:15:11 -05:00
parent 5279061acf
commit 3093aaa6f8
11 changed files with 497 additions and 0 deletions

View File

@ -54,6 +54,12 @@ class Constants:
""" """
return [v for k, v in cls.__dict__.items() if k.startswith('XML')] return [v for k, v in cls.__dict__.items() if k.startswith('XML')]
class HTTPResponseCodes(object): class HTTPResponseCodes(object):
NOT_FOUND = 404 NOT_FOUND = 404
SERVER_ERROR = 500 SERVER_ERROR = 500
BAD_REQUEST = 400
UNAUTHORIZED = 401
FORBIDDEN = 403
CONFLICT = 409
REQUEST_ENTITY_TOO_LARGE = 413

View File

@ -0,0 +1,15 @@
"""
Copyright 2015 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,149 @@
"""
Copyright 2015 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.http.client import AutoMarshallingHTTPClient
from cloudcafe.compute.extensions.ip_associations_api.models.response \
import IPAssociation, IPAssociations
class IPAssociationsClient(AutoMarshallingHTTPClient):
def __init__(self, url, auth_token, serialize_format=None,
deserialize_format=None):
"""
@summary: Rackspace Compute API IP Associations extension client
@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 serialize_format: Format for serializing requests
@type serialize_format: string
@param deserialize_format: Format for de-serializing responses
@type deserialize_format: string
"""
super(IPAssociationsClient, self).__init__(serialize_format,
deserialize_format)
self.auth_token = auth_token
self.default_headers['X-Auth-Token'] = auth_token
ct = '{content_type}/{content_subtype}'.format(
content_type='application',
content_subtype=self.serialize_format)
accept = '{content_type}/{content_subtype}'.format(
content_type='application',
content_subtype=self.deserialize_format)
self.default_headers['Content-Type'] = ct
self.default_headers['Accept'] = accept
self.url = url
def list_ip_associations(self, server_id, ip_address_id=None,
address=None, limit=None, marker=None,
page_reverse=None, requestslib_kwargs=None):
"""
@summary: Lists IP associations by server, filtered by params if given
@param server_id: server UUID to get shared IP associations
@type server_id: str
@param ip_address_id: shared IP UUID to filter by
@type ip_address_id: str
@param address: IPv4 or IPv6 shared IP address to filter by
@type address: str
@param limit: page size
@type limit: int
@param marker: Id of the last item of the previous page
@type marker: string
@param page_reverse: direction of the page
@type page_reverse: bool
@return: IP associations list response
@rtype: Requests.response
"""
params = {'id': ip_address_id, 'address': address,
'limit': limit, 'marker': marker,
'page_reverse': page_reverse}
url = '{base_url}/servers/{server_id}/ip_associations'.format(
base_url=self.url, server_id=server_id)
resp = self.request('GET', url, params=params,
response_entity_type=IPAssociations,
requestslib_kwargs=requestslib_kwargs)
return resp
def get_ip_association(self, server_id, ip_address_id,
requestslib_kwargs=None):
"""
@summary: Shows a specific IP association
@param server_id: server UUID to get shared IP association
@type server_id: str
@param ip_address_id: shared IP UUID
@type ip_address_id: str
@return: IP association get response
@rtype: Requests.response
"""
url = ('{base_url}/servers/{server_id}/ip_associations/'
'{ip_address_id}').format(base_url=self.url,
server_id=server_id,
ip_address_id=ip_address_id)
resp = self.request('GET', url,
response_entity_type=IPAssociation,
requestslib_kwargs=requestslib_kwargs)
return resp
def create_ip_association(self, server_id, ip_address_id,
requestslib_kwargs=None):
"""
@summary: Creates a shared IP association with a server instance
@param server_id: server UUID to create shared IP association
@type server_id: str
@param ip_address_id: shared IP UUID to associate with server
@type ip_address_id: str
@return: IP association get response
@rtype: Requests.response
"""
url = ('{base_url}/servers/{server_id}/ip_associations/'
'{ip_address_id}').format(base_url=self.url,
server_id=server_id,
ip_address_id=ip_address_id)
# Currently this call does NOT requires a request body
resp = self.request('PUT', url,
response_entity_type=IPAssociation,
requestslib_kwargs=requestslib_kwargs)
return resp
def delete_ip_association(self, server_id, ip_address_id,
requestslib_kwargs=None):
"""
@summary: Deletes a shared IP association with a server instance
@param server_id: server UUID to remove shared IP association
@type server_id: str
@param ip_address_id: shared IP UUID to disassociate from server
@type ip_address_id: str
@return: IP association delete response
@rtype: Requests.response
"""
url = ('{base_url}/servers/{server_id}/ip_associations/'
'{ip_address_id}').format(base_url=self.url,
server_id=server_id,
ip_address_id=ip_address_id)
# Currently this call does NOT requires a request body
resp = self.request('DELETE', url,
requestslib_kwargs=requestslib_kwargs)
return resp

View File

@ -0,0 +1,29 @@
"""
Copyright 2015 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 cloudcafe.compute.common.composites import BaseComputeComposite
from cloudcafe.compute.extensions.ip_associations_api.client import \
IPAssociationsClient
class IPAssociationsComposite(BaseComputeComposite):
def __init__(self, auth_composite):
super(IPAssociationsComposite, self).__init__(auth_composite)
self.client = IPAssociationsClient(
**self.compute_auth_composite.client_args)
self.config = None
self.behaviors = None

View File

@ -0,0 +1,25 @@
"""
Copyright 2015 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 cloudcafe.compute.common.constants import HTTPResponseCodes
class IPAssociationsResponseCodes(HTTPResponseCodes):
"""HTTP IP Associations API Expected Response codes"""
LIST_IP_ASSOCIATIONS = 200
GET_IP_ASSOCIATION = 200
CREATE_IP_ASSOCIATION = 201
DELETE_IP_ASSOCIATION = 204

View File

@ -0,0 +1,15 @@
"""
Copyright 2015 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,42 @@
"""
Copyright 2015 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 cafe.engine.models.base import AutoMarshallingModel
class IPAssociationRequest(AutoMarshallingModel):
"""
@summary: IP Association model request object for the Shared IPs Rackspace
Compute v2.0 API extension for creating, by an API PUT call,
a Shared IP addresses association with a server instance
"""
def __init__(self, **kwargs):
super(IPAssociationRequest, self).__init__()
# Currently the IPAssociation is done without the need of a
# request body with the following API call,
# PUT https://{novaUri}/{version}/servers/{serverId}/ip_associations/
# {ipAddressId}
# This model is to be updated and used later on with body params
def _obj_to_json(self):
body = {}
main_body = {'ip_association': body}
return json.dumps(main_body)

View File

@ -0,0 +1,85 @@
"""
Copyright 2015 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 cafe.engine.models.base import AutoMarshallingListModel, \
AutoMarshallingModel
class IPAssociation(AutoMarshallingModel):
"""
@summary: IP Association model response object for the Shared IPs Rackspace
Compute v2.0 API extension.
@param id_: Server instance shared IP address ID
@type id_: str
@param address: IPv4 or IPv6 shared IP address
@type address: str
"""
IP_ASSOCIATION = 'ip_association'
def __init__(self, id_=None, address=None, **kwargs):
# kwargs to be used for checking unexpected response attrs
super(IPAssociation, self).__init__()
self.id = id_
self.address = address
@classmethod
def _json_to_obj(cls, serialized_str):
"""
@summary: Return IP association object from a JSON serialized string
"""
ret = None
json_dict = json.loads(serialized_str)
# Replacing attribute response names if they are Python reserved words
# with a trailing underscore, for ex. id for id_
json_dict = cls._replace_dict_key(
json_dict, 'id', 'id_', recursion=True)
if cls.IP_ASSOCIATION in json_dict:
ip_address_dict = json_dict.get(cls.IP_ASSOCIATION)
ret = IPAssociation(**ip_address_dict)
return ret
class IPAssociations(AutoMarshallingListModel):
IP_ASSOCIATIONS = 'ip_associations'
@classmethod
def _json_to_obj(cls, serialized_str):
"""
@summary: Return a list of IP association objects from a JSON
serialized string
"""
ret = cls()
json_dict = json.loads(serialized_str)
# Replacing attribute response names if they are Python reserved words
# with a trailing underscore, for ex. id for id_
json_dict = cls._replace_dict_key(
json_dict, 'id', 'id_', recursion=True)
if cls.IP_ASSOCIATIONS in json_dict:
ip_associations = json_dict.get(cls.IP_ASSOCIATIONS)
for ip_association in ip_associations:
result = IPAssociation(**ip_association)
ret.append(result)
return ret

View File

@ -0,0 +1,15 @@
"""
Copyright 2015 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 2015 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,101 @@
"""
Copyright 2015 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 unittest
from cloudcafe.compute.extensions.ip_associations_api.models.request \
import IPAssociationRequest
from cloudcafe.compute.extensions.ip_associations_api.models.response \
import IPAssociation, IPAssociations
ERROR_MSG_REQ = ('JSON unexpected IP Association request serialization\n'
'Actual Serialization:\n{request}\n'
'Expected Serialization:\n{expected}\n')
ERROR_MSG_RESP = ('JSON to Obj response different than expected\n'
'Actual Response:\n{response}\n'
'Expected Response:\n{expected}\n')
class CreateIPAssociationTest(unittest.TestCase):
"""
@summary: Test for the IP Associations PUT model object request body
"""
@classmethod
def setUpClass(cls):
cls.ip_association_model = IPAssociationRequest()
cls.expected_json_output = ('{"ip_association": {}}')
def test_json_request(self):
request_body = self.ip_association_model._obj_to_json()
msg = ERROR_MSG_REQ.format(request=request_body,
expected=self.expected_json_output)
self.assertEqual(request_body, self.expected_json_output, msg)
class GetIPAssociationTest(unittest.TestCase):
"""
@sumary: Test for the IP Association GET model object response
"""
@classmethod
def setUpClass(cls):
# Setting the expected response object
get_attrs = dict(id_='1', address='10.1.1.1')
cls.expected_response = IPAssociation(**get_attrs)
# Data simulating the JSON API response
cls.api_json_resp = ("""
{"ip_association": {"id": "1", "address": "10.1.1.1"}}""")
def test_json_response(self):
response_obj = IPAssociation()._json_to_obj(self.api_json_resp)
msg = ERROR_MSG_RESP.format(response=response_obj,
expected=self.expected_response)
self.assertEqual(response_obj, self.expected_response, msg)
class ListIPAssociationsTest(unittest.TestCase):
"""
@sumary: Test for the IP Associations (List) GET model object response
"""
@classmethod
def setUpClass(cls):
# Setting the expected response object
get_attrs1 = dict(id_='1', address='10.1.1.1')
get_attrs2 = dict(id_='2', address='10.1.1.2')
get_attrs3 = dict(id_='3', address='10.1.1.3')
ip_association1 = IPAssociation(**get_attrs1)
ip_association2 = IPAssociation(**get_attrs2)
ip_association3 = IPAssociation(**get_attrs3)
cls.expected_response = [ip_association1, ip_association2,
ip_association3]
# Data simulating the JSON API response
cls.api_json_resp = ("""
{"ip_associations": [{"id": "1", "address": "10.1.1.1"},
{"id": "2", "address": "10.1.1.2"},
{"id": "3", "address": "10.1.1.3"}]}
""")
def test_json_response(self):
response_obj = IPAssociations()._json_to_obj(self.api_json_resp)
msg = ERROR_MSG_RESP.format(response=response_obj,
expected=self.expected_response)
self.assertEqual(response_obj, self.expected_response, msg)
if __name__ == "__main__":
unittest.main()