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

View File

@@ -16,8 +16,7 @@ limitations under the License.
from cafe.engine.clients.rest import AutoMarshallingRestClient from cafe.engine.clients.rest import AutoMarshallingRestClient
from cloudcafe.blockstorage.v1.volumes_api.models.requests import ( from cloudcafe.blockstorage.v1.volumes_api.models.requests import (
VolumeRequest, VolumeRequest, VolumeSnapshotRequest)
VolumeSnapshotRequest)
from cloudcafe.blockstorage.v1.volumes_api.models.responses import( from cloudcafe.blockstorage.v1.volumes_api.models.responses import(
VolumeResponse, VolumeSnapshotResponse, VolumeTypeResponse, 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 cafe.engine.clients.rest import AutoMarshallingRestClient
from cloudcafe.compute.volume_attachments_api.models.requests. \ from cloudcafe.compute.volume_attachments_api.models.requests import \
volume_attachments import VolumeAttachmentRequest VolumeAttachmentRequest
from cloudcafe.compute.volume_attachments_api.models.responses. \ from cloudcafe.compute.volume_attachments_api.models.responses import \
volume_attachments import VolumeAttachmentListResponse VolumeAttachmentResponse, VolumeAttachmentListResponse
class VolumeAttachmentsAPIClient(AutoMarshallingRestClient): class VolumeAttachmentsAPIClient(AutoMarshallingRestClient):
def __init__(self, url, auth_token, tenant_id, serialize_format=None, def __init__(
deserialize_format=None): self, url, auth_token, serialize_format=None,
deserialize_format=None):
super(VolumeAttachmentsAPIClient, self).__init__( super(VolumeAttachmentsAPIClient, self).__init__(
serialize_format, deserialize_format) serialize_format, deserialize_format)
self.url = url url = url.rstrip('/')
self.auth_token = auth_token self.auth_token = auth_token
self.tenant_id = tenant_id
self.default_headers['X-Auth-Token'] = auth_token self.default_headers['X-Auth-Token'] = auth_token
self.default_headers['Content-Type'] = 'application/{0}'.format( self.default_headers['Content-Type'] = 'application/{0}'.format(
self.serialize_format) self.serialize_format)
self.default_headers['Accept'] = 'application/{0}'.format( self.default_headers['Accept'] = 'application/{0}'.format(
self.deserialize_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, def attach_volume(self, server_id, volume_id, device=None,
requestslib_kwargs=None): requestslib_kwargs=None):
""" """
POST POST
v2/{tenant_id}/servers/{server_id}/os-volume_attachments servers/{server_id}/os-volume_attachments
""" """
url = '{0}/servers/{1}/os-volume_attachments'.format( url = self.url.format(server_id=server_id)
self.url, server_id) req_ent = VolumeAttachmentRequest(volume_id, device)
va = VolumeAttachmentRequest(volume_id, device)
return self.request( return self.request(
'POST', url, response_entity_type=VolumeAttachmentListResponse, 'POST', url, response_entity_type=VolumeAttachmentResponse,
request_entity=va, requestslib_kwargs=requestslib_kwargs) request_entity=req_ent, requestslib_kwargs=requestslib_kwargs)
def delete_volume_attachment(self, attachment_id, server_id, def delete_volume_attachment(self, attachment_id, server_id,
requestslib_kwargs=None): requestslib_kwargs=None):
""" """
DELETE 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( url = "{0}/{1}".format(
self.url, server_id, attachment_id) self.url.format(server_id=server_id), attachment_id)
params = {
'tenant_id': self.tenant_id, 'server_id': server_id,
'attachment_id': attachment_id}
return self.request( return self.request(
'DELETE', url, params=params, 'DELETE', url, requestslib_kwargs=requestslib_kwargs)
requestslib_kwargs=requestslib_kwargs)
def get_server_volume_attachments(self, server_id, def get_server_volume_attachments(self, server_id,
requestslib_kwargs=None): requestslib_kwargs=None):
""" """
GET GET
v2/servers/{server_id}/os-volume_attachments/ servers/{server_id}/os-volume_attachments
""" """
url = '{0}/servers/{1}/os-volume_attachments'.format( url = self.url.format(server_id=server_id)
self.url, server_id)
params = {'tenant_id': self.tenant_id, 'server_id': server_id}
return self.request( 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, def get_volume_attachment_details(self, attachment_id, server_id,
requestslib_kwargs=None): 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, url = "{0}/{1}".format(
'server_id': server_id, self.url.format(server_id=server_id), attachment_id)
'attachment_id': attachment_id}
return self.request( 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 import json
from xml.etree import ElementTree
from cafe.engine.models.base import AutoMarshallingModel from cafe.engine.models.base import AutoMarshallingModel
@@ -23,18 +24,17 @@ class VolumeAttachmentRequest(AutoMarshallingModel):
def __init__(self, volume_id=None, device=None): def __init__(self, volume_id=None, device=None):
super(VolumeAttachmentRequest, self).__init__() super(VolumeAttachmentRequest, self).__init__()
self.id = None
self.server_id = None
self.volume_id = volume_id self.volume_id = volume_id
self.device = device self.device = device
def _obj_to_json(self): 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): def _obj_to_xml(self):
sub_body = {"volumeId": self.volume_id} element = ElementTree.Element('volumeAttachment')
sub_body["device"] = self.device element = self._set_xml_etree_element(element, self._obj_to_dict())
sub_body = self._remove_empty_values(sub_body) return ElementTree.tostring(element)
body = {"volumeAttachment": sub_body}
body = self._remove_empty_values(body) def _obj_to_dict(self):
return json.dumps(body) 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