Merge "Nova IP Associations Rackspace Extension for Shared IPs"
This commit is contained in:
commit
8700ab625d
@ -54,6 +54,12 @@ class Constants:
|
||||
"""
|
||||
return [v for k, v in cls.__dict__.items() if k.startswith('XML')]
|
||||
|
||||
|
||||
class HTTPResponseCodes(object):
|
||||
NOT_FOUND = 404
|
||||
SERVER_ERROR = 500
|
||||
BAD_REQUEST = 400
|
||||
UNAUTHORIZED = 401
|
||||
FORBIDDEN = 403
|
||||
CONFLICT = 409
|
||||
REQUEST_ENTITY_TOO_LARGE = 413
|
||||
|
15
cloudcafe/compute/extensions/ip_associations_api/__init__.py
Normal file
15
cloudcafe/compute/extensions/ip_associations_api/__init__.py
Normal 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.
|
||||
"""
|
149
cloudcafe/compute/extensions/ip_associations_api/client.py
Normal file
149
cloudcafe/compute/extensions/ip_associations_api/client.py
Normal 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
|
@ -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
|
@ -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
|
@ -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.
|
||||
"""
|
@ -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)
|
@ -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
|
@ -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.
|
||||
"""
|
@ -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.
|
||||
"""
|
@ -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()
|
Loading…
Reference in New Issue
Block a user