Merge "Volume Attachments API Buildout"

This commit is contained in:
Jenkins
2013-12-13 20:57:20 +00:00
committed by Gerrit Code Review
10 changed files with 249 additions and 160 deletions

View File

@@ -51,14 +51,14 @@ class VolumesAPI_Behaviors(BaseBehavior):
@behavior(VolumesClient)
def wait_for_volume_status(
self, volume_id, expected_status, timeout, wait_period=None):
self, volume_id, expected_status, timeout, poll_rate=5):
""" Waits for a specific status and returns None when that status is
observed.
Note: Unreliable for transient statuses like 'deleting'.
"""
wait_period = float(
wait_period or self.config.volume_status_poll_frequency)
poll_rate = int(
poll_rate or self.config.volume_status_poll_frequency)
end_time = time() + int(timeout)
while time() < end_time:
@@ -68,7 +68,7 @@ class VolumesAPI_Behaviors(BaseBehavior):
msg = (
"wait_for_volume_status() failure: "
"get_volume_info() call failed with status_code {0} while "
"waiting for volume to reach the {1} status".format(
"waiting for volume to reach the '{1}' status".format(
resp.status_code, expected_status))
self._log.error(msg)
raise VolumesAPIBehaviorException(msg)
@@ -86,8 +86,7 @@ class VolumesAPI_Behaviors(BaseBehavior):
'Expected Volume status "{0}" observed as expected'.format(
expected_status))
break
sleep(wait_period)
sleep(poll_rate)
else:
msg = (

View File

@@ -16,8 +16,7 @@ limitations under the License.
from cafe.engine.clients.rest import AutoMarshallingRestClient
from cloudcafe.blockstorage.v1.volumes_api.models.requests import (
VolumeRequest,
VolumeSnapshotRequest)
VolumeRequest, VolumeSnapshotRequest)
from cloudcafe.blockstorage.v1.volumes_api.models.responses import(
VolumeResponse, VolumeSnapshotResponse, VolumeTypeResponse,

View File

@@ -0,0 +1,96 @@
from time import sleep, time
from cafe.engine.behaviors import BaseBehavior, behavior
from cloudcafe.compute.volume_attachments_api.client import \
VolumeAttachmentsAPIClient
from cloudcafe.compute.volume_attachments_api.config import \
VolumeAttachmentsAPIConfig
from cloudcafe.blockstorage.v1.volumes_api.client import VolumesClient
from cloudcafe.blockstorage.v1.volumes_api.config import VolumesAPIConfig
from cloudcafe.blockstorage.v1.volumes_api.behaviors import \
VolumesAPI_Behaviors
class VolumeAttachmentBehaviorError(Exception):
pass
class VolumeAttachmentsAPI_Behaviors(BaseBehavior):
def __init__(
self, volume_attachments_client=None, volumes_client=None,
volume_attachments_config=None, volumes_config=None):
self.client = volume_attachments_client
self.config = volume_attachments_config or VolumeAttachmentsAPIConfig()
self.volumes_client = volumes_client
self.volumes_behaviors = VolumesAPI_Behaviors(volumes_client)
self.volumes_config = volumes_config or VolumesAPIConfig()
@behavior(VolumeAttachmentsAPIClient)
def wait_for_attachment_to_propagate(
self, attachment_id, server_id, timeout=None, poll_rate=5):
timeout = timeout or self.config.attachment_propagation_timeout
poll_rate = poll_rate or self.config.api_poll_rate
endtime = time() + int(timeout)
while time() < endtime:
resp = \
self.client.get_volume_attachment_details(
attachment_id, server_id)
if resp.ok:
return True
sleep(poll_rate)
else:
return False
@behavior(VolumeAttachmentsAPIClient, VolumesClient)
def attach_volume_to_server(
self, server_id, volume_id, device=None,
expected_volume_status='in-use', volume_status_timeout=120,
attachment_propagation_timeout=60):
"""Returns a VolumeAttachment object"""
attachment_propagation_timeout = (
attachment_propagation_timeout
or self.config.attachment_propagation_timeout)
resp = self.client.attach_volume(server_id, volume_id, device=device)
if not resp.ok:
raise VolumeAttachmentBehaviorError(
"Volume attachment failed in auto_attach_volume_to_server"
" with a {0}. Could not attach volume {1} to server {2}"
.format(resp.status_code, volume_id, server_id))
if resp.entity is None:
raise VolumeAttachmentBehaviorError(
"Volume attachment failed in auto_attach_volume_to_server."
" Could not deserialize volume attachment response body. Could"
" not attach volume {1} to server {2}".format(
volume_id, server_id))
attachment = resp.entity
#Confirm volume attachment propagation
propagated = self.wait_for_attachment_to_propagate(
attachment.id_, server_id, timeout=attachment_propagation_timeout)
if not propagated:
raise VolumeAttachmentBehaviorError(
"Volume attachment {0} belonging to server {1} failed to"
"propagate to the relevant cell within {2} seconds".format(
attachment.id_, server_id, attachment_propagation_timeout))
# Confirm volume status
try:
self.volumes_behaviors.wait_for_volume_status(
volume_id, expected_volume_status, volume_status_timeout)
except:
raise VolumeAttachmentBehaviorError(
"Volume did not attain the '{0}' status within {1}"
"seconds after being attached to a server".format(
expected_volume_status, volume_status_timeout))
return attachment

View File

@@ -15,84 +15,80 @@ limitations under the License.
"""
from cafe.engine.clients.rest import AutoMarshallingRestClient
from cloudcafe.compute.volume_attachments_api.models.requests. \
volume_attachments import VolumeAttachmentRequest
from cloudcafe.compute.volume_attachments_api.models.responses. \
volume_attachments import VolumeAttachmentListResponse
from cloudcafe.compute.volume_attachments_api.models.requests import \
VolumeAttachmentRequest
from cloudcafe.compute.volume_attachments_api.models.responses import \
VolumeAttachmentResponse, VolumeAttachmentListResponse
class VolumeAttachmentsAPIClient(AutoMarshallingRestClient):
def __init__(self, url, auth_token, tenant_id, serialize_format=None,
deserialize_format=None):
def __init__(
self, url, auth_token, serialize_format=None,
deserialize_format=None):
super(VolumeAttachmentsAPIClient, self).__init__(
serialize_format, deserialize_format)
self.url = url
url = url.rstrip('/')
self.auth_token = auth_token
self.tenant_id = tenant_id
self.default_headers['X-Auth-Token'] = auth_token
self.default_headers['Content-Type'] = 'application/{0}'.format(
self.serialize_format)
self.default_headers['Accept'] = 'application/{0}'.format(
self.deserialize_format)
self.url = "{0}/servers/{1}/os-volume_attachments".format(
url, "{server_id}")
def attach_volume(self, server_id, volume_id, device=None,
requestslib_kwargs=None):
"""
POST
v2/{tenant_id}/servers/{server_id}/os-volume_attachments
servers/{server_id}/os-volume_attachments
"""
url = '{0}/servers/{1}/os-volume_attachments'.format(
self.url, server_id)
va = VolumeAttachmentRequest(volume_id, device)
url = self.url.format(server_id=server_id)
req_ent = VolumeAttachmentRequest(volume_id, device)
return self.request(
'POST', url, response_entity_type=VolumeAttachmentListResponse,
request_entity=va, requestslib_kwargs=requestslib_kwargs)
'POST', url, response_entity_type=VolumeAttachmentResponse,
request_entity=req_ent, requestslib_kwargs=requestslib_kwargs)
def delete_volume_attachment(self, attachment_id, server_id,
requestslib_kwargs=None):
"""
DELETE
v2/servers/{server_id}/os-volume_attachments/{attachment_id}
servers/{server_id}/os-volume_attachments/{attachment_id}
"""
url = '{0}/servers/{1}/os-volume_attachments/{2}'.format(
self.url, server_id, attachment_id)
params = {
'tenant_id': self.tenant_id, 'server_id': server_id,
'attachment_id': attachment_id}
url = "{0}/{1}".format(
self.url.format(server_id=server_id), attachment_id)
return self.request(
'DELETE', url, params=params,
requestslib_kwargs=requestslib_kwargs)
'DELETE', url, requestslib_kwargs=requestslib_kwargs)
def get_server_volume_attachments(self, server_id,
requestslib_kwargs=None):
"""
GET
v2/servers/{server_id}/os-volume_attachments/
servers/{server_id}/os-volume_attachments
"""
url = '{0}/servers/{1}/os-volume_attachments'.format(
self.url, server_id)
params = {'tenant_id': self.tenant_id, 'server_id': server_id}
url = self.url.format(server_id=server_id)
return self.request(
'GET', url, params=params, requestslib_kwargs=requestslib_kwargs)
'GET', url, response_entity_type=VolumeAttachmentListResponse,
requestslib_kwargs=requestslib_kwargs)
def get_volume_attachment_details(self, attachment_id, server_id,
requestslib_kwargs=None):
url = '{0}/servers/{1}/os-volume_attachments/{2}'.format(
self.url, server_id, attachment_id)
"""
GET
servers/{server_id}/os-volume_attachments/{attachment_id}
"""
params = {'tenant_id': self.tenant_id,
'server_id': server_id,
'attachment_id': attachment_id}
url = "{0}/{1}".format(
self.url.format(server_id=server_id), attachment_id)
return self.request(
'GET', url, params=params, requestslib_kwargs=requestslib_kwargs)
'GET', url, response_entity_type=VolumeAttachmentResponse,
requestslib_kwargs=requestslib_kwargs)

View File

@@ -0,0 +1,37 @@
"""
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 cloudcafe.common.models.configuration import ConfigSectionInterface
class VolumeAttachmentsAPIConfig(ConfigSectionInterface):
SECTION_NAME = 'volume_attachments'
@property
def attachment_propagation_timeout(self):
"""
Seconds it should take for a new volume attachment instance to
propagate.
"""
return self.get("attachment_propagation_timeout", 60)
@property
def api_poll_rate(self):
"""
Seconds to wait between polling the os-volume_attachments API in loops.
"""
return self.get("api_poll_rate", 5)

View File

@@ -15,6 +15,7 @@ limitations under the License.
"""
import json
from xml.etree import ElementTree
from cafe.engine.models.base import AutoMarshallingModel
@@ -23,18 +24,17 @@ class VolumeAttachmentRequest(AutoMarshallingModel):
def __init__(self, volume_id=None, device=None):
super(VolumeAttachmentRequest, self).__init__()
self.id = None
self.server_id = None
self.volume_id = volume_id
self.device = device
def _obj_to_json(self):
return self._obj_to_json_ele()
data = {"volumeAttachment": self._obj_to_dict()}
return json.dumps(data)
def _obj_to_json_ele(self):
sub_body = {"volumeId": self.volume_id}
sub_body["device"] = self.device
sub_body = self._remove_empty_values(sub_body)
body = {"volumeAttachment": sub_body}
body = self._remove_empty_values(body)
return json.dumps(body)
def _obj_to_xml(self):
element = ElementTree.Element('volumeAttachment')
element = self._set_xml_etree_element(element, self._obj_to_dict())
return ElementTree.tostring(element)
def _obj_to_dict(self):
return {"volumeId": self.volume_id, "device": self.device}

View File

@@ -1,15 +0,0 @@
"""
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,68 @@
"""
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 xml.etree import ElementTree
from cafe.engine.models.base import \
AutoMarshallingModel, AutoMarshallingListModel
class VolumeAttachmentResponse(AutoMarshallingModel):
def __init__(self, id_=None, volume_id=None, server_id=None, device=None):
super(VolumeAttachmentResponse, self).__init__()
self.id_ = id_
self.volume_id = volume_id
self.server_id = server_id
self.device = device
@classmethod
def _json_to_obj(cls, serialized_str):
data = json.loads(serialized_str)
data = data.get("volumeAttachment", dict())
return cls._dict_to_obj(data)
@classmethod
def _xml_to_obj(cls, serialized_str):
return cls._dict_to_obj(ElementTree.fromstring(serialized_str))
@classmethod
def _dict_to_obj(cls, obj_dict):
return VolumeAttachmentResponse(
id_=obj_dict.get('id', None),
volume_id=obj_dict.get('volumeId', None),
server_id=obj_dict.get('serverId', None),
device=obj_dict.get('device', None))
class VolumeAttachmentListResponse(AutoMarshallingListModel):
"""Represents a list of VolumeAttachmentResponse objects"""
@classmethod
def _xml_to_obj(cls, serialized_str):
return cls._xml_ele_to_obj(ElementTree.fromstring(serialized_str))
@classmethod
def _json_to_obj(cls, serialized_str):
data = json.loads(serialized_str)
data = data.get("volumeAttachments")
return cls._list_to_obj(data)
@classmethod
def _list_to_obj(cls, obj_list):
return VolumeAttachmentListResponse(
[VolumeAttachmentResponse._dict_to_obj(obj) for obj in obj_list])

View File

@@ -1,15 +0,0 @@
"""
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

@@ -1,76 +0,0 @@
"""
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 cafe.engine.models.base import \
AutoMarshallingModel, AutoMarshallingListModel
class VolumeAttachment(AutoMarshallingModel):
def __init__(self, id_=None, volume_id=None, server_id=None, device=None):
super(VolumeAttachment, self).__init__()
self.id_ = None
self.server_id = None
self.volume_id = volume_id
self.device = device
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
return VolumeAttachment(
id_=json_dict.get('id'),
volume_id=json_dict.get('volumeId'),
server_id=json_dict.get('serverId'),
device=json_dict.get('device'))
class VolumeAttachmentListResponse(AutoMarshallingListModel):
@classmethod
def _json_to_obj(cls, serialized_str):
'''
Handles both the single and list version of the Volume
call, obviating the need for separate domain objects for "Volumes"
and "Lists of Volumes" responses.
Returns a list-like VolumeAttachmentListResponse
of VolumeAttachment objects, even if there is only one volume
attachment present.
'''
json_dict = json.loads(serialized_str)
is_list = True if json_dict.get('volumeAttachments', None) else False
va_list = VolumeAttachmentListResponse()
if is_list:
for volume_attachment in json_dict.get('volumeAttachments'):
va = VolumeAttachment(
id_=volume_attachment.get('id'),
volume_id=volume_attachment.get('volumeId'),
server_id=volume_attachment.get('serverId'),
device=volume_attachment.get('device'))
va_list.append(va)
else:
volume_attachment = json_dict.get('volumeAttachment')
va_list.append(
VolumeAttachment(
id_=volume_attachment.get('id'),
volume_id=volume_attachment.get('volumeId'),
server_id=volume_attachment.get('serverId'),
device=volume_attachment.get('device')))
va_list.append(va)
return va_list