1# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. 2# All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may 5# not use this file except in compliance with the License. You may obtain 6# a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations 14# under the License. 15 16""" 17Handles all requests relating to transferring ownership of volumes. 18""" 19 20 21import hashlib 22import hmac 23import os 24 25from oslo_config import cfg 26from oslo_log import log as logging 27from oslo_utils import excutils 28import six 29 30from cinder.db import base 31from cinder import exception 32from cinder.i18n import _ 33from cinder import objects 34from cinder.policies import volume_transfer as policy 35from cinder import quota 36from cinder import quota_utils 37from cinder.volume import api as volume_api 38from cinder.volume import utils as volume_utils 39 40 41volume_transfer_opts = [ 42 cfg.IntOpt('volume_transfer_salt_length', default=8, 43 help='The number of characters in the salt.'), 44 cfg.IntOpt('volume_transfer_key_length', default=16, 45 help='The number of characters in the ' 46 'autogenerated auth key.'), ] 47 48CONF = cfg.CONF 49CONF.register_opts(volume_transfer_opts) 50 51LOG = logging.getLogger(__name__) 52QUOTAS = quota.QUOTAS 53 54 55class API(base.Base): 56 """API for interacting volume transfers.""" 57 58 def __init__(self, db_driver=None): 59 self.volume_api = volume_api.API() 60 super(API, self).__init__(db_driver) 61 62 def get(self, context, transfer_id): 63 context.authorize(policy.GET_POLICY) 64 rv = self.db.transfer_get(context, transfer_id) 65 return dict(rv) 66 67 def delete(self, context, transfer_id): 68 """Make the RPC call to delete a volume transfer.""" 69 transfer = self.db.transfer_get(context, transfer_id) 70 71 volume_ref = self.db.volume_get(context, transfer.volume_id) 72 context.authorize(policy.DELETE_POLICY, target_obj=volume_ref) 73 volume_utils.notify_about_volume_usage(context, volume_ref, 74 "transfer.delete.start") 75 if volume_ref['status'] != 'awaiting-transfer': 76 LOG.error("Volume in unexpected state") 77 self.db.transfer_destroy(context, transfer_id) 78 volume_utils.notify_about_volume_usage(context, volume_ref, 79 "transfer.delete.end") 80 81 def get_all(self, context, filters=None): 82 filters = filters or {} 83 context.authorize(policy.GET_ALL_POLICY) 84 if context.is_admin and 'all_tenants' in filters: 85 transfers = self.db.transfer_get_all(context) 86 else: 87 transfers = self.db.transfer_get_all_by_project(context, 88 context.project_id) 89 return transfers 90 91 def _get_random_string(self, length): 92 """Get a random hex string of the specified length.""" 93 rndstr = "" 94 95 # Note that the string returned by this function must contain only 96 # characters that the recipient can enter on their keyboard. The 97 # function ssh224().hexdigit() achieves this by generating a hash 98 # which will only contain hexadecimal digits. 99 while len(rndstr) < length: 100 rndstr += hashlib.sha224(os.urandom(255)).hexdigest() 101 102 return rndstr[0:length] 103 104 def _get_crypt_hash(self, salt, auth_key): 105 """Generate a random hash based on the salt and the auth key.""" 106 if not isinstance(salt, (six.binary_type, six.text_type)): 107 salt = str(salt) 108 if isinstance(salt, six.text_type): 109 salt = salt.encode('utf-8') 110 if not isinstance(auth_key, (six.binary_type, six.text_type)): 111 auth_key = str(auth_key) 112 if isinstance(auth_key, six.text_type): 113 auth_key = auth_key.encode('utf-8') 114 return hmac.new(salt, auth_key, hashlib.sha1).hexdigest() 115 116 def create(self, context, volume_id, display_name): 117 """Creates an entry in the transfers table.""" 118 LOG.info("Generating transfer record for volume %s", volume_id) 119 volume_ref = self.db.volume_get(context, volume_id) 120 context.authorize(policy.CREATE_POLICY, target_obj=volume_ref) 121 if volume_ref['status'] != "available": 122 raise exception.InvalidVolume(reason=_("status must be available")) 123 if volume_ref['encryption_key_id'] is not None: 124 raise exception.InvalidVolume( 125 reason=_("transferring encrypted volume is not supported")) 126 127 volume_utils.notify_about_volume_usage(context, volume_ref, 128 "transfer.create.start") 129 # The salt is just a short random string. 130 salt = self._get_random_string(CONF.volume_transfer_salt_length) 131 auth_key = self._get_random_string(CONF.volume_transfer_key_length) 132 crypt_hash = self._get_crypt_hash(salt, auth_key) 133 134 # TODO(ollie): Transfer expiry needs to be implemented. 135 transfer_rec = {'volume_id': volume_id, 136 'display_name': display_name, 137 'salt': salt, 138 'crypt_hash': crypt_hash, 139 'expires_at': None} 140 141 try: 142 transfer = self.db.transfer_create(context, transfer_rec) 143 except Exception: 144 LOG.error("Failed to create transfer record for %s", volume_id) 145 raise 146 volume_utils.notify_about_volume_usage(context, volume_ref, 147 "transfer.create.end") 148 return {'id': transfer['id'], 149 'volume_id': transfer['volume_id'], 150 'display_name': transfer['display_name'], 151 'auth_key': auth_key, 152 'created_at': transfer['created_at']} 153 154 def accept(self, context, transfer_id, auth_key): 155 """Accept a volume that has been offered for transfer.""" 156 # We must use an elevated context to see the volume that is still 157 # owned by the donor. 158 context.authorize(policy.ACCEPT_POLICY) 159 transfer = self.db.transfer_get(context.elevated(), transfer_id) 160 161 crypt_hash = self._get_crypt_hash(transfer['salt'], auth_key) 162 if crypt_hash != transfer['crypt_hash']: 163 msg = (_("Attempt to transfer %s with invalid auth key.") % 164 transfer_id) 165 LOG.error(msg) 166 raise exception.InvalidAuthKey(reason=msg) 167 168 volume_id = transfer['volume_id'] 169 vol_ref = objects.Volume.get_by_id(context.elevated(), volume_id) 170 if vol_ref['consistencygroup_id']: 171 msg = _("Volume %s must not be part of a consistency " 172 "group.") % vol_ref['id'] 173 LOG.error(msg) 174 raise exception.InvalidVolume(reason=msg) 175 176 try: 177 values = {'per_volume_gigabytes': vol_ref.size} 178 QUOTAS.limit_check(context, project_id=context.project_id, 179 **values) 180 except exception.OverQuota as e: 181 quotas = e.kwargs['quotas'] 182 raise exception.VolumeSizeExceedsLimit( 183 size=vol_ref.size, limit=quotas['per_volume_gigabytes']) 184 185 try: 186 reserve_opts = {'volumes': 1, 'gigabytes': vol_ref.size} 187 QUOTAS.add_volume_type_opts(context, 188 reserve_opts, 189 vol_ref.volume_type_id) 190 reservations = QUOTAS.reserve(context, **reserve_opts) 191 except exception.OverQuota as e: 192 quota_utils.process_reserve_over_quota(context, e, 193 resource='volumes', 194 size=vol_ref.size) 195 try: 196 donor_id = vol_ref['project_id'] 197 reserve_opts = {'volumes': -1, 'gigabytes': -vol_ref.size} 198 QUOTAS.add_volume_type_opts(context, 199 reserve_opts, 200 vol_ref.volume_type_id) 201 donor_reservations = QUOTAS.reserve(context.elevated(), 202 project_id=donor_id, 203 **reserve_opts) 204 except Exception: 205 donor_reservations = None 206 LOG.exception("Failed to update quota donating volume" 207 " transfer id %s", transfer_id) 208 209 volume_utils.notify_about_volume_usage(context, vol_ref, 210 "transfer.accept.start") 211 try: 212 # Transfer ownership of the volume now, must use an elevated 213 # context. 214 self.volume_api.accept_transfer(context, 215 vol_ref, 216 context.user_id, 217 context.project_id) 218 self.db.transfer_accept(context.elevated(), 219 transfer_id, 220 context.user_id, 221 context.project_id) 222 QUOTAS.commit(context, reservations) 223 if donor_reservations: 224 QUOTAS.commit(context, donor_reservations, project_id=donor_id) 225 LOG.info("Volume %s has been transferred.", volume_id) 226 except Exception: 227 with excutils.save_and_reraise_exception(): 228 QUOTAS.rollback(context, reservations) 229 if donor_reservations: 230 QUOTAS.rollback(context, donor_reservations, 231 project_id=donor_id) 232 233 vol_ref = self.db.volume_get(context, volume_id) 234 volume_utils.notify_about_volume_usage(context, vol_ref, 235 "transfer.accept.end") 236 return {'id': transfer_id, 237 'display_name': transfer['display_name'], 238 'volume_id': vol_ref['id']} 239