
This feature changes Cinder to transfer snapshots with volumes at the same time by default. If user doesn't want to transfer snapshots, they could use a new optional argument '--no-snapshots' after microversion 3.55. And we also introduce the new V3 api 'v3/volume_transfers' to move this API out of contrib into Cinder V3 API. The cinderclient patch: https://review.openstack.org/#/c/577611/ Change-Id: If848d131e5edcdb77d0b3c2ca45a99c4d5e14d1e Implements: blueprint transfer-snps-with-vols
309 lines
13 KiB
Python
309 lines
13 KiB
Python
# Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
|
|
# All Rights Reserved.
|
|
#
|
|
# 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.
|
|
|
|
"""
|
|
Handles all requests relating to transferring ownership of volumes.
|
|
"""
|
|
|
|
|
|
import hashlib
|
|
import hmac
|
|
import os
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import excutils
|
|
import six
|
|
|
|
from cinder.db import base
|
|
from cinder import exception
|
|
from cinder.i18n import _
|
|
from cinder import objects
|
|
from cinder.policies import volume_transfer as policy
|
|
from cinder import quota
|
|
from cinder import quota_utils
|
|
from cinder.volume import api as volume_api
|
|
from cinder.volume import utils as volume_utils
|
|
|
|
|
|
volume_transfer_opts = [
|
|
cfg.IntOpt('volume_transfer_salt_length', default=8,
|
|
help='The number of characters in the salt.'),
|
|
cfg.IntOpt('volume_transfer_key_length', default=16,
|
|
help='The number of characters in the '
|
|
'autogenerated auth key.'), ]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(volume_transfer_opts)
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
QUOTAS = quota.QUOTAS
|
|
|
|
|
|
class API(base.Base):
|
|
"""API for interacting volume transfers."""
|
|
|
|
def __init__(self, db_driver=None):
|
|
self.volume_api = volume_api.API()
|
|
super(API, self).__init__(db_driver)
|
|
|
|
def get(self, context, transfer_id):
|
|
context.authorize(policy.GET_POLICY)
|
|
rv = self.db.transfer_get(context, transfer_id)
|
|
return dict(rv)
|
|
|
|
def delete(self, context, transfer_id):
|
|
"""Make the RPC call to delete a volume transfer."""
|
|
transfer = self.db.transfer_get(context, transfer_id)
|
|
|
|
volume_ref = self.db.volume_get(context, transfer.volume_id)
|
|
context.authorize(policy.DELETE_POLICY, target_obj=volume_ref)
|
|
volume_utils.notify_about_volume_usage(context, volume_ref,
|
|
"transfer.delete.start")
|
|
if volume_ref['status'] != 'awaiting-transfer':
|
|
LOG.error("Volume in unexpected state")
|
|
self.db.transfer_destroy(context, transfer_id)
|
|
volume_utils.notify_about_volume_usage(context, volume_ref,
|
|
"transfer.delete.end")
|
|
|
|
def get_all(self, context, filters=None):
|
|
filters = filters or {}
|
|
context.authorize(policy.GET_ALL_POLICY)
|
|
if context.is_admin and 'all_tenants' in filters:
|
|
transfers = self.db.transfer_get_all(context)
|
|
else:
|
|
transfers = self.db.transfer_get_all_by_project(context,
|
|
context.project_id)
|
|
return transfers
|
|
|
|
def _get_random_string(self, length):
|
|
"""Get a random hex string of the specified length."""
|
|
rndstr = ""
|
|
|
|
# Note that the string returned by this function must contain only
|
|
# characters that the recipient can enter on their keyboard. The
|
|
# function ssh224().hexdigit() achieves this by generating a hash
|
|
# which will only contain hexadecimal digits.
|
|
while len(rndstr) < length:
|
|
rndstr += hashlib.sha224(os.urandom(255)).hexdigest()
|
|
|
|
return rndstr[0:length]
|
|
|
|
def _get_crypt_hash(self, salt, auth_key):
|
|
"""Generate a random hash based on the salt and the auth key."""
|
|
if not isinstance(salt, (six.binary_type, six.text_type)):
|
|
salt = str(salt)
|
|
if isinstance(salt, six.text_type):
|
|
salt = salt.encode('utf-8')
|
|
if not isinstance(auth_key, (six.binary_type, six.text_type)):
|
|
auth_key = str(auth_key)
|
|
if isinstance(auth_key, six.text_type):
|
|
auth_key = auth_key.encode('utf-8')
|
|
return hmac.new(salt, auth_key, hashlib.sha1).hexdigest()
|
|
|
|
def create(self, context, volume_id, display_name, no_snapshots=False):
|
|
"""Creates an entry in the transfers table."""
|
|
LOG.info("Generating transfer record for volume %s", volume_id)
|
|
volume_ref = self.db.volume_get(context, volume_id)
|
|
context.authorize(policy.CREATE_POLICY, target_obj=volume_ref)
|
|
if volume_ref['status'] != "available":
|
|
raise exception.InvalidVolume(reason=_("status must be available"))
|
|
if volume_ref['encryption_key_id'] is not None:
|
|
raise exception.InvalidVolume(
|
|
reason=_("transferring encrypted volume is not supported"))
|
|
|
|
if not no_snapshots:
|
|
snapshots = self.db.snapshot_get_all_for_volume(context, volume_id)
|
|
for snapshot in snapshots:
|
|
if snapshot['status'] != "available":
|
|
msg = _("snapshot: %s status must be "
|
|
"available") % snapshot['id']
|
|
raise exception.InvalidSnapshot(reason=msg)
|
|
if snapshot.get('encryption_key_id'):
|
|
msg = _("snapshot: %s encrypted snapshots cannot be "
|
|
"transferred") % snapshot['id']
|
|
raise exception.InvalidSnapshot(reason=msg)
|
|
|
|
volume_utils.notify_about_volume_usage(context, volume_ref,
|
|
"transfer.create.start")
|
|
# The salt is just a short random string.
|
|
salt = self._get_random_string(CONF.volume_transfer_salt_length)
|
|
auth_key = self._get_random_string(CONF.volume_transfer_key_length)
|
|
crypt_hash = self._get_crypt_hash(salt, auth_key)
|
|
|
|
# TODO(ollie): Transfer expiry needs to be implemented.
|
|
transfer_rec = {'volume_id': volume_id,
|
|
'display_name': display_name,
|
|
'salt': salt,
|
|
'crypt_hash': crypt_hash,
|
|
'expires_at': None,
|
|
'no_snapshots': no_snapshots}
|
|
|
|
try:
|
|
transfer = self.db.transfer_create(context, transfer_rec)
|
|
except Exception:
|
|
LOG.error("Failed to create transfer record for %s", volume_id)
|
|
raise
|
|
volume_utils.notify_about_volume_usage(context, volume_ref,
|
|
"transfer.create.end")
|
|
return {'id': transfer['id'],
|
|
'volume_id': transfer['volume_id'],
|
|
'display_name': transfer['display_name'],
|
|
'auth_key': auth_key,
|
|
'created_at': transfer['created_at'],
|
|
'no_snapshots': transfer['no_snapshots']}
|
|
|
|
def _handle_snapshot_quota(self, context, snapshots, volume_type_id,
|
|
donor_id):
|
|
snapshots_num = len(snapshots)
|
|
volume_sizes = 0
|
|
if not CONF.no_snapshot_gb_quota:
|
|
for snapshot in snapshots:
|
|
volume_sizes += snapshot.volume_size
|
|
try:
|
|
reserve_opts = {'snapshots': snapshots_num,
|
|
'gigabytes': volume_sizes}
|
|
QUOTAS.add_volume_type_opts(context,
|
|
reserve_opts,
|
|
volume_type_id)
|
|
reservations = QUOTAS.reserve(context, **reserve_opts)
|
|
except exception.OverQuota as e:
|
|
quota_utils.process_reserve_over_quota(
|
|
context, e,
|
|
resource='snapshots',
|
|
size=volume_sizes)
|
|
|
|
try:
|
|
reserve_opts = {'snapshots': -snapshots_num,
|
|
'gigabytes': -volume_sizes}
|
|
QUOTAS.add_volume_type_opts(context.elevated(),
|
|
reserve_opts,
|
|
volume_type_id)
|
|
donor_reservations = QUOTAS.reserve(context,
|
|
project_id=donor_id,
|
|
**reserve_opts)
|
|
except exception.OverQuota as e:
|
|
donor_reservations = None
|
|
LOG.exception("Failed to update volume providing snapshots quota:"
|
|
" Over quota.")
|
|
|
|
return reservations, donor_reservations
|
|
|
|
def accept(self, context, transfer_id, auth_key):
|
|
"""Accept a volume that has been offered for transfer."""
|
|
# We must use an elevated context to see the volume that is still
|
|
# owned by the donor.
|
|
context.authorize(policy.ACCEPT_POLICY)
|
|
transfer = self.db.transfer_get(context.elevated(), transfer_id)
|
|
|
|
crypt_hash = self._get_crypt_hash(transfer['salt'], auth_key)
|
|
if crypt_hash != transfer['crypt_hash']:
|
|
msg = (_("Attempt to transfer %s with invalid auth key.") %
|
|
transfer_id)
|
|
LOG.error(msg)
|
|
raise exception.InvalidAuthKey(reason=msg)
|
|
|
|
volume_id = transfer['volume_id']
|
|
vol_ref = objects.Volume.get_by_id(context.elevated(), volume_id)
|
|
if vol_ref['consistencygroup_id']:
|
|
msg = _("Volume %s must not be part of a consistency "
|
|
"group.") % vol_ref['id']
|
|
LOG.error(msg)
|
|
raise exception.InvalidVolume(reason=msg)
|
|
|
|
try:
|
|
values = {'per_volume_gigabytes': vol_ref.size}
|
|
QUOTAS.limit_check(context, project_id=context.project_id,
|
|
**values)
|
|
except exception.OverQuota as e:
|
|
quotas = e.kwargs['quotas']
|
|
raise exception.VolumeSizeExceedsLimit(
|
|
size=vol_ref.size, limit=quotas['per_volume_gigabytes'])
|
|
|
|
try:
|
|
reserve_opts = {'volumes': 1, 'gigabytes': vol_ref.size}
|
|
QUOTAS.add_volume_type_opts(context,
|
|
reserve_opts,
|
|
vol_ref.volume_type_id)
|
|
reservations = QUOTAS.reserve(context, **reserve_opts)
|
|
except exception.OverQuota as e:
|
|
quota_utils.process_reserve_over_quota(context, e,
|
|
resource='volumes',
|
|
size=vol_ref.size)
|
|
try:
|
|
donor_id = vol_ref['project_id']
|
|
reserve_opts = {'volumes': -1, 'gigabytes': -vol_ref.size}
|
|
QUOTAS.add_volume_type_opts(context,
|
|
reserve_opts,
|
|
vol_ref.volume_type_id)
|
|
donor_reservations = QUOTAS.reserve(context.elevated(),
|
|
project_id=donor_id,
|
|
**reserve_opts)
|
|
except Exception:
|
|
donor_reservations = None
|
|
LOG.exception("Failed to update quota donating volume"
|
|
" transfer id %s", transfer_id)
|
|
|
|
snap_res = None
|
|
snap_donor_res = None
|
|
if transfer['no_snapshots'] is False:
|
|
snapshots = objects.SnapshotList.get_all_for_volume(
|
|
context.elevated(), volume_id)
|
|
volume_type_id = vol_ref.volume_type_id
|
|
snap_res, snap_donor_res = self._handle_snapshot_quota(
|
|
context, snapshots, volume_type_id, vol_ref['project_id'])
|
|
|
|
volume_utils.notify_about_volume_usage(context, vol_ref,
|
|
"transfer.accept.start")
|
|
try:
|
|
# Transfer ownership of the volume now, must use an elevated
|
|
# context.
|
|
self.volume_api.accept_transfer(context,
|
|
vol_ref,
|
|
context.user_id,
|
|
context.project_id,
|
|
transfer['no_snapshots'])
|
|
self.db.transfer_accept(context.elevated(),
|
|
transfer_id,
|
|
context.user_id,
|
|
context.project_id,
|
|
transfer['no_snapshots'])
|
|
QUOTAS.commit(context, reservations)
|
|
if snap_res:
|
|
QUOTAS.commit(context, snap_res)
|
|
if donor_reservations:
|
|
QUOTAS.commit(context, donor_reservations, project_id=donor_id)
|
|
if snap_donor_res:
|
|
QUOTAS.commit(context, snap_donor_res, project_id=donor_id)
|
|
LOG.info("Volume %s has been transferred.", volume_id)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
QUOTAS.rollback(context, reservations)
|
|
if snap_res:
|
|
QUOTAS.rollback(context, snap_res)
|
|
if donor_reservations:
|
|
QUOTAS.rollback(context, donor_reservations,
|
|
project_id=donor_id)
|
|
if snap_donor_res:
|
|
QUOTAS.rollback(context, snap_donor_res,
|
|
project_id=donor_id)
|
|
|
|
vol_ref = self.db.volume_get(context, volume_id)
|
|
volume_utils.notify_about_volume_usage(context, vol_ref,
|
|
"transfer.accept.end")
|
|
return {'id': transfer_id,
|
|
'display_name': transfer['display_name'],
|
|
'volume_id': vol_ref['id']}
|