heat/heat/engine/resources/openstack/cinder/volume.py

603 lines
24 KiB
Python

#
# 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 oslo_log import log as logging
from oslo_serialization import jsonutils
import six
from heat.common import exception
from heat.common.i18n import _
from heat.common.i18n import _LI
from heat.engine import attributes
from heat.engine.clients.os import cinder as heat_cinder
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine.resources import volume_base as vb
from heat.engine import support
LOG = logging.getLogger(__name__)
class CinderVolume(vb.BaseVolume):
PROPERTIES = (
AVAILABILITY_ZONE, SIZE, SNAPSHOT_ID, BACKUP_ID, NAME,
DESCRIPTION, VOLUME_TYPE, METADATA, IMAGE_REF, IMAGE,
SOURCE_VOLID, CINDER_SCHEDULER_HINTS, READ_ONLY,
) = (
'availability_zone', 'size', 'snapshot_id', 'backup_id', 'name',
'description', 'volume_type', 'metadata', 'imageRef', 'image',
'source_volid', 'scheduler_hints', 'read_only',
)
ATTRIBUTES = (
AVAILABILITY_ZONE_ATTR, SIZE_ATTR, SNAPSHOT_ID_ATTR, DISPLAY_NAME_ATTR,
DISPLAY_DESCRIPTION_ATTR, VOLUME_TYPE_ATTR, METADATA_ATTR,
SOURCE_VOLID_ATTR, STATUS, CREATED_AT, BOOTABLE, METADATA_VALUES_ATTR,
ENCRYPTED_ATTR, ATTACHMENTS,
) = (
'availability_zone', 'size', 'snapshot_id', 'display_name',
'display_description', 'volume_type', 'metadata',
'source_volid', 'status', 'created_at', 'bootable', 'metadata_values',
'encrypted', 'attachments',
)
properties_schema = {
AVAILABILITY_ZONE: properties.Schema(
properties.Schema.STRING,
_('The availability zone in which the volume will be created.')
),
SIZE: properties.Schema(
properties.Schema.INTEGER,
_('The size of the volume in GB. '
'On update only increase in size is supported.'),
update_allowed=True,
constraints=[
constraints.Range(min=1),
]
),
SNAPSHOT_ID: properties.Schema(
properties.Schema.STRING,
_('If specified, the snapshot to create the volume from.'),
constraints=[
constraints.CustomConstraint('cinder.snapshot')
]
),
BACKUP_ID: properties.Schema(
properties.Schema.STRING,
_('If specified, the backup to create the volume from.')
),
NAME: properties.Schema(
properties.Schema.STRING,
_('A name used to distinguish the volume.'),
update_allowed=True,
),
DESCRIPTION: properties.Schema(
properties.Schema.STRING,
_('A description of the volume.'),
update_allowed=True,
),
VOLUME_TYPE: properties.Schema(
properties.Schema.STRING,
_('If specified, the type of volume to use, mapping to a '
'specific backend.'),
constraints=[
constraints.CustomConstraint('cinder.vtype')
],
update_allowed=True
),
METADATA: properties.Schema(
properties.Schema.MAP,
_('Key/value pairs to associate with the volume.'),
update_allowed=True,
),
IMAGE_REF: properties.Schema(
properties.Schema.STRING,
_('The ID of the image to create the volume from.'),
support_status=support.SupportStatus(
status=support.DEPRECATED,
message=_('Use property %s.') % IMAGE,
version='2014.1')
),
IMAGE: properties.Schema(
properties.Schema.STRING,
_('If specified, the name or ID of the image to create the '
'volume from.'),
constraints=[
constraints.CustomConstraint('glance.image')
]
),
SOURCE_VOLID: properties.Schema(
properties.Schema.STRING,
_('If specified, the volume to use as source.'),
constraints=[
constraints.CustomConstraint('cinder.volume')
]
),
CINDER_SCHEDULER_HINTS: properties.Schema(
properties.Schema.MAP,
_('Arbitrary key-value pairs specified by the client to help '
'the Cinder scheduler creating a volume.'),
support_status=support.SupportStatus(version='2015.1')
),
READ_ONLY: properties.Schema(
properties.Schema.BOOLEAN,
_('Enables or disables read-only access mode of volume.'),
support_status=support.SupportStatus(version='5.0.0'),
update_allowed=True,
),
}
attributes_schema = {
AVAILABILITY_ZONE_ATTR: attributes.Schema(
_('The availability zone in which the volume is located.'),
type=attributes.Schema.STRING
),
SIZE_ATTR: attributes.Schema(
_('The size of the volume in GB.'),
type=attributes.Schema.STRING
),
SNAPSHOT_ID_ATTR: attributes.Schema(
_('The snapshot the volume was created from, if any.'),
type=attributes.Schema.STRING
),
DISPLAY_NAME_ATTR: attributes.Schema(
_('Name of the volume.'),
type=attributes.Schema.STRING
),
DISPLAY_DESCRIPTION_ATTR: attributes.Schema(
_('Description of the volume.'),
type=attributes.Schema.STRING
),
VOLUME_TYPE_ATTR: attributes.Schema(
_('The type of the volume mapping to a backend, if any.'),
type=attributes.Schema.STRING
),
METADATA_ATTR: attributes.Schema(
_('Key/value pairs associated with the volume.'),
type=attributes.Schema.STRING
),
SOURCE_VOLID_ATTR: attributes.Schema(
_('The volume used as source, if any.'),
type=attributes.Schema.STRING
),
STATUS: attributes.Schema(
_('The current status of the volume.'),
type=attributes.Schema.STRING
),
CREATED_AT: attributes.Schema(
_('The timestamp indicating volume creation.'),
type=attributes.Schema.STRING
),
BOOTABLE: attributes.Schema(
_('Boolean indicating if the volume can be booted or not.'),
type=attributes.Schema.STRING
),
METADATA_VALUES_ATTR: attributes.Schema(
_('Key/value pairs associated with the volume in raw dict form.'),
type=attributes.Schema.MAP
),
ENCRYPTED_ATTR: attributes.Schema(
_('Boolean indicating if the volume is encrypted or not.'),
type=attributes.Schema.STRING
),
ATTACHMENTS: attributes.Schema(
_('The list of attachments of the volume.'),
type=attributes.Schema.STRING
),
}
_volume_creating_status = ['creating', 'restoring-backup', 'downloading']
def _name(self):
name = self.properties[self.NAME]
if name:
return name
return super(CinderVolume, self)._name()
def _description(self):
return self.properties[self.DESCRIPTION]
def _create_arguments(self):
arguments = {
'size': self.properties[self.SIZE],
'availability_zone': self.properties[self.AVAILABILITY_ZONE]
}
if self.properties[self.IMAGE]:
arguments['imageRef'] = self.client_plugin('glance').get_image_id(
self.properties[self.IMAGE])
elif self.properties[self.IMAGE_REF]:
arguments['imageRef'] = self.properties[self.IMAGE_REF]
optionals = (self.SNAPSHOT_ID, self.VOLUME_TYPE, self.SOURCE_VOLID,
self.METADATA, self.CINDER_SCHEDULER_HINTS)
arguments.update((prop, self.properties[prop]) for prop in optionals
if self.properties[prop])
return arguments
def _resolve_attribute(self, name):
cinder = self.client()
vol = cinder.volumes.get(self.resource_id)
if name == self.METADATA_ATTR:
return six.text_type(jsonutils.dumps(vol.metadata))
elif name == self.METADATA_VALUES_ATTR:
return vol.metadata
if cinder.volume_api_version >= 2:
if name == self.DISPLAY_NAME_ATTR:
return vol.name
elif name == self.DISPLAY_DESCRIPTION_ATTR:
return vol.description
return six.text_type(getattr(vol, name))
def handle_create(self):
vol_id = super(CinderVolume, self).handle_create()
read_only_flag = self.properties.get(self.READ_ONLY)
if read_only_flag is not None:
self.client().volumes.update_readonly_flag(vol_id,
read_only_flag)
return vol_id
def _extend_volume(self, new_size):
try:
self.client().volumes.extend(self.resource_id, new_size)
except Exception as ex:
if self.client_plugin().is_client_exception(ex):
raise exception.Error(_(
"Failed to extend volume %(vol)s - %(err)s") % {
'vol': self.resource_id, 'err': str(ex)})
else:
raise
return True
def _check_extend_volume_complete(self):
vol = self.client().volumes.get(self.resource_id)
if vol.status == 'extending':
LOG.debug("Volume %s is being extended" % vol.id)
return False
if vol.status != 'available':
LOG.info(_LI("Resize failed: Volume %(vol)s "
"is in %(status)s state."),
{'vol': vol.id, 'status': vol.status})
raise resource.ResourceUnknownStatus(
resource_status=vol.status,
result=_('Volume resize failed'))
LOG.info(_LI('Volume %(id)s resize complete'), {'id': vol.id})
return True
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
vol = None
cinder = self.client()
prg_resize = None
prg_attach = None
prg_detach = None
# update the name and description for cinder volume
if self.NAME in prop_diff or self.DESCRIPTION in prop_diff:
vol = cinder.volumes.get(self.resource_id)
update_name = (prop_diff.get(self.NAME) or
self.properties[self.NAME])
update_description = (prop_diff.get(self.DESCRIPTION) or
self.properties[self.DESCRIPTION])
kwargs = self._fetch_name_and_description(
cinder.volume_api_version, update_name, update_description)
cinder.volumes.update(vol, **kwargs)
# update the metadata for cinder volume
if self.METADATA in prop_diff:
if not vol:
vol = cinder.volumes.get(self.resource_id)
metadata = prop_diff.get(self.METADATA)
cinder.volumes.update_all_metadata(vol, metadata)
# retype
if self.VOLUME_TYPE in prop_diff:
if cinder.volume_api_version == 1:
LOG.info(_LI('Volume type update not supported '
'by Cinder API V1.'))
raise exception.NotSupported(
feature=_('Using Cinder API V1, volume_type update'))
else:
if not vol:
vol = cinder.volumes.get(self.resource_id)
new_vol_type = prop_diff.get(self.VOLUME_TYPE)
cinder.volumes.retype(vol, new_vol_type, 'never')
# update read_only access mode
if self.READ_ONLY in prop_diff:
flag = prop_diff.get(self.READ_ONLY)
cinder.volumes.update_readonly_flag(self.resource_id, flag)
# extend volume size
if self.SIZE in prop_diff:
if not vol:
vol = cinder.volumes.get(self.resource_id)
new_size = prop_diff[self.SIZE]
if new_size < vol.size:
raise exception.NotSupported(feature=_("Shrinking volume"))
elif new_size > vol.size:
prg_resize = heat_cinder.VolumeResizeProgress(size=new_size)
if vol.attachments:
# NOTE(pshchelo):
# this relies on current behavior of cinder attachments,
# i.e. volume attachments is a list with len<=1,
# so the volume can be attached only to single instance,
# and id of attachment is the same as id of the volume
# it describes, so detach/attach the same volume
# will not change volume attachment id.
server_id = vol.attachments[0]['server_id']
device = vol.attachments[0]['device']
attachment_id = vol.attachments[0]['id']
prg_detach = heat_cinder.VolumeDetachProgress(
server_id, vol.id, attachment_id)
prg_attach = heat_cinder.VolumeAttachProgress(
server_id, vol.id, device)
return prg_detach, prg_resize, prg_attach
def _detach_volume_to_complete(self, prg_detach):
if not prg_detach.called:
self.client_plugin('nova').detach_volume(prg_detach.srv_id,
prg_detach.attach_id)
prg_detach.called = True
return False
if not prg_detach.cinder_complete:
cinder_complete_res = self.client_plugin(
).check_detach_volume_complete(prg_detach.vol_id)
prg_detach.cinder_complete = cinder_complete_res
return False
if not prg_detach.nova_complete:
prg_detach.nova_complete = self.client_plugin(
'nova').check_detach_volume_complete(prg_detach.srv_id,
prg_detach.attach_id)
return False
def _attach_volume_to_complete(self, prg_attach):
if not prg_attach.called:
prg_attach.called = self.client_plugin('nova').attach_volume(
prg_attach.srv_id, prg_attach.vol_id, prg_attach.device)
return False
if not prg_attach.complete:
prg_attach.complete = self.client_plugin(
).check_attach_volume_complete(prg_attach.vol_id)
return prg_attach.complete
def check_update_complete(self, checkers):
prg_detach, prg_resize, prg_attach = checkers
if not prg_resize:
return True
# detach volume
if prg_detach:
if not prg_detach.nova_complete:
self._detach_volume_to_complete(prg_detach)
return False
# resize volume
if not prg_resize.called:
prg_resize.called = self._extend_volume(prg_resize.size)
return False
if not prg_resize.complete:
prg_resize.complete = self._check_extend_volume_complete()
return prg_resize.complete and not prg_attach
# reattach volume back
if prg_attach:
return self._attach_volume_to_complete(prg_attach)
return True
def handle_snapshot(self):
backup = self.client().backups.create(self.resource_id)
self.data_set('backup_id', backup.id)
return backup.id
def check_snapshot_complete(self, backup_id):
backup = self.client().backups.get(backup_id)
if backup.status == 'creating':
return False
if backup.status == 'available':
return True
raise exception.Error(backup.fail_reason)
def handle_delete_snapshot(self, snapshot):
backup_id = snapshot['resource_data'].get('backup_id')
if not backup_id:
return
try:
self.client().backups.delete(backup_id)
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
return
else:
return backup_id
def check_delete_snapshot_complete(self, backup_id):
if not backup_id:
return True
try:
self.client().backups.get(backup_id)
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
return True
else:
return False
def _build_exclusive_options(self):
exclusive_options = []
if self.properties.get(self.SNAPSHOT_ID):
exclusive_options.append(self.SNAPSHOT_ID)
if self.properties.get(self.SOURCE_VOLID):
exclusive_options.append(self.SOURCE_VOLID)
if self.properties.get(self.IMAGE):
exclusive_options.append(self.IMAGE)
if self.properties.get(self.IMAGE_REF):
exclusive_options.append(self.IMAGE_REF)
return exclusive_options
def _validate_create_sources(self):
exclusive_options = self._build_exclusive_options()
size = self.properties.get(self.SIZE)
if size is None and len(exclusive_options) != 1:
msg = (_('If neither "%(backup_id)s" nor "%(size)s" is '
'provided, one and only one of '
'"%(image)s", "%(image_ref)s", "%(source_vol)s", '
'"%(snapshot_id)s" must be specified, but currently '
'specified options: %(exclusive_options)s.')
% {'backup_id': self.BACKUP_ID,
'size': self.SIZE,
'image': self.IMAGE,
'image_ref': self.IMAGE_REF,
'source_vol': self.SOURCE_VOLID,
'snapshot_id': self.SNAPSHOT_ID,
'exclusive_options': exclusive_options})
raise exception.StackValidationFailed(message=msg)
elif size and len(exclusive_options) > 1:
msg = (_('If "%(size)s" is provided, only one of '
'"%(image)s", "%(image_ref)s", "%(source_vol)s", '
'"%(snapshot_id)s" can be specified, but currently '
'specified options: %(exclusive_options)s.')
% {'size': self.SIZE,
'image': self.IMAGE,
'image_ref': self.IMAGE_REF,
'source_vol': self.SOURCE_VOLID,
'snapshot_id': self.SNAPSHOT_ID,
'exclusive_options': exclusive_options})
raise exception.StackValidationFailed(message=msg)
def validate(self):
"""Validate provided params."""
res = super(CinderVolume, self).validate()
if res is not None:
return res
# Scheduler hints are only supported from Cinder API v2
if (self.properties[self.CINDER_SCHEDULER_HINTS]
and self.client().volume_api_version == 1):
raise exception.StackValidationFailed(
message=_('Scheduler hints are not supported by the current '
'volume API.'))
# can not specify both image and imageRef
image = self.properties.get(self.IMAGE)
imageRef = self.properties.get(self.IMAGE_REF)
if image and imageRef:
raise exception.ResourcePropertyConflict(self.IMAGE,
self.IMAGE_REF)
# if not create from backup, need to check other create sources
if not self.properties.get(self.BACKUP_ID):
self._validate_create_sources()
def handle_restore(self, defn, restore_data):
backup_id = restore_data['resource_data']['backup_id']
ignore_props = (
self.IMAGE_REF, self.IMAGE, self.SOURCE_VOLID, self.SIZE)
props = dict(
(key, value) for (key, value) in
six.iteritems(defn.properties(self.properties_schema))
if key not in ignore_props and value is not None)
props[self.BACKUP_ID] = backup_id
return defn.freeze(properties=props)
class CinderVolumeAttachment(vb.BaseVolumeAttachment):
PROPERTIES = (
INSTANCE_ID, VOLUME_ID, DEVICE,
) = (
'instance_uuid', 'volume_id', 'mountpoint',
)
properties_schema = {
INSTANCE_ID: properties.Schema(
properties.Schema.STRING,
_('The ID of the server to which the volume attaches.'),
required=True,
update_allowed=True
),
VOLUME_ID: properties.Schema(
properties.Schema.STRING,
_('The ID of the volume to be attached.'),
required=True,
update_allowed=True,
constraints=[
constraints.CustomConstraint('cinder.volume')
]
),
DEVICE: properties.Schema(
properties.Schema.STRING,
_('The location where the volume is exposed on the instance. This '
'assignment may not be honored and it is advised that the path '
'/dev/disk/by-id/virtio-<VolumeId> be used instead.'),
update_allowed=True
),
}
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
prg_attach = None
prg_detach = None
if prop_diff:
# Even though some combinations of changed properties
# could be updated in UpdateReplace manner,
# we still first detach the old resource so that
# self.resource_id is not replaced prematurely
volume_id = self.properties[self.VOLUME_ID]
server_id = self._stored_properties_data.get(self.INSTANCE_ID)
self.client_plugin('nova').detach_volume(server_id,
self.resource_id)
prg_detach = heat_cinder.VolumeDetachProgress(
server_id, volume_id, self.resource_id)
prg_detach.called = True
if self.VOLUME_ID in prop_diff:
volume_id = prop_diff.get(self.VOLUME_ID)
device = self.properties[self.DEVICE]
if self.DEVICE in prop_diff:
device = prop_diff.get(self.DEVICE)
if self.INSTANCE_ID in prop_diff:
server_id = prop_diff.get(self.INSTANCE_ID)
prg_attach = heat_cinder.VolumeAttachProgress(
server_id, volume_id, device)
return prg_detach, prg_attach
def check_update_complete(self, checkers):
prg_detach, prg_attach = checkers
if not (prg_detach and prg_attach):
return True
if not prg_detach.cinder_complete:
prg_detach.cinder_complete = self.client_plugin(
).check_detach_volume_complete(prg_detach.vol_id)
return False
if not prg_detach.nova_complete:
prg_detach.nova_complete = self.client_plugin(
'nova').check_detach_volume_complete(prg_detach.srv_id,
self.resource_id)
return False
if not prg_attach.called:
prg_attach.called = self.client_plugin('nova').attach_volume(
prg_attach.srv_id, prg_attach.vol_id, prg_attach.device)
return False
if not prg_attach.complete:
prg_attach.complete = self.client_plugin(
).check_attach_volume_complete(prg_attach.vol_id)
if prg_attach.complete:
self.resource_id_set(prg_attach.called)
return prg_attach.complete
return True
def resource_mapping():
return {
'OS::Cinder::Volume': CinderVolume,
'OS::Cinder::VolumeAttachment': CinderVolumeAttachment,
}