diff --git a/cloudcafe/compute/common/constants.py b/cloudcafe/compute/common/constants.py index b587efb0..0bf9895b 100644 --- a/cloudcafe/compute/common/constants.py +++ b/cloudcafe/compute/common/constants.py @@ -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 diff --git a/cloudcafe/compute/extensions/ip_associations_api/__init__.py b/cloudcafe/compute/extensions/ip_associations_api/__init__.py new file mode 100644 index 00000000..14b45c9a --- /dev/null +++ b/cloudcafe/compute/extensions/ip_associations_api/__init__.py @@ -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. +""" diff --git a/cloudcafe/compute/extensions/ip_associations_api/client.py b/cloudcafe/compute/extensions/ip_associations_api/client.py new file mode 100644 index 00000000..c8504bf2 --- /dev/null +++ b/cloudcafe/compute/extensions/ip_associations_api/client.py @@ -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 diff --git a/cloudcafe/compute/extensions/ip_associations_api/composites.py b/cloudcafe/compute/extensions/ip_associations_api/composites.py new file mode 100644 index 00000000..1385e7f5 --- /dev/null +++ b/cloudcafe/compute/extensions/ip_associations_api/composites.py @@ -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 diff --git a/cloudcafe/compute/extensions/ip_associations_api/constants.py b/cloudcafe/compute/extensions/ip_associations_api/constants.py new file mode 100644 index 00000000..91a1b1ba --- /dev/null +++ b/cloudcafe/compute/extensions/ip_associations_api/constants.py @@ -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 diff --git a/cloudcafe/compute/extensions/ip_associations_api/models/__init__.py b/cloudcafe/compute/extensions/ip_associations_api/models/__init__.py new file mode 100644 index 00000000..14b45c9a --- /dev/null +++ b/cloudcafe/compute/extensions/ip_associations_api/models/__init__.py @@ -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. +""" diff --git a/cloudcafe/compute/extensions/ip_associations_api/models/request.py b/cloudcafe/compute/extensions/ip_associations_api/models/request.py new file mode 100644 index 00000000..8b95bf01 --- /dev/null +++ b/cloudcafe/compute/extensions/ip_associations_api/models/request.py @@ -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) diff --git a/cloudcafe/compute/extensions/ip_associations_api/models/response.py b/cloudcafe/compute/extensions/ip_associations_api/models/response.py new file mode 100644 index 00000000..565d404e --- /dev/null +++ b/cloudcafe/compute/extensions/ip_associations_api/models/response.py @@ -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 diff --git a/metatests/cloudcafe/compute/extensions/ip_associations_api/__init__.py b/metatests/cloudcafe/compute/extensions/ip_associations_api/__init__.py new file mode 100644 index 00000000..14b45c9a --- /dev/null +++ b/metatests/cloudcafe/compute/extensions/ip_associations_api/__init__.py @@ -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. +""" diff --git a/metatests/cloudcafe/compute/extensions/ip_associations_api/models/__init__.py b/metatests/cloudcafe/compute/extensions/ip_associations_api/models/__init__.py new file mode 100644 index 00000000..14b45c9a --- /dev/null +++ b/metatests/cloudcafe/compute/extensions/ip_associations_api/models/__init__.py @@ -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. +""" diff --git a/metatests/cloudcafe/compute/extensions/ip_associations_api/models/test_ip_associations.py b/metatests/cloudcafe/compute/extensions/ip_associations_api/models/test_ip_associations.py new file mode 100755 index 00000000..45f5ac7c --- /dev/null +++ b/metatests/cloudcafe/compute/extensions/ip_associations_api/models/test_ip_associations.py @@ -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()