Merge "Volume Attachments API Buildout"
This commit is contained in:
@@ -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 = (
|
||||
|
||||
@@ -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,
|
||||
|
||||
96
cloudcafe/compute/volume_attachments_api/behaviors.py
Normal file
96
cloudcafe/compute/volume_attachments_api/behaviors.py
Normal 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
|
||||
@@ -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)
|
||||
37
cloudcafe/compute/volume_attachments_api/config.py
Normal file
37
cloudcafe/compute/volume_attachments_api/config.py
Normal 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)
|
||||
@@ -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}
|
||||
@@ -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.
|
||||
"""
|
||||
68
cloudcafe/compute/volume_attachments_api/models/responses.py
Normal file
68
cloudcafe/compute/volume_attachments_api/models/responses.py
Normal 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])
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user