OpenStack Block Storage (Cinder)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

4973 lines
219KB

  1. # Copyright 2010 United States Government as represented by the
  2. # Administrator of the National Aeronautics and Space Administration.
  3. # All Rights Reserved.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  6. # not use this file except in compliance with the License. You may obtain
  7. # a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  13. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  14. # License for the specific language governing permissions and limitations
  15. # under the License.
  16. """
  17. Volume manager manages creating, attaching, detaching, and persistent storage.
  18. Persistent storage volumes keep their state independent of instances. You can
  19. attach to an instance, terminate the instance, spawn a new instance (even
  20. one from a different image) and re-attach the volume with the same data
  21. intact.
  22. **Related Flags**
  23. :volume_manager: The module name of a class derived from
  24. :class:`manager.Manager` (default:
  25. :class:`cinder.volume.manager.Manager`).
  26. :volume_driver: Used by :class:`Manager`. Defaults to
  27. :class:`cinder.volume.drivers.lvm.LVMVolumeDriver`.
  28. :volume_group: Name of the group that will contain exported volumes (default:
  29. `cinder-volumes`)
  30. :num_shell_tries: Number of times to attempt to run commands (default: 3)
  31. """
  32. import requests
  33. import time
  34. from castellan import key_manager
  35. from oslo_config import cfg
  36. from oslo_log import log as logging
  37. import oslo_messaging as messaging
  38. from oslo_serialization import jsonutils
  39. from oslo_service import periodic_task
  40. from oslo_utils import excutils
  41. from oslo_utils import importutils
  42. from oslo_utils import timeutils
  43. from oslo_utils import units
  44. from oslo_utils import uuidutils
  45. profiler = importutils.try_import('osprofiler.profiler')
  46. import six
  47. from taskflow import exceptions as tfe
  48. from cinder.common import constants
  49. from cinder import compute
  50. from cinder import context
  51. from cinder import coordination
  52. from cinder import db
  53. from cinder import exception
  54. from cinder import flow_utils
  55. from cinder.i18n import _
  56. from cinder.image import cache as image_cache
  57. from cinder.image import glance
  58. from cinder.image import image_utils
  59. from cinder.keymgr import migration as key_migration
  60. from cinder import manager
  61. from cinder.message import api as message_api
  62. from cinder.message import message_field
  63. from cinder import objects
  64. from cinder.objects import cgsnapshot
  65. from cinder.objects import consistencygroup
  66. from cinder.objects import fields
  67. from cinder import quota
  68. from cinder import utils
  69. from cinder import volume as cinder_volume
  70. from cinder.volume import configuration as config
  71. from cinder.volume.flows.manager import create_volume
  72. from cinder.volume.flows.manager import manage_existing
  73. from cinder.volume.flows.manager import manage_existing_snapshot
  74. from cinder.volume import group_types
  75. from cinder.volume import rpcapi as volume_rpcapi
  76. from cinder.volume import utils as vol_utils
  77. from cinder.volume import volume_migration
  78. from cinder.volume import volume_types
  79. LOG = logging.getLogger(__name__)
  80. QUOTAS = quota.QUOTAS
  81. CGQUOTAS = quota.CGQUOTAS
  82. GROUP_QUOTAS = quota.GROUP_QUOTAS
  83. VALID_REMOVE_VOL_FROM_CG_STATUS = (
  84. 'available',
  85. 'in-use',
  86. 'error',
  87. 'error_deleting')
  88. VALID_REMOVE_VOL_FROM_GROUP_STATUS = (
  89. 'available',
  90. 'in-use',
  91. 'error',
  92. 'error_deleting')
  93. VALID_ADD_VOL_TO_CG_STATUS = (
  94. 'available',
  95. 'in-use')
  96. VALID_ADD_VOL_TO_GROUP_STATUS = (
  97. 'available',
  98. 'in-use')
  99. VALID_CREATE_CG_SRC_SNAP_STATUS = (fields.SnapshotStatus.AVAILABLE,)
  100. VALID_CREATE_GROUP_SRC_SNAP_STATUS = (fields.SnapshotStatus.AVAILABLE,)
  101. VALID_CREATE_CG_SRC_CG_STATUS = ('available',)
  102. VALID_CREATE_GROUP_SRC_GROUP_STATUS = ('available',)
  103. VA_LIST = objects.VolumeAttachmentList
  104. volume_manager_opts = [
  105. cfg.IntOpt('migration_create_volume_timeout_secs',
  106. default=300,
  107. help='Timeout for creating the volume to migrate to '
  108. 'when performing volume migration (seconds)'),
  109. cfg.BoolOpt('volume_service_inithost_offload',
  110. default=False,
  111. help='Offload pending volume delete during '
  112. 'volume service startup'),
  113. cfg.StrOpt('zoning_mode',
  114. help="FC Zoning mode configured, only 'fabric' is "
  115. "supported now."),
  116. cfg.IntOpt('reinit_driver_count',
  117. default=3,
  118. help='Maximum times to reintialize the driver '
  119. 'if volume initialization fails. The interval of retry is '
  120. 'exponentially backoff, and will be 1s, 2s, 4s etc.'),
  121. cfg.IntOpt('init_host_max_objects_retrieval',
  122. default=0,
  123. help='Max number of volumes and snapshots to be retrieved '
  124. 'per batch during volume manager host initialization. '
  125. 'Query results will be obtained in batches from the '
  126. 'database and not in one shot to avoid extreme memory '
  127. 'usage. Set 0 to turn off this functionality.'),
  128. ]
  129. volume_backend_opts = [
  130. cfg.StrOpt('volume_driver',
  131. default='cinder.volume.drivers.lvm.LVMVolumeDriver',
  132. help='Driver to use for volume creation'),
  133. cfg.StrOpt('extra_capabilities',
  134. default='{}',
  135. help='User defined capabilities, a JSON formatted string '
  136. 'specifying key/value pairs. The key/value pairs can '
  137. 'be used by the CapabilitiesFilter to select between '
  138. 'backends when requests specify volume types. For '
  139. 'example, specifying a service level or the geographical '
  140. 'location of a backend, then creating a volume type to '
  141. 'allow the user to select by these different '
  142. 'properties.'),
  143. cfg.BoolOpt('suppress_requests_ssl_warnings',
  144. default=False,
  145. help='Suppress requests library SSL certificate warnings.'),
  146. cfg.IntOpt('backend_native_threads_pool_size',
  147. default=20,
  148. min=20,
  149. help='Size of the native threads pool for the backend. '
  150. 'Increase for backends that heavily rely on this, like '
  151. 'the RBD driver.'),
  152. ]
  153. CONF = cfg.CONF
  154. CONF.register_opts(volume_manager_opts)
  155. CONF.register_opts(volume_backend_opts, group=config.SHARED_CONF_GROUP)
  156. # MAPPING is used for driver renames to keep backwards compatibilty. When a
  157. # driver is renamed, add a mapping here from the old name (the dict key) to the
  158. # new name (the dict value) for at least a cycle to allow time for deployments
  159. # to transition.
  160. MAPPING = {
  161. 'cinder.volume.drivers.dell_emc.vmax.iscsi.VMAXISCSIDriver':
  162. 'cinder.volume.drivers.dell_emc.powermax.iscsi.PowerMaxISCSIDriver',
  163. 'cinder.volume.drivers.dell_emc.vmax.fc.VMAXFCDriver':
  164. 'cinder.volume.drivers.dell_emc.powermax.fc.PowerMaxFCDriver',
  165. 'cinder.volume.drivers.fujitsu.eternus_dx_fc.FJDXFCDriver':
  166. 'cinder.volume.drivers.fujitsu.eternus_dx.eternus_dx_fc.FJDXFCDriver',
  167. 'cinder.volume.drivers.fujitsu.eternus_dx_iscsi.FJDXISCSIDriver':
  168. 'cinder.volume.drivers.fujitsu.eternus_dx.eternus_dx_iscsi.FJDXISCSIDriver'
  169. }
  170. class VolumeManager(manager.CleanableManager,
  171. manager.SchedulerDependentManager):
  172. """Manages attachable block storage devices."""
  173. RPC_API_VERSION = volume_rpcapi.VolumeAPI.RPC_API_VERSION
  174. FAILBACK_SENTINEL = 'default'
  175. target = messaging.Target(version=RPC_API_VERSION)
  176. # On cloning a volume, we shouldn't copy volume_type, consistencygroup
  177. # and volume_attachment, because the db sets that according to [field]_id,
  178. # which we do copy. We also skip some other values that are set during
  179. # creation of Volume object.
  180. _VOLUME_CLONE_SKIP_PROPERTIES = {
  181. 'id', '_name_id', 'name_id', 'name', 'status',
  182. 'attach_status', 'migration_status', 'volume_type',
  183. 'consistencygroup', 'volume_attachment', 'group'}
  184. def _get_service(self, host=None, binary=constants.VOLUME_BINARY):
  185. host = host or self.host
  186. ctxt = context.get_admin_context()
  187. svc_host = vol_utils.extract_host(host, 'backend')
  188. return objects.Service.get_by_args(ctxt, svc_host, binary)
  189. def __init__(self, volume_driver=None, service_name=None,
  190. *args, **kwargs):
  191. """Load the driver from the one specified in args, or from flags."""
  192. # update_service_capabilities needs service_name to be volume
  193. super(VolumeManager, self).__init__(service_name='volume',
  194. *args, **kwargs)
  195. # NOTE(dulek): service_name=None means we're running in unit tests.
  196. service_name = service_name or 'backend_defaults'
  197. self.configuration = config.Configuration(volume_backend_opts,
  198. config_group=service_name)
  199. self._set_tpool_size(
  200. self.configuration.backend_native_threads_pool_size)
  201. self.stats = {}
  202. self.service_uuid = None
  203. if not volume_driver:
  204. # Get from configuration, which will get the default
  205. # if its not using the multi backend
  206. volume_driver = self.configuration.volume_driver
  207. if volume_driver in MAPPING:
  208. LOG.warning("Driver path %s is deprecated, update your "
  209. "configuration to the new path.", volume_driver)
  210. volume_driver = MAPPING[volume_driver]
  211. vol_db_empty = self._set_voldb_empty_at_startup_indicator(
  212. context.get_admin_context())
  213. LOG.debug("Cinder Volume DB check: vol_db_empty=%s", vol_db_empty)
  214. # We pass the current setting for service.active_backend_id to
  215. # the driver on init, in case there was a restart or something
  216. curr_active_backend_id = None
  217. try:
  218. service = self._get_service()
  219. except exception.ServiceNotFound:
  220. # NOTE(jdg): This is to solve problems with unit tests
  221. LOG.info("Service not found for updating "
  222. "active_backend_id, assuming default "
  223. "for driver init.")
  224. else:
  225. curr_active_backend_id = service.active_backend_id
  226. self.service_uuid = service.uuid
  227. if self.configuration.suppress_requests_ssl_warnings:
  228. LOG.warning("Suppressing requests library SSL Warnings")
  229. requests.packages.urllib3.disable_warnings(
  230. requests.packages.urllib3.exceptions.InsecureRequestWarning)
  231. requests.packages.urllib3.disable_warnings(
  232. requests.packages.urllib3.exceptions.InsecurePlatformWarning)
  233. self.key_manager = key_manager.API(CONF)
  234. self.driver = importutils.import_object(
  235. volume_driver,
  236. configuration=self.configuration,
  237. db=self.db,
  238. host=self.host,
  239. cluster_name=self.cluster,
  240. is_vol_db_empty=vol_db_empty,
  241. active_backend_id=curr_active_backend_id)
  242. if self.cluster and not self.driver.SUPPORTS_ACTIVE_ACTIVE:
  243. msg = _('Active-Active configuration is not currently supported '
  244. 'by driver %s.') % volume_driver
  245. LOG.error(msg)
  246. raise exception.VolumeDriverException(message=msg)
  247. self.message_api = message_api.API()
  248. if CONF.profiler.enabled and profiler is not None:
  249. self.driver = profiler.trace_cls("driver")(self.driver)
  250. try:
  251. self.extra_capabilities = jsonutils.loads(
  252. self.driver.configuration.extra_capabilities)
  253. except AttributeError:
  254. self.extra_capabilities = {}
  255. except Exception:
  256. with excutils.save_and_reraise_exception():
  257. LOG.error("Invalid JSON: %s",
  258. self.driver.configuration.extra_capabilities)
  259. # Check if a per-backend AZ has been specified
  260. backend_zone = self.driver.configuration.safe_get(
  261. 'backend_availability_zone')
  262. if backend_zone:
  263. self.availability_zone = backend_zone
  264. if self.driver.configuration.safe_get(
  265. 'image_volume_cache_enabled'):
  266. max_cache_size = self.driver.configuration.safe_get(
  267. 'image_volume_cache_max_size_gb')
  268. max_cache_entries = self.driver.configuration.safe_get(
  269. 'image_volume_cache_max_count')
  270. self.image_volume_cache = image_cache.ImageVolumeCache(
  271. self.db,
  272. cinder_volume.API(),
  273. max_cache_size,
  274. max_cache_entries
  275. )
  276. LOG.info('Image-volume cache enabled for host %(host)s.',
  277. {'host': self.host})
  278. else:
  279. LOG.info('Image-volume cache disabled for host %(host)s.',
  280. {'host': self.host})
  281. self.image_volume_cache = None
  282. def _count_allocated_capacity(self, ctxt, volume):
  283. pool = vol_utils.extract_host(volume['host'], 'pool')
  284. if pool is None:
  285. # No pool name encoded in host, so this is a legacy
  286. # volume created before pool is introduced, ask
  287. # driver to provide pool info if it has such
  288. # knowledge and update the DB.
  289. try:
  290. pool = self.driver.get_pool(volume)
  291. except Exception:
  292. LOG.exception('Fetch volume pool name failed.',
  293. resource=volume)
  294. return
  295. if pool:
  296. new_host = vol_utils.append_host(volume['host'],
  297. pool)
  298. self.db.volume_update(ctxt, volume['id'],
  299. {'host': new_host})
  300. else:
  301. # Otherwise, put them into a special fixed pool with
  302. # volume_backend_name being the pool name, if
  303. # volume_backend_name is None, use default pool name.
  304. # This is only for counting purpose, doesn't update DB.
  305. pool = (self.driver.configuration.safe_get(
  306. 'volume_backend_name') or vol_utils.extract_host(
  307. volume['host'], 'pool', True))
  308. try:
  309. pool_stat = self.stats['pools'][pool]
  310. except KeyError:
  311. # First volume in the pool
  312. self.stats['pools'][pool] = dict(
  313. allocated_capacity_gb=0)
  314. pool_stat = self.stats['pools'][pool]
  315. pool_sum = pool_stat['allocated_capacity_gb']
  316. pool_sum += volume['size']
  317. self.stats['pools'][pool]['allocated_capacity_gb'] = pool_sum
  318. self.stats['allocated_capacity_gb'] += volume['size']
  319. def _set_voldb_empty_at_startup_indicator(self, ctxt):
  320. """Determine if the Cinder volume DB is empty.
  321. A check of the volume DB is done to determine whether it is empty or
  322. not at this point.
  323. :param ctxt: our working context
  324. """
  325. vol_entries = self.db.volume_get_all(ctxt, None, 1, filters=None)
  326. if len(vol_entries) == 0:
  327. LOG.info("Determined volume DB was empty at startup.")
  328. return True
  329. else:
  330. LOG.info("Determined volume DB was not empty at startup.")
  331. return False
  332. def _sync_provider_info(self, ctxt, volumes, snapshots):
  333. # NOTE(jdg): For now this just updates provider_id, we can add more
  334. # items to the update if they're relevant but we need to be safe in
  335. # what we allow and add a list of allowed keys. Things that make sense
  336. # are provider_*, replication_status etc
  337. updates, snapshot_updates = self.driver.update_provider_info(
  338. volumes, snapshots)
  339. if updates:
  340. for volume in volumes:
  341. # NOTE(JDG): Make sure returned item is in this hosts volumes
  342. update = (
  343. [updt for updt in updates if updt['id'] ==
  344. volume['id']])
  345. if update:
  346. update = update[0]
  347. self.db.volume_update(
  348. ctxt,
  349. update['id'],
  350. {'provider_id': update['provider_id']})
  351. if snapshot_updates:
  352. for snap in snapshots:
  353. # NOTE(jdg): For now we only update those that have no entry
  354. if not snap.get('provider_id', None):
  355. update = (
  356. [updt for updt in snapshot_updates if updt['id'] ==
  357. snap['id']][0])
  358. if update:
  359. self.db.snapshot_update(
  360. ctxt,
  361. update['id'],
  362. {'provider_id': update['provider_id']})
  363. def _include_resources_in_cluster(self, ctxt):
  364. LOG.info('Including all resources from host %(host)s in cluster '
  365. '%(cluster)s.',
  366. {'host': self.host, 'cluster': self.cluster})
  367. num_vols = objects.VolumeList.include_in_cluster(
  368. ctxt, self.cluster, host=self.host)
  369. num_cgs = objects.ConsistencyGroupList.include_in_cluster(
  370. ctxt, self.cluster, host=self.host)
  371. num_gs = objects.GroupList.include_in_cluster(
  372. ctxt, self.cluster, host=self.host)
  373. num_cache = db.image_volume_cache_include_in_cluster(
  374. ctxt, self.cluster, host=self.host)
  375. LOG.info('%(num_vols)s volumes, %(num_cgs)s consistency groups, '
  376. '%(num_gs)s generic groups and %(num_cache)s image '
  377. 'volume caches from host %(host)s have been included in '
  378. 'cluster %(cluster)s.',
  379. {'num_vols': num_vols, 'num_cgs': num_cgs, 'num_gs': num_gs,
  380. 'host': self.host, 'cluster': self.cluster,
  381. 'num_cache': num_cache})
  382. def init_host(self, added_to_cluster=None, **kwargs):
  383. """Perform any required initialization."""
  384. if not self.driver.supported:
  385. utils.log_unsupported_driver_warning(self.driver)
  386. if not self.configuration.enable_unsupported_driver:
  387. LOG.error("Unsupported drivers are disabled."
  388. " You can re-enable by adding "
  389. "enable_unsupported_driver=True to the "
  390. "driver section in cinder.conf",
  391. resource={'type': 'driver',
  392. 'id': self.__class__.__name__})
  393. return
  394. self._init_host(added_to_cluster, **kwargs)
  395. if not self.driver.initialized:
  396. reinit_count = 0
  397. while reinit_count < CONF.reinit_driver_count:
  398. time.sleep(2 ** reinit_count)
  399. self._init_host(added_to_cluster, **kwargs)
  400. if self.driver.initialized:
  401. return
  402. reinit_count += 1
  403. def _init_host(self, added_to_cluster=None, **kwargs):
  404. ctxt = context.get_admin_context()
  405. # If we have just added this host to a cluster we have to include all
  406. # our resources in that cluster.
  407. if added_to_cluster:
  408. self._include_resources_in_cluster(ctxt)
  409. LOG.info("Starting volume driver %(driver_name)s (%(version)s)",
  410. {'driver_name': self.driver.__class__.__name__,
  411. 'version': self.driver.get_version()})
  412. try:
  413. self.driver.do_setup(ctxt)
  414. self.driver.check_for_setup_error()
  415. except Exception:
  416. LOG.exception("Failed to initialize driver.",
  417. resource={'type': 'driver',
  418. 'id': self.__class__.__name__})
  419. # we don't want to continue since we failed
  420. # to initialize the driver correctly.
  421. return
  422. # Initialize backend capabilities list
  423. self.driver.init_capabilities()
  424. # Zero stats
  425. self.stats['pools'] = {}
  426. self.stats.update({'allocated_capacity_gb': 0})
  427. # Batch retrieval volumes and snapshots
  428. num_vols, num_snaps, max_objs_num, req_range = None, None, None, [0]
  429. req_limit = CONF.init_host_max_objects_retrieval
  430. use_batch_objects_retrieval = req_limit > 0
  431. if use_batch_objects_retrieval:
  432. # Get total number of volumes
  433. num_vols, __, __ = self._get_my_volumes_summary(ctxt)
  434. # Get total number of snapshots
  435. num_snaps, __ = self._get_my_snapshots_summary(ctxt)
  436. # Calculate highest number of the objects (volumes or snapshots)
  437. max_objs_num = max(num_vols, num_snaps)
  438. # Make batch request loop counter
  439. req_range = range(0, max_objs_num, req_limit)
  440. volumes_to_migrate = volume_migration.VolumeMigrationList()
  441. for req_offset in req_range:
  442. # Retrieve 'req_limit' number of objects starting from
  443. # 'req_offset' position
  444. volumes, snapshots = None, None
  445. if use_batch_objects_retrieval:
  446. if req_offset < num_vols:
  447. volumes = self._get_my_volumes(ctxt,
  448. limit=req_limit,
  449. offset=req_offset)
  450. else:
  451. volumes = objects.VolumeList()
  452. if req_offset < num_snaps:
  453. snapshots = self._get_my_snapshots(ctxt,
  454. limit=req_limit,
  455. offset=req_offset)
  456. else:
  457. snapshots = objects.SnapshotList()
  458. # or retrieve all volumes and snapshots per single request
  459. else:
  460. volumes = self._get_my_volumes(ctxt)
  461. snapshots = self._get_my_snapshots(ctxt)
  462. self._sync_provider_info(ctxt, volumes, snapshots)
  463. # FIXME volume count for exporting is wrong
  464. try:
  465. for volume in volumes:
  466. # available volume should also be counted into allocated
  467. if volume['status'] in ['in-use', 'available']:
  468. # calculate allocated capacity for driver
  469. self._count_allocated_capacity(ctxt, volume)
  470. try:
  471. if volume['status'] in ['in-use']:
  472. self.driver.ensure_export(ctxt, volume)
  473. except Exception:
  474. LOG.exception("Failed to re-export volume, "
  475. "setting to ERROR.",
  476. resource=volume)
  477. volume.conditional_update({'status': 'error'},
  478. {'status': 'in-use'})
  479. # All other cleanups are processed by parent class -
  480. # CleanableManager
  481. except Exception:
  482. LOG.exception("Error during re-export on driver init.",
  483. resource=volume)
  484. return
  485. if len(volumes):
  486. volumes_to_migrate.append(volumes, ctxt)
  487. del volumes
  488. del snapshots
  489. self.driver.set_throttle()
  490. # at this point the driver is considered initialized.
  491. # NOTE(jdg): Careful though because that doesn't mean
  492. # that an entry exists in the service table
  493. self.driver.set_initialized()
  494. # Keep the image tmp file clean when init host.
  495. backend_name = vol_utils.extract_host(self.service_topic_queue)
  496. image_utils.cleanup_temporary_file(backend_name)
  497. # Migrate any ConfKeyManager keys based on fixed_key to the currently
  498. # configured key manager.
  499. self._add_to_threadpool(key_migration.migrate_fixed_key,
  500. volumes=volumes_to_migrate)
  501. # collect and publish service capabilities
  502. self.publish_service_capabilities(ctxt)
  503. LOG.info("Driver initialization completed successfully.",
  504. resource={'type': 'driver',
  505. 'id': self.driver.__class__.__name__})
  506. # Make sure to call CleanableManager to do the cleanup
  507. super(VolumeManager, self).init_host(added_to_cluster=added_to_cluster,
  508. **kwargs)
  509. def init_host_with_rpc(self):
  510. LOG.info("Initializing RPC dependent components of volume "
  511. "driver %(driver_name)s (%(version)s)",
  512. {'driver_name': self.driver.__class__.__name__,
  513. 'version': self.driver.get_version()})
  514. try:
  515. # Make sure the driver is initialized first
  516. utils.log_unsupported_driver_warning(self.driver)
  517. utils.require_driver_initialized(self.driver)
  518. except exception.DriverNotInitialized:
  519. LOG.error("Cannot complete RPC initialization because "
  520. "driver isn't initialized properly.",
  521. resource={'type': 'driver',
  522. 'id': self.driver.__class__.__name__})
  523. return
  524. stats = self.driver.get_volume_stats(refresh=True)
  525. try:
  526. service = self._get_service()
  527. except exception.ServiceNotFound:
  528. with excutils.save_and_reraise_exception():
  529. LOG.error("Service not found for updating replication_status.")
  530. if service.replication_status != fields.ReplicationStatus.FAILED_OVER:
  531. if stats and stats.get('replication_enabled', False):
  532. replication_status = fields.ReplicationStatus.ENABLED
  533. else:
  534. replication_status = fields.ReplicationStatus.DISABLED
  535. if replication_status != service.replication_status:
  536. service.replication_status = replication_status
  537. service.save()
  538. # Update the cluster replication status if necessary
  539. cluster = service.cluster
  540. if (cluster and
  541. cluster.replication_status != service.replication_status):
  542. cluster.replication_status = service.replication_status
  543. cluster.save()
  544. LOG.info("Driver post RPC initialization completed successfully.",
  545. resource={'type': 'driver',
  546. 'id': self.driver.__class__.__name__})
  547. def _do_cleanup(self, ctxt, vo_resource):
  548. if isinstance(vo_resource, objects.Volume):
  549. if vo_resource.status == 'downloading':
  550. self.driver.clear_download(ctxt, vo_resource)
  551. elif vo_resource.status == 'uploading':
  552. # Set volume status to available or in-use.
  553. self.db.volume_update_status_based_on_attachment(
  554. ctxt, vo_resource.id)
  555. elif vo_resource.status == 'deleting':
  556. if CONF.volume_service_inithost_offload:
  557. # Offload all the pending volume delete operations to the
  558. # threadpool to prevent the main volume service thread
  559. # from being blocked.
  560. self._add_to_threadpool(self.delete_volume, ctxt,
  561. vo_resource, cascade=True)
  562. else:
  563. # By default, delete volumes sequentially
  564. self.delete_volume(ctxt, vo_resource, cascade=True)
  565. # We signal that we take care of cleaning the worker ourselves
  566. # (with set_workers decorator in delete_volume method) so
  567. # do_cleanup method doesn't need to remove it.
  568. return True
  569. # For Volume creating and downloading and for Snapshot downloading
  570. # statuses we have to set status to error
  571. if vo_resource.status in ('creating', 'downloading'):
  572. vo_resource.status = 'error'
  573. vo_resource.save()
  574. def is_working(self):
  575. """Return if Manager is ready to accept requests.
  576. This is to inform Service class that in case of volume driver
  577. initialization failure the manager is actually down and not ready to
  578. accept any requests.
  579. """
  580. return self.driver.initialized
  581. def _set_resource_host(self, resource):
  582. """Set the host field on the DB to our own when we are clustered."""
  583. if (resource.is_clustered and
  584. not vol_utils.hosts_are_equivalent(resource.host, self.host)):
  585. pool = vol_utils.extract_host(resource.host, 'pool')
  586. resource.host = vol_utils.append_host(self.host, pool)
  587. resource.save()
  588. @objects.Volume.set_workers
  589. def create_volume(self, context, volume, request_spec=None,
  590. filter_properties=None, allow_reschedule=True):
  591. """Creates the volume."""
  592. # Log about unsupported drivers
  593. utils.log_unsupported_driver_warning(self.driver)
  594. # Make sure the host in the DB matches our own when clustered
  595. self._set_resource_host(volume)
  596. # Update our allocated capacity counter early to minimize race
  597. # conditions with the scheduler.
  598. self._update_allocated_capacity(volume)
  599. # We lose the host value if we reschedule, so keep it here
  600. original_host = volume.host
  601. context_elevated = context.elevated()
  602. if filter_properties is None:
  603. filter_properties = {}
  604. if request_spec is None:
  605. request_spec = objects.RequestSpec()
  606. try:
  607. # NOTE(flaper87): Driver initialization is
  608. # verified by the task itself.
  609. flow_engine = create_volume.get_flow(
  610. context_elevated,
  611. self,
  612. self.db,
  613. self.driver,
  614. self.scheduler_rpcapi,
  615. self.host,
  616. volume,
  617. allow_reschedule,
  618. context,
  619. request_spec,
  620. filter_properties,
  621. image_volume_cache=self.image_volume_cache,
  622. )
  623. except Exception:
  624. msg = _("Create manager volume flow failed.")
  625. LOG.exception(msg, resource={'type': 'volume', 'id': volume.id})
  626. raise exception.CinderException(msg)
  627. snapshot_id = request_spec.get('snapshot_id')
  628. source_volid = request_spec.get('source_volid')
  629. if snapshot_id is not None:
  630. # Make sure the snapshot is not deleted until we are done with it.
  631. locked_action = "%s-%s" % (snapshot_id, 'delete_snapshot')
  632. elif source_volid is not None:
  633. # Make sure the volume is not deleted until we are done with it.
  634. locked_action = "%s-%s" % (source_volid, 'delete_volume')
  635. else:
  636. locked_action = None
  637. def _run_flow():
  638. # This code executes create volume flow. If something goes wrong,
  639. # flow reverts all job that was done and reraises an exception.
  640. # Otherwise, all data that was generated by flow becomes available
  641. # in flow engine's storage.
  642. with flow_utils.DynamicLogListener(flow_engine, logger=LOG):
  643. flow_engine.run()
  644. # NOTE(dulek): Flag to indicate if volume was rescheduled. Used to
  645. # decide if allocated_capacity should be incremented.
  646. rescheduled = False
  647. try:
  648. if locked_action is None:
  649. _run_flow()
  650. else:
  651. with coordination.COORDINATOR.get_lock(locked_action):
  652. _run_flow()
  653. finally:
  654. try:
  655. flow_engine.storage.fetch('refreshed')
  656. except tfe.NotFound:
  657. # If there's no vol_ref, then flow is reverted. Lets check out
  658. # if rescheduling occurred.
  659. try:
  660. rescheduled = flow_engine.storage.get_revert_result(
  661. create_volume.OnFailureRescheduleTask.make_name(
  662. [create_volume.ACTION]))
  663. except tfe.NotFound:
  664. pass
  665. if rescheduled:
  666. # NOTE(geguileo): Volume was rescheduled so we need to update
  667. # volume stats because the volume wasn't created here.
  668. # Volume.host is None now, so we pass the original host value.
  669. self._update_allocated_capacity(volume, decrement=True,
  670. host=original_host)
  671. # Shared targets is only relevant for iSCSI connections.
  672. # We default to True to be on the safe side.
  673. volume.shared_targets = (
  674. self.driver.capabilities.get('storage_protocol') == 'iSCSI' and
  675. self.driver.capabilities.get('shared_targets', True))
  676. # TODO(geguileo): service_uuid won't be enough on Active/Active
  677. # deployments. There can be 2 services handling volumes from the same
  678. # backend.
  679. volume.service_uuid = self.service_uuid
  680. volume.save()
  681. LOG.info("Created volume successfully.", resource=volume)
  682. return volume.id
  683. def _check_is_our_resource(self, resource):
  684. if resource.host:
  685. res_backend = vol_utils.extract_host(resource.service_topic_queue)
  686. backend = vol_utils.extract_host(self.service_topic_queue)
  687. if res_backend != backend:
  688. msg = (_('Invalid %(resource)s: %(resource)s %(id)s is not '
  689. 'local to %(backend)s.') %
  690. {'resource': resource.obj_name, 'id': resource.id,
  691. 'backend': backend})
  692. raise exception.Invalid(msg)
  693. @coordination.synchronized('{volume.id}-{f_name}')
  694. @objects.Volume.set_workers
  695. def delete_volume(self, context, volume, unmanage_only=False,
  696. cascade=False):
  697. """Deletes and unexports volume.
  698. 1. Delete a volume(normal case)
  699. Delete a volume and update quotas.
  700. 2. Delete a migration volume
  701. If deleting the volume in a migration, we want to skip
  702. quotas but we need database updates for the volume.
  703. 3. Delete a temp volume for backup
  704. If deleting the temp volume for backup, we want to skip
  705. quotas but we need database updates for the volume.
  706. """
  707. context = context.elevated()
  708. try:
  709. volume.refresh()
  710. except exception.VolumeNotFound:
  711. # NOTE(thingee): It could be possible for a volume to
  712. # be deleted when resuming deletes from init_host().
  713. LOG.debug("Attempted delete of non-existent volume: %s", volume.id)
  714. return
  715. if context.project_id != volume.project_id:
  716. project_id = volume.project_id
  717. else:
  718. project_id = context.project_id
  719. if volume['attach_status'] == fields.VolumeAttachStatus.ATTACHED:
  720. # Volume is still attached, need to detach first
  721. raise exception.VolumeAttached(volume_id=volume.id)
  722. self._check_is_our_resource(volume)
  723. if unmanage_only and volume.encryption_key_id is not None:
  724. raise exception.Invalid(
  725. reason=_("Unmanaging encrypted volumes is not "
  726. "supported."))
  727. if unmanage_only and cascade:
  728. # This could be done, but is ruled out for now just
  729. # for simplicity.
  730. raise exception.Invalid(
  731. reason=_("Unmanage and cascade delete options "
  732. "are mutually exclusive."))
  733. # To backup a snapshot or a 'in-use' volume, create a temp volume
  734. # from the snapshot or in-use volume, and back it up.
  735. # Get admin_metadata (needs admin context) to detect temporary volume.
  736. is_temp_vol = False
  737. with volume.obj_as_admin():
  738. if volume.admin_metadata.get('temporary', 'False') == 'True':
  739. is_temp_vol = True
  740. LOG.info("Trying to delete temp volume: %s", volume.id)
  741. # The status 'deleting' is not included, because it only applies to
  742. # the source volume to be deleted after a migration. No quota
  743. # needs to be handled for it.
  744. is_migrating = volume.migration_status not in (None, 'error',
  745. 'success')
  746. is_migrating_dest = (is_migrating and
  747. volume.migration_status.startswith(
  748. 'target:'))
  749. notification = "delete.start"
  750. if unmanage_only:
  751. notification = "unmanage.start"
  752. if not is_temp_vol:
  753. self._notify_about_volume_usage(context, volume, notification)
  754. try:
  755. # NOTE(flaper87): Verify the driver is enabled
  756. # before going forward. The exception will be caught
  757. # and the volume status updated.
  758. utils.require_driver_initialized(self.driver)
  759. self.driver.remove_export(context, volume)
  760. if unmanage_only:
  761. self.driver.unmanage(volume)
  762. elif cascade:
  763. LOG.debug('Performing cascade delete.')
  764. snapshots = objects.SnapshotList.get_all_for_volume(context,
  765. volume.id)
  766. for s in snapshots:
  767. if s.status != fields.SnapshotStatus.DELETING:
  768. self._clear_db(context, is_migrating_dest, volume,
  769. 'error_deleting')
  770. msg = (_("Snapshot %(id)s was found in state "
  771. "%(state)s rather than 'deleting' during "
  772. "cascade delete.") % {'id': s.id,
  773. 'state': s.status})
  774. raise exception.InvalidSnapshot(reason=msg)
  775. self.delete_snapshot(context, s)
  776. LOG.debug('Snapshots deleted, issuing volume delete')
  777. self.driver.delete_volume(volume)
  778. else:
  779. self.driver.delete_volume(volume)
  780. except exception.VolumeIsBusy:
  781. LOG.error("Unable to delete busy volume.",
  782. resource=volume)
  783. # If this is a destination volume, we have to clear the database
  784. # record to avoid user confusion.
  785. self._clear_db(context, is_migrating_dest, volume,
  786. 'available')
  787. return
  788. except Exception:
  789. with excutils.save_and_reraise_exception():
  790. # If this is a destination volume, we have to clear the
  791. # database record to avoid user confusion.
  792. new_status = 'error_deleting'
  793. if unmanage_only is True:
  794. new_status = 'error_unmanaging'
  795. self._clear_db(context, is_migrating_dest, volume,
  796. new_status)
  797. # If deleting source/destination volume in a migration or a temp
  798. # volume for backup, we should skip quotas.
  799. skip_quota = is_migrating or is_temp_vol
  800. if not skip_quota:
  801. # Get reservations
  802. try:
  803. reservations = None
  804. if volume.status != 'error_managing_deleting':
  805. reserve_opts = {'volumes': -1,
  806. 'gigabytes': -volume.size}
  807. QUOTAS.add_volume_type_opts(context,
  808. reserve_opts,
  809. volume.volume_type_id)
  810. reservations = QUOTAS.reserve(context,
  811. project_id=project_id,
  812. **reserve_opts)
  813. except Exception:
  814. LOG.exception("Failed to update usages deleting volume.",
  815. resource=volume)
  816. volume.destroy()
  817. # If deleting source/destination volume in a migration or a temp
  818. # volume for backup, we should skip quotas.
  819. if not skip_quota:
  820. notification = "delete.end"
  821. if unmanage_only:
  822. notification = "unmanage.end"
  823. self._notify_about_volume_usage(context, volume, notification)
  824. # Commit the reservations
  825. if reservations:
  826. QUOTAS.commit(context, reservations, project_id=project_id)
  827. self._update_allocated_capacity(volume, decrement=True)
  828. self.publish_service_capabilities(context)
  829. msg = "Deleted volume successfully."
  830. if unmanage_only:
  831. msg = "Unmanaged volume successfully."
  832. LOG.info(msg, resource=volume)
  833. def _clear_db(self, context, is_migrating_dest, volume_ref, status):
  834. # This method is called when driver.unmanage() or
  835. # driver.delete_volume() fails in delete_volume(), so it is already
  836. # in the exception handling part.
  837. if is_migrating_dest:
  838. volume_ref.destroy()
  839. LOG.error("Unable to delete the destination volume "
  840. "during volume migration, (NOTE: database "
  841. "record needs to be deleted).", resource=volume_ref)
  842. else:
  843. volume_ref.status = status
  844. volume_ref.save()
  845. def _revert_to_snapshot_generic(self, ctxt, volume, snapshot):
  846. """Generic way to revert volume to a snapshot.
  847. the framework will use the generic way to implement the revert
  848. to snapshot feature:
  849. 1. create a temporary volume from snapshot
  850. 2. mount two volumes to host
  851. 3. copy data from temporary volume to original volume
  852. 4. detach and destroy temporary volume
  853. """
  854. temp_vol = None
  855. try:
  856. v_options = {'display_name': '[revert] temporary volume created '
  857. 'from snapshot %s' % snapshot.id}
  858. ctxt = context.get_internal_tenant_context() or ctxt
  859. temp_vol = self.driver._create_temp_volume_from_snapshot(
  860. ctxt, volume, snapshot, volume_options=v_options)
  861. self._copy_volume_data(ctxt, temp_vol, volume)
  862. self.driver.delete_volume(temp_vol)
  863. temp_vol.destroy()
  864. except Exception:
  865. with excutils.save_and_reraise_exception():
  866. LOG.exception(
  867. "Failed to use snapshot %(snapshot)s to create "
  868. "a temporary volume and copy data to volume "
  869. " %(volume)s.",
  870. {'snapshot': snapshot.id,
  871. 'volume': volume.id})
  872. if temp_vol and temp_vol.status == 'available':
  873. self.driver.delete_volume(temp_vol)
  874. temp_vol.destroy()
  875. def _revert_to_snapshot(self, context, volume, snapshot):
  876. """Use driver or generic method to rollback volume."""
  877. try:
  878. self.driver.revert_to_snapshot(context, volume, snapshot)
  879. except (NotImplementedError, AttributeError):
  880. LOG.info("Driver's 'revert_to_snapshot' is not found. "
  881. "Try to use copy-snapshot-to-volume method.")
  882. self._revert_to_snapshot_generic(context, volume, snapshot)
  883. def _create_backup_snapshot(self, context, volume):
  884. kwargs = {
  885. 'volume_id': volume.id,
  886. 'user_id': context.user_id,
  887. 'project_id': context.project_id,
  888. 'status': fields.SnapshotStatus.CREATING,
  889. 'progress': '0%',
  890. 'volume_size': volume.size,
  891. 'display_name': '[revert] volume %s backup snapshot' % volume.id,
  892. 'display_description': 'This is only used for backup when '
  893. 'reverting. If the reverting process '
  894. 'failed, you can restore you data by '
  895. 'creating new volume with this snapshot.',
  896. 'volume_type_id': volume.volume_type_id,
  897. 'encryption_key_id': volume.encryption_key_id,
  898. 'metadata': {}
  899. }
  900. snapshot = objects.Snapshot(context=context, **kwargs)
  901. snapshot.create()
  902. self.create_snapshot(context, snapshot)
  903. return snapshot
  904. def revert_to_snapshot(self, context, volume, snapshot):
  905. """Revert a volume to a snapshot.
  906. The process of reverting to snapshot consists of several steps:
  907. 1. create a snapshot for backup (in case of data loss)
  908. 2.1. use driver's specific logic to revert volume
  909. 2.2. try the generic way to revert volume if driver's method is missing
  910. 3. delete the backup snapshot
  911. """
  912. backup_snapshot = None
  913. try:
  914. LOG.info("Start to perform revert to snapshot process.")
  915. self._notify_about_volume_usage(context, volume,
  916. "revert.start")
  917. self._notify_about_snapshot_usage(context, snapshot,
  918. "revert.start")
  919. # Create a snapshot which can be used to restore the volume
  920. # data by hand if revert process failed.
  921. if self.driver.snapshot_revert_use_temp_snapshot():
  922. backup_snapshot = self._create_backup_snapshot(context,
  923. volume)
  924. self._revert_to_snapshot(context, volume, snapshot)
  925. except Exception as error:
  926. with excutils.save_and_reraise_exception():
  927. self._notify_about_volume_usage(context, volume,
  928. "revert.end")
  929. self._notify_about_snapshot_usage(context, snapshot,
  930. "revert.end")
  931. msg = ('Volume %(v_id)s revert to '
  932. 'snapshot %(s_id)s failed with %(error)s.')
  933. msg_args = {'v_id': volume.id,
  934. 's_id': snapshot.id,
  935. 'error': six.text_type(error)}
  936. v_res = volume.update_single_status_where(
  937. 'error',
  938. 'reverting')
  939. if not v_res:
  940. msg_args = {"id": volume.id,
  941. "status": 'error'}
  942. msg += ("Failed to reset volume %(id)s "
  943. "status to %(status)s.") % msg_args
  944. s_res = snapshot.update_single_status_where(
  945. fields.SnapshotStatus.AVAILABLE,
  946. fields.SnapshotStatus.RESTORING)
  947. if not s_res:
  948. msg_args = {"id": snapshot.id,
  949. "status":
  950. fields.SnapshotStatus.AVAILABLE}
  951. msg += ("Failed to reset snapshot %(id)s "
  952. "status to %(status)s." % msg_args)
  953. LOG.exception(msg, msg_args)
  954. v_res = volume.update_single_status_where(
  955. 'available', 'reverting')
  956. if not v_res:
  957. msg_args = {"id": volume.id,
  958. "status": 'available'}
  959. msg = _("Revert finished, but failed to reset "
  960. "volume %(id)s status to %(status)s, "
  961. "please manually reset it.") % msg_args
  962. raise exception.BadResetResourceStatus(reason=msg)
  963. s_res = snapshot.update_single_status_where(
  964. fields.SnapshotStatus.AVAILABLE,
  965. fields.SnapshotStatus.RESTORING)
  966. if not s_res:
  967. msg_args = {"id": snapshot.id,
  968. "status":
  969. fields.SnapshotStatus.AVAILABLE}
  970. msg = _("Revert finished, but failed to reset "
  971. "snapshot %(id)s status to %(status)s, "
  972. "please manually reset it.") % msg_args
  973. raise exception.BadResetResourceStatus(reason=msg)
  974. if backup_snapshot:
  975. self.delete_snapshot(context,
  976. backup_snapshot, handle_quota=False)
  977. msg = ('Volume %(v_id)s reverted to snapshot %(snap_id)s '
  978. 'successfully.')
  979. msg_args = {'v_id': volume.id, 'snap_id': snapshot.id}
  980. LOG.info(msg, msg_args)
  981. self._notify_about_volume_usage(context, volume, "revert.end")
  982. self._notify_about_snapshot_usage(context, snapshot, "revert.end")
  983. @objects.Snapshot.set_workers
  984. def create_snapshot(self, context, snapshot):
  985. """Creates and exports the snapshot."""
  986. context = context.elevated()
  987. self._notify_about_snapshot_usage(
  988. context, snapshot, "create.start")
  989. try:
  990. # NOTE(flaper87): Verify the driver is enabled
  991. # before going forward. The exception will be caught
  992. # and the snapshot status updated.
  993. utils.require_driver_initialized(self.driver)
  994. # Pass context so that drivers that want to use it, can,
  995. # but it is not a requirement for all drivers.
  996. snapshot.context = context
  997. model_update = self.driver.create_snapshot(snapshot)
  998. if model_update:
  999. snapshot.update(model_update)
  1000. snapshot.save()
  1001. except Exception:
  1002. with excutils.save_and_reraise_exception():
  1003. snapshot.status = fields.SnapshotStatus.ERROR
  1004. snapshot.save()
  1005. vol_ref = self.db.volume_get(context, snapshot.volume_id)
  1006. if vol_ref.bootable:
  1007. try:
  1008. self.db.volume_glance_metadata_copy_to_snapshot(
  1009. context, snapshot.id, snapshot.volume_id)
  1010. except exception.GlanceMetadataNotFound:
  1011. # If volume is not created from image, No glance metadata
  1012. # would be available for that volume in
  1013. # volume glance metadata table
  1014. pass
  1015. except exception.CinderException as ex:
  1016. LOG.exception("Failed updating snapshot"
  1017. " metadata using the provided volumes"
  1018. " %(volume_id)s metadata",
  1019. {'volume_id': snapshot.volume_id},
  1020. resource=snapshot)
  1021. snapshot.status = fields.SnapshotStatus.ERROR
  1022. snapshot.save()
  1023. raise exception.MetadataCopyFailure(reason=six.text_type(ex))
  1024. snapshot.status = fields.SnapshotStatus.AVAILABLE
  1025. snapshot.progress = '100%'
  1026. # Resync with the volume's DB value. This addresses the case where
  1027. # the snapshot creation was in flight just prior to when the volume's
  1028. # fixed_key encryption key ID was migrated to Barbican.
  1029. snapshot.encryption_key_id = vol_ref.encryption_key_id
  1030. snapshot.save()
  1031. self._notify_about_snapshot_usage(context, snapshot, "create.end")
  1032. LOG.info("Create snapshot completed successfully",
  1033. resource=snapshot)
  1034. return snapshot.id
  1035. @coordination.synchronized('{snapshot.id}-{f_name}')
  1036. def delete_snapshot(self, context, snapshot,
  1037. unmanage_only=False, handle_quota=True):
  1038. """Deletes and unexports snapshot."""
  1039. context = context.elevated()
  1040. snapshot._context = context
  1041. project_id = snapshot.project_id
  1042. self._notify_about_snapshot_usage(
  1043. context, snapshot, "delete.start")
  1044. try:
  1045. # NOTE(flaper87): Verify the driver is enabled
  1046. # before going forward. The exception will be caught
  1047. # and the snapshot status updated.
  1048. utils.require_driver_initialized(self.driver)
  1049. # Pass context so that drivers that want to use it, can,
  1050. # but it is not a requirement for all drivers.
  1051. snapshot.context = context
  1052. snapshot.save()
  1053. if unmanage_only:
  1054. self.driver.unmanage_snapshot(snapshot)
  1055. else:
  1056. self.driver.delete_snapshot(snapshot)
  1057. except exception.SnapshotIsBusy:
  1058. LOG.error("Delete snapshot failed, due to snapshot busy.",
  1059. resource=snapshot)
  1060. snapshot.status = fields.SnapshotStatus.AVAILABLE
  1061. snapshot.save()
  1062. return
  1063. except Exception:
  1064. with excutils.save_and_reraise_exception():
  1065. snapshot.status = fields.SnapshotStatus.ERROR_DELETING
  1066. snapshot.save()
  1067. # Get reservations
  1068. reservations = None
  1069. try:
  1070. if handle_quota:
  1071. if CONF.no_snapshot_gb_quota:
  1072. reserve_opts = {'snapshots': -1}
  1073. else:
  1074. reserve_opts = {
  1075. 'snapshots': -1,
  1076. 'gigabytes': -snapshot.volume_size,
  1077. }
  1078. volume_ref = self.db.volume_get(context, snapshot.volume_id)
  1079. QUOTAS.add_volume_type_opts(context,
  1080. reserve_opts,
  1081. volume_ref.get('volume_type_id'))
  1082. reservations = QUOTAS.reserve(context,
  1083. project_id=project_id,
  1084. **reserve_opts)
  1085. except Exception:
  1086. reservations = None
  1087. LOG.exception("Update snapshot usages failed.",
  1088. resource=snapshot)
  1089. self.db.volume_glance_metadata_delete_by_snapshot(context, snapshot.id)
  1090. snapshot.destroy()
  1091. self._notify_about_snapshot_usage(context, snapshot, "delete.end")
  1092. # Commit the reservations
  1093. if reservations:
  1094. QUOTAS.commit(context, reservations, project_id=project_id)
  1095. msg = "Delete snapshot completed successfully."
  1096. if unmanage_only:
  1097. msg = "Unmanage snapshot completed successfully."
  1098. LOG.info(msg, resource=snapshot)
  1099. @coordination.synchronized('{volume_id}')
  1100. def attach_volume(self, context, volume_id, instance_uuid, host_name,
  1101. mountpoint, mode, volume=None):
  1102. """Updates db to show volume is attached."""
  1103. # FIXME(lixiaoy1): Remove this in v4.0 of RPC API.
  1104. if volume is None:
  1105. # For older clients, mimic the old behavior and look
  1106. # up the volume by its volume_id.
  1107. volume = objects.Volume.get_by_id(context, volume_id)
  1108. # Get admin_metadata. This needs admin context.
  1109. with volume.obj_as_admin():
  1110. volume_metadata = volume.admin_metadata
  1111. # check the volume status before attaching
  1112. if volume.status == 'attaching':
  1113. if (volume_metadata.get('attached_mode') and
  1114. volume_metadata.get('attached_mode') != mode):
  1115. raise exception.InvalidVolume(
  1116. reason=_("being attached by different mode"))
  1117. host_name_sanitized = utils.sanitize_hostname(
  1118. host_name) if host_name else None
  1119. if instance_uuid:
  1120. attachments = (
  1121. VA_LIST.get_all_by_instance_uuid(
  1122. context, instance_uuid))
  1123. else:
  1124. attachments = (
  1125. VA_LIST.get_all_by_host(
  1126. context, host_name_sanitized))
  1127. if attachments:
  1128. # check if volume<->instance mapping is already tracked in DB
  1129. for attachment in attachments:
  1130. if attachment['volume_id'] == volume_id:
  1131. volume.status = 'in-use'
  1132. volume.save()
  1133. return attachment
  1134. if (volume.status == 'in-use' and not volume.multiattach
  1135. and not volume.migration_status):
  1136. raise exception.InvalidVolume(
  1137. reason=_("volume is already attached and multiple attachments "
  1138. "are not enabled"))
  1139. self._notify_about_volume_usage(context, volume,
  1140. "attach.start")
  1141. attachment = volume.begin_attach(mode)
  1142. if instance_uuid and not uuidutils.is_uuid_like(instance_uuid):
  1143. attachment.attach_status = (
  1144. fields.VolumeAttachStatus.ERROR_ATTACHING)
  1145. attachment.save()
  1146. raise exception.InvalidUUID(uuid=instance_uuid)
  1147. try:
  1148. if volume_metadata.get('readonly') == 'True' and mode != 'ro':
  1149. raise exception.InvalidVolumeAttachMode(mode=mode,
  1150. volume_id=volume.id)
  1151. # NOTE(flaper87): Verify the driver is enabled
  1152. # before going forward. The exception will be caught
  1153. # and the volume status updated.
  1154. utils.require_driver_initialized(self.driver)
  1155. LOG.info('Attaching volume %(volume_id)s to instance '
  1156. '%(instance)s at mountpoint %(mount)s on host '
  1157. '%(host)s.',
  1158. {'volume_id': volume_id, 'instance': instance_uuid,
  1159. 'mount': mountpoint, 'host': host_name_sanitized},
  1160. resource=volume)
  1161. self.driver.attach_volume(context,
  1162. volume,
  1163. instance_uuid,
  1164. host_name_sanitized,
  1165. mountpoint)
  1166. except Exception as excep:
  1167. with excutils.save_and_reraise_exception():
  1168. self.message_api.create(
  1169. context,
  1170. message_field.Action.ATTACH_VOLUME,
  1171. resource_uuid=volume_id,
  1172. exception=excep)
  1173. attachment.attach_status = (
  1174. fields.VolumeAttachStatus.ERROR_ATTACHING)
  1175. attachment.save()
  1176. volume = attachment.finish_attach(
  1177. instance_uuid,
  1178. host_name_sanitized,
  1179. mountpoint,
  1180. mode)
  1181. self._notify_about_volume_usage(context, volume, "attach.end")
  1182. LOG.info("Attach volume completed successfully.",
  1183. resource=volume)
  1184. return attachment
  1185. @coordination.synchronized('{volume_id}-{f_name}')
  1186. def detach_volume(self, context, volume_id, attachment_id=None,
  1187. volume=None):
  1188. """Updates db to show volume is detached."""
  1189. # TODO(vish): refactor this into a more general "unreserve"
  1190. # FIXME(lixiaoy1): Remove this in v4.0 of RPC API.
  1191. if volume is None:
  1192. # For older clients, mimic the old behavior and look up the volume
  1193. # by its volume_id.
  1194. volume = objects.Volume.get_by_id(context, volume_id)
  1195. if attachment_id:
  1196. try:
  1197. attachment = objects.VolumeAttachment.get_by_id(context,
  1198. attachment_id)
  1199. except exception.VolumeAttachmentNotFound:
  1200. LOG.info("Volume detach called, but volume not attached.",
  1201. resource=volume)
  1202. # We need to make sure the volume status is set to the correct
  1203. # status. It could be in detaching status now, and we don't
  1204. # want to leave it there.
  1205. volume.finish_detach(attachment_id)
  1206. return
  1207. else:
  1208. # We can try and degrade gracefully here by trying to detach
  1209. # a volume without the attachment_id here if the volume only has
  1210. # one attachment. This is for backwards compatibility.
  1211. attachments = volume.volume_attachment
  1212. if len(attachments) > 1:
  1213. # There are more than 1 attachments for this volume
  1214. # we have to have an attachment id.
  1215. msg = _("Detach volume failed: More than one attachment, "
  1216. "but no attachment_id provided.")
  1217. LOG.error(msg, resource=volume)
  1218. raise exception.InvalidVolume(reason=msg)
  1219. elif len(attachments) == 1:
  1220. attachment = attachments[0]
  1221. else:
  1222. # there aren't any attachments for this volume.
  1223. # so set the status to available and move on.
  1224. LOG.info("Volume detach called, but volume not attached.",
  1225. resource=volume)
  1226. volume.status = 'available'
  1227. volume.attach_status = fields.VolumeAttachStatus.DETACHED
  1228. volume.save()
  1229. return
  1230. self._notify_about_volume_usage(context, volume, "detach.start")
  1231. try:
  1232. # NOTE(flaper87): Verify the driver is enabled
  1233. # before going forward. The exception will be caught
  1234. # and the volume status updated.
  1235. utils.require_driver_initialized(self.driver)
  1236. LOG.info('Detaching volume %(volume_id)s from instance '
  1237. '%(instance)s.',
  1238. {'volume_id': volume_id,
  1239. 'instance': attachment.get('instance_uuid')},
  1240. resource=volume)
  1241. self.driver.detach_volume(context, volume, attachment)
  1242. except Exception:
  1243. with excutils.save_and_reraise_exception():
  1244. self.db.volume_attachment_update(
  1245. context, attachment.get('id'), {
  1246. 'attach_status':
  1247. fields.VolumeAttachStatus.ERROR_DETACHING})
  1248. # NOTE(jdg): We used to do an ensure export here to
  1249. # catch upgrades while volumes were attached (E->F)
  1250. # this was necessary to convert in-use volumes from
  1251. # int ID's to UUID's. Don't need this any longer
  1252. # We're going to remove the export here
  1253. # (delete the iscsi target)
  1254. try:
  1255. utils.require_driver_initialized(self.driver)
  1256. self.driver.remove_export(context.elevated(), volume)
  1257. except exception.DriverNotInitialized:
  1258. with excutils.save_and_reraise_exception():
  1259. LOG.exception("Detach volume failed, due to "
  1260. "uninitialized driver.",
  1261. resource=volume)
  1262. except Exception as ex:
  1263. LOG.exception("Detach volume failed, due to "
  1264. "remove-export failure.",
  1265. resource=volume)
  1266. raise exception.RemoveExportException(volume=volume_id,
  1267. reason=six.text_type(ex))
  1268. volume.finish_detach(attachment.id)
  1269. self._notify_about_volume_usage(context, volume, "detach.end")
  1270. LOG.info("Detach volume completed successfully.", resource=volume)
  1271. def _create_image_cache_volume_entry(self, ctx, volume_ref,
  1272. image_id, image_meta):
  1273. """Create a new image-volume and cache entry for it.
  1274. This assumes that the image has already been downloaded and stored
  1275. in the volume described by the volume_ref.
  1276. """
  1277. cache_entry = self.image_volume_cache.get_entry(ctx,
  1278. volume_ref,
  1279. image_id,
  1280. image_meta)
  1281. if cache_entry:
  1282. LOG.debug('Cache entry already exists with image ID %'
  1283. '(image_id)s',
  1284. {'image_id': image_id})
  1285. return
  1286. image_volume = None
  1287. try:
  1288. if not self.image_volume_cache.ensure_space(ctx, volume_ref):
  1289. LOG.warning('Unable to ensure space for image-volume in'
  1290. ' cache. Will skip creating entry for image'
  1291. ' %(image)s on %(service)s.',
  1292. {'image': image_id,
  1293. 'service': volume_ref.service_topic_queue})
  1294. return
  1295. image_volume = self._clone_image_volume(ctx,
  1296. volume_ref,
  1297. image_meta)
  1298. if not image_volume:
  1299. LOG.warning('Unable to clone image_volume for image '
  1300. '%(image_id)s will not create cache entry.',
  1301. {'image_id': image_id})
  1302. return
  1303. self.image_volume_cache.create_cache_entry(
  1304. ctx,
  1305. image_volume,
  1306. image_id,
  1307. image_meta
  1308. )
  1309. except exception.CinderException as e:
  1310. LOG.warning('Failed to create new image-volume cache entry.'
  1311. ' Error: %(exception)s', {'exception': e})
  1312. if image_volume:
  1313. self.delete_volume(ctx, image_volume)
  1314. def _clone_image_volume(self, ctx, volume, image_meta):
  1315. volume_type_id = volume.get('volume_type_id')
  1316. reserve_opts = {'volumes': 1, 'gigabytes': volume.size}
  1317. QUOTAS.add_volume_type_opts(ctx, reserve_opts, volume_type_id)
  1318. reservations = QUOTAS.reserve(ctx, **reserve_opts)
  1319. # NOTE(yikun): Skip 'snapshot_id', 'source_volid' keys to avoid
  1320. # creating tmp img vol from wrong snapshot or wrong source vol.
  1321. skip = {'snapshot_id', 'source_volid'}
  1322. skip.update(self._VOLUME_CLONE_SKIP_PROPERTIES)
  1323. try:
  1324. new_vol_values = {k: volume[k] for k in set(volume.keys()) - skip}
  1325. new_vol_values['volume_type_id'] = volume_type_id
  1326. new_vol_values['attach_status'] = (
  1327. fields.VolumeAttachStatus.DETACHED)
  1328. new_vol_values['status'] = 'creating'
  1329. new_vol_values['project_id'] = ctx.project_id
  1330. new_vol_values['display_name'] = 'image-%s' % image_meta['id']
  1331. new_vol_values['source_volid'] = volume.id
  1332. LOG.debug('Creating image volume entry: %s.', new_vol_values)
  1333. image_volume = objects.Volume(context=ctx, **new_vol_values)
  1334. image_volume.create()
  1335. except Exception as ex:
  1336. LOG.exception('Create clone_image_volume: %(volume_id)s '
  1337. 'for image %(image_id)s, '
  1338. 'failed (Exception: %(except)s)',
  1339. {'volume_id': volume.id,
  1340. 'image_id': image_meta['id'],
  1341. 'except': ex})
  1342. QUOTAS.rollback(ctx, reservations)
  1343. return
  1344. QUOTAS.commit(ctx, reservations,
  1345. project_id=new_vol_values['project_id'])
  1346. try:
  1347. self.create_volume(ctx, image_volume, allow_reschedule=False)
  1348. image_volume.refresh()
  1349. if image_volume.status != 'available':
  1350. raise exception.InvalidVolume(_('Volume is not available.'))
  1351. self.db.volume_admin_metadata_update(ctx.elevated(),
  1352. image_volume.id,
  1353. {'readonly': 'True'},
  1354. False)
  1355. return image_volume
  1356. except exception.CinderException:
  1357. LOG.exception('Failed to clone volume %(volume_id)s for '
  1358. 'image %(image_id)s.',
  1359. {'volume_id': volume.id,
  1360. 'image_id': image_meta['id']})
  1361. try:
  1362. self.delete_volume(ctx, image_volume)
  1363. except exception.CinderException:
  1364. LOG.exception('Could not delete the image volume %(id)s.',
  1365. {'id': volume.id})
  1366. return
  1367. def _clone_image_volume_and_add_location(self, ctx, volume, image_service,
  1368. image_meta):
  1369. """Create a cloned volume and register its location to the image."""
  1370. if (image_meta['disk_format'] != 'raw' or
  1371. image_meta['container_format'] != 'bare'):
  1372. return False
  1373. image_volume_context = ctx
  1374. if self.driver.configuration.image_upload_use_internal_tenant:
  1375. internal_ctx = context.get_internal_tenant_context()
  1376. if internal_ctx:
  1377. image_volume_context = internal_ctx
  1378. image_volume = self._clone_image_volume(image_volume_context,
  1379. volume,
  1380. image_meta)
  1381. if not image_volume:
  1382. return False
  1383. # The image_owner metadata should be set before uri is added to
  1384. # the image so glance cinder store can check its owner.
  1385. image_volume_meta = {'image_owner': ctx.project_id}
  1386. self.db.volume_metadata_update(image_volume_context,
  1387. image_volume.id,
  1388. image_volume_meta,
  1389. False)
  1390. uri = 'cinder://%s' % image_volume.id
  1391. image_registered = None
  1392. try:
  1393. image_registered = image_service.add_location(
  1394. ctx, image_meta['id'], uri, {})
  1395. except (exception.NotAuthorized, exception.Invalid,
  1396. exception.NotFound):
  1397. LOG.exception('Failed to register image volume location '
  1398. '%(uri)s.', {'uri': uri})
  1399. if not image_registered:
  1400. LOG.warning('Registration of image volume URI %(uri)s '
  1401. 'to image %(image_id)s failed.',
  1402. {'uri': uri, 'image_id': image_meta['id']})
  1403. try:
  1404. self.delete_volume(image_volume_context, image_volume)
  1405. except exception.CinderException:
  1406. LOG.exception('Could not delete failed image volume '
  1407. '%(id)s.', {'id': image_volume.id})
  1408. return False
  1409. image_volume_meta['glance_image_id'] = image_meta['id']
  1410. self.db.volume_metadata_update(image_volume_context,
  1411. image_volume.id,
  1412. image_volume_meta,
  1413. False)
  1414. return True
  1415. def copy_volume_to_image(self, context, volume_id, image_meta):
  1416. """Uploads the specified volume to Glance.
  1417. image_meta is a dictionary containing the following keys:
  1418. 'id', 'container_format', 'disk_format'
  1419. """
  1420. payload = {'volume_id': volume_id, 'image_id': image_meta['id']}
  1421. image_service = None
  1422. try:
  1423. volume = objects.Volume.get_by_id(context, volume_id)
  1424. # NOTE(flaper87): Verify the driver is enabled
  1425. # before going forward. The exception will be caught
  1426. # and the volume status updated.
  1427. utils.require_driver_initialized(self.driver)
  1428. image_service, image_id = \
  1429. glance.get_remote_image_service(context, image_meta['id'])
  1430. if (self.driver.configuration.image_upload_use_cinder_backend
  1431. and self._clone_image_volume_and_add_location(
  1432. context, volume, image_service, image_meta)):
  1433. LOG.debug("Registered image volume location to glance "
  1434. "image-id: %(image_id)s.",
  1435. {'image_id': image_meta['id']},
  1436. resource=volume)
  1437. else:
  1438. self.driver.copy_volume_to_image(context, volume,
  1439. image_service, image_meta)
  1440. LOG.debug("Uploaded volume to glance image-id: %(image_id)s.",
  1441. {'image_id': image_meta['id']},
  1442. resource=volume)
  1443. except Exception as error:
  1444. LOG.error("Upload volume to image encountered an error "
  1445. "(image-id: %(image_id)s).",
  1446. {'image_id': image_meta['id']},
  1447. resource=volume)
  1448. self.message_api.create(
  1449. context,
  1450. message_field.Action.COPY_VOLUME_TO_IMAGE,
  1451. resource_uuid=volume_id,
  1452. exception=error,
  1453. detail=message_field.Detail.FAILED_TO_UPLOAD_VOLUME)
  1454. if image_service is not None:
  1455. # Deletes the image if it is in queued or saving state
  1456. self._delete_image(context, image_meta['id'], image_service)
  1457. with excutils.save_and_reraise_exception():
  1458. payload['message'] = six.text_type(error)
  1459. finally:
  1460. self.db.volume_update_status_based_on_attachment(context,
  1461. volume_id)
  1462. LOG.info("Copy volume to image completed successfully.",
  1463. resource=volume)
  1464. def _delete_image(self, context, image_id, image_service):
  1465. """Deletes an image stuck in queued or saving state."""
  1466. try:
  1467. image_meta = image_service.show(context, image_id)
  1468. image_status = image_meta.get('status')
  1469. if image_status == 'queued' or image_status == 'saving':
  1470. LOG.warning("Deleting image in unexpected status: "
  1471. "%(image_status)s.",
  1472. {'image_status': image_status},
  1473. resource={'type': 'image', 'id': image_id})
  1474. image_service.delete(context, image_id)
  1475. except Exception:
  1476. LOG.warning("Image delete encountered an error.",
  1477. exc_info=True, resource={'type': 'image',
  1478. 'id': image_id})
  1479. def _parse_connection_options(self, context, volume, conn_info):
  1480. # Add qos_specs to connection info
  1481. typeid = volume.volume_type_id
  1482. specs = None
  1483. if typeid:
  1484. res = volume_types.get_volume_type_qos_specs(typeid)
  1485. qos = res['qos_specs']
  1486. # only pass qos_specs that is designated to be consumed by
  1487. # front-end, or both front-end and back-end.
  1488. if qos and qos.get('consumer') in ['front-end', 'both']:
  1489. specs = qos.get('specs')
  1490. # NOTE(mnaser): The following configures for per-GB QoS
  1491. if specs is not None:
  1492. volume_size = int(volume.size)
  1493. tune_opts = ('read_iops_sec', 'read_bytes_sec',
  1494. 'write_iops_sec', 'write_bytes_sec',
  1495. 'total_iops_sec', 'total_bytes_sec')
  1496. for option in tune_opts:
  1497. option_per_gb = '%s_per_gb' % option
  1498. option_per_gb_min = '%s_per_gb_min' % option
  1499. option_max = '%s_max' % option
  1500. if option_per_gb in specs:
  1501. minimum_value = int(specs.pop(option_per_gb_min, 0))
  1502. value = int(specs[option_per_gb]) * volume_size
  1503. per_gb_value = max(minimum_value, value)
  1504. max_value = int(specs.pop(option_max, per_gb_value))
  1505. specs[option] = min(per_gb_value, max_value)
  1506. specs.pop(option_per_gb)
  1507. qos_spec = dict(qos_specs=specs)
  1508. conn_info['data'].update(qos_spec)
  1509. # Add access_mode to connection info
  1510. volume_metadata = volume.admin_metadata
  1511. access_mode = volume_metadata.get('attached_mode')
  1512. if access_mode is None:
  1513. # NOTE(zhiyan): client didn't call 'os-attach' before
  1514. access_mode = ('ro'
  1515. if volume_metadata.get('readonly') == 'True'
  1516. else 'rw')
  1517. conn_info['data']['access_mode'] = access_mode
  1518. # Add encrypted flag to connection_info if not set in the driver.
  1519. if conn_info['data'].get('encrypted') is None:
  1520. encrypted = bool(volume.encryption_key_id)
  1521. conn_info['data']['encrypted'] = encrypted
  1522. # Add discard flag to connection_info if not set in the driver and
  1523. # configured to be reported.
  1524. if conn_info['data'].get('discard') is None:
  1525. discard_supported = (self.driver.configuration
  1526. .safe_get('report_discard_supported'))
  1527. if discard_supported:
  1528. conn_info['data']['discard'] = True
  1529. return conn_info
  1530. def initialize_connection(self, context, volume, connector):
  1531. """Prepare volume for connection from host represented by connector.
  1532. This method calls the driver initialize_connection and returns
  1533. it to the caller. The connector parameter is a dictionary with
  1534. information about the host that will connect to the volume in the
  1535. following format:
  1536. .. code:: json
  1537. {
  1538. "ip": "<ip>",
  1539. "initiator": "<initiator>"
  1540. }
  1541. ip:
  1542. the ip address of the connecting machine
  1543. initiator:
  1544. the iscsi initiator name of the connecting machine. This can be
  1545. None if the connecting machine does not support iscsi connections.
  1546. driver is responsible for doing any necessary security setup and
  1547. returning a connection_info dictionary in the following format:
  1548. .. code:: json
  1549. {
  1550. "driver_volume_type": "<driver_volume_type>",
  1551. "data": "<data>"
  1552. }
  1553. driver_volume_type:
  1554. a string to identify the type of volume. This can be used by the
  1555. calling code to determine the strategy for connecting to the
  1556. volume. This could be 'iscsi', 'rbd', 'sheepdog', etc.
  1557. data:
  1558. this is the data that the calling code will use to connect to the
  1559. volume. Keep in mind that this will be serialized to json in
  1560. various places, so it should not contain any non-json data types.
  1561. """
  1562. # NOTE(flaper87): Verify the driver is enabled
  1563. # before going forward. The exception will be caught
  1564. # and the volume status updated.
  1565. # TODO(jdg): Add deprecation warning
  1566. utils.require_driver_initialized(self.driver)
  1567. try:
  1568. self.driver.validate_connector(connector)
  1569. except exception.InvalidConnectorException as err:
  1570. raise exception.InvalidInput(reason=six.text_type(err))
  1571. except Exception as err:
  1572. err_msg = (_("Validate volume connection failed "
  1573. "(error: %(err)s).") % {'err': six.text_type(err)})
  1574. LOG.exception(err_msg, resource=volume)
  1575. raise exception.VolumeBackendAPIException(data=err_msg)
  1576. try:
  1577. model_update = self.driver.create_export(context.elevated(),
  1578. volume, connector)
  1579. except exception.CinderException as ex:
  1580. msg = _("Create export of volume failed (%s)") % ex.msg
  1581. LOG.exception(msg, resource=volume)
  1582. raise exception.VolumeBackendAPIException(data=msg)
  1583. try:
  1584. if model_update:
  1585. volume.update(model_update)
  1586. volume.save()
  1587. except Exception as ex:
  1588. LOG.exception("Model update failed.", resource=volume)
  1589. try:
  1590. self.driver.remove_export(context.elevated(), volume)
  1591. except Exception:
  1592. LOG.exception('Could not remove export after DB model failed.')
  1593. raise exception.ExportFailure(reason=six.text_type(ex))
  1594. try:
  1595. conn_info = self.driver.initialize_connection(volume, connector)
  1596. except Exception as err:
  1597. err_msg = (_("Driver initialize connection failed "
  1598. "(error: %(err)s).") % {'err': six.text_type(err)})
  1599. LOG.exception(err_msg, resource=volume)
  1600. self.driver.remove_export(context.elevated(), volume)
  1601. raise exception.VolumeBackendAPIException(data=err_msg)
  1602. conn_info = self._parse_connection_options(context, volume, conn_info)
  1603. LOG.info("Initialize volume connection completed successfully.",
  1604. resource=volume)
  1605. return conn_info
  1606. def initialize_connection_snapshot(self, ctxt, snapshot_id, connector):
  1607. utils.require_driver_initialized(self.driver)
  1608. snapshot = objects.Snapshot.get_by_id(ctxt, snapshot_id)
  1609. try:
  1610. self.driver.validate_connector(connector)
  1611. except exception.InvalidConnectorException as err:
  1612. raise exception.InvalidInput(reason=six.text_type(err))
  1613. except Exception as err:
  1614. err_msg = (_("Validate snapshot connection failed "
  1615. "(error: %(err)s).") % {'err': six.text_type(err)})
  1616. LOG.exception(err_msg, resource=snapshot)
  1617. raise exception.VolumeBackendAPIException(data=err_msg)
  1618. model_update = None
  1619. try:
  1620. LOG.debug("Snapshot %s: creating export.", snapshot.id)
  1621. model_update = self.driver.create_export_snapshot(
  1622. ctxt.elevated(), snapshot, connector)
  1623. if model_update:
  1624. snapshot.provider_location = model_update.get(
  1625. 'provider_location', None)
  1626. snapshot.provider_auth = model_update.get(
  1627. 'provider_auth', None)
  1628. snapshot.save()
  1629. except exception.CinderException as ex:
  1630. msg = _("Create export of snapshot failed (%s)") % ex.msg
  1631. LOG.exception(msg, resource=snapshot)
  1632. raise exception.VolumeBackendAPIException(data=msg)
  1633. try:
  1634. if model_update:
  1635. snapshot.update(model_update)
  1636. snapshot.save()
  1637. except exception.CinderException as ex:
  1638. LOG.exception("Model update failed.", resource=snapshot)
  1639. raise exception.ExportFailure(reason=six.text_type(ex))
  1640. try:
  1641. conn = self.driver.initialize_connection_snapshot(snapshot,
  1642. connector)
  1643. except Exception as err:
  1644. try:
  1645. err_msg = (_('Unable to fetch connection information from '
  1646. 'backend: %(err)s') %
  1647. {'err': six.text_type(err)})
  1648. LOG.error(err_msg)
  1649. LOG.debug("Cleaning up failed connect initialization.")
  1650. self.driver.remove_export_snapshot(ctxt.elevated(), snapshot)
  1651. except Exception as ex:
  1652. ex_msg = (_('Error encountered during cleanup '
  1653. 'of a failed attach: %(ex)s') %
  1654. {'ex': six.text_type(ex)})
  1655. LOG.error(ex_msg)
  1656. raise exception.VolumeBackendAPIException(data=ex_msg)
  1657. raise exception.VolumeBackendAPIException(data=err_msg)
  1658. LOG.info("Initialize snapshot connection completed successfully.",
  1659. resource=snapshot)
  1660. return conn
  1661. def terminate_connection(self, context, volume_id, connector, force=False):
  1662. """Cleanup connection from host represented by connector.
  1663. The format of connector is the same as for initialize_connection.
  1664. """
  1665. # NOTE(flaper87): Verify the driver is enabled
  1666. # before going forward. The exception will be caught
  1667. # and the volume status updated.
  1668. utils.require_driver_initialized(self.driver)
  1669. volume_ref = self.db.volume_get(context, volume_id)
  1670. try:
  1671. self.driver.terminate_connection(volume_ref, connector,
  1672. force=force)
  1673. except Exception as err:
  1674. err_msg = (_('Terminate volume connection failed: %(err)s')
  1675. % {'err': six.text_type(err)})
  1676. LOG.exception(err_msg, resource=volume_ref)
  1677. raise exception.VolumeBackendAPIException(data=err_msg)
  1678. LOG.info("Terminate volume connection completed successfully.",
  1679. resource=volume_ref)
  1680. def terminate_connection_snapshot(self, ctxt, snapshot_id,
  1681. connector, force=False):
  1682. utils.require_driver_initialized(self.driver)
  1683. snapshot = objects.Snapshot.get_by_id(ctxt, snapshot_id)
  1684. try:
  1685. self.driver.terminate_connection_snapshot(snapshot, connector,
  1686. force=force)
  1687. except Exception as err:
  1688. err_msg = (_('Terminate snapshot connection failed: %(err)s')
  1689. % {'err': six.text_type(err)})
  1690. LOG.exception(err_msg, resource=snapshot)
  1691. raise exception.VolumeBackendAPIException(data=err_msg)
  1692. LOG.info("Terminate snapshot connection completed successfully.",
  1693. resource=snapshot)
  1694. def remove_export(self, context, volume_id):
  1695. """Removes an export for a volume."""
  1696. utils.require_driver_initialized(self.driver)
  1697. volume_ref = self.db.volume_get(context, volume_id)
  1698. try:
  1699. self.driver.remove_export(context, volume_ref)
  1700. except Exception:
  1701. msg = _("Remove volume export failed.")
  1702. LOG.exception(msg, resource=volume_ref)
  1703. raise exception.VolumeBackendAPIException(data=msg)
  1704. LOG.info("Remove volume export completed successfully.",
  1705. resource=volume_ref)
  1706. def remove_export_snapshot(self, ctxt, snapshot_id):
  1707. """Removes an export for a snapshot."""
  1708. utils.require_driver_initialized(self.driver)
  1709. snapshot = objects.Snapshot.get_by_id(ctxt, snapshot_id)
  1710. try:
  1711. self.driver.remove_export_snapshot(ctxt, snapshot)
  1712. except Exception:
  1713. msg = _("Remove snapshot export failed.")
  1714. LOG.exception(msg, resource=snapshot)
  1715. raise exception.VolumeBackendAPIException(data=msg)
  1716. LOG.info("Remove snapshot export completed successfully.",
  1717. resource=snapshot)
  1718. def accept_transfer(self, context, volume_id, new_user, new_project,
  1719. no_snapshots=False):
  1720. # NOTE(flaper87): Verify the driver is enabled
  1721. # before going forward. The exception will be caught
  1722. # and the volume status updated.
  1723. utils.require_driver_initialized(self.driver)
  1724. # NOTE(jdg): need elevated context as we haven't "given" the vol
  1725. # yet
  1726. volume_ref = self.db.volume_get(context.elevated(), volume_id)
  1727. # NOTE(jdg): Some drivers tie provider info (CHAP) to tenant
  1728. # for those that do allow them to return updated model info
  1729. model_update = self.driver.accept_transfer(context,
  1730. volume_ref,
  1731. new_user,
  1732. new_project)
  1733. if model_update:
  1734. try:
  1735. self.db.volume_update(context.elevated(),
  1736. volume_id,
  1737. model_update)
  1738. except exception.CinderException:
  1739. with excutils.save_and_reraise_exception():
  1740. LOG.exception("Update volume model for "
  1741. "transfer operation failed.",
  1742. resource=volume_ref)
  1743. self.db.volume_update(context.elevated(),
  1744. volume_id,
  1745. {'status': 'error'})
  1746. LOG.info("Transfer volume completed successfully.",
  1747. resource=volume_ref)
  1748. return model_update
  1749. def _connect_device(self, conn):
  1750. use_multipath = self.configuration.use_multipath_for_image_xfer
  1751. device_scan_attempts = self.configuration.num_volume_device_scan_tries
  1752. protocol = conn['driver_volume_type']
  1753. connector = utils.brick_get_connector(
  1754. protocol,
  1755. use_multipath=use_multipath,
  1756. device_scan_attempts=device_scan_attempts,
  1757. conn=conn)
  1758. vol_handle = connector.connect_volume(conn['data'])
  1759. root_access = True
  1760. if not connector.check_valid_device(vol_handle['path'], root_access):
  1761. if isinstance(vol_handle['path'], six.string_types):
  1762. raise exception.DeviceUnavailable(
  1763. path=vol_handle['path'],
  1764. reason=(_("Unable to access the backend storage via the "
  1765. "path %(path)s.") %
  1766. {'path': vol_handle['path']}))
  1767. else:
  1768. raise exception.DeviceUnavailable(
  1769. path=None,
  1770. reason=(_("Unable to access the backend storage via file "
  1771. "handle.")))
  1772. return {'conn': conn, 'device': vol_handle, 'connector': connector}
  1773. def _attach_volume(self, ctxt, volume, properties, remote=False,
  1774. attach_encryptor=False):
  1775. status = volume['status']
  1776. if remote:
  1777. rpcapi = volume_rpcapi.VolumeAPI()
  1778. try:
  1779. conn = rpcapi.initialize_connection(ctxt, volume, properties)
  1780. except Exception:
  1781. with excutils.save_and_reraise_exception():
  1782. LOG.error("Failed to attach volume %(vol)s.",
  1783. {'vol': volume['id']})
  1784. self.db.volume_update(ctxt, volume['id'],
  1785. {'status': status})
  1786. else:
  1787. conn = self.initialize_connection(ctxt, volume, properties)
  1788. attach_info = self._connect_device(conn)
  1789. try:
  1790. if attach_encryptor and (
  1791. volume_types.is_encrypted(ctxt,
  1792. volume.volume_type_id)):
  1793. encryption = self.db.volume_encryption_metadata_get(
  1794. ctxt.elevated(), volume.id)
  1795. if encryption:
  1796. utils.brick_attach_volume_encryptor(ctxt,
  1797. attach_info,
  1798. encryption)
  1799. except Exception:
  1800. with excutils.save_and_reraise_exception():
  1801. LOG.error("Failed to attach volume encryptor"
  1802. " %(vol)s.", {'vol': volume['id']})
  1803. self._detach_volume(ctxt, attach_info, volume, properties,
  1804. force=True)
  1805. return attach_info
  1806. def _detach_volume(self, ctxt, attach_info, volume, properties,
  1807. force=False, remote=False,
  1808. attach_encryptor=False):
  1809. connector = attach_info['connector']
  1810. if attach_encryptor and (
  1811. volume_types.is_encrypted(ctxt,
  1812. volume.volume_type_id)):
  1813. encryption = self.db.volume_encryption_metadata_get(
  1814. ctxt.elevated(), volume.id)
  1815. if encryption:
  1816. utils.brick_detach_volume_encryptor(attach_info, encryption)
  1817. connector.disconnect_volume(attach_info['conn']['data'],
  1818. attach_info['device'], force=force)
  1819. if remote:
  1820. rpcapi = volume_rpcapi.VolumeAPI()
  1821. rpcapi.terminate_connection(ctxt, volume, properties, force=force)
  1822. rpcapi.remove_export(ctxt, volume)
  1823. else:
  1824. try:
  1825. self.terminate_connection(ctxt, volume['id'], properties,
  1826. force=force)
  1827. self.remove_export(ctxt, volume['id'])
  1828. except Exception as err:
  1829. with excutils.save_and_reraise_exception():
  1830. LOG.error('Unable to terminate volume connection: '
  1831. '%(err)s.', {'err': err})
  1832. def _copy_volume_data(self, ctxt, src_vol, dest_vol, remote=None):
  1833. """Copy data from src_vol to dest_vol."""
  1834. LOG.debug('_copy_volume_data %(src)s -> %(dest)s.',
  1835. {'src': src_vol['name'], 'dest': dest_vol['name']})
  1836. attach_encryptor = False
  1837. # If the encryption method or key is changed, we have to
  1838. # copy data through dm-crypt.
  1839. if volume_types.volume_types_encryption_changed(
  1840. ctxt,
  1841. src_vol.volume_type_id,
  1842. dest_vol.volume_type_id):
  1843. attach_encryptor = True
  1844. use_multipath = self.configuration.use_multipath_for_image_xfer
  1845. enforce_multipath = self.configuration.enforce_multipath_for_image_xfer
  1846. properties = utils.brick_get_connector_properties(use_multipath,
  1847. enforce_multipath)
  1848. dest_remote = remote in ['dest', 'both']
  1849. dest_attach_info = self._attach_volume(
  1850. ctxt, dest_vol, properties,
  1851. remote=dest_remote,
  1852. attach_encryptor=attach_encryptor)
  1853. try:
  1854. src_remote = remote in ['src', 'both']
  1855. src_attach_info = self._attach_volume(
  1856. ctxt, src_vol, properties,
  1857. remote=src_remote,
  1858. attach_encryptor=attach_encryptor)
  1859. except Exception:
  1860. with excutils.save_and_reraise_exception():
  1861. LOG.error("Failed to attach source volume for copy.")
  1862. self._detach_volume(ctxt, dest_attach_info, dest_vol,
  1863. properties, remote=dest_remote,
  1864. attach_encryptor=attach_encryptor,
  1865. force=True)
  1866. # Check the backend capabilities of migration destination host.
  1867. rpcapi = volume_rpcapi.VolumeAPI()
  1868. capabilities = rpcapi.get_capabilities(ctxt,
  1869. dest_vol.service_topic_queue,
  1870. False)
  1871. sparse_copy_volume = bool(capabilities and
  1872. capabilities.get('sparse_copy_volume',
  1873. False))
  1874. try:
  1875. size_in_mb = int(src_vol['size']) * units.Ki # vol size is in GB
  1876. vol_utils.copy_volume(src_attach_info['device']['path'],
  1877. dest_attach_info['device']['path'],
  1878. size_in_mb,
  1879. self.configuration.volume_dd_blocksize,
  1880. sparse=sparse_copy_volume)
  1881. except Exception:
  1882. with excutils.save_and_reraise_exception():
  1883. LOG.error("Failed to copy volume %(src)s to %(dest)s.",
  1884. {'src': src_vol['id'], 'dest': dest_vol['id']})
  1885. finally:
  1886. try:
  1887. self._detach_volume(ctxt, dest_attach_info, dest_vol,
  1888. properties, force=True,
  1889. remote=dest_remote,
  1890. attach_encryptor=attach_encryptor)
  1891. finally:
  1892. self._detach_volume(ctxt, src_attach_info, src_vol,
  1893. properties, force=True,
  1894. remote=src_remote,
  1895. attach_encryptor=attach_encryptor)
  1896. def _migrate_volume_generic(self, ctxt, volume, backend, new_type_id):
  1897. rpcapi = volume_rpcapi.VolumeAPI()
  1898. # Create new volume on remote host
  1899. tmp_skip = {'snapshot_id', 'source_volid'}
  1900. skip = {'host', 'cluster_name', 'availability_zone'}
  1901. skip.update(tmp_skip)
  1902. skip.update(self._VOLUME_CLONE_SKIP_PROPERTIES)
  1903. new_vol_values = {k: volume[k] for k in set(volume.keys()) - skip}
  1904. if new_type_id:
  1905. new_vol_values['volume_type_id'] = new_type_id
  1906. if volume_types.volume_types_encryption_changed(
  1907. ctxt, volume.volume_type_id, new_type_id):
  1908. encryption_key_id = vol_utils.create_encryption_key(
  1909. ctxt, self.key_manager, new_type_id)
  1910. new_vol_values['encryption_key_id'] = encryption_key_id
  1911. dst_service = self._get_service(backend['host'])
  1912. new_volume = objects.Volume(
  1913. context=ctxt,
  1914. host=backend['host'],
  1915. availability_zone=dst_service.availability_zone,
  1916. cluster_name=backend.get('cluster_name'),
  1917. status='creating',
  1918. attach_status=fields.VolumeAttachStatus.DETACHED,
  1919. migration_status='target:%s' % volume['id'],
  1920. **new_vol_values
  1921. )
  1922. new_volume.create()
  1923. rpcapi.create_volume(ctxt, new_volume, None, None,
  1924. allow_reschedule=False)
  1925. # Wait for new_volume to become ready
  1926. starttime = time.time()
  1927. deadline = starttime + CONF.migration_create_volume_timeout_secs
  1928. new_volume.refresh()
  1929. tries = 0
  1930. while new_volume.status != 'available':
  1931. tries += 1
  1932. now = time.time()
  1933. if new_volume.status == 'error':
  1934. msg = _("failed to create new_volume on destination")
  1935. self._clean_temporary_volume(ctxt, volume,
  1936. new_volume,
  1937. clean_db_only=True)
  1938. raise exception.VolumeMigrationFailed(reason=msg)
  1939. elif now > deadline:
  1940. msg = _("timeout creating new_volume on destination")
  1941. self._clean_temporary_volume(ctxt, volume,
  1942. new_volume,
  1943. clean_db_only=True)
  1944. raise exception.VolumeMigrationFailed(reason=msg)
  1945. else:
  1946. time.sleep(tries ** 2)
  1947. new_volume.refresh()
  1948. # Set skipped value to avoid calling
  1949. # function except for _create_raw_volume
  1950. tmp_skipped_values = {k: volume[k] for k in tmp_skip if volume.get(k)}
  1951. if tmp_skipped_values:
  1952. new_volume.update(tmp_skipped_values)
  1953. new_volume.save()
  1954. # Copy the source volume to the destination volume
  1955. try:
  1956. attachments = volume.volume_attachment
  1957. if not attachments:
  1958. # Pre- and post-copy driver-specific actions
  1959. self.driver.before_volume_copy(ctxt, volume, new_volume,
  1960. remote='dest')
  1961. self._copy_volume_data(ctxt, volume, new_volume, remote='dest')
  1962. self.driver.after_volume_copy(ctxt, volume, new_volume,
  1963. remote='dest')
  1964. # The above call is synchronous so we complete the migration
  1965. self.migrate_volume_completion(ctxt, volume, new_volume,
  1966. error=False)
  1967. else:
  1968. nova_api = compute.API()
  1969. # This is an async call to Nova, which will call the completion
  1970. # when it's done
  1971. for attachment in attachments:
  1972. instance_uuid = attachment['instance_uuid']
  1973. nova_api.update_server_volume(ctxt, instance_uuid,
  1974. volume.id,
  1975. new_volume.id)
  1976. except Exception:
  1977. with excutils.save_and_reraise_exception():
  1978. LOG.exception(
  1979. "Failed to copy volume %(vol1)s to %(vol2)s", {
  1980. 'vol1': volume.id, 'vol2': new_volume.id})
  1981. self._clean_temporary_volume(ctxt, volume,
  1982. new_volume)
  1983. def _clean_temporary_volume(self, ctxt, volume, new_volume,
  1984. clean_db_only=False):
  1985. # If we're in the migrating phase, we need to cleanup
  1986. # destination volume because source volume is remaining
  1987. if volume.migration_status == 'migrating':
  1988. try:
  1989. if clean_db_only:
  1990. # The temporary volume is not created, only DB data
  1991. # is created
  1992. new_volume.destroy()
  1993. else:
  1994. # The temporary volume is already created
  1995. rpcapi = volume_rpcapi.VolumeAPI()
  1996. rpcapi.delete_volume(ctxt, new_volume)
  1997. except exception.VolumeNotFound:
  1998. LOG.info("Couldn't find the temporary volume "
  1999. "%(vol)s in the database. There is no need "
  2000. "to clean up this volume.",
  2001. {'vol': new_volume.id})
  2002. else:
  2003. # If we're in the completing phase don't delete the
  2004. # destination because we may have already deleted the
  2005. # source! But the migration_status in database should
  2006. # be cleared to handle volume after migration failure
  2007. try:
  2008. new_volume.migration_status = None
  2009. new_volume.save()
  2010. except exception.VolumeNotFound:
  2011. LOG.info("Couldn't find destination volume "
  2012. "%(vol)s in the database. The entry might be "
  2013. "successfully deleted during migration "
  2014. "completion phase.",
  2015. {'vol': new_volume.id})
  2016. LOG.warning("Failed to migrate volume. The destination "
  2017. "volume %(vol)s is not deleted since the "
  2018. "source volume may have been deleted.",
  2019. {'vol': new_volume.id})
  2020. def migrate_volume_completion(self, ctxt, volume, new_volume, error=False):
  2021. try:
  2022. # NOTE(flaper87): Verify the driver is enabled
  2023. # before going forward. The exception will be caught
  2024. # and the migration status updated.
  2025. utils.require_driver_initialized(self.driver)
  2026. except exception.DriverNotInitialized:
  2027. with excutils.save_and_reraise_exception():
  2028. volume.migration_status = 'error'
  2029. volume.save()
  2030. # NOTE(jdg): Things get a little hairy in here and we do a lot of
  2031. # things based on volume previous-status and current-status. At some
  2032. # point this should all be reworked but for now we need to maintain
  2033. # backward compatibility and NOT change the API so we're going to try
  2034. # and make this work best we can
  2035. LOG.debug("migrate_volume_completion: completing migration for "
  2036. "volume %(vol1)s (temporary volume %(vol2)s",
  2037. {'vol1': volume.id, 'vol2': new_volume.id})
  2038. rpcapi = volume_rpcapi.VolumeAPI()
  2039. orig_volume_status = volume.previous_status
  2040. if error:
  2041. LOG.info("migrate_volume_completion is cleaning up an error "
  2042. "for volume %(vol1)s (temporary volume %(vol2)s",
  2043. {'vol1': volume['id'], 'vol2': new_volume.id})
  2044. rpcapi.delete_volume(ctxt, new_volume)
  2045. updates = {'migration_status': 'error',
  2046. 'status': orig_volume_status}
  2047. volume.update(updates)
  2048. volume.save()
  2049. return volume.id
  2050. volume.migration_status = 'completing'
  2051. volume.save()
  2052. volume_attachments = []
  2053. # NOTE(jdg): With new attach flow, we deleted the attachment, so the
  2054. # original volume should now be listed as available, we still need to
  2055. # do the magic swappy thing of name.id etc but we're done with the
  2056. # original attachment record
  2057. # In the "old flow" at this point the orig_volume_status will be in-use
  2058. # and the current status will be retyping. This is sort of a
  2059. # misleading deal, because Nova has already called terminate
  2060. # connection
  2061. # New Attach Flow, Nova has gone ahead and deleted the attachemnt, this
  2062. # is the source/original volume, we've already migrated the data, we're
  2063. # basically done with it at this point. We don't need to issue the
  2064. # detach to toggle the status
  2065. if orig_volume_status == 'in-use' and volume.status != 'available':
  2066. for attachment in volume.volume_attachment:
  2067. # Save the attachments the volume currently have
  2068. volume_attachments.append(attachment)
  2069. try:
  2070. self.detach_volume(ctxt, volume.id, attachment.id)
  2071. except Exception as ex:
  2072. LOG.error("Detach migration source volume "
  2073. "%(volume.id)s from attachment "
  2074. "%(attachment.id)s failed: %(err)s",
  2075. {'err': ex,
  2076. 'volume.id': volume.id,
  2077. 'attachment.id': attachment.id},
  2078. resource=volume)
  2079. # Give driver (new_volume) a chance to update things as needed
  2080. # after a successful migration.
  2081. # Note this needs to go through rpc to the host of the new volume
  2082. # the current host and driver object is for the "existing" volume.
  2083. rpcapi.update_migrated_volume(ctxt, volume, new_volume,
  2084. orig_volume_status)
  2085. volume.refresh()
  2086. new_volume.refresh()
  2087. # Swap src and dest DB records so we can continue using the src id and
  2088. # asynchronously delete the destination id
  2089. updated_new = volume.finish_volume_migration(new_volume)
  2090. updates = {'status': orig_volume_status,
  2091. 'previous_status': volume.status,
  2092. 'migration_status': 'success'}
  2093. # NOTE(jdg): With new attachment API's nova will delete the
  2094. # attachment for the source volume for us before calling the
  2095. # migration-completion, now we just need to do the swapping on the
  2096. # volume record, but don't jack with the attachments other than
  2097. # updating volume_id
  2098. # In the old flow at this point the volumes are in attaching and
  2099. # deleting status (dest/new is deleting, but we've done our magic
  2100. # swappy thing so it's a bit confusing, but it does unwind properly
  2101. # when you step through it)
  2102. # In the new flow we simlified this and we don't need it, instead of
  2103. # doing a bunch of swapping we just do attachment-create/delete on the
  2104. # nova side, and then here we just do the ID swaps that are necessary
  2105. # to maintain the old beahvior
  2106. # Restore the attachments for old flow use-case
  2107. if orig_volume_status == 'in-use' and volume.status in ['available',
  2108. 'reserved',
  2109. 'attaching']:
  2110. for attachment in volume_attachments:
  2111. LOG.debug('Re-attaching: %s', attachment)
  2112. # This is just a db state toggle, the volume is actually
  2113. # already attach and in-use, new attachment flow won't allow
  2114. # this
  2115. rpcapi.attach_volume(ctxt, volume,
  2116. attachment.instance_uuid,
  2117. attachment.attached_host,
  2118. attachment.mountpoint,
  2119. attachment.attach_mode or 'rw')
  2120. # At this point we now have done almost all of our swapping and
  2121. # state-changes. The target volume is now marked back to
  2122. # "in-use" the destination/worker volume is now in deleting
  2123. # state and the next steps will finish the deletion steps
  2124. volume.update(updates)
  2125. volume.save()
  2126. # Asynchronous deletion of the source volume in the back-end (now
  2127. # pointed by the target volume id)
  2128. try:
  2129. rpcapi.delete_volume(ctxt, updated_new)
  2130. except Exception as ex:
  2131. LOG.error('Failed to request async delete of migration source '
  2132. 'vol %(vol)s: %(err)s',
  2133. {'vol': volume.id, 'err': ex})
  2134. # For the new flow this is really the key part. We just use the
  2135. # attachments to the worker/destination volumes that we created and
  2136. # used for the libvirt migration and we'll just swap their volume_id
  2137. # entries to coorespond with the volume.id swap we did
  2138. for attachment in VA_LIST.get_all_by_volume_id(ctxt, updated_new.id):
  2139. attachment.volume_id = volume.id
  2140. attachment.save()
  2141. # Phewww.. that was easy! Once we get to a point where the old attach
  2142. # flow can go away we really should rewrite all of this.
  2143. LOG.info("Complete-Migrate volume completed successfully.",
  2144. resource=volume)
  2145. return volume.id
  2146. def migrate_volume(self, ctxt, volume, host, force_host_copy=False,
  2147. new_type_id=None):
  2148. """Migrate the volume to the specified host (called on source host)."""
  2149. try:
  2150. # NOTE(flaper87): Verify the driver is enabled
  2151. # before going forward. The exception will be caught
  2152. # and the migration status updated.
  2153. utils.require_driver_initialized(self.driver)
  2154. except exception.DriverNotInitialized:
  2155. with excutils.save_and_reraise_exception():
  2156. volume.migration_status = 'error'
  2157. volume.save()
  2158. model_update = None
  2159. moved = False
  2160. status_update = None
  2161. if volume.status in ('retyping', 'maintenance'):
  2162. status_update = {'status': volume.previous_status}
  2163. volume.migration_status = 'migrating'
  2164. volume.save()
  2165. if not force_host_copy and new_type_id is None:
  2166. try:
  2167. LOG.debug("Issue driver.migrate_volume.", resource=volume)
  2168. moved, model_update = self.driver.migrate_volume(ctxt,
  2169. volume,
  2170. host)
  2171. if moved:
  2172. dst_service = self._get_service(host['host'])
  2173. updates = {
  2174. 'host': host['host'],
  2175. 'cluster_name': host.get('cluster_name'),
  2176. 'migration_status': 'success',
  2177. 'availability_zone': dst_service.availability_zone,
  2178. 'previous_status': volume.status,
  2179. }
  2180. if status_update:
  2181. updates.update(status_update)
  2182. if model_update:
  2183. updates.update(model_update)
  2184. volume.update(updates)
  2185. volume.save()
  2186. except Exception:
  2187. with excutils.save_and_reraise_exception():
  2188. updates = {'migration_status': 'error'}
  2189. if status_update:
  2190. updates.update(status_update)
  2191. volume.update(updates)
  2192. volume.save()
  2193. if not moved:
  2194. try:
  2195. self._migrate_volume_generic(ctxt, volume, host, new_type_id)
  2196. except Exception:
  2197. with excutils.save_and_reraise_exception():
  2198. updates = {'migration_status': 'error'}
  2199. if status_update:
  2200. updates.update(status_update)
  2201. volume.update(updates)
  2202. volume.save()
  2203. LOG.info("Migrate volume completed successfully.",
  2204. resource=volume)
  2205. def _report_driver_status(self, context):
  2206. # It's possible during live db migration that the self.service_uuid
  2207. # value isn't set (we didn't restart services), so we'll go ahead
  2208. # and make this a part of the service periodic
  2209. if not self.service_uuid:
  2210. # We hack this with a try/except for unit tests temporarily
  2211. try:
  2212. service = self._get_service()
  2213. self.service_uuid = service.uuid
  2214. except exception.ServiceNotFound:
  2215. LOG.warning("Attempt to update service_uuid "
  2216. "resulted in a Service NotFound "
  2217. "exception, service_uuid field on "
  2218. "volumes will be NULL.")
  2219. if not self.driver.initialized:
  2220. if self.driver.configuration.config_group is None:
  2221. config_group = ''
  2222. else:
  2223. config_group = ('(config name %s)' %
  2224. self.driver.configuration.config_group)
  2225. LOG.warning("Update driver status failed: %(config_group)s "
  2226. "is uninitialized.",
  2227. {'config_group': config_group},
  2228. resource={'type': 'driver',
  2229. 'id': self.driver.__class__.__name__})
  2230. else:
  2231. volume_stats = self.driver.get_volume_stats(refresh=True)
  2232. if self.extra_capabilities:
  2233. volume_stats.update(self.extra_capabilities)
  2234. if volume_stats:
  2235. # NOTE(xyang): If driver reports replication_status to be
  2236. # 'error' in volume_stats, get model updates from driver
  2237. # and update db
  2238. if volume_stats.get('replication_status') == (
  2239. fields.ReplicationStatus.ERROR):
  2240. filters = self._get_cluster_or_host_filters()
  2241. groups = objects.GroupList.get_all_replicated(
  2242. context, filters=filters)
  2243. group_model_updates, volume_model_updates = (
  2244. self.driver.get_replication_error_status(context,
  2245. groups))
  2246. for grp_update in group_model_updates:
  2247. try:
  2248. grp_obj = objects.Group.get_by_id(
  2249. context, grp_update['group_id'])
  2250. grp_obj.update(grp_update)
  2251. grp_obj.save()
  2252. except exception.GroupNotFound:
  2253. # Group may be deleted already. Log a warning
  2254. # and continue.
  2255. LOG.warning("Group %(grp)s not found while "
  2256. "updating driver status.",
  2257. {'grp': grp_update['group_id']},
  2258. resource={
  2259. 'type': 'group',
  2260. 'id': grp_update['group_id']})
  2261. for vol_update in volume_model_updates:
  2262. try:
  2263. vol_obj = objects.Volume.get_by_id(
  2264. context, vol_update['volume_id'])
  2265. vol_obj.update(vol_update)
  2266. vol_obj.save()
  2267. except exception.VolumeNotFound:
  2268. # Volume may be deleted already. Log a warning
  2269. # and continue.
  2270. LOG.warning("Volume %(vol)s not found while "
  2271. "updating driver status.",
  2272. {'vol': vol_update['volume_id']},
  2273. resource={
  2274. 'type': 'volume',
  2275. 'id': vol_update['volume_id']})
  2276. # Append volume stats with 'allocated_capacity_gb'
  2277. self._append_volume_stats(volume_stats)
  2278. # Append filter and goodness function if needed
  2279. volume_stats = (
  2280. self._append_filter_goodness_functions(volume_stats))
  2281. # queue it to be sent to the Schedulers.
  2282. self.update_service_capabilities(volume_stats)
  2283. def _append_volume_stats(self, vol_stats):
  2284. pools = vol_stats.get('pools', None)
  2285. if pools:
  2286. if isinstance(pools, list):
  2287. for pool in pools:
  2288. pool_name = pool['pool_name']
  2289. try:
  2290. pool_stats = self.stats['pools'][pool_name]
  2291. except KeyError:
  2292. # Pool not found in volume manager
  2293. pool_stats = dict(allocated_capacity_gb=0)
  2294. pool.update(pool_stats)
  2295. else:
  2296. raise exception.ProgrammingError(
  2297. reason='Pools stats reported by the driver are not '
  2298. 'reported in a list')
  2299. # For drivers that are not reporting their stats by pool we will use
  2300. # the data from the special fixed pool created by
  2301. # _count_allocated_capacity.
  2302. elif self.stats.get('pools'):
  2303. vol_stats.update(next(iter(self.stats['pools'].values())))
  2304. # This is a special subcase of the above no pool case that happens when
  2305. # we don't have any volumes yet.
  2306. else:
  2307. vol_stats.update(self.stats)
  2308. vol_stats.pop('pools', None)
  2309. def _append_filter_goodness_functions(self, volume_stats):
  2310. """Returns volume_stats updated as needed."""
  2311. # Append filter_function if needed
  2312. if 'filter_function' not in volume_stats:
  2313. volume_stats['filter_function'] = (
  2314. self.driver.get_filter_function())
  2315. # Append goodness_function if needed
  2316. if 'goodness_function' not in volume_stats:
  2317. volume_stats['goodness_function'] = (
  2318. self.driver.get_goodness_function())
  2319. return volume_stats
  2320. @periodic_task.periodic_task
  2321. def publish_service_capabilities(self, context):
  2322. """Collect driver status and then publish."""
  2323. self._report_driver_status(context)
  2324. self._publish_service_capabilities(context)
  2325. def _notify_about_volume_usage(self,
  2326. context,
  2327. volume,
  2328. event_suffix,
  2329. extra_usage_info=None):
  2330. vol_utils.notify_about_volume_usage(
  2331. context, volume, event_suffix,
  2332. extra_usage_info=extra_usage_info, host=self.host)
  2333. def _notify_about_snapshot_usage(self,
  2334. context,
  2335. snapshot,
  2336. event_suffix,
  2337. extra_usage_info=None):
  2338. vol_utils.notify_about_snapshot_usage(
  2339. context, snapshot, event_suffix,
  2340. extra_usage_info=extra_usage_info, host=self.host)
  2341. def _notify_about_group_usage(self,
  2342. context,
  2343. group,
  2344. event_suffix,
  2345. volumes=None,
  2346. extra_usage_info=None):
  2347. vol_utils.notify_about_group_usage(
  2348. context, group, event_suffix,
  2349. extra_usage_info=extra_usage_info, host=self.host)
  2350. if not volumes:
  2351. volumes = objects.VolumeList.get_all_by_generic_group(
  2352. context, group.id)
  2353. if volumes:
  2354. for volume in volumes:
  2355. vol_utils.notify_about_volume_usage(
  2356. context, volume, event_suffix,
  2357. extra_usage_info=extra_usage_info, host=self.host)
  2358. def _notify_about_group_snapshot_usage(self,
  2359. context,
  2360. group_snapshot,
  2361. event_suffix,
  2362. snapshots=None,
  2363. extra_usage_info=None):
  2364. vol_utils.notify_about_group_snapshot_usage(
  2365. context, group_snapshot, event_suffix,
  2366. extra_usage_info=extra_usage_info, host=self.host)
  2367. if not snapshots:
  2368. snapshots = objects.SnapshotList.get_all_for_group_snapshot(
  2369. context, group_snapshot.id)
  2370. if snapshots:
  2371. for snapshot in snapshots:
  2372. vol_utils.notify_about_snapshot_usage(
  2373. context, snapshot, event_suffix,
  2374. extra_usage_info=extra_usage_info, host=self.host)
  2375. def extend_volume(self, context, volume, new_size, reservations):
  2376. try:
  2377. # NOTE(flaper87): Verify the driver is enabled
  2378. # before going forward. The exception will be caught
  2379. # and the volume status updated.
  2380. utils.require_driver_initialized(self.driver)
  2381. except exception.DriverNotInitialized:
  2382. with excutils.save_and_reraise_exception():
  2383. volume.status = 'error_extending'
  2384. volume.save()
  2385. project_id = volume.project_id
  2386. size_increase = (int(new_size)) - volume.size
  2387. self._notify_about_volume_usage(context, volume, "resize.start")
  2388. try:
  2389. self.driver.extend_volume(volume, new_size)
  2390. except exception.TargetUpdateFailed:
  2391. # We just want to log this but continue on with quota commit
  2392. LOG.warning('Volume extended but failed to update target.')
  2393. except Exception:
  2394. LOG.exception("Extend volume failed.",
  2395. resource=volume)
  2396. self.message_api.create(
  2397. context,
  2398. message_field.Action.EXTEND_VOLUME,
  2399. resource_uuid=volume.id,
  2400. detail=message_field.Detail.DRIVER_FAILED_EXTEND)
  2401. try:
  2402. self.db.volume_update(context, volume.id,
  2403. {'status': 'error_extending'})
  2404. raise exception.CinderException(_("Volume %s: Error trying "
  2405. "to extend volume") %
  2406. volume.id)
  2407. finally:
  2408. QUOTAS.rollback(context, reservations, project_id=project_id)
  2409. return
  2410. QUOTAS.commit(context, reservations, project_id=project_id)
  2411. attachments = volume.volume_attachment
  2412. if not attachments:
  2413. orig_volume_status = 'available'
  2414. else:
  2415. orig_volume_status = 'in-use'
  2416. volume.update({'size': int(new_size), 'status': orig_volume_status})
  2417. volume.save()
  2418. if orig_volume_status == 'in-use':
  2419. nova_api = compute.API()
  2420. instance_uuids = [attachment.instance_uuid
  2421. for attachment in attachments]
  2422. nova_api.extend_volume(context, instance_uuids, volume.id)
  2423. pool = vol_utils.extract_host(volume.host, 'pool')
  2424. if pool is None:
  2425. # Legacy volume, put them into default pool
  2426. pool = self.driver.configuration.safe_get(
  2427. 'volume_backend_name') or vol_utils.extract_host(
  2428. volume.host, 'pool', True)
  2429. try:
  2430. self.stats['pools'][pool]['allocated_capacity_gb'] += size_increase
  2431. except KeyError:
  2432. self.stats['pools'][pool] = dict(
  2433. allocated_capacity_gb=size_increase)
  2434. self._notify_about_volume_usage(
  2435. context, volume, "resize.end",
  2436. extra_usage_info={'size': int(new_size)})
  2437. LOG.info("Extend volume completed successfully.",
  2438. resource=volume)
  2439. def _is_our_backend(self, host, cluster_name):
  2440. return ((not cluster_name and
  2441. vol_utils.hosts_are_equivalent(self.driver.host, host)) or
  2442. (cluster_name and
  2443. vol_utils.hosts_are_equivalent(self.driver.cluster_name,
  2444. cluster_name)))
  2445. def retype(self, context, volume, new_type_id, host,
  2446. migration_policy='never', reservations=None,
  2447. old_reservations=None):
  2448. def _retype_error(context, volume, old_reservations,
  2449. new_reservations, status_update):
  2450. try:
  2451. volume.update(status_update)
  2452. volume.save()
  2453. finally:
  2454. if old_reservations:
  2455. QUOTAS.rollback(context, old_reservations)
  2456. if new_reservations:
  2457. QUOTAS.rollback(context, new_reservations)
  2458. previous_status = (
  2459. volume.previous_status or volume.status)
  2460. status_update = {'status': previous_status}
  2461. if context.project_id != volume.project_id:
  2462. project_id = volume.project_id
  2463. else:
  2464. project_id = context.project_id
  2465. try:
  2466. # NOTE(flaper87): Verify the driver is enabled
  2467. # before going forward. The exception will be caught
  2468. # and the volume status updated.
  2469. utils.require_driver_initialized(self.driver)
  2470. except exception.DriverNotInitialized:
  2471. with excutils.save_and_reraise_exception():
  2472. # NOTE(flaper87): Other exceptions in this method don't
  2473. # set the volume status to error. Should that be done
  2474. # here? Setting the volume back to it's original status
  2475. # for now.
  2476. volume.update(status_update)
  2477. volume.save()
  2478. # We already got the new reservations
  2479. new_reservations = reservations
  2480. # If volume types have the same contents, no need to do anything.
  2481. # Use the admin contex to be able to access volume extra_specs
  2482. retyped = False
  2483. diff, all_equal = volume_types.volume_types_diff(
  2484. context.elevated(), volume.volume_type_id, new_type_id)
  2485. if all_equal:
  2486. retyped = True
  2487. # Call driver to try and change the type
  2488. retype_model_update = None
  2489. # NOTE(jdg): Check to see if the destination host or cluster (depending
  2490. # if it's the volume is in a clustered backend or not) is the same as
  2491. # the current. If it's not don't call the driver.retype method,
  2492. # otherwise drivers that implement retype may report success, but it's
  2493. # invalid in the case of a migrate.
  2494. # We assume that those that support pools do this internally
  2495. # so we strip off the pools designation
  2496. if (not retyped and
  2497. not diff.get('encryption') and
  2498. self._is_our_backend(host['host'], host.get('cluster_name'))):
  2499. try:
  2500. new_type = volume_types.get_volume_type(context.elevated(),
  2501. new_type_id)
  2502. with volume.obj_as_admin():
  2503. ret = self.driver.retype(context,
  2504. volume,
  2505. new_type,
  2506. diff,
  2507. host)
  2508. # Check if the driver retype provided a model update or
  2509. # just a retype indication
  2510. if type(ret) == tuple:
  2511. retyped, retype_model_update = ret
  2512. else:
  2513. retyped = ret
  2514. if retyped:
  2515. LOG.info("Volume %s: retyped successfully.", volume.id)
  2516. except Exception:
  2517. retyped = False
  2518. LOG.exception("Volume %s: driver error when trying to "
  2519. "retype, falling back to generic "
  2520. "mechanism.", volume.id)
  2521. # We could not change the type, so we need to migrate the volume, where
  2522. # the destination volume will be of the new type
  2523. if not retyped:
  2524. if migration_policy == 'never':
  2525. _retype_error(context, volume, old_reservations,
  2526. new_reservations, status_update)
  2527. msg = _("Retype requires migration but is not allowed.")
  2528. raise exception.VolumeMigrationFailed(reason=msg)
  2529. snaps = objects.SnapshotList.get_all_for_volume(context,
  2530. volume.id)
  2531. if snaps:
  2532. _retype_error(context, volume, old_reservations,
  2533. new_reservations, status_update)
  2534. msg = _("Volume must not have snapshots.")
  2535. LOG.error(msg)
  2536. raise exception.InvalidVolume(reason=msg)
  2537. # Don't allow volume with replicas to be migrated
  2538. rep_status = volume.replication_status
  2539. if(rep_status is not None and rep_status not in
  2540. [fields.ReplicationStatus.DISABLED,
  2541. fields.ReplicationStatus.NOT_CAPABLE]):
  2542. _retype_error(context, volume, old_reservations,
  2543. new_reservations, status_update)
  2544. msg = _("Volume must not be replicated.")
  2545. LOG.error(msg)
  2546. raise exception.InvalidVolume(reason=msg)
  2547. volume.migration_status = 'starting'
  2548. volume.save()
  2549. try:
  2550. self.migrate_volume(context, volume, host,
  2551. new_type_id=new_type_id)
  2552. except Exception:
  2553. with excutils.save_and_reraise_exception():
  2554. _retype_error(context, volume, old_reservations,
  2555. new_reservations, status_update)
  2556. else:
  2557. model_update = {'volume_type_id': new_type_id,
  2558. 'host': host['host'],
  2559. 'cluster_name': host.get('cluster_name'),
  2560. 'status': status_update['status']}
  2561. if retype_model_update:
  2562. model_update.update(retype_model_update)
  2563. self._set_replication_status(diff, model_update)
  2564. volume.update(model_update)
  2565. volume.save()
  2566. if old_reservations:
  2567. QUOTAS.commit(context, old_reservations, project_id=project_id)
  2568. if new_reservations:
  2569. QUOTAS.commit(context, new_reservations, project_id=project_id)
  2570. self._notify_about_volume_usage(
  2571. context, volume, "retype",
  2572. extra_usage_info={'volume_type': new_type_id})
  2573. self.publish_service_capabilities(context)
  2574. LOG.info("Retype volume completed successfully.",
  2575. resource=volume)
  2576. @staticmethod
  2577. def _set_replication_status(diff, model_update):
  2578. """Update replication_status in model_update if it has changed."""
  2579. if not diff or model_update.get('replication_status'):
  2580. return
  2581. diff_specs = diff.get('extra_specs', {})
  2582. replication_diff = diff_specs.get('replication_enabled')
  2583. if replication_diff:
  2584. is_replicated = vol_utils.is_boolean_str(replication_diff[1])
  2585. if is_replicated:
  2586. replication_status = fields.ReplicationStatus.ENABLED
  2587. else:
  2588. replication_status = fields.ReplicationStatus.DISABLED
  2589. model_update['replication_status'] = replication_status
  2590. def manage_existing(self, ctxt, volume, ref=None):
  2591. vol_ref = self._run_manage_existing_flow_engine(
  2592. ctxt, volume, ref)
  2593. self._update_stats_for_managed(vol_ref)
  2594. LOG.info("Manage existing volume completed successfully.",
  2595. resource=vol_ref)
  2596. return vol_ref.id
  2597. def _update_stats_for_managed(self, volume_reference):
  2598. # Update volume stats
  2599. pool = vol_utils.extract_host(volume_reference.host, 'pool')
  2600. if pool is None:
  2601. # Legacy volume, put them into default pool
  2602. pool = self.driver.configuration.safe_get(
  2603. 'volume_backend_name') or vol_utils.extract_host(
  2604. volume_reference.host, 'pool', True)
  2605. try:
  2606. self.stats['pools'][pool]['allocated_capacity_gb'] \
  2607. += volume_reference.size
  2608. except KeyError:
  2609. self.stats['pools'][pool] = dict(
  2610. allocated_capacity_gb=volume_reference.size)
  2611. def _run_manage_existing_flow_engine(self, ctxt, volume, ref):
  2612. try:
  2613. flow_engine = manage_existing.get_flow(
  2614. ctxt,
  2615. self.db,
  2616. self.driver,
  2617. self.host,
  2618. volume,
  2619. ref,
  2620. )
  2621. except Exception:
  2622. msg = _("Failed to create manage_existing flow.")
  2623. LOG.exception(msg, resource={'type': 'volume', 'id': volume.id})
  2624. raise exception.CinderException(msg)
  2625. with flow_utils.DynamicLogListener(flow_engine, logger=LOG):
  2626. flow_engine.run()
  2627. # Fetch created volume from storage
  2628. vol_ref = flow_engine.storage.fetch('volume')
  2629. return vol_ref
  2630. def _get_cluster_or_host_filters(self):
  2631. if self.cluster:
  2632. filters = {'cluster_name': self.cluster}
  2633. else:
  2634. filters = {'host': self.host}
  2635. return filters
  2636. def _get_my_volumes_summary(self, ctxt):
  2637. filters = self._get_cluster_or_host_filters()
  2638. return objects.VolumeList.get_volume_summary(ctxt, False, filters)
  2639. def _get_my_snapshots_summary(self, ctxt):
  2640. filters = self._get_cluster_or_host_filters()
  2641. return objects.SnapshotList.get_snapshot_summary(ctxt, False, filters)
  2642. def _get_my_resources(self, ctxt, ovo_class_list, limit=None, offset=None):
  2643. filters = self._get_cluster_or_host_filters()
  2644. return getattr(ovo_class_list, 'get_all')(ctxt, filters=filters,
  2645. limit=limit,
  2646. offset=offset)
  2647. def _get_my_volumes(self, ctxt, limit=None, offset=None):
  2648. return self._get_my_resources(ctxt, objects.VolumeList,
  2649. limit, offset)
  2650. def _get_my_snapshots(self, ctxt, limit=None, offset=None):
  2651. return self._get_my_resources(ctxt, objects.SnapshotList,
  2652. limit, offset)
  2653. def get_manageable_volumes(self, ctxt, marker, limit, offset, sort_keys,
  2654. sort_dirs, want_objects=False):
  2655. try:
  2656. utils.require_driver_initialized(self.driver)
  2657. except exception.DriverNotInitialized:
  2658. with excutils.save_and_reraise_exception():
  2659. LOG.exception("Listing manageable volumes failed, due "
  2660. "to uninitialized driver.")
  2661. cinder_volumes = self._get_my_volumes(ctxt)
  2662. try:
  2663. driver_entries = self.driver.get_manageable_volumes(
  2664. cinder_volumes, marker, limit, offset, sort_keys, sort_dirs)
  2665. if want_objects:
  2666. driver_entries = (objects.ManageableVolumeList.
  2667. from_primitives(ctxt, driver_entries))
  2668. except AttributeError:
  2669. LOG.debug('Driver does not support listing manageable volumes.')
  2670. return []
  2671. except Exception:
  2672. with excutils.save_and_reraise_exception():
  2673. LOG.exception("Listing manageable volumes failed, due "
  2674. "to driver error.")
  2675. return driver_entries
  2676. def create_group(self, context, group):
  2677. """Creates the group."""
  2678. context = context.elevated()
  2679. # Make sure the host in the DB matches our own when clustered
  2680. self._set_resource_host(group)
  2681. status = fields.GroupStatus.AVAILABLE
  2682. model_update = None
  2683. self._notify_about_group_usage(context, group, "create.start")
  2684. try:
  2685. utils.require_driver_initialized(self.driver)
  2686. LOG.info("Group %s: creating", group.name)
  2687. try:
  2688. model_update = self.driver.create_group(context, group)
  2689. except NotImplementedError:
  2690. if not group_types.is_default_cgsnapshot_type(
  2691. group.group_type_id):
  2692. model_update = self._create_group_generic(context, group)
  2693. else:
  2694. cg, __ = self._convert_group_to_cg(group, [])
  2695. model_update = self.driver.create_consistencygroup(
  2696. context, cg)
  2697. if model_update:
  2698. if (model_update['status'] ==
  2699. fields.GroupStatus.ERROR):
  2700. msg = (_('Create group failed.'))
  2701. LOG.error(msg,
  2702. resource={'type': 'group',
  2703. 'id': group.id})
  2704. raise exception.VolumeDriverException(message=msg)
  2705. else:
  2706. group.update(model_update)
  2707. group.save()
  2708. except Exception:
  2709. with excutils.save_and_reraise_exception():
  2710. group.status = fields.GroupStatus.ERROR
  2711. group.save()
  2712. LOG.error("Group %s: create failed",
  2713. group.name)
  2714. group.status = status
  2715. group.created_at = timeutils.utcnow()
  2716. group.save()
  2717. LOG.info("Group %s: created successfully", group.name)
  2718. self._notify_about_group_usage(context, group, "create.end")
  2719. LOG.info("Create group completed successfully.",
  2720. resource={'type': 'group',
  2721. 'id': group.id})
  2722. return group
  2723. def create_group_from_src(self, context, group,
  2724. group_snapshot=None, source_group=None):
  2725. """Creates the group from source.
  2726. The source can be a group snapshot or a source group.
  2727. """
  2728. source_name = None
  2729. snapshots = None
  2730. source_vols = None
  2731. try:
  2732. volumes = objects.VolumeList.get_all_by_generic_group(context,
  2733. group.id)
  2734. if group_snapshot:
  2735. try:
  2736. # Check if group_snapshot still exists
  2737. group_snapshot.refresh()
  2738. except exception.GroupSnapshotNotFound:
  2739. LOG.error("Create group from snapshot-%(snap)s failed: "
  2740. "SnapshotNotFound.",
  2741. {'snap': group_snapshot.id},
  2742. resource={'type': 'group',
  2743. 'id': group.id})
  2744. raise
  2745. source_name = _("snapshot-%s") % group_snapshot.id
  2746. snapshots = objects.SnapshotList.get_all_for_group_snapshot(
  2747. context, group_snapshot.id)
  2748. for snap in snapshots:
  2749. if (snap.status not in
  2750. VALID_CREATE_GROUP_SRC_SNAP_STATUS):
  2751. msg = (_("Cannot create group "
  2752. "%(group)s because snapshot %(snap)s is "
  2753. "not in a valid state. Valid states are: "
  2754. "%(valid)s.") %
  2755. {'group': group.id,
  2756. 'snap': snap['id'],
  2757. 'valid': VALID_CREATE_GROUP_SRC_SNAP_STATUS})
  2758. raise exception.InvalidGroup(reason=msg)
  2759. if source_group:
  2760. try:
  2761. source_group.refresh()
  2762. except exception.GroupNotFound:
  2763. LOG.error("Create group "
  2764. "from source group-%(group)s failed: "
  2765. "GroupNotFound.",
  2766. {'group': source_group.id},
  2767. resource={'type': 'group',
  2768. 'id': group.id})
  2769. raise
  2770. source_name = _("group-%s") % source_group.id
  2771. source_vols = objects.VolumeList.get_all_by_generic_group(
  2772. context, source_group.id)
  2773. for source_vol in source_vols:
  2774. if (source_vol.status not in
  2775. VALID_CREATE_GROUP_SRC_GROUP_STATUS):
  2776. msg = (_("Cannot create group "
  2777. "%(group)s because source volume "
  2778. "%(source_vol)s is not in a valid "
  2779. "state. Valid states are: "
  2780. "%(valid)s.") %
  2781. {'group': group.id,
  2782. 'source_vol': source_vol.id,
  2783. 'valid': VALID_CREATE_GROUP_SRC_GROUP_STATUS})
  2784. raise exception.InvalidGroup(reason=msg)
  2785. # Sort source snapshots so that they are in the same order as their
  2786. # corresponding target volumes.
  2787. sorted_snapshots = None
  2788. if group_snapshot and snapshots:
  2789. sorted_snapshots = self._sort_snapshots(volumes, snapshots)
  2790. # Sort source volumes so that they are in the same order as their
  2791. # corresponding target volumes.
  2792. sorted_source_vols = None
  2793. if source_group and source_vols:
  2794. sorted_source_vols = self._sort_source_vols(volumes,
  2795. source_vols)
  2796. self._notify_about_group_usage(
  2797. context, group, "create.start")
  2798. utils.require_driver_initialized(self.driver)
  2799. try:
  2800. model_update, volumes_model_update = (
  2801. self.driver.create_group_from_src(
  2802. context, group, volumes, group_snapshot,
  2803. sorted_snapshots, source_group, sorted_source_vols))
  2804. except NotImplementedError:
  2805. if not group_types.is_default_cgsnapshot_type(
  2806. group.group_type_id):
  2807. model_update, volumes_model_update = (
  2808. self._create_group_from_src_generic(
  2809. context, group, volumes, group_snapshot,
  2810. sorted_snapshots, source_group,
  2811. sorted_source_vols))
  2812. else:
  2813. cg, volumes = self._convert_group_to_cg(
  2814. group, volumes)
  2815. cgsnapshot, sorted_snapshots = (
  2816. self._convert_group_snapshot_to_cgsnapshot(
  2817. group_snapshot, sorted_snapshots, context))
  2818. source_cg, sorted_source_vols = (
  2819. self._convert_group_to_cg(source_group,
  2820. sorted_source_vols))
  2821. model_update, volumes_model_update = (
  2822. self.driver.create_consistencygroup_from_src(
  2823. context, cg, volumes, cgsnapshot,
  2824. sorted_snapshots, source_cg, sorted_source_vols))
  2825. self._remove_cgsnapshot_id_from_snapshots(sorted_snapshots)
  2826. self._remove_consistencygroup_id_from_volumes(volumes)
  2827. self._remove_consistencygroup_id_from_volumes(
  2828. sorted_source_vols)
  2829. if volumes_model_update:
  2830. for update in volumes_model_update:
  2831. self.db.volume_update(context, update['id'], update)
  2832. if model_update:
  2833. group.update(model_update)
  2834. group.save()
  2835. except Exception:
  2836. with excutils.save_and_reraise_exception():
  2837. group.status = fields.GroupStatus.ERROR
  2838. group.save()
  2839. LOG.error("Create group "
  2840. "from source %(source)s failed.",
  2841. {'source': source_name},
  2842. resource={'type': 'group',
  2843. 'id': group.id})
  2844. # Update volume status to 'error' as well.
  2845. self._remove_consistencygroup_id_from_volumes(volumes)
  2846. for vol in volumes:
  2847. vol.status = 'error'
  2848. vol.save()
  2849. now = timeutils.utcnow()
  2850. status = 'available'
  2851. for vol in volumes:
  2852. update = {'status': status, 'created_at': now}
  2853. self._update_volume_from_src(context, vol, update, group=group)
  2854. self._update_allocated_capacity(vol)
  2855. group.status = status
  2856. group.created_at = now
  2857. group.save()
  2858. self._notify_about_group_usage(
  2859. context, group, "create.end")
  2860. LOG.info("Create group "
  2861. "from source-%(source)s completed successfully.",
  2862. {'source': source_name},
  2863. resource={'type': 'group',
  2864. 'id': group.id})
  2865. return group
  2866. def _create_group_from_src_generic(self, context, group, volumes,
  2867. group_snapshot=None, snapshots=None,
  2868. source_group=None, source_vols=None):
  2869. """Creates a group from source.
  2870. :param context: the context of the caller.
  2871. :param group: the Group object to be created.
  2872. :param volumes: a list of volume objects in the group.
  2873. :param group_snapshot: the GroupSnapshot object as source.
  2874. :param snapshots: a list of snapshot objects in group_snapshot.
  2875. :param source_group: the Group object as source.
  2876. :param source_vols: a list of volume objects in the source_group.
  2877. :returns: model_update, volumes_model_update
  2878. """
  2879. model_update = {'status': 'available'}
  2880. volumes_model_update = []
  2881. for vol in volumes:
  2882. if snapshots:
  2883. for snapshot in snapshots:
  2884. if vol.snapshot_id == snapshot.id:
  2885. vol_model_update = {'id': vol.id}
  2886. try:
  2887. driver_update = (
  2888. self.driver.create_volume_from_snapshot(
  2889. vol, snapshot))
  2890. if driver_update:
  2891. driver_update.pop('id', None)
  2892. vol_model_update.update(driver_update)
  2893. if 'status' not in vol_model_update:
  2894. vol_model_update['status'] = 'available'
  2895. except Exception:
  2896. vol_model_update['status'] = 'error'
  2897. model_update['status'] = 'error'
  2898. volumes_model_update.append(vol_model_update)
  2899. break
  2900. elif source_vols:
  2901. for source_vol in source_vols:
  2902. if vol.source_volid == source_vol.id:
  2903. vol_model_update = {'id': vol.id}
  2904. try:
  2905. driver_update = self.driver.create_cloned_volume(
  2906. vol, source_vol)
  2907. if driver_update:
  2908. driver_update.pop('id', None)
  2909. vol_model_update.update(driver_update)
  2910. if 'status' not in vol_model_update:
  2911. vol_model_update['status'] = 'available'
  2912. except Exception:
  2913. vol_model_update['status'] = 'error'
  2914. model_update['status'] = 'error'
  2915. volumes_model_update.append(vol_model_update)
  2916. break
  2917. return model_update, volumes_model_update
  2918. def _sort_snapshots(self, volumes, snapshots):
  2919. # Sort source snapshots so that they are in the same order as their
  2920. # corresponding target volumes. Each source snapshot in the snapshots
  2921. # list should have a corresponding target volume in the volumes list.
  2922. if not volumes or not snapshots or len(volumes) != len(snapshots):
  2923. msg = _("Input volumes or snapshots are invalid.")
  2924. LOG.error(msg)
  2925. raise exception.InvalidInput(reason=msg)
  2926. sorted_snapshots = []
  2927. for vol in volumes:
  2928. found_snaps = [snap for snap in snapshots
  2929. if snap['id'] == vol['snapshot_id']]
  2930. if not found_snaps:
  2931. LOG.error("Source snapshot cannot be found for target "
  2932. "volume %(volume_id)s.",
  2933. {'volume_id': vol['id']})
  2934. raise exception.SnapshotNotFound(
  2935. snapshot_id=vol['snapshot_id'])
  2936. sorted_snapshots.extend(found_snaps)
  2937. return sorted_snapshots
  2938. def _sort_source_vols(self, volumes, source_vols):
  2939. # Sort source volumes so that they are in the same order as their
  2940. # corresponding target volumes. Each source volume in the source_vols
  2941. # list should have a corresponding target volume in the volumes list.
  2942. if not volumes or not source_vols or len(volumes) != len(source_vols):
  2943. msg = _("Input volumes or source volumes are invalid.")
  2944. LOG.error(msg)
  2945. raise exception.InvalidInput(reason=msg)
  2946. sorted_source_vols = []
  2947. for vol in volumes:
  2948. found_source_vols = [source_vol for source_vol in source_vols
  2949. if source_vol['id'] == vol['source_volid']]
  2950. if not found_source_vols:
  2951. LOG.error("Source volumes cannot be found for target "
  2952. "volume %(volume_id)s.",
  2953. {'volume_id': vol['id']})
  2954. raise exception.VolumeNotFound(
  2955. volume_id=vol['source_volid'])
  2956. sorted_source_vols.extend(found_source_vols)
  2957. return sorted_source_vols
  2958. def _update_volume_from_src(self, context, vol, update, group=None):
  2959. try:
  2960. snapshot_id = vol.get('snapshot_id')
  2961. source_volid = vol.get('source_volid')
  2962. if snapshot_id:
  2963. snapshot = objects.Snapshot.get_by_id(context, snapshot_id)
  2964. orig_vref = self.db.volume_get(context,
  2965. snapshot.volume_id)
  2966. if orig_vref.bootable:
  2967. update['bootable'] = True
  2968. self.db.volume_glance_metadata_copy_to_volume(
  2969. context, vol['id'], snapshot_id)
  2970. if source_volid:
  2971. source_vol = objects.Volume.get_by_id(context, source_volid)
  2972. if source_vol.bootable:
  2973. update['bootable'] = True
  2974. self.db.volume_glance_metadata_copy_from_volume_to_volume(
  2975. context, source_volid, vol['id'])
  2976. if source_vol.multiattach:
  2977. update['multiattach'] = True
  2978. except exception.SnapshotNotFound:
  2979. LOG.error("Source snapshot %(snapshot_id)s cannot be found.",
  2980. {'snapshot_id': vol['snapshot_id']})
  2981. self.db.volume_update(context, vol['id'],
  2982. {'status': 'error'})
  2983. if group:
  2984. group.status = fields.GroupStatus.ERROR
  2985. group.save()
  2986. raise
  2987. except exception.VolumeNotFound:
  2988. LOG.error("The source volume %(volume_id)s "
  2989. "cannot be found.",
  2990. {'volume_id': snapshot.volume_id})
  2991. self.db.volume_update(context, vol['id'],
  2992. {'status': 'error'})
  2993. if group:
  2994. group.status = fields.GroupStatus.ERROR
  2995. group.save()
  2996. raise
  2997. except exception.CinderException as ex:
  2998. LOG.error("Failed to update %(volume_id)s"
  2999. " metadata using the provided snapshot"
  3000. " %(snapshot_id)s metadata.",
  3001. {'volume_id': vol['id'],
  3002. 'snapshot_id': vol['snapshot_id']})
  3003. self.db.volume_update(context, vol['id'],
  3004. {'status': 'error'})
  3005. if group:
  3006. group.status = fields.GroupStatus.ERROR
  3007. group.save()
  3008. raise exception.MetadataCopyFailure(reason=six.text_type(ex))
  3009. self.db.volume_update(context, vol['id'], update)
  3010. def _update_allocated_capacity(self, vol, decrement=False, host=None):
  3011. # Update allocated capacity in volume stats
  3012. host = host or vol['host']
  3013. pool = vol_utils.extract_host(host, 'pool')
  3014. if pool is None:
  3015. # Legacy volume, put them into default pool
  3016. pool = self.driver.configuration.safe_get(
  3017. 'volume_backend_name') or vol_utils.extract_host(host, 'pool',
  3018. True)
  3019. vol_size = -vol['size'] if decrement else vol['size']
  3020. try:
  3021. self.stats['pools'][pool]['allocated_capacity_gb'] += vol_size
  3022. except KeyError:
  3023. self.stats['pools'][pool] = dict(
  3024. allocated_capacity_gb=max(vol_size, 0))
  3025. def delete_group(self, context, group):
  3026. """Deletes group and the volumes in the group."""
  3027. context = context.elevated()
  3028. project_id = group.project_id
  3029. if context.project_id != group.project_id:
  3030. project_id = group.project_id
  3031. else:
  3032. project_id = context.project_id
  3033. volumes = objects.VolumeList.get_all_by_generic_group(
  3034. context, group.id)
  3035. for vol_obj in volumes:
  3036. if vol_obj.attach_status == "attached":
  3037. # Volume is still attached, need to detach first
  3038. raise exception.VolumeAttached(volume_id=vol_obj.id)
  3039. self._check_is_our_resource(vol_obj)
  3040. self._notify_about_group_usage(
  3041. context, group, "delete.start")
  3042. volumes_model_update = None
  3043. model_update = None
  3044. try:
  3045. utils.require_driver_initialized(self.driver)
  3046. try:
  3047. model_update, volumes_model_update = (
  3048. self.driver.delete_group(context, group, volumes))
  3049. except NotImplementedError:
  3050. if not group_types.is_default_cgsnapshot_type(
  3051. group.group_type_id):
  3052. model_update, volumes_model_update = (
  3053. self._delete_group_generic(context, group, volumes))
  3054. else:
  3055. cg, volumes = self._convert_group_to_cg(
  3056. group, volumes)
  3057. model_update, volumes_model_update = (
  3058. self.driver.delete_consistencygroup(context, cg,
  3059. volumes))
  3060. self._remove_consistencygroup_id_from_volumes(volumes)
  3061. if volumes_model_update:
  3062. for update in volumes_model_update:
  3063. # If we failed to delete a volume, make sure the
  3064. # status for the group is set to error as well
  3065. if (update['status'] in ['error_deleting', 'error']
  3066. and model_update['status'] not in
  3067. ['error_deleting', 'error']):
  3068. model_update['status'] = update['status']
  3069. self.db.volumes_update(context, volumes_model_update)
  3070. if model_update:
  3071. if model_update['status'] in ['error_deleting', 'error']:
  3072. msg = (_('Delete group failed.'))
  3073. LOG.error(msg,
  3074. resource={'type': 'group',
  3075. 'id': group.id})
  3076. raise exception.VolumeDriverException(message=msg)
  3077. else:
  3078. group.update(model_update)
  3079. group.save()
  3080. except Exception:
  3081. with excutils.save_and_reraise_exception():
  3082. group.status = fields.GroupStatus.ERROR
  3083. group.save()
  3084. # Update volume status to 'error' if driver returns
  3085. # None for volumes_model_update.
  3086. if not volumes_model_update:
  3087. self._remove_consistencygroup_id_from_volumes(volumes)
  3088. for vol_obj in volumes:
  3089. vol_obj.status = 'error'
  3090. vol_obj.save()
  3091. # Get reservations for group
  3092. try:
  3093. reserve_opts = {'groups': -1}
  3094. grpreservations = GROUP_QUOTAS.reserve(context,
  3095. project_id=project_id,
  3096. **reserve_opts)
  3097. except Exception:
  3098. grpreservations = None
  3099. LOG.exception("Delete group "
  3100. "failed to update usages.",
  3101. resource={'type': 'group',
  3102. 'id': group.id})
  3103. for vol in volumes:
  3104. # Get reservations for volume
  3105. try:
  3106. reserve_opts = {'volumes': -1,
  3107. 'gigabytes': -vol.size}
  3108. QUOTAS.add_volume_type_opts(context,
  3109. reserve_opts,
  3110. vol.volume_type_id)
  3111. reservations = QUOTAS.reserve(context,
  3112. project_id=project_id,
  3113. **reserve_opts)
  3114. except Exception:
  3115. reservations = None
  3116. LOG.exception("Delete group "
  3117. "failed to update usages.",
  3118. resource={'type': 'group',
  3119. 'id': group.id})
  3120. vol.destroy()
  3121. # Commit the reservations
  3122. if reservations:
  3123. QUOTAS.commit(context, reservations, project_id=project_id)
  3124. self.stats['allocated_capacity_gb'] -= vol.size
  3125. if grpreservations:
  3126. GROUP_QUOTAS.commit(context, grpreservations,
  3127. project_id=project_id)
  3128. group.destroy()
  3129. self._notify_about_group_usage(
  3130. context, group, "delete.end")
  3131. self.publish_service_capabilities(context)
  3132. LOG.info("Delete group "
  3133. "completed successfully.",
  3134. resource={'type': 'group',
  3135. 'id': group.id})
  3136. def _convert_group_to_cg(self, group, volumes):
  3137. if not group:
  3138. return None, None
  3139. cg = consistencygroup.ConsistencyGroup()
  3140. cg.from_group(group)
  3141. for vol in volumes:
  3142. vol.consistencygroup_id = vol.group_id
  3143. vol.consistencygroup = cg
  3144. return cg, volumes
  3145. def _remove_consistencygroup_id_from_volumes(self, volumes):
  3146. if not volumes:
  3147. return
  3148. for vol in volumes:
  3149. vol.consistencygroup_id = None
  3150. vol.consistencygroup = None
  3151. def _convert_group_snapshot_to_cgsnapshot(self, group_snapshot, snapshots,
  3152. ctxt):
  3153. if not group_snapshot:
  3154. return None, None
  3155. cgsnap = cgsnapshot.CGSnapshot()
  3156. cgsnap.from_group_snapshot(group_snapshot)
  3157. # Populate consistencygroup object
  3158. grp = objects.Group.get_by_id(ctxt, group_snapshot.group_id)
  3159. cg, __ = self._convert_group_to_cg(grp, [])
  3160. cgsnap.consistencygroup = cg
  3161. for snap in snapshots:
  3162. snap.cgsnapshot_id = snap.group_snapshot_id
  3163. snap.cgsnapshot = cgsnap
  3164. return cgsnap, snapshots
  3165. def _remove_cgsnapshot_id_from_snapshots(self, snapshots):
  3166. if not snapshots:
  3167. return
  3168. for snap in snapshots:
  3169. snap.cgsnapshot_id = None
  3170. snap.cgsnapshot = None
  3171. def _create_group_generic(self, context, group):
  3172. """Creates a group."""
  3173. # A group entry is already created in db. Just returns a status here.
  3174. model_update = {'status': fields.GroupStatus.AVAILABLE,
  3175. 'created_at': timeutils.utcnow()}
  3176. return model_update
  3177. def _delete_group_generic(self, context, group, volumes):
  3178. """Deletes a group and volumes in the group."""
  3179. model_update = {'status': group.status}
  3180. volume_model_updates = []
  3181. for volume_ref in volumes:
  3182. volume_model_update = {'id': volume_ref.id}
  3183. try:
  3184. self.driver.remove_export(context, volume_ref)
  3185. self.driver.delete_volume(volume_ref)
  3186. volume_model_update['status'] = 'deleted'
  3187. except exception.VolumeIsBusy:
  3188. volume_model_update['status'] = 'available'
  3189. except Exception:
  3190. volume_model_update['status'] = 'error'
  3191. model_update['status'] = fields.GroupStatus.ERROR
  3192. volume_model_updates.append(volume_model_update)
  3193. return model_update, volume_model_updates
  3194. def _update_group_generic(self, context, group,
  3195. add_volumes=None, remove_volumes=None):
  3196. """Updates a group."""
  3197. # NOTE(xyang): The volume manager adds/removes the volume to/from the
  3198. # group in the database. This default implementation does not do
  3199. # anything in the backend storage.
  3200. return None, None, None
  3201. def _collect_volumes_for_group(self, context, group, volumes, add=True):
  3202. if add:
  3203. valid_status = VALID_ADD_VOL_TO_GROUP_STATUS
  3204. else:
  3205. valid_status = VALID_REMOVE_VOL_FROM_GROUP_STATUS
  3206. volumes_ref = []
  3207. if not volumes:
  3208. return volumes_ref
  3209. for add_vol in volumes.split(','):
  3210. try:
  3211. add_vol_ref = objects.Volume.get_by_id(context, add_vol)
  3212. except exception.VolumeNotFound:
  3213. LOG.error("Update group "
  3214. "failed to %(op)s volume-%(volume_id)s: "
  3215. "VolumeNotFound.",
  3216. {'volume_id': add_vol,
  3217. 'op': 'add' if add else 'remove'},
  3218. resource={'type': 'group',
  3219. 'id': group.id})
  3220. raise
  3221. if add_vol_ref.status not in valid_status:
  3222. msg = (_("Can not %(op)s volume %(volume_id)s to "
  3223. "group %(group_id)s because volume is in an invalid "
  3224. "state: %(status)s. Valid states are: %(valid)s.") %
  3225. {'volume_id': add_vol_ref.id,
  3226. 'group_id': group.id,
  3227. 'status': add_vol_ref.status,
  3228. 'valid': valid_status,
  3229. 'op': 'add' if add else 'remove'})
  3230. raise exception.InvalidVolume(reason=msg)
  3231. if add:
  3232. self._check_is_our_resource(add_vol_ref)
  3233. volumes_ref.append(add_vol_ref)
  3234. return volumes_ref
  3235. def update_group(self, context, group,
  3236. add_volumes=None, remove_volumes=None):
  3237. """Updates group.
  3238. Update group by adding volumes to the group,
  3239. or removing volumes from the group.
  3240. """
  3241. add_volumes_ref = self._collect_volumes_for_group(context,
  3242. group,
  3243. add_volumes,
  3244. add=True)
  3245. remove_volumes_ref = self._collect_volumes_for_group(context,
  3246. group,
  3247. remove_volumes,
  3248. add=False)
  3249. self._notify_about_group_usage(
  3250. context, group, "update.start")
  3251. try:
  3252. utils.require_driver_initialized(self.driver)
  3253. try:
  3254. model_update, add_volumes_update, remove_volumes_update = (
  3255. self.driver.update_group(
  3256. context, group,
  3257. add_volumes=add_volumes_ref,
  3258. remove_volumes=remove_volumes_ref))
  3259. except NotImplementedError:
  3260. if not group_types.is_default_cgsnapshot_type(
  3261. group.group_type_id):
  3262. model_update, add_volumes_update, remove_volumes_update = (
  3263. self._update_group_generic(
  3264. context, group,
  3265. add_volumes=add_volumes_ref,
  3266. remove_volumes=remove_volumes_ref))
  3267. else:
  3268. cg, remove_volumes_ref = self._convert_group_to_cg(
  3269. group, remove_volumes_ref)
  3270. model_update, add_volumes_update, remove_volumes_update = (
  3271. self.driver.update_consistencygroup(
  3272. context, cg,
  3273. add_volumes=add_volumes_ref,
  3274. remove_volumes=remove_volumes_ref))
  3275. self._remove_consistencygroup_id_from_volumes(
  3276. remove_volumes_ref)
  3277. volumes_to_update = []
  3278. if add_volumes_update:
  3279. volumes_to_update.extend(add_volumes_update)
  3280. if remove_volumes_update:
  3281. volumes_to_update.extend(remove_volumes_update)
  3282. self.db.volumes_update(context, volumes_to_update)
  3283. if model_update:
  3284. if model_update['status'] in (
  3285. [fields.GroupStatus.ERROR]):
  3286. msg = (_('Error occurred when updating group '
  3287. '%s.') % group.id)
  3288. LOG.error(msg)
  3289. raise exception.VolumeDriverException(message=msg)
  3290. group.update(model_update)
  3291. group.save()
  3292. except Exception as e:
  3293. with excutils.save_and_reraise_exception():
  3294. if isinstance(e, exception.VolumeDriverException):
  3295. LOG.error("Error occurred in the volume driver when "
  3296. "updating group %(group_id)s.",
  3297. {'group_id': group.id})
  3298. else:
  3299. LOG.error("Failed to update group %(group_id)s.",
  3300. {'group_id': group.id})
  3301. group.status = fields.GroupStatus.ERROR
  3302. group.save()
  3303. for add_vol in add_volumes_ref:
  3304. add_vol.status = 'error'
  3305. add_vol.save()
  3306. for rem_vol in remove_volumes_ref:
  3307. if isinstance(e, exception.VolumeDriverException):
  3308. rem_vol.consistencygroup_id = None
  3309. rem_vol.consistencygroup = None
  3310. rem_vol.status = 'error'
  3311. rem_vol.save()
  3312. for add_vol in add_volumes_ref:
  3313. add_vol.group_id = group.id
  3314. add_vol.save()
  3315. for rem_vol in remove_volumes_ref:
  3316. rem_vol.group_id = None
  3317. rem_vol.save()
  3318. group.status = fields.GroupStatus.AVAILABLE
  3319. group.save()
  3320. self._notify_about_group_usage(
  3321. context, group, "update.end")
  3322. LOG.info("Update group completed successfully.",
  3323. resource={'type': 'group',
  3324. 'id': group.id})
  3325. def create_group_snapshot(self, context, group_snapshot):
  3326. """Creates the group_snapshot."""
  3327. caller_context = context
  3328. context = context.elevated()
  3329. LOG.info("GroupSnapshot %s: creating.", group_snapshot.id)
  3330. snapshots = objects.SnapshotList.get_all_for_group_snapshot(
  3331. context, group_snapshot.id)
  3332. self._notify_about_group_snapshot_usage(
  3333. context, group_snapshot, "create.start")
  3334. snapshots_model_update = None
  3335. model_update = None
  3336. try:
  3337. utils.require_driver_initialized(self.driver)
  3338. LOG.debug("Group snapshot %(grp_snap_id)s: creating.",
  3339. {'grp_snap_id': group_snapshot.id})
  3340. # Pass context so that drivers that want to use it, can,
  3341. # but it is not a requirement for all drivers.
  3342. group_snapshot.context = caller_context
  3343. for snapshot in snapshots:
  3344. snapshot.context = caller_context
  3345. try:
  3346. model_update, snapshots_model_update = (
  3347. self.driver.create_group_snapshot(context, group_snapshot,
  3348. snapshots))
  3349. except NotImplementedError:
  3350. if not group_types.is_default_cgsnapshot_type(
  3351. group_snapshot.group_type_id):
  3352. model_update, snapshots_model_update = (
  3353. self._create_group_snapshot_generic(
  3354. context, group_snapshot, snapshots))
  3355. else:
  3356. cgsnapshot, snapshots = (
  3357. self._convert_group_snapshot_to_cgsnapshot(
  3358. group_snapshot, snapshots, context))
  3359. model_update, snapshots_model_update = (
  3360. self.driver.create_cgsnapshot(context, cgsnapshot,
  3361. snapshots))
  3362. self._remove_cgsnapshot_id_from_snapshots(snapshots)
  3363. if snapshots_model_update:
  3364. for snap_model in snapshots_model_update:
  3365. # Update db for snapshot.
  3366. # NOTE(xyang): snapshots is a list of snapshot objects.
  3367. # snapshots_model_update should be a list of dicts.
  3368. snap_id = snap_model.pop('id')
  3369. snap_obj = objects.Snapshot.get_by_id(context, snap_id)
  3370. snap_obj.update(snap_model)
  3371. snap_obj.save()
  3372. if (snap_model['status'] in [
  3373. fields.SnapshotStatus.ERROR_DELETING,
  3374. fields.SnapshotStatus.ERROR] and
  3375. model_update['status'] not in
  3376. [fields.GroupSnapshotStatus.ERROR_DELETING,
  3377. fields.GroupSnapshotStatus.ERROR]):
  3378. model_update['status'] = snap_model['status']
  3379. if model_update:
  3380. if model_update['status'] == fields.GroupSnapshotStatus.ERROR:
  3381. msg = (_('Error occurred when creating group_snapshot '
  3382. '%s.') % group_snapshot.id)
  3383. LOG.error(msg)
  3384. raise exception.VolumeDriverException(message=msg)
  3385. group_snapshot.update(model_update)
  3386. group_snapshot.save()
  3387. except exception.CinderException:
  3388. with excutils.save_and_reraise_exception():
  3389. group_snapshot.status = fields.GroupSnapshotStatus.ERROR
  3390. group_snapshot.save()
  3391. # Update snapshot status to 'error' if driver returns
  3392. # None for snapshots_model_update.
  3393. self._remove_cgsnapshot_id_from_snapshots(snapshots)
  3394. if not snapshots_model_update:
  3395. for snapshot in snapshots:
  3396. snapshot.status = fields.SnapshotStatus.ERROR
  3397. snapshot.save()
  3398. for snapshot in snapshots:
  3399. volume_id = snapshot.volume_id
  3400. snapshot_id = snapshot.id
  3401. vol_obj = objects.Volume.get_by_id(context, volume_id)
  3402. if vol_obj.bootable:
  3403. try:
  3404. self.db.volume_glance_metadata_copy_to_snapshot(
  3405. context, snapshot_id, volume_id)
  3406. except exception.GlanceMetadataNotFound:
  3407. # If volume is not created from image, No glance metadata
  3408. # would be available for that volume in
  3409. # volume glance metadata table
  3410. pass
  3411. except exception.CinderException as ex:
  3412. LOG.error("Failed updating %(snapshot_id)s"
  3413. " metadata using the provided volumes"
  3414. " %(volume_id)s metadata.",
  3415. {'volume_id': volume_id,
  3416. 'snapshot_id': snapshot_id})
  3417. snapshot.status = fields.SnapshotStatus.ERROR
  3418. snapshot.save()
  3419. raise exception.MetadataCopyFailure(
  3420. reason=six.text_type(ex))
  3421. snapshot.status = fields.SnapshotStatus.AVAILABLE
  3422. snapshot.progress = '100%'
  3423. snapshot.save()
  3424. group_snapshot.status = fields.GroupSnapshotStatus.AVAILABLE
  3425. group_snapshot.save()
  3426. LOG.info("group_snapshot %s: created successfully",
  3427. group_snapshot.id)
  3428. self._notify_about_group_snapshot_usage(
  3429. context, group_snapshot, "create.end")
  3430. return group_snapshot
  3431. def _create_group_snapshot_generic(self, context, group_snapshot,
  3432. snapshots):
  3433. """Creates a group_snapshot."""
  3434. model_update = {'status': 'available'}
  3435. snapshot_model_updates = []
  3436. for snapshot in snapshots:
  3437. snapshot_model_update = {'id': snapshot.id}
  3438. try:
  3439. driver_update = self.driver.create_snapshot(snapshot)
  3440. if driver_update:
  3441. driver_update.pop('id', None)
  3442. snapshot_model_update.update(driver_update)
  3443. if 'status' not in snapshot_model_update:
  3444. snapshot_model_update['status'] = (
  3445. fields.SnapshotStatus.AVAILABLE)
  3446. except Exception:
  3447. snapshot_model_update['status'] = (
  3448. fields.SnapshotStatus.ERROR)
  3449. model_update['status'] = 'error'
  3450. snapshot_model_updates.append(snapshot_model_update)
  3451. return model_update, snapshot_model_updates
  3452. def _delete_group_snapshot_generic(self, context, group_snapshot,
  3453. snapshots):
  3454. """Deletes a group_snapshot."""
  3455. model_update = {'status': group_snapshot.status}
  3456. snapshot_model_updates = []
  3457. for snapshot in snapshots:
  3458. snapshot_model_update = {'id': snapshot.id}
  3459. try:
  3460. self.driver.delete_snapshot(snapshot)
  3461. snapshot_model_update['status'] = (
  3462. fields.SnapshotStatus.DELETED)
  3463. except exception.SnapshotIsBusy:
  3464. snapshot_model_update['status'] = (
  3465. fields.SnapshotStatus.AVAILABLE)
  3466. except Exception:
  3467. snapshot_model_update['status'] = (
  3468. fields.SnapshotStatus.ERROR)
  3469. model_update['status'] = 'error'
  3470. snapshot_model_updates.append(snapshot_model_update)
  3471. return model_update, snapshot_model_updates
  3472. def delete_group_snapshot(self, context, group_snapshot):
  3473. """Deletes group_snapshot."""
  3474. caller_context = context
  3475. context = context.elevated()
  3476. project_id = group_snapshot.project_id
  3477. LOG.info("group_snapshot %s: deleting", group_snapshot.id)
  3478. snapshots = objects.SnapshotList.get_all_for_group_snapshot(
  3479. context, group_snapshot.id)
  3480. self._notify_about_group_snapshot_usage(
  3481. context, group_snapshot, "delete.start")
  3482. snapshots_model_update = None
  3483. model_update = None
  3484. try:
  3485. utils.require_driver_initialized(self.driver)
  3486. LOG.debug("group_snapshot %(grp_snap_id)s: deleting",
  3487. {'grp_snap_id': group_snapshot.id})
  3488. # Pass context so that drivers that want to use it, can,
  3489. # but it is not a requirement for all drivers.
  3490. group_snapshot.context = caller_context
  3491. for snapshot in snapshots:
  3492. snapshot.context = caller_context
  3493. try:
  3494. model_update, snapshots_model_update = (
  3495. self.driver.delete_group_snapshot(context, group_snapshot,
  3496. snapshots))
  3497. except NotImplementedError:
  3498. if not group_types.is_default_cgsnapshot_type(
  3499. group_snapshot.group_type_id):
  3500. model_update, snapshots_model_update = (
  3501. self._delete_group_snapshot_generic(
  3502. context, group_snapshot, snapshots))
  3503. else:
  3504. cgsnapshot, snapshots = (
  3505. self._convert_group_snapshot_to_cgsnapshot(
  3506. group_snapshot, snapshots, context))
  3507. model_update, snapshots_model_update = (
  3508. self.driver.delete_cgsnapshot(context, cgsnapshot,
  3509. snapshots))
  3510. self._remove_cgsnapshot_id_from_snapshots(snapshots)
  3511. if snapshots_model_update:
  3512. for snap_model in snapshots_model_update:
  3513. # NOTE(xyang): snapshots is a list of snapshot objects.
  3514. # snapshots_model_update should be a list of dicts.
  3515. snap = next((item for item in snapshots if
  3516. item.id == snap_model['id']), None)
  3517. if snap:
  3518. snap_model.pop('id')
  3519. snap.update(snap_model)
  3520. snap.save()
  3521. if (snap_model['status'] in
  3522. [fields.SnapshotStatus.ERROR_DELETING,
  3523. fields.SnapshotStatus.ERROR] and
  3524. model_update['status'] not in
  3525. ['error_deleting', 'error']):
  3526. model_update['status'] = snap_model['status']
  3527. if model_update:
  3528. if model_update['status'] in ['error_deleting', 'error']:
  3529. msg = (_('Error occurred when deleting group_snapshot '
  3530. '%s.') % group_snapshot.id)
  3531. LOG.error(msg)
  3532. raise exception.VolumeDriverException(message=msg)
  3533. else:
  3534. group_snapshot.update(model_update)
  3535. group_snapshot.save()
  3536. except exception.CinderException:
  3537. with excutils.save_and_reraise_exception():
  3538. group_snapshot.status = fields.GroupSnapshotStatus.ERROR
  3539. group_snapshot.save()
  3540. # Update snapshot status to 'error' if driver returns
  3541. # None for snapshots_model_update.
  3542. if not snapshots_model_update:
  3543. self._remove_cgsnapshot_id_from_snapshots(snapshots)
  3544. for snapshot in snapshots:
  3545. snapshot.status = fields.SnapshotStatus.ERROR
  3546. snapshot.save()
  3547. for snapshot in snapshots:
  3548. # Get reservations
  3549. try:
  3550. reserve_opts = {'snapshots': -1}
  3551. if not CONF.no_snapshot_gb_quota:
  3552. reserve_opts['gigabytes'] = -snapshot.volume_size
  3553. volume_ref = objects.Volume.get_by_id(context,
  3554. snapshot.volume_id)
  3555. QUOTAS.add_volume_type_opts(context,
  3556. reserve_opts,
  3557. volume_ref.volume_type_id)
  3558. reservations = QUOTAS.reserve(context,
  3559. project_id=project_id,
  3560. **reserve_opts)
  3561. except Exception:
  3562. reservations = None
  3563. LOG.exception("Failed to update usages deleting snapshot")
  3564. self.db.volume_glance_metadata_delete_by_snapshot(context,
  3565. snapshot.id)
  3566. snapshot.destroy()
  3567. # Commit the reservations
  3568. if reservations:
  3569. QUOTAS.commit(context, reservations, project_id=project_id)
  3570. group_snapshot.destroy()
  3571. LOG.info("group_snapshot %s: deleted successfully",
  3572. group_snapshot.id)
  3573. self._notify_about_group_snapshot_usage(context, group_snapshot,
  3574. "delete.end",
  3575. snapshots)
  3576. def update_migrated_volume(self, ctxt, volume, new_volume, volume_status):
  3577. """Finalize migration process on backend device."""
  3578. model_update = None
  3579. model_update_default = {'_name_id': new_volume.name_id,
  3580. 'provider_location':
  3581. new_volume.provider_location}
  3582. try:
  3583. model_update = self.driver.update_migrated_volume(ctxt,
  3584. volume,
  3585. new_volume,
  3586. volume_status)
  3587. except NotImplementedError:
  3588. # If update_migrated_volume is not implemented for the driver,
  3589. # _name_id and provider_location will be set with the values
  3590. # from new_volume.
  3591. model_update = model_update_default
  3592. if model_update:
  3593. model_update_default.update(model_update)
  3594. # Swap keys that were changed in the source so we keep their values
  3595. # in the temporary volume's DB record.
  3596. # Need to convert 'metadata' and 'admin_metadata' since
  3597. # they are not keys of volume, their corresponding keys are
  3598. # 'volume_metadata' and 'volume_admin_metadata'.
  3599. model_update_new = dict()
  3600. for key in model_update:
  3601. if key == 'metadata':
  3602. if volume.get('volume_metadata'):
  3603. model_update_new[key] = {
  3604. metadata['key']: metadata['value']
  3605. for metadata in volume.volume_metadata}
  3606. elif key == 'admin_metadata':
  3607. model_update_new[key] = {
  3608. metadata['key']: metadata['value']
  3609. for metadata in volume.volume_admin_metadata}
  3610. else:
  3611. model_update_new[key] = volume[key]
  3612. with new_volume.obj_as_admin():
  3613. new_volume.update(model_update_new)
  3614. new_volume.save()
  3615. with volume.obj_as_admin():
  3616. volume.update(model_update_default)
  3617. volume.save()
  3618. # Replication V2.1 and a/a method
  3619. def failover(self, context, secondary_backend_id=None):
  3620. """Failover a backend to a secondary replication target.
  3621. Instructs a replication capable/configured backend to failover
  3622. to one of it's secondary replication targets. host=None is
  3623. an acceetable input, and leaves it to the driver to failover
  3624. to the only configured target, or to choose a target on it's
  3625. own. All of the hosts volumes will be passed on to the driver
  3626. in order for it to determine the replicated volumes on the host,
  3627. if needed.
  3628. :param context: security context
  3629. :param secondary_backend_id: Specifies backend_id to fail over to
  3630. """
  3631. updates = {}
  3632. repl_status = fields.ReplicationStatus
  3633. service = self._get_service()
  3634. # TODO(geguileo): We should optimize these updates by doing them
  3635. # directly on the DB with just 3 queries, one to change the volumes
  3636. # another to change all the snapshots, and another to get replicated
  3637. # volumes.
  3638. # Change non replicated volumes and their snapshots to error if we are
  3639. # failing over, leave them as they are for failback
  3640. volumes = self._get_my_volumes(context)
  3641. replicated_vols = []
  3642. for volume in volumes:
  3643. if volume.replication_status not in (repl_status.DISABLED,
  3644. repl_status.NOT_CAPABLE):
  3645. replicated_vols.append(volume)
  3646. elif secondary_backend_id != self.FAILBACK_SENTINEL:
  3647. volume.previous_status = volume.status
  3648. volume.status = 'error'
  3649. volume.replication_status = repl_status.NOT_CAPABLE
  3650. volume.save()
  3651. for snapshot in volume.snapshots:
  3652. snapshot.status = fields.SnapshotStatus.ERROR
  3653. snapshot.save()
  3654. volume_update_list = None
  3655. group_update_list = None
  3656. try:
  3657. # For non clustered we can call v2.1 failover_host, but for
  3658. # clustered we call a/a failover method. We know a/a method
  3659. # exists because BaseVD class wouldn't have started if it didn't.
  3660. failover = getattr(self.driver,
  3661. 'failover' if service.is_clustered
  3662. else 'failover_host')
  3663. # expected form of volume_update_list:
  3664. # [{volume_id: <cinder-volid>, updates: {'provider_id': xxxx....}},
  3665. # {volume_id: <cinder-volid>, updates: {'provider_id': xxxx....}}]
  3666. # It includes volumes in replication groups and those not in them
  3667. # expected form of group_update_list:
  3668. # [{group_id: <cinder-grpid>, updates: {'xxxx': xxxx....}},
  3669. # {group_id: <cinder-grpid>, updates: {'xxxx': xxxx....}}]
  3670. filters = self._get_cluster_or_host_filters()
  3671. groups = objects.GroupList.get_all_replicated(context,
  3672. filters=filters)
  3673. active_backend_id, volume_update_list, group_update_list = (
  3674. failover(context,
  3675. replicated_vols,
  3676. secondary_id=secondary_backend_id,
  3677. groups=groups))
  3678. try:
  3679. update_data = {u['volume_id']: u['updates']
  3680. for u in volume_update_list}
  3681. except KeyError:
  3682. msg = "Update list, doesn't include volume_id"
  3683. raise exception.ProgrammingError(reason=msg)
  3684. try:
  3685. update_group_data = {g['group_id']: g['updates']
  3686. for g in group_update_list}
  3687. except KeyError:
  3688. msg = "Update list, doesn't include group_id"
  3689. raise exception.ProgrammingError(reason=msg)
  3690. except Exception as exc:
  3691. # NOTE(jdg): Drivers need to be aware if they fail during
  3692. # a failover sequence, we're expecting them to cleanup
  3693. # and make sure the driver state is such that the original
  3694. # backend is still set as primary as per driver memory
  3695. # We don't want to log the exception trace invalid replication
  3696. # target
  3697. if isinstance(exc, exception.InvalidReplicationTarget):
  3698. log_method = LOG.error
  3699. # Preserve the replication_status: Status should be failed over
  3700. # if we were failing back or if we were failing over from one
  3701. # secondary to another secondary. In both cases
  3702. # active_backend_id will be set.
  3703. if service.active_backend_id:
  3704. updates['replication_status'] = repl_status.FAILED_OVER
  3705. else:
  3706. updates['replication_status'] = repl_status.ENABLED
  3707. else:
  3708. log_method = LOG.exception
  3709. updates.update(disabled=True,
  3710. replication_status=repl_status.FAILOVER_ERROR)
  3711. log_method("Error encountered during failover on host: %(host)s "
  3712. "to %(backend_id)s: %(error)s",
  3713. {'host': self.host, 'backend_id': secondary_backend_id,
  3714. 'error': exc})
  3715. # We dump the update list for manual recovery
  3716. LOG.error('Failed update_list is: %s', volume_update_list)
  3717. self.finish_failover(context, service, updates)
  3718. return
  3719. if secondary_backend_id == "default":
  3720. updates['replication_status'] = repl_status.ENABLED
  3721. updates['active_backend_id'] = ''
  3722. updates['disabled'] = service.frozen
  3723. updates['disabled_reason'] = 'frozen' if service.frozen else ''
  3724. else:
  3725. updates['replication_status'] = repl_status.FAILED_OVER
  3726. updates['active_backend_id'] = active_backend_id
  3727. updates['disabled'] = True
  3728. updates['disabled_reason'] = 'failed-over'
  3729. self.finish_failover(context, service, updates)
  3730. for volume in replicated_vols:
  3731. update = update_data.get(volume.id, {})
  3732. if update.get('status', '') == 'error':
  3733. update['replication_status'] = repl_status.FAILOVER_ERROR
  3734. elif update.get('replication_status') in (None,
  3735. repl_status.FAILED_OVER):
  3736. update['replication_status'] = updates['replication_status']
  3737. if update['replication_status'] == repl_status.FAILOVER_ERROR:
  3738. update.setdefault('status', 'error')
  3739. # Set all volume snapshots to error
  3740. for snapshot in volume.snapshots:
  3741. snapshot.status = fields.SnapshotStatus.ERROR
  3742. snapshot.save()
  3743. if 'status' in update:
  3744. update['previous_status'] = volume.status
  3745. volume.update(update)
  3746. volume.save()
  3747. for grp in groups:
  3748. update = update_group_data.get(grp.id, {})
  3749. if update.get('status', '') == 'error':
  3750. update['replication_status'] = repl_status.FAILOVER_ERROR
  3751. elif update.get('replication_status') in (None,
  3752. repl_status.FAILED_OVER):
  3753. update['replication_status'] = updates['replication_status']
  3754. if update['replication_status'] == repl_status.FAILOVER_ERROR:
  3755. update.setdefault('status', 'error')
  3756. grp.update(update)
  3757. grp.save()
  3758. LOG.info("Failed over to replication target successfully.")
  3759. # TODO(geguileo): In P - remove this
  3760. failover_host = failover
  3761. def finish_failover(self, context, service, updates):
  3762. """Completion of the failover locally or via RPC."""
  3763. # If the service is clustered, broadcast the service changes to all
  3764. # volume services, including this one.
  3765. if service.is_clustered:
  3766. # We have to update the cluster with the same data, and we do it
  3767. # before broadcasting the failover_completed RPC call to prevent
  3768. # races with services that may be starting..
  3769. for key, value in updates.items():
  3770. setattr(service.cluster, key, value)
  3771. service.cluster.save()
  3772. rpcapi = volume_rpcapi.VolumeAPI()
  3773. rpcapi.failover_completed(context, service, updates)
  3774. else:
  3775. service.update(updates)
  3776. service.save()
  3777. def failover_completed(self, context, updates):
  3778. """Finalize failover of this backend.
  3779. When a service is clustered and replicated the failover has 2 stages,
  3780. one that does the failover of the volumes and another that finalizes
  3781. the failover of the services themselves.
  3782. This method takes care of the last part and is called from the service
  3783. doing the failover of the volumes after finished processing the
  3784. volumes.
  3785. """
  3786. service = self._get_service()
  3787. service.update(updates)
  3788. try:
  3789. self.driver.failover_completed(context, service.active_backend_id)
  3790. except Exception:
  3791. msg = _('Driver reported error during replication failover '
  3792. 'completion.')
  3793. LOG.exception(msg)
  3794. service.disabled = True
  3795. service.disabled_reason = msg
  3796. service.replication_status = (
  3797. fields.ReplicationStatus.ERROR)
  3798. service.save()
  3799. def freeze_host(self, context):
  3800. """Freeze management plane on this backend.
  3801. Basically puts the control/management plane into a
  3802. Read Only state. We should handle this in the scheduler,
  3803. however this is provided to let the driver know in case it
  3804. needs/wants to do something specific on the backend.
  3805. :param context: security context
  3806. """
  3807. # TODO(jdg): Return from driver? or catch?
  3808. # Update status column in service entry
  3809. try:
  3810. self.driver.freeze_backend(context)
  3811. except exception.VolumeDriverException:
  3812. # NOTE(jdg): In the case of freeze, we don't really
  3813. # need the backend's consent or anything, we'll just
  3814. # disable the service, so we can just log this and
  3815. # go about our business
  3816. LOG.warning('Error encountered on Cinder backend during '
  3817. 'freeze operation, service is frozen, however '
  3818. 'notification to driver has failed.')
  3819. service = self._get_service()
  3820. service.disabled = True
  3821. service.disabled_reason = "frozen"
  3822. service.save()
  3823. LOG.info("Set backend status to frozen successfully.")
  3824. return True
  3825. def thaw_host(self, context):
  3826. """UnFreeze management plane on this backend.
  3827. Basically puts the control/management plane back into
  3828. a normal state. We should handle this in the scheduler,
  3829. however this is provided to let the driver know in case it
  3830. needs/wants to do something specific on the backend.
  3831. :param context: security context
  3832. """
  3833. # TODO(jdg): Return from driver? or catch?
  3834. # Update status column in service entry
  3835. try:
  3836. self.driver.thaw_backend(context)
  3837. except exception.VolumeDriverException:
  3838. # NOTE(jdg): Thaw actually matters, if this call
  3839. # to the backend fails, we're stuck and can't re-enable
  3840. LOG.error('Error encountered on Cinder backend during '
  3841. 'thaw operation, service will remain frozen.')
  3842. return False
  3843. service = self._get_service()
  3844. service.disabled = False
  3845. service.disabled_reason = ""
  3846. service.save()
  3847. LOG.info("Thawed backend successfully.")
  3848. return True
  3849. def manage_existing_snapshot(self, ctxt, snapshot, ref=None):
  3850. LOG.debug('manage_existing_snapshot: managing %s.', ref)
  3851. try:
  3852. flow_engine = manage_existing_snapshot.get_flow(
  3853. ctxt,
  3854. self.db,
  3855. self.driver,
  3856. self.host,
  3857. snapshot.id,
  3858. ref)
  3859. except Exception:
  3860. LOG.exception("Failed to create manage_existing flow: "
  3861. "%(object_type)s %(object_id)s.",
  3862. {'object_type': 'snapshot',
  3863. 'object_id': snapshot.id})
  3864. raise exception.CinderException(
  3865. _("Failed to create manage existing flow."))
  3866. with flow_utils.DynamicLogListener(flow_engine, logger=LOG):
  3867. flow_engine.run()
  3868. return snapshot.id
  3869. def get_manageable_snapshots(self, ctxt, marker, limit, offset,
  3870. sort_keys, sort_dirs, want_objects=False):
  3871. try:
  3872. utils.require_driver_initialized(self.driver)
  3873. except exception.DriverNotInitialized:
  3874. with excutils.save_and_reraise_exception():
  3875. LOG.exception("Listing manageable snapshots failed, due "
  3876. "to uninitialized driver.")
  3877. cinder_snapshots = self._get_my_snapshots(ctxt)
  3878. try:
  3879. driver_entries = self.driver.get_manageable_snapshots(
  3880. cinder_snapshots, marker, limit, offset, sort_keys, sort_dirs)
  3881. if want_objects:
  3882. driver_entries = (objects.ManageableSnapshotList.
  3883. from_primitives(ctxt, driver_entries))
  3884. except AttributeError:
  3885. LOG.debug('Driver does not support listing manageable snapshots.')
  3886. return []
  3887. except Exception:
  3888. with excutils.save_and_reraise_exception():
  3889. LOG.exception("Listing manageable snapshots failed, due "
  3890. "to driver error.")
  3891. return driver_entries
  3892. def get_capabilities(self, context, discover):
  3893. """Get capabilities of backend storage."""
  3894. if discover:
  3895. self.driver.init_capabilities()
  3896. capabilities = self.driver.capabilities
  3897. LOG.debug("Obtained capabilities list: %s.", capabilities)
  3898. return capabilities
  3899. def get_backup_device(self, ctxt, backup, want_objects=False):
  3900. (backup_device, is_snapshot) = (
  3901. self.driver.get_backup_device(ctxt, backup))
  3902. secure_enabled = self.driver.secure_file_operations_enabled()
  3903. backup_device_dict = {'backup_device': backup_device,
  3904. 'secure_enabled': secure_enabled,
  3905. 'is_snapshot': is_snapshot, }
  3906. # TODO(sborkows): from_primitive method will be removed in O, so there
  3907. # is a need to clean here then.
  3908. return (objects.BackupDeviceInfo.from_primitive(backup_device_dict,
  3909. ctxt)
  3910. if want_objects else backup_device_dict)
  3911. def secure_file_operations_enabled(self, ctxt, volume):
  3912. secure_enabled = self.driver.secure_file_operations_enabled()
  3913. return secure_enabled
  3914. def _connection_create(self, ctxt, volume, attachment, connector):
  3915. try:
  3916. self.driver.validate_connector(connector)
  3917. except exception.InvalidConnectorException as err:
  3918. raise exception.InvalidInput(reason=six.text_type(err))
  3919. except Exception as err:
  3920. err_msg = (_("Validate volume connection failed "
  3921. "(error: %(err)s).") % {'err': six.text_type(err)})
  3922. LOG.error(err_msg, resource=volume)
  3923. raise exception.VolumeBackendAPIException(data=err_msg)
  3924. try:
  3925. model_update = self.driver.create_export(ctxt.elevated(),
  3926. volume, connector)
  3927. except exception.CinderException as ex:
  3928. err_msg = (_("Create export for volume failed (%s).") % ex.msg)
  3929. LOG.exception(err_msg, resource=volume)
  3930. raise exception.VolumeBackendAPIException(data=err_msg)
  3931. try:
  3932. if model_update:
  3933. volume.update(model_update)
  3934. volume.save()
  3935. except exception.CinderException as ex:
  3936. LOG.exception("Model update failed.", resource=volume)
  3937. raise exception.ExportFailure(reason=six.text_type(ex))
  3938. try:
  3939. conn_info = self.driver.initialize_connection(volume, connector)
  3940. except Exception as err:
  3941. err_msg = (_("Driver initialize connection failed "
  3942. "(error: %(err)s).") % {'err': six.text_type(err)})
  3943. LOG.exception(err_msg, resource=volume)
  3944. self.driver.remove_export(ctxt.elevated(), volume)
  3945. raise exception.VolumeBackendAPIException(data=err_msg)
  3946. conn_info = self._parse_connection_options(ctxt, volume, conn_info)
  3947. # NOTE(jdg): Get rid of the nested dict (data key)
  3948. conn_data = conn_info.pop('data', {})
  3949. connection_info = conn_data.copy()
  3950. connection_info.update(conn_info)
  3951. values = {'volume_id': volume.id,
  3952. 'attach_status': 'attaching',
  3953. 'connector': jsonutils.dumps(connector)}
  3954. # TODO(mriedem): Use VolumeAttachment.save() here.
  3955. self.db.volume_attachment_update(ctxt, attachment.id, values)
  3956. connection_info['attachment_id'] = attachment.id
  3957. return connection_info
  3958. def attachment_update(self,
  3959. context,
  3960. vref,
  3961. connector,
  3962. attachment_id):
  3963. """Update/Finalize an attachment.
  3964. This call updates a valid attachment record to associate with a volume
  3965. and provide the caller with the proper connection info. Note that
  3966. this call requires an `attachment_ref`. It's expected that prior to
  3967. this call that the volume and an attachment UUID has been reserved.
  3968. param: vref: Volume object to create attachment for
  3969. param: connector: Connector object to use for attachment creation
  3970. param: attachment_ref: ID of the attachment record to update
  3971. """
  3972. mode = connector.get('mode', 'rw')
  3973. self._notify_about_volume_usage(context, vref, 'attach.start')
  3974. attachment_ref = objects.VolumeAttachment.get_by_id(context,
  3975. attachment_id)
  3976. # Check to see if a mode parameter was set during attachment-create;
  3977. # this seems kinda wonky, but it's how we're keeping back compatability
  3978. # with the use of connector.mode for now. In other words, we're
  3979. # making sure we still honor ro settings from the connector but
  3980. # we override that if a value was specified in attachment-create
  3981. if attachment_ref.attach_mode != 'null':
  3982. mode = attachment_ref.attach_mode
  3983. connector['mode'] = mode
  3984. connection_info = self._connection_create(context,
  3985. vref,
  3986. attachment_ref,
  3987. connector)
  3988. try:
  3989. utils.require_driver_initialized(self.driver)
  3990. self.driver.attach_volume(context,
  3991. vref,
  3992. attachment_ref.instance_uuid,
  3993. connector.get('host', ''),
  3994. connector.get('mountpoint', 'na'))
  3995. except Exception as err:
  3996. self.message_api.create(
  3997. context, message_field.Action.UPDATE_ATTACHMENT,
  3998. resource_uuid=vref.id,
  3999. exception=err)
  4000. with excutils.save_and_reraise_exception():
  4001. self.db.volume_attachment_update(
  4002. context, attachment_ref.id,
  4003. {'attach_status':
  4004. fields.VolumeAttachStatus.ERROR_ATTACHING})
  4005. self.db.volume_attached(context.elevated(),
  4006. attachment_ref.id,
  4007. attachment_ref.instance_uuid,
  4008. connector.get('host', ''),
  4009. connector.get('mountpoint', 'na'),
  4010. mode,
  4011. False)
  4012. vref.refresh()
  4013. attachment_ref.refresh()
  4014. LOG.info("attachment_update completed successfully.",
  4015. resource=vref)
  4016. return connection_info
  4017. def _connection_terminate(self, context, volume,
  4018. attachment, force=False):
  4019. """Remove a volume connection, but leave attachment.
  4020. Exits early if the attachment does not have a connector and returns
  4021. None to indicate shared connections are irrelevant.
  4022. """
  4023. utils.require_driver_initialized(self.driver)
  4024. connector = attachment.connector
  4025. if not connector and not force:
  4026. # It's possible to attach a volume to a shelved offloaded server
  4027. # in nova, and a shelved offloaded server is not on a compute host,
  4028. # which means the attachment was made without a host connector,
  4029. # so if we don't have a connector we can't terminate a connection
  4030. # that was never actually made to the storage backend, so just
  4031. # log a message and exit.
  4032. LOG.debug('No connector for attachment %s; skipping storage '
  4033. 'backend terminate_connection call.', attachment.id)
  4034. # None indicates we don't know and don't care.
  4035. return None
  4036. try:
  4037. shared_connections = self.driver.terminate_connection(volume,
  4038. connector,
  4039. force=force)
  4040. if not isinstance(shared_connections, bool):
  4041. shared_connections = False
  4042. except Exception as err:
  4043. err_msg = (_('Terminate volume connection failed: %(err)s')
  4044. % {'err': six.text_type(err)})
  4045. LOG.exception(err_msg, resource=volume)
  4046. raise exception.VolumeBackendAPIException(data=err_msg)
  4047. LOG.info("Terminate volume connection completed successfully.",
  4048. resource=volume)
  4049. # NOTE(jdg): Return True/False if there are other outstanding
  4050. # attachments that share this connection. If True should signify
  4051. # caller to preserve the actual host connection (work should be
  4052. # done in the brick connector as it has the knowledge of what's
  4053. # going on here.
  4054. return shared_connections
  4055. def attachment_delete(self, context, attachment_id, vref):
  4056. """Delete/Detach the specified attachment.
  4057. Notifies the backend device that we're detaching the specified
  4058. attachment instance.
  4059. param: vref: Volume object associated with the attachment
  4060. param: attachment: Attachment reference object to remove
  4061. NOTE if the attachment reference is None, we remove all existing
  4062. attachments for the specified volume object.
  4063. """
  4064. attachment_ref = objects.VolumeAttachment.get_by_id(context,
  4065. attachment_id)
  4066. if not attachment_ref:
  4067. for attachment in VA_LIST.get_all_by_volume_id(context, vref.id):
  4068. self._do_attachment_delete(context, vref, attachment)
  4069. else:
  4070. self._do_attachment_delete(context, vref, attachment_ref)
  4071. def _do_attachment_delete(self, context, vref, attachment):
  4072. utils.require_driver_initialized(self.driver)
  4073. self._notify_about_volume_usage(context, vref, "detach.start")
  4074. has_shared_connection = self._connection_terminate(context,
  4075. vref,
  4076. attachment)
  4077. try:
  4078. LOG.debug('Deleting attachment %(attachment_id)s.',
  4079. {'attachment_id': attachment.id},
  4080. resource=vref)
  4081. self.driver.detach_volume(context, vref, attachment)
  4082. if has_shared_connection is not None and not has_shared_connection:
  4083. self.driver.remove_export(context.elevated(), vref)
  4084. except Exception:
  4085. # FIXME(jdg): Obviously our volume object is going to need some
  4086. # changes to deal with multi-attach and figuring out how to
  4087. # represent a single failed attach out of multiple attachments
  4088. # TODO(jdg): object method here
  4089. self.db.volume_attachment_update(
  4090. context, attachment.get('id'),
  4091. {'attach_status': fields.VolumeAttachStatus.ERROR_DETACHING})
  4092. else:
  4093. self.db.volume_detached(context.elevated(), vref.id,
  4094. attachment.get('id'))
  4095. self.db.volume_admin_metadata_delete(context.elevated(),
  4096. vref.id,
  4097. 'attached_mode')
  4098. self._notify_about_volume_usage(context, vref, "detach.end")
  4099. # Replication group API (Tiramisu)
  4100. def enable_replication(self, ctxt, group):
  4101. """Enable replication."""
  4102. group.refresh()
  4103. if group.replication_status != fields.ReplicationStatus.ENABLING:
  4104. msg = _("Replication status in group %s is not "
  4105. "enabling. Cannot enable replication.") % group.id
  4106. LOG.error(msg)
  4107. raise exception.InvalidGroup(reason=msg)
  4108. volumes = group.volumes
  4109. for vol in volumes:
  4110. vol.refresh()
  4111. if vol.replication_status != fields.ReplicationStatus.ENABLING:
  4112. msg = _("Replication status in volume %s is not "
  4113. "enabling. Cannot enable replication.") % vol.id
  4114. LOG.error(msg)
  4115. raise exception.InvalidVolume(reason=msg)
  4116. self._notify_about_group_usage(
  4117. ctxt, group, "enable_replication.start")
  4118. volumes_model_update = None
  4119. model_update = None
  4120. try:
  4121. utils.require_driver_initialized(self.driver)
  4122. model_update, volumes_model_update = (
  4123. self.driver.enable_replication(ctxt, group, volumes))
  4124. if volumes_model_update:
  4125. for update in volumes_model_update:
  4126. vol_obj = objects.Volume.get_by_id(ctxt, update['id'])
  4127. vol_obj.update(update)
  4128. vol_obj.save()
  4129. # If we failed to enable a volume, make sure the status
  4130. # for the group is set to error as well
  4131. if (update.get('replication_status') ==
  4132. fields.ReplicationStatus.ERROR and
  4133. model_update.get('replication_status') !=
  4134. fields.ReplicationStatus.ERROR):
  4135. model_update['replication_status'] = update.get(
  4136. 'replication_status')
  4137. if model_update:
  4138. if (model_update.get('replication_status') ==
  4139. fields.ReplicationStatus.ERROR):
  4140. msg = _('Enable replication failed.')
  4141. LOG.error(msg,
  4142. resource={'type': 'group',
  4143. 'id': group.id})
  4144. raise exception.VolumeDriverException(message=msg)
  4145. else:
  4146. group.update(model_update)
  4147. group.save()
  4148. except exception.CinderException as ex:
  4149. group.status = fields.GroupStatus.ERROR
  4150. group.replication_status = fields.ReplicationStatus.ERROR
  4151. group.save()
  4152. # Update volume status to 'error' if driver returns
  4153. # None for volumes_model_update.
  4154. if not volumes_model_update:
  4155. for vol in volumes:
  4156. vol.status = 'error'
  4157. vol.replication_status = fields.ReplicationStatus.ERROR
  4158. vol.save()
  4159. err_msg = _("Enable replication group failed: "
  4160. "%s.") % six.text_type(ex)
  4161. raise exception.ReplicationGroupError(reason=err_msg,
  4162. group_id=group.id)
  4163. for vol in volumes:
  4164. vol.replication_status = fields.ReplicationStatus.ENABLED
  4165. vol.save()
  4166. group.replication_status = fields.ReplicationStatus.ENABLED
  4167. group.save()
  4168. self._notify_about_group_usage(
  4169. ctxt, group, "enable_replication.end", volumes)
  4170. LOG.info("Enable replication completed successfully.",
  4171. resource={'type': 'group',
  4172. 'id': group.id})
  4173. # Replication group API (Tiramisu)
  4174. def disable_replication(self, ctxt, group):
  4175. """Disable replication."""
  4176. group.refresh()
  4177. if group.replication_status != fields.ReplicationStatus.DISABLING:
  4178. msg = _("Replication status in group %s is not "
  4179. "disabling. Cannot disable replication.") % group.id
  4180. LOG.error(msg)
  4181. raise exception.InvalidGroup(reason=msg)
  4182. volumes = group.volumes
  4183. for vol in volumes:
  4184. vol.refresh()
  4185. if (vol.replication_status !=
  4186. fields.ReplicationStatus.DISABLING):
  4187. msg = _("Replication status in volume %s is not "
  4188. "disabling. Cannot disable replication.") % vol.id
  4189. LOG.error(msg)
  4190. raise exception.InvalidVolume(reason=msg)
  4191. self._notify_about_group_usage(
  4192. ctxt, group, "disable_replication.start")
  4193. volumes_model_update = None
  4194. model_update = None
  4195. try:
  4196. utils.require_driver_initialized(self.driver)
  4197. model_update, volumes_model_update = (
  4198. self.driver.disable_replication(ctxt, group, volumes))
  4199. if volumes_model_update:
  4200. for update in volumes_model_update:
  4201. vol_obj = objects.Volume.get_by_id(ctxt, update['id'])
  4202. vol_obj.update(update)
  4203. vol_obj.save()
  4204. # If we failed to enable a volume, make sure the status
  4205. # for the group is set to error as well
  4206. if (update.get('replication_status') ==
  4207. fields.ReplicationStatus.ERROR and
  4208. model_update.get('replication_status') !=
  4209. fields.ReplicationStatus.ERROR):
  4210. model_update['replication_status'] = update.get(
  4211. 'replication_status')
  4212. if model_update:
  4213. if (model_update.get('replication_status') ==
  4214. fields.ReplicationStatus.ERROR):
  4215. msg = _('Disable replication failed.')
  4216. LOG.error(msg,
  4217. resource={'type': 'group',
  4218. 'id': group.id})
  4219. raise exception.VolumeDriverException(message=msg)
  4220. else:
  4221. group.update(model_update)
  4222. group.save()
  4223. except exception.CinderException as ex:
  4224. group.status = fields.GroupStatus.ERROR
  4225. group.replication_status = fields.ReplicationStatus.ERROR
  4226. group.save()
  4227. # Update volume status to 'error' if driver returns
  4228. # None for volumes_model_update.
  4229. if not volumes_model_update:
  4230. for vol in volumes:
  4231. vol.status = 'error'
  4232. vol.replication_status = fields.ReplicationStatus.ERROR
  4233. vol.save()
  4234. err_msg = _("Disable replication group failed: "
  4235. "%s.") % six.text_type(ex)
  4236. raise exception.ReplicationGroupError(reason=err_msg,
  4237. group_id=group.id)
  4238. for vol in volumes:
  4239. vol.replication_status = fields.ReplicationStatus.DISABLED
  4240. vol.save()
  4241. group.replication_status = fields.ReplicationStatus.DISABLED
  4242. group.save()
  4243. self._notify_about_group_usage(
  4244. ctxt, group, "disable_replication.end", volumes)
  4245. LOG.info("Disable replication completed successfully.",
  4246. resource={'type': 'group',
  4247. 'id': group.id})
  4248. # Replication group API (Tiramisu)
  4249. def failover_replication(self, ctxt, group, allow_attached_volume=False,
  4250. secondary_backend_id=None):
  4251. """Failover replication."""
  4252. group.refresh()
  4253. if group.replication_status != fields.ReplicationStatus.FAILING_OVER:
  4254. msg = _("Replication status in group %s is not "
  4255. "failing-over. Cannot failover replication.") % group.id
  4256. LOG.error(msg)
  4257. raise exception.InvalidGroup(reason=msg)
  4258. volumes = group.volumes
  4259. for vol in volumes:
  4260. vol.refresh()
  4261. if vol.status == 'in-use' and not allow_attached_volume:
  4262. msg = _("Volume %s is attached but allow_attached_volume flag "
  4263. "is False. Cannot failover replication.") % vol.id
  4264. LOG.error(msg)
  4265. raise exception.InvalidVolume(reason=msg)
  4266. if (vol.replication_status !=
  4267. fields.ReplicationStatus.FAILING_OVER):
  4268. msg = _("Replication status in volume %s is not "
  4269. "failing-over. Cannot failover replication.") % vol.id
  4270. LOG.error(msg)
  4271. raise exception.InvalidVolume(reason=msg)
  4272. self._notify_about_group_usage(
  4273. ctxt, group, "failover_replication.start")
  4274. volumes_model_update = None
  4275. model_update = None
  4276. try:
  4277. utils.require_driver_initialized(self.driver)
  4278. model_update, volumes_model_update = (
  4279. self.driver.failover_replication(
  4280. ctxt, group, volumes, secondary_backend_id))
  4281. if volumes_model_update:
  4282. for update in volumes_model_update:
  4283. vol_obj = objects.Volume.get_by_id(ctxt, update['id'])
  4284. vol_obj.update(update)
  4285. vol_obj.save()
  4286. # If we failed to enable a volume, make sure the status
  4287. # for the group is set to error as well
  4288. if (update.get('replication_status') ==
  4289. fields.ReplicationStatus.ERROR and
  4290. model_update.get('replication_status') !=
  4291. fields.ReplicationStatus.ERROR):
  4292. model_update['replication_status'] = update.get(
  4293. 'replication_status')
  4294. if model_update:
  4295. if (model_update.get('replication_status') ==
  4296. fields.ReplicationStatus.ERROR):
  4297. msg = _('Failover replication failed.')
  4298. LOG.error(msg,
  4299. resource={'type': 'group',
  4300. 'id': group.id})
  4301. raise exception.VolumeDriverException(message=msg)
  4302. else:
  4303. group.update(model_update)
  4304. group.save()
  4305. except exception.CinderException as ex:
  4306. group.status = fields.GroupStatus.ERROR
  4307. group.replication_status = fields.ReplicationStatus.ERROR
  4308. group.save()
  4309. # Update volume status to 'error' if driver returns
  4310. # None for volumes_model_update.
  4311. if not volumes_model_update:
  4312. for vol in volumes:
  4313. vol.status = 'error'
  4314. vol.replication_status = fields.ReplicationStatus.ERROR
  4315. vol.save()
  4316. err_msg = _("Failover replication group failed: "
  4317. "%s.") % six.text_type(ex)
  4318. raise exception.ReplicationGroupError(reason=err_msg,
  4319. group_id=group.id)
  4320. for vol in volumes:
  4321. if secondary_backend_id == "default":
  4322. vol.replication_status = fields.ReplicationStatus.ENABLED
  4323. else:
  4324. vol.replication_status = (
  4325. fields.ReplicationStatus.FAILED_OVER)
  4326. vol.save()
  4327. if secondary_backend_id == "default":
  4328. group.replication_status = fields.ReplicationStatus.ENABLED
  4329. else:
  4330. group.replication_status = fields.ReplicationStatus.FAILED_OVER
  4331. group.save()
  4332. self._notify_about_group_usage(
  4333. ctxt, group, "failover_replication.end", volumes)
  4334. LOG.info("Failover replication completed successfully.",
  4335. resource={'type': 'group',
  4336. 'id': group.id})
  4337. def list_replication_targets(self, ctxt, group):
  4338. """Provide a means to obtain replication targets for a group.
  4339. This method is used to find the replication_device config
  4340. info. 'backend_id' is a required key in 'replication_device'.
  4341. Response Example for admin:
  4342. .. code:: json
  4343. {
  4344. "replication_targets": [
  4345. {
  4346. "backend_id": "vendor-id-1",
  4347. "unique_key": "val1"
  4348. },
  4349. {
  4350. "backend_id": "vendor-id-2",
  4351. "unique_key": "val2"
  4352. }
  4353. ]
  4354. }
  4355. Response example for non-admin:
  4356. .. code:: json
  4357. {
  4358. "replication_targets": [
  4359. {
  4360. "backend_id": "vendor-id-1"
  4361. },
  4362. {
  4363. "backend_id": "vendor-id-2"
  4364. }
  4365. ]
  4366. }
  4367. """
  4368. replication_targets = []
  4369. try:
  4370. group.refresh()
  4371. if self.configuration.replication_device:
  4372. if ctxt.is_admin:
  4373. for rep_dev in self.configuration.replication_device:
  4374. keys = rep_dev.keys()
  4375. dev = {}
  4376. for k in keys:
  4377. dev[k] = rep_dev[k]
  4378. replication_targets.append(dev)
  4379. else:
  4380. for rep_dev in self.configuration.replication_device:
  4381. dev = rep_dev.get('backend_id')
  4382. if dev:
  4383. replication_targets.append({'backend_id': dev})
  4384. except exception.GroupNotFound:
  4385. err_msg = (_("Get replication targets failed. Group %s not "
  4386. "found.") % group.id)
  4387. LOG.exception(err_msg)
  4388. raise exception.VolumeBackendAPIException(data=err_msg)
  4389. return {'replication_targets': replication_targets}