def tests(status, test): pool = GreenPool(size=500) for host, s in status['servers'].iteritems(): for t in test: if t.name in s: pool.spawn_n(t.test, host, s) pool.waitall()
def run_once(self, *args, **kwargs): """ Executes a single pass, looking for objects to expire. :param args: Extra args to fulfill the Daemon interface; this daemon has no additional args. :param kwargs: Extra keyword args to fulfill the Daemon interface; this daemon accepts processes and process keyword args. These will override the values from the config file if provided. """ processes, process = self.get_process_values(kwargs) pool = GreenPool(self.concurrency) containers_to_delete = [] self.report_first_time = self.report_last_time = time() self.report_objects = 0 try: self.logger.debug(_('Run begin')) containers, objects = \ self.swift.get_account_info(self.expiring_objects_account) self.logger.info( _('Pass beginning; %s possible containers; %s ' 'possible objects') % (containers, objects)) for c in self.swift.iter_containers(self.expiring_objects_account): container = c['name'] timestamp = int(container) if timestamp > int(time()): break containers_to_delete.append(container) for o in self.swift.iter_objects(self.expiring_objects_account, container): obj = o['name'].encode('utf8') if processes > 0: obj_process = int( hashlib.md5('%s/%s' % (container, obj)).hexdigest(), 16) if obj_process % processes != process: continue timestamp, actual_obj = obj.split('-', 1) timestamp = int(timestamp) if timestamp > int(time()): break pool.spawn_n(self.delete_object, actual_obj, timestamp, container, obj) pool.waitall() for container in containers_to_delete: try: self.swift.delete_container( self.expiring_objects_account, container, acceptable_statuses=(2, HTTP_NOT_FOUND, HTTP_CONFLICT)) except (Exception, Timeout) as err: self.logger.exception( _('Exception while deleting container %s %s') % (container, str(err))) self.logger.debug(_('Run end')) self.report(final=True) except (Exception, Timeout): self.logger.exception(_('Unhandled exception'))
def run_once(self, *args, **kwargs): """ Executes a single pass, looking for objects to expire. :param args: Extra args to fulfill the Daemon interface; this daemon has no additional args. :param kwargs: Extra keyword args to fulfill the Daemon interface; this daemon accepts processes and process keyword args. These will override the values from the config file if provided. """ self.get_process_values(kwargs) pool = GreenPool(self.concurrency) containers_to_delete = set([]) self.report_first_time = self.report_last_time = time() self.report_objects = 0 try: self.logger.debug('Run begin') containers, objects = \ self.swift.get_account_info(self.expiring_objects_account) self.logger.info( _('Pass beginning; ' '%(containers)s possible containers; ' '%(objects)s possible objects') % { 'containers': containers, 'objects': objects }) for container, obj in self.iter_cont_objs_to_expire(): containers_to_delete.add(container) if not obj: continue timestamp, actual_obj = obj.split('-', 1) timestamp = int(timestamp) if timestamp > int(time()): break pool.spawn_n(self.delete_object, actual_obj, timestamp, container, obj) pool.waitall() for container in containers_to_delete: try: self.swift.delete_container( self.expiring_objects_account, container, acceptable_statuses=(2, HTTP_NOT_FOUND, HTTP_CONFLICT)) except (Exception, Timeout) as err: self.logger.exception( _('Exception while deleting container %(container)s ' '%(err)s') % { 'container': container, 'err': str(err) }) self.logger.debug('Run end') self.report(final=True) except (Exception, Timeout): self.logger.exception(_('Unhandled exception'))
def run_once(self, *args, **kwargs): """ Executes a single pass, looking for objects to expire. :param args: Extra args to fulfill the Daemon interface; this daemon has no additional args. :param kwargs: Extra keyword args to fulfill the Daemon interface; this daemon accepts processes and process keyword args. These will override the values from the config file if provided. """ processes, process = self.get_process_values(kwargs) pool = GreenPool(self.concurrency) containers_to_delete = [] self.report_first_time = self.report_last_time = time() self.report_objects = 0 try: self.logger.debug(_('Run begin')) containers, objects = \ self.swift.get_account_info(self.expiring_objects_account) self.logger.info(_('Pass beginning; %s possible containers; %s ' 'possible objects') % (containers, objects)) for c in self.swift.iter_containers(self.expiring_objects_account): container = c['name'] timestamp = int(container) if timestamp > int(time()): break containers_to_delete.append(container) for o in self.swift.iter_objects(self.expiring_objects_account, container): obj = o['name'].encode('utf8') if processes > 0: obj_process = int( hashlib.md5('%s/%s' % (container, obj)). hexdigest(), 16) if obj_process % processes != process: continue timestamp, actual_obj = obj.split('-', 1) timestamp = int(timestamp) if timestamp > int(time()): break pool.spawn_n( self.delete_object, actual_obj, timestamp, container, obj) pool.waitall() for container in containers_to_delete: try: self.swift.delete_container( self.expiring_objects_account, container, acceptable_statuses=(2, HTTP_NOT_FOUND, HTTP_CONFLICT)) except (Exception, Timeout) as err: self.logger.exception( _('Exception while deleting container %s %s') % (container, str(err))) self.logger.debug(_('Run end')) self.report(final=True) except (Exception, Timeout): self.logger.exception(_('Unhandled exception'))
def run_once(self, *args, **kwargs): processes, process = self.get_process_values(kwargs) pool = GreenPool(self.concurrency) self.report_first_time = self.report_last_time = time() self.report_objects = 0 self.report_containers = 0 containers_to_delete = [] try: self.logger.debug(_('Run begin')) containers, objects = \ self.swift.get_account_info(self.sample_account) self.logger.info(_('Pass beginning; %s possible containers; %s ' 'possible objects') % (containers, objects)) for c in self.swift.iter_containers(self.sample_account): container = c['name'] try: timestamp, account = container.split('_', 1) timestamp = float(timestamp) except ValueError: self.logger.debug('ValueError: %s, ' 'need more than 1 value to unpack' % \ container) else: if processes > 0: obj_proc = int(hashlib.md5(container).hexdigest(), 16) if obj_proc % processes != process: continue n = (float(time()) // self.sample_rate) * self.sample_rate if timestamp <= n: containers_to_delete.append(container) pool.spawn_n(self.aggregate_container, container) pool.waitall() for container in containers_to_delete: try: self.logger.debug('delete container: %s' % container) self.swift.delete_container(self.sample_account, container, acceptable_statuses=( 2, HTTP_NOT_FOUND, HTTP_CONFLICT)) except (Exception, Timeout) as err: self.logger.exception( _('Exception while deleting container %s %s') % (container, str(err))) tenants_to_fillup = list() for c in self.swift.iter_containers(self.aggregate_account): tenant_id = c['name'] if processes > 0: c_proc = int(hashlib.md5(tenant_id).hexdigest(), 16) if c_proc % processes != process: continue tenants_to_fillup.append(tenant_id) # fillup lossed usage data self.fillup_lossed_usage_data(tenants_to_fillup) self.logger.debug(_('Run end')) self.report(final=True) except (Exception, Timeout): self.logger.exception(_('Unhandled exception'))
def run_once(self, *args, **kwargs): """ Executes a single pass, looking for objects to expire. :param args: Extra args to fulfill the Daemon interface; this daemon has no additional args. :param kwargs: Extra keyword args to fulfill the Daemon interface; this daemon accepts processes and process keyword args. These will override the values from the config file if provided. """ self.get_process_values(kwargs) pool = GreenPool(self.concurrency) self.report_first_time = self.report_last_time = time() self.report_objects = 0 try: self.logger.debug('Run begin') containers, objects = \ self.swift.get_account_info(self.expiring_objects_account) self.logger.info( _('Pass beginning; ' '%(containers)s possible containers; ' '%(objects)s possible objects') % { 'containers': containers, 'objects': objects }) task_containers = list(self.iter_task_containers_to_expire()) # delete_task_iter is a generator to yield a dict of # task_container, task_object, delete_timestamp, target_path # to handle delete actual object and pop the task from the queue. delete_task_iter = self.round_robin_order( self.iter_task_to_expire(task_containers)) for delete_task in delete_task_iter: pool.spawn_n(self.delete_object, **delete_task) pool.waitall() for container in task_containers: try: self.swift.delete_container( self.expiring_objects_account, container, acceptable_statuses=(2, HTTP_NOT_FOUND, HTTP_CONFLICT)) except (Exception, Timeout) as err: self.logger.exception( _('Exception while deleting container %(container)s ' '%(err)s') % { 'container': container, 'err': str(err) }) self.logger.debug('Run end') self.report(final=True) except (Exception, Timeout): self.logger.exception(_('Unhandled exception'))
def run_once(self, *args, **kwargs): """ Executes a single pass, looking for objects to expire. :param args: Extra args to fulfill the Daemon interface; this daemon has no additional args. :param kwargs: Extra keyword args to fulfill the Daemon interface; this daemon accepts processes and process keyword args. These will override the values from the config file if provided. """ self.get_process_values(kwargs) pool = GreenPool(self.concurrency) containers_to_delete = set([]) self.report_first_time = self.report_last_time = time() self.report_objects = 0 try: self.logger.debug('Run begin') containers, objects = \ self.swift.get_account_info(self.expiring_objects_account) self.logger.info(_('Pass beginning; ' '%(containers)s possible containers; ' '%(objects)s possible objects') % { 'containers': containers, 'objects': objects}) for container, obj in self.iter_cont_objs_to_expire(): containers_to_delete.add(container) if not obj: continue timestamp, actual_obj = obj.split('-', 1) timestamp = int(timestamp) if timestamp > int(time()): break pool.spawn_n( self.delete_object, actual_obj, timestamp, container, obj) pool.waitall() for container in containers_to_delete: try: self.swift.delete_container( self.expiring_objects_account, container, acceptable_statuses=(2, HTTP_NOT_FOUND, HTTP_CONFLICT)) except (Exception, Timeout) as err: self.logger.exception( _('Exception while deleting container %(container)s ' '%(err)s') % {'container': container, 'err': str(err)}) self.logger.debug('Run end') self.report(final=True) except (Exception, Timeout): self.logger.exception(_('Unhandled exception'))
def run_once(self, *args, **kwargs): """ Executes a single pass, looking for objects to expire. :param args: Extra args to fulfill the Daemon interface; this daemon has no additional args. :param kwargs: Extra keyword args to fulfill the Daemon interface; this daemon accepts processes and process keyword args. These will override the values from the config file if provided. """ processes, process = self.get_process_values(kwargs) pool = GreenPool(self.concurrency) self.report_first_time = self.report_last_time = time() self.report_objects = 0 try: self.logger.debug(_('Run begin')) for o in self.swift.iter_objects(self.restoring_object_account, self.todo_container): obj = o['name'].encode('utf8') if processes > 0: obj_process = int( hashlib.md5('%s/%s' % (self.todo_container, obj)). hexdigest(), 16) if obj_process % processes != process: continue pool.spawn_n(self.start_object_restoring, obj) pool.waitall() for o in self.swift.iter_objects(self.restoring_object_account, self.restoring_container): obj = o['name'].encode('utf8') if processes > 0: obj_process = int( hashlib.md5('%s/%s' % (self.restoring_container, obj)). hexdigest(), 16) if obj_process % processes != process: continue pool.spawn_n(self.check_object_restored, obj) pool.waitall() self.logger.debug(_('Run end')) self.report(final=True) except (Exception, Timeout) as e: report_exception(self.logger, _('Unhandled exception'), self.client)
def discovery(status, test): pool = GreenPool(size=500) for d in settings.discovery: servers = d().get_servers() # [('ip', 'host')] for server in servers: ip = server[0] host = server[1] if host in settings.exclude: continue if host not in status['servers']: # do discovery status['servers'][host] = {} logging.info('performing discovery on %r', server) for t in test: pool.spawn_n(t.discover, ip, status['servers'][host]) status['servers'][host]['ip'] = ip pool.waitall()
def discovery(status, test): pool = GreenPool(size=500) for d in settings.discovery: servers = d().get_servers() # [('ip', 'host')] for server in servers: ip = server[0] host = server[1] if host in settings.exclude: continue if host not in status["servers"]: # do discovery status["servers"][host] = {} logging.info("performing discovery on %r", server) for t in test: pool.spawn_n(t.discover, ip, status["servers"][host]) status["servers"][host]["ip"] = ip pool.waitall()
class Checker(object): def __init__(self, namespace, concurrency=50, error_file=None, rebuild_file=None, full=True): self.pool = GreenPool(concurrency) self.error_file = error_file self.full = bool(full) if self.error_file: f = open(self.error_file, 'a') self.error_writer = csv.writer(f, delimiter=' ') self.rebuild_file = rebuild_file if self.rebuild_file: fd = open(self.rebuild_file, 'a') self.rebuild_writer = csv.writer(fd, delimiter='|') conf = {'namespace': namespace} self.account_client = AccountClient(conf) self.container_client = ContainerClient(conf) self.blob_client = BlobClient() self.accounts_checked = 0 self.containers_checked = 0 self.objects_checked = 0 self.chunks_checked = 0 self.account_not_found = 0 self.container_not_found = 0 self.object_not_found = 0 self.chunk_not_found = 0 self.account_exceptions = 0 self.container_exceptions = 0 self.object_exceptions = 0 self.chunk_exceptions = 0 self.list_cache = {} self.running = {} def write_error(self, target): error = [target.account] if target.container: error.append(target.container) if target.obj: error.append(target.obj) if target.chunk: error.append(target.chunk) self.error_writer.writerow(error) def write_rebuilder_input(self, target, obj_meta, ct_meta): try: cid = ct_meta['system']['sys.name'].split('.', 1)[0] except KeyError: cid = ct_meta['properties']['sys.name'].split('.', 1)[0] self.rebuild_writer.writerow((cid, obj_meta['id'], target.chunk)) def write_chunk_error(self, target, obj_meta, chunk=None): if chunk is not None: target = target.copy() target.chunk = chunk if self.error_file: self.write_error(target) if self.rebuild_file: self.write_rebuilder_input( target, obj_meta, self.list_cache[(target.account, target.container)][1]) def _check_chunk_xattr(self, target, obj_meta, xattr_meta): error = False # Composed position -> erasure coding attr_prefix = 'meta' if '.' in obj_meta['pos'] else '' attr_key = attr_prefix + 'chunk_size' if str(obj_meta['size']) != xattr_meta.get(attr_key): print( " Chunk %s '%s' xattr (%s) " "differs from size in meta2 (%s)" % (target, attr_key, xattr_meta.get(attr_key), obj_meta['size'])) error = True attr_key = attr_prefix + 'chunk_hash' if obj_meta['hash'] != xattr_meta.get(attr_key): print( " Chunk %s '%s' xattr (%s) " "differs from hash in meta2 (%s)" % (target, attr_key, xattr_meta.get(attr_key), obj_meta['hash'])) error = True return error def check_chunk(self, target): chunk = target.chunk obj_listing, obj_meta = self.check_obj(target) error = False if chunk not in obj_listing: print(' Chunk %s missing from object listing' % target) error = True db_meta = dict() else: db_meta = obj_listing[chunk] try: xattr_meta = self.blob_client.chunk_head(chunk, xattr=self.full) except exc.NotFound as e: self.chunk_not_found += 1 error = True print(' Not found chunk "%s": %s' % (target, str(e))) except Exception as e: self.chunk_exceptions += 1 error = True print(' Exception chunk "%s": %s' % (target, str(e))) else: if db_meta and self.full: error = self._check_chunk_xattr(target, db_meta, xattr_meta) if error: self.write_chunk_error(target, obj_meta) self.chunks_checked += 1 def check_obj_policy(self, target, obj_meta, chunks): """ Check that the list of chunks of an object matches the object's storage policy. """ stg_met = STORAGE_METHODS.load(obj_meta['chunk_method']) chunks_by_pos = _sort_chunks(chunks, stg_met.ec) if stg_met.ec: required = stg_met.ec_nb_data + stg_met.ec_nb_parity else: required = stg_met.nb_copy for pos, clist in chunks_by_pos.iteritems(): if len(clist) < required: print(' Missing %d chunks at position %s of %s' % (required - len(clist), pos, target)) if stg_met.ec: subs = {x['num'] for x in clist} for sub in range(required): if sub not in subs: self.write_chunk_error(target, obj_meta, '%d.%d' % (pos, sub)) else: self.write_chunk_error(target, obj_meta, str(pos)) def check_obj(self, target, recurse=False): account = target.account container = target.container obj = target.obj if (account, container, obj) in self.running: self.running[(account, container, obj)].wait() if (account, container, obj) in self.list_cache: return self.list_cache[(account, container, obj)] self.running[(account, container, obj)] = Event() print('Checking object "%s"' % target) container_listing, ct_meta = self.check_container(target) error = False if obj not in container_listing: print(' Object %s missing from container listing' % target) error = True # checksum = None else: # TODO check checksum match # checksum = container_listing[obj]['hash'] pass results = [] meta = dict() try: meta, results = self.container_client.content_locate( account=account, reference=container, path=obj) except exc.NotFound as e: self.object_not_found += 1 error = True print(' Not found object "%s": %s' % (target, str(e))) except Exception as e: self.object_exceptions += 1 error = True print(' Exception object "%s": %s' % (target, str(e))) chunk_listing = dict() for chunk in results: chunk_listing[chunk['url']] = chunk self.check_obj_policy(target.copy(), meta, results) self.objects_checked += 1 self.list_cache[(account, container, obj)] = (chunk_listing, meta) self.running[(account, container, obj)].send(True) del self.running[(account, container, obj)] if recurse: for chunk in chunk_listing: t = target.copy() t.chunk = chunk self.pool.spawn_n(self.check_chunk, t) if error and self.error_file: self.write_error(target) return chunk_listing, meta def check_container(self, target, recurse=False): account = target.account container = target.container if (account, container) in self.running: self.running[(account, container)].wait() if (account, container) in self.list_cache: return self.list_cache[(account, container)] self.running[(account, container)] = Event() print('Checking container "%s"' % target) account_listing = self.check_account(target) error = False if container not in account_listing: error = True print(' Container %s missing from account listing' % target) marker = None results = [] ct_meta = dict() while True: try: _, resp = self.container_client.content_list( account=account, reference=container, marker=marker) except exc.NotFound as e: self.container_not_found += 1 error = True print(' Not found container "%s": %s' % (target, str(e))) break except Exception as e: self.container_exceptions += 1 error = True print(' Exception container "%s": %s' % (target, str(e))) break if resp['objects']: marker = resp['objects'][-1]['name'] results.extend(resp['objects']) else: ct_meta = resp ct_meta.pop('objects') break container_listing = dict() for obj in results: container_listing[obj['name']] = obj self.containers_checked += 1 self.list_cache[(account, container)] = container_listing, ct_meta self.running[(account, container)].send(True) del self.running[(account, container)] if recurse: for obj in container_listing: t = target.copy() t.obj = obj self.pool.spawn_n(self.check_obj, t, True) if error and self.error_file: self.write_error(target) return container_listing, ct_meta def check_account(self, target, recurse=False): account = target.account if account in self.running: self.running[account].wait() if account in self.list_cache: return self.list_cache[account] self.running[account] = Event() print('Checking account "%s"' % target) error = False marker = None results = [] while True: try: resp = self.account_client.container_list(account, marker=marker) except Exception as e: self.account_exceptions += 1 error = True print(' Exception account "%s": %s' % (target, str(e))) break if resp['listing']: marker = resp['listing'][-1][0] else: break results.extend(resp['listing']) containers = dict() for e in results: containers[e[0]] = (e[1], e[2]) self.list_cache[account] = containers self.running[account].send(True) del self.running[account] self.accounts_checked += 1 if recurse: for container in containers: t = target.copy() t.container = container self.pool.spawn_n(self.check_container, t, True) if error and self.error_file: self.write_error(target) return containers def check(self, target): if target.chunk and target.obj and target.container: self.pool.spawn_n(self.check_chunk, target) elif target.obj and target.container: self.pool.spawn_n(self.check_obj, target, True) elif target.container: self.pool.spawn_n(self.check_container, target, True) else: self.pool.spawn_n(self.check_account, target, True) def wait(self): self.pool.waitall() def report(self): def _report_stat(name, stat): print("{0:18}: {1}".format(name, stat)) print() print('Report') _report_stat("Accounts checked", self.accounts_checked) if self.account_not_found: _report_stat("Missing accounts", self.account_not_found) if self.account_exceptions: _report_stat("Exceptions", self.account_not_found) print() _report_stat("Containers checked", self.containers_checked) if self.container_not_found: _report_stat("Missing containers", self.container_not_found) if self.container_exceptions: _report_stat("Exceptions", self.container_exceptions) print() _report_stat("Objects checked", self.objects_checked) if self.object_not_found: _report_stat("Missing objects", self.object_not_found) if self.object_exceptions: _report_stat("Exceptions", self.object_exceptions) print() _report_stat("Chunks checked", self.chunks_checked) if self.chunk_not_found: _report_stat("Missing chunks", self.chunk_not_found) if self.chunk_exceptions: _report_stat("Exceptions", self.chunk_exceptions)
class VolumeManager(manager.SchedulerDependentManager): """Manages attachable block storage devices.""" RPC_API_VERSION = "1.12" def __init__(self, volume_driver=None, service_name=None, *args, **kwargs): """Load the driver from the one specified in args, or from flags.""" # update_service_capabilities needs service_name to be volume super(VolumeManager, self).__init__(service_name="volume", *args, **kwargs) self.configuration = Configuration(volume_manager_opts, config_group=service_name) self._tp = GreenPool() self.stats = {} if not volume_driver: # Get from configuration, which will get the default # if its not using the multi backend volume_driver = self.configuration.volume_driver if volume_driver in MAPPING: LOG.warn(_("Driver path %s is deprecated, update your " "configuration to the new path."), volume_driver) volume_driver = MAPPING[volume_driver] if volume_driver == "cinder.volume.drivers.lvm.ThinLVMVolumeDriver": # Deprecated in Havana # Not handled in MAPPING because it requires setting a conf option LOG.warn( _( "ThinLVMVolumeDriver is deprecated, please configure " "LVMISCSIDriver and lvm_type=thin. Continuing with " "those settings." ) ) volume_driver = "cinder.volume.drivers.lvm.LVMISCSIDriver" self.configuration.lvm_type = "thin" self.driver = importutils.import_object(volume_driver, configuration=self.configuration, db=self.db) def _add_to_threadpool(self, func, *args, **kwargs): self._tp.spawn_n(func, *args, **kwargs) def init_host(self): """Do any initialization that needs to be run if this is a standalone service. """ ctxt = context.get_admin_context() LOG.info( _("Starting volume driver %(driver_name)s (%(version)s)") % {"driver_name": self.driver.__class__.__name__, "version": self.driver.get_version()} ) try: self.driver.do_setup(ctxt) self.driver.check_for_setup_error() except Exception as ex: LOG.error( _("Error encountered during " "initialization of driver: %(name)s") % {"name": self.driver.__class__.__name__} ) LOG.exception(ex) # we don't want to continue since we failed # to initialize the driver correctly. return volumes = self.db.volume_get_all_by_host(ctxt, self.host) LOG.debug(_("Re-exporting %s volumes"), len(volumes)) try: sum = 0 self.stats.update({"allocated_capacity_gb": sum}) for volume in volumes: if volume["status"] in ["available", "in-use"]: # calculate allocated capacity for driver sum += volume["size"] self.stats["allocated_capacity_gb"] = sum self.driver.ensure_export(ctxt, volume) elif volume["status"] == "downloading": LOG.info(_("volume %s stuck in a downloading state"), volume["id"]) self.driver.clear_download(ctxt, volume) self.db.volume_update(ctxt, volume["id"], {"status": "error"}) else: LOG.info(_("volume %s: skipping export"), volume["id"]) except Exception as ex: LOG.error( _("Error encountered during " "re-exporting phase of driver initialization: " " %(name)s") % {"name": self.driver.__class__.__name__} ) LOG.exception(ex) return # at this point the driver is considered initialized. self.driver.set_initialized() LOG.debug(_("Resuming any in progress delete operations")) for volume in volumes: if volume["status"] == "deleting": LOG.info(_("Resuming delete on volume: %s") % volume["id"]) if CONF.volume_service_inithost_offload: # Offload all the pending volume delete operations to the # threadpool to prevent the main volume service thread # from being blocked. self._add_to_threadpool(self.delete_volume(ctxt, volume["id"])) else: # By default, delete volumes sequentially self.delete_volume(ctxt, volume["id"]) # collect and publish service capabilities self.publish_service_capabilities(ctxt) def create_volume( self, context, volume_id, request_spec=None, filter_properties=None, allow_reschedule=True, snapshot_id=None, image_id=None, source_volid=None, ): """Creates and exports the volume.""" context_saved = context.deepcopy() context = context.elevated() if filter_properties is None: filter_properties = {} try: # NOTE(flaper87): Driver initialization is # verified by the task itself. flow_engine = create_volume.get_flow( context, self.db, self.driver, self.scheduler_rpcapi, self.host, volume_id, snapshot_id=snapshot_id, image_id=image_id, source_volid=source_volid, allow_reschedule=allow_reschedule, reschedule_context=context_saved, request_spec=request_spec, filter_properties=filter_properties, ) except Exception: LOG.exception(_("Failed to create manager volume flow")) raise exception.CinderException(_("Failed to create manager volume flow")) if snapshot_id is not None: # Make sure the snapshot is not deleted until we are done with it. locked_action = "%s-%s" % (snapshot_id, "delete_snapshot") elif source_volid is not None: # Make sure the volume is not deleted until we are done with it. locked_action = "%s-%s" % (source_volid, "delete_volume") else: locked_action = None def _run_flow(): # This code executes create volume flow. If something goes wrong, # flow reverts all job that was done and reraises an exception. # Otherwise, all data that was generated by flow becomes available # in flow engine's storage. flow_engine.run() @utils.synchronized(locked_action, external=True) def _run_flow_locked(): _run_flow() if locked_action is None: _run_flow() else: _run_flow_locked() # Fetch created volume from storage volume_ref = flow_engine.storage.fetch("volume") # Update volume stats self.stats["allocated_capacity_gb"] += volume_ref["size"] return volume_ref["id"] @locked_volume_operation def delete_volume(self, context, volume_id): """Deletes and unexports volume.""" context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) if context.project_id != volume_ref["project_id"]: project_id = volume_ref["project_id"] else: project_id = context.project_id LOG.info(_("volume %s: deleting"), volume_ref["id"]) if volume_ref["attach_status"] == "attached": # Volume is still attached, need to detach first raise exception.VolumeAttached(volume_id=volume_id) if volume_ref["host"] != self.host: raise exception.InvalidVolume(reason=_("volume is not local to this node")) self._notify_about_volume_usage(context, volume_ref, "delete.start") try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the volume status updated. utils.require_driver_initialized(self.driver) LOG.debug(_("volume %s: removing export"), volume_ref["id"]) self.driver.remove_export(context, volume_ref) LOG.debug(_("volume %s: deleting"), volume_ref["id"]) self.driver.delete_volume(volume_ref) except exception.VolumeIsBusy: LOG.error(_("Cannot delete volume %s: volume is busy"), volume_ref["id"]) self.driver.ensure_export(context, volume_ref) self.db.volume_update(context, volume_ref["id"], {"status": "available"}) return True except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_ref["id"], {"status": "error_deleting"}) # If deleting the source volume in a migration, we want to skip quotas # and other database updates. if volume_ref["migration_status"]: return True # Get reservations try: reserve_opts = {"volumes": -1, "gigabytes": -volume_ref["size"]} QUOTAS.add_volume_type_opts(context, reserve_opts, volume_ref.get("volume_type_id")) reservations = QUOTAS.reserve(context, project_id=project_id, **reserve_opts) except Exception: reservations = None LOG.exception(_("Failed to update usages deleting volume")) # Delete glance metadata if it exists try: self.db.volume_glance_metadata_delete_by_volume(context, volume_id) LOG.debug(_("volume %s: glance metadata deleted"), volume_ref["id"]) except exception.GlanceMetadataNotFound: LOG.debug(_("no glance metadata found for volume %s"), volume_ref["id"]) self.db.volume_destroy(context, volume_id) LOG.info(_("volume %s: deleted successfully"), volume_ref["id"]) self._notify_about_volume_usage(context, volume_ref, "delete.end") # Commit the reservations if reservations: QUOTAS.commit(context, reservations, project_id=project_id) self.stats["allocated_capacity_gb"] -= volume_ref["size"] self.publish_service_capabilities(context) return True def create_snapshot(self, context, volume_id, snapshot_id): """Creates and exports the snapshot.""" caller_context = context context = context.elevated() snapshot_ref = self.db.snapshot_get(context, snapshot_id) LOG.info(_("snapshot %s: creating"), snapshot_ref["id"]) self._notify_about_snapshot_usage(context, snapshot_ref, "create.start") try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the snapshot status updated. utils.require_driver_initialized(self.driver) LOG.debug(_("snapshot %(snap_id)s: creating"), {"snap_id": snapshot_ref["id"]}) # Pass context so that drivers that want to use it, can, # but it is not a requirement for all drivers. snapshot_ref["context"] = caller_context model_update = self.driver.create_snapshot(snapshot_ref) if model_update: self.db.snapshot_update(context, snapshot_ref["id"], model_update) except Exception: with excutils.save_and_reraise_exception(): self.db.snapshot_update(context, snapshot_ref["id"], {"status": "error"}) self.db.snapshot_update(context, snapshot_ref["id"], {"status": "available", "progress": "100%"}) vol_ref = self.db.volume_get(context, volume_id) if vol_ref.bootable: try: self.db.volume_glance_metadata_copy_to_snapshot(context, snapshot_ref["id"], volume_id) except exception.CinderException as ex: LOG.exception( _( "Failed updating %(snapshot_id)s" " metadata using the provided volumes" " %(volume_id)s metadata" ) % {"volume_id": volume_id, "snapshot_id": snapshot_id} ) raise exception.MetadataCopyFailure(reason=ex) LOG.info(_("snapshot %s: created successfully"), snapshot_ref["id"]) self._notify_about_snapshot_usage(context, snapshot_ref, "create.end") return snapshot_id @locked_snapshot_operation def delete_snapshot(self, context, snapshot_id): """Deletes and unexports snapshot.""" caller_context = context context = context.elevated() snapshot_ref = self.db.snapshot_get(context, snapshot_id) project_id = snapshot_ref["project_id"] LOG.info(_("snapshot %s: deleting"), snapshot_ref["id"]) self._notify_about_snapshot_usage(context, snapshot_ref, "delete.start") try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the snapshot status updated. utils.require_driver_initialized(self.driver) LOG.debug(_("snapshot %s: deleting"), snapshot_ref["id"]) # Pass context so that drivers that want to use it, can, # but it is not a requirement for all drivers. snapshot_ref["context"] = caller_context self.driver.delete_snapshot(snapshot_ref) except exception.SnapshotIsBusy: LOG.error(_("Cannot delete snapshot %s: snapshot is busy"), snapshot_ref["id"]) self.db.snapshot_update(context, snapshot_ref["id"], {"status": "available"}) return True except Exception: with excutils.save_and_reraise_exception(): self.db.snapshot_update(context, snapshot_ref["id"], {"status": "error_deleting"}) # Get reservations try: if CONF.no_snapshot_gb_quota: reserve_opts = {"snapshots": -1} else: reserve_opts = {"snapshots": -1, "gigabytes": -snapshot_ref["volume_size"]} volume_ref = self.db.volume_get(context, snapshot_ref["volume_id"]) QUOTAS.add_volume_type_opts(context, reserve_opts, volume_ref.get("volume_type_id")) reservations = QUOTAS.reserve(context, project_id=project_id, **reserve_opts) except Exception: reservations = None LOG.exception(_("Failed to update usages deleting snapshot")) self.db.volume_glance_metadata_delete_by_snapshot(context, snapshot_id) self.db.snapshot_destroy(context, snapshot_id) LOG.info(_("snapshot %s: deleted successfully"), snapshot_ref["id"]) self._notify_about_snapshot_usage(context, snapshot_ref, "delete.end") # Commit the reservations if reservations: QUOTAS.commit(context, reservations, project_id=project_id) return True def attach_volume(self, context, volume_id, instance_uuid, host_name, mountpoint, mode): """Updates db to show volume is attached.""" @utils.synchronized(volume_id, external=True) def do_attach(): # check the volume status before attaching volume = self.db.volume_get(context, volume_id) volume_metadata = self.db.volume_admin_metadata_get(context.elevated(), volume_id) if volume["status"] == "attaching": if volume["instance_uuid"] and volume["instance_uuid"] != instance_uuid: msg = _("being attached by another instance") raise exception.InvalidVolume(reason=msg) if volume["attached_host"] and volume["attached_host"] != host_name: msg = _("being attached by another host") raise exception.InvalidVolume(reason=msg) if volume_metadata.get("attached_mode") and volume_metadata.get("attached_mode") != mode: msg = _("being attached by different mode") raise exception.InvalidVolume(reason=msg) elif volume["status"] != "available": msg = _("status must be available or attaching") raise exception.InvalidVolume(reason=msg) # TODO(jdg): attach_time column is currently varchar # we should update this to a date-time object # also consider adding detach_time? self._notify_about_volume_usage(context, volume, "attach.start") self.db.volume_update( context, volume_id, { "instance_uuid": instance_uuid, "attached_host": host_name, "status": "attaching", "attach_time": timeutils.strtime(), }, ) self.db.volume_admin_metadata_update(context.elevated(), volume_id, {"attached_mode": mode}, False) if instance_uuid and not uuidutils.is_uuid_like(instance_uuid): self.db.volume_update(context, volume_id, {"status": "error_attaching"}) raise exception.InvalidUUID(uuid=instance_uuid) host_name_sanitized = utils.sanitize_hostname(host_name) if host_name else None volume = self.db.volume_get(context, volume_id) if volume_metadata.get("readonly") == "True" and mode != "ro": self.db.volume_update(context, volume_id, {"status": "error_attaching"}) raise exception.InvalidVolumeAttachMode(mode=mode, volume_id=volume_id) try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the volume status updated. utils.require_driver_initialized(self.driver) self.driver.attach_volume(context, volume, instance_uuid, host_name_sanitized, mountpoint) except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {"status": "error_attaching"}) volume = self.db.volume_attached( context.elevated(), volume_id, instance_uuid, host_name_sanitized, mountpoint ) self._notify_about_volume_usage(context, volume, "attach.end") return do_attach() def detach_volume(self, context, volume_id): """Updates db to show volume is detached.""" # TODO(vish): refactor this into a more general "unreserve" # TODO(sleepsonthefloor): Is this 'elevated' appropriate? volume = self.db.volume_get(context, volume_id) self._notify_about_volume_usage(context, volume, "detach.start") try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the volume status updated. utils.require_driver_initialized(self.driver) self.driver.detach_volume(context, volume) except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {"status": "error_detaching"}) self.db.volume_detached(context.elevated(), volume_id) self.db.volume_admin_metadata_delete(context.elevated(), volume_id, "attached_mode") # Check for https://bugs.launchpad.net/cinder/+bug/1065702 volume = self.db.volume_get(context, volume_id) if volume["provider_location"] and volume["name"] not in volume["provider_location"]: self.driver.ensure_export(context, volume) self._notify_about_volume_usage(context, volume, "detach.end") def copy_volume_to_image(self, context, volume_id, image_meta): """Uploads the specified volume to Glance. image_meta is a dictionary containing the following keys: 'id', 'container_format', 'disk_format' """ payload = {"volume_id": volume_id, "image_id": image_meta["id"]} try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the volume status updated. utils.require_driver_initialized(self.driver) volume = self.db.volume_get(context, volume_id) self.driver.ensure_export(context.elevated(), volume) image_service, image_id = glance.get_remote_image_service(context, image_meta["id"]) self.driver.copy_volume_to_image(context, volume, image_service, image_meta) LOG.debug( _("Uploaded volume %(volume_id)s to " "image (%(image_id)s) successfully"), {"volume_id": volume_id, "image_id": image_id}, ) except Exception as error: with excutils.save_and_reraise_exception(): payload["message"] = unicode(error) finally: if volume["instance_uuid"] is None and volume["attached_host"] is None: self.db.volume_update(context, volume_id, {"status": "available"}) else: self.db.volume_update(context, volume_id, {"status": "in-use"}) def initialize_connection(self, context, volume_id, connector): """Prepare volume for connection from host represented by connector. This method calls the driver initialize_connection and returns it to the caller. The connector parameter is a dictionary with information about the host that will connect to the volume in the following format:: { 'ip': ip, 'initiator': initiator, } ip: the ip address of the connecting machine initiator: the iscsi initiator name of the connecting machine. This can be None if the connecting machine does not support iscsi connections. driver is responsible for doing any necessary security setup and returning a connection_info dictionary in the following format:: { 'driver_volume_type': driver_volume_type, 'data': data, } driver_volume_type: a string to identify the type of volume. This can be used by the calling code to determine the strategy for connecting to the volume. This could be 'iscsi', 'rbd', 'sheepdog', etc. data: this is the data that the calling code will use to connect to the volume. Keep in mind that this will be serialized to json in various places, so it should not contain any non-json data types. """ # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the volume status updated. utils.require_driver_initialized(self.driver) volume = self.db.volume_get(context, volume_id) self.driver.validate_connector(connector) try: conn_info = self.driver.initialize_connection(volume, connector) except Exception as err: err_msg = _("Unable to fetch connection information from " "backend: %(err)s") % {"err": str(err)} LOG.error(err_msg) raise exception.VolumeBackendAPIException(data=err_msg) # Add qos_specs to connection info typeid = volume["volume_type_id"] specs = None if typeid: res = volume_types.get_volume_type_qos_specs(typeid) qos = res["qos_specs"] # only pass qos_specs that is designated to be consumed by # front-end, or both front-end and back-end. if qos and qos.get("consumer") in ["front-end", "both"]: specs = qos.get("specs") qos_spec = dict(qos_specs=specs) conn_info["data"].update(qos_spec) # Add access_mode to connection info volume_metadata = self.db.volume_admin_metadata_get(context.elevated(), volume_id) if conn_info["data"].get("access_mode") is None: access_mode = volume_metadata.get("attached_mode") if access_mode is None: # NOTE(zhiyan): client didn't call 'os-attach' before access_mode = "ro" if volume_metadata.get("readonly") == "True" else "rw" conn_info["data"]["access_mode"] = access_mode return conn_info def terminate_connection(self, context, volume_id, connector, force=False): """Cleanup connection from host represented by connector. The format of connector is the same as for initialize_connection. """ # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the volume status updated. utils.require_driver_initialized(self.driver) volume_ref = self.db.volume_get(context, volume_id) try: self.driver.terminate_connection(volume_ref, connector, force=force) except Exception as err: err_msg = _("Unable to terminate volume connection: %(err)s") % {"err": str(err)} LOG.error(err_msg) raise exception.VolumeBackendAPIException(data=err_msg) def accept_transfer(self, context, volume_id, new_user, new_project): # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the volume status updated. utils.require_driver_initialized(self.driver) # NOTE(jdg): need elevated context as we haven't "given" the vol # yet volume_ref = self.db.volume_get(context.elevated(), volume_id) self.driver.accept_transfer(context, volume_ref, new_user, new_project) def _migrate_volume_generic(self, ctxt, volume, host, new_type_id): rpcapi = volume_rpcapi.VolumeAPI() # Create new volume on remote host new_vol_values = {} for k, v in volume.iteritems(): new_vol_values[k] = v del new_vol_values["id"] del new_vol_values["_name_id"] # We don't copy volume_type because the db sets that according to # volume_type_id, which we do copy del new_vol_values["volume_type"] if new_type_id: new_vol_values["volume_type_id"] = new_type_id new_vol_values["host"] = host["host"] new_vol_values["status"] = "creating" new_vol_values["migration_status"] = "target:%s" % volume["id"] new_vol_values["attach_status"] = "detached" new_volume = self.db.volume_create(ctxt, new_vol_values) rpcapi.create_volume(ctxt, new_volume, host["host"], None, None, allow_reschedule=False) # Wait for new_volume to become ready starttime = time.time() deadline = starttime + CONF.migration_create_volume_timeout_secs new_volume = self.db.volume_get(ctxt, new_volume["id"]) tries = 0 while new_volume["status"] != "available": tries = tries + 1 now = time.time() if new_volume["status"] == "error": msg = _("failed to create new_volume on destination host") raise exception.VolumeMigrationFailed(reason=msg) elif now > deadline: msg = _("timeout creating new_volume on destination host") raise exception.VolumeMigrationFailed(reason=msg) else: time.sleep(tries ** 2) new_volume = self.db.volume_get(ctxt, new_volume["id"]) # Copy the source volume to the destination volume try: if volume["instance_uuid"] is None and volume["attached_host"] is None: self.driver.copy_volume_data(ctxt, volume, new_volume, remote="dest") # The above call is synchronous so we complete the migration self.migrate_volume_completion(ctxt, volume["id"], new_volume["id"], error=False) else: nova_api = compute.API() # This is an async call to Nova, which will call the completion # when it's done nova_api.update_server_volume(ctxt, volume["instance_uuid"], volume["id"], new_volume["id"]) except Exception: with excutils.save_and_reraise_exception(): msg = _("Failed to copy volume %(vol1)s to %(vol2)s") LOG.error(msg % {"vol1": volume["id"], "vol2": new_volume["id"]}) volume = self.db.volume_get(ctxt, volume["id"]) # If we're in the completing phase don't delete the target # because we may have already deleted the source! if volume["migration_status"] == "migrating": rpcapi.delete_volume(ctxt, new_volume) new_volume["migration_status"] = None def _get_original_status(self, volume): if volume["instance_uuid"] is None and volume["attached_host"] is None: return "available" else: return "in-use" def migrate_volume_completion(self, ctxt, volume_id, new_volume_id, error=False): try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the migration status updated. utils.require_driver_initialized(self.driver) except exception.DriverNotInitialized: with excutils.save_and_reraise_exception(): self.db.volume_update(ctxt, volume_id, {"migration_status": "error"}) msg = _("migrate_volume_completion: completing migration for " "volume %(vol1)s (temporary volume %(vol2)s") LOG.debug(msg % {"vol1": volume_id, "vol2": new_volume_id}) volume = self.db.volume_get(ctxt, volume_id) new_volume = self.db.volume_get(ctxt, new_volume_id) rpcapi = volume_rpcapi.VolumeAPI() status_update = None if volume["status"] == "retyping": status_update = {"status": self._get_original_status(volume)} if error: msg = _( "migrate_volume_completion is cleaning up an error " "for volume %(vol1)s (temporary volume %(vol2)s" ) LOG.info(msg % {"vol1": volume["id"], "vol2": new_volume["id"]}) new_volume["migration_status"] = None rpcapi.delete_volume(ctxt, new_volume) updates = {"migration_status": None} if status_update: updates.update(status_update) self.db.volume_update(ctxt, volume_id, updates) return volume_id self.db.volume_update(ctxt, volume_id, {"migration_status": "completing"}) # Delete the source volume (if it fails, don't fail the migration) try: self.delete_volume(ctxt, volume_id) except Exception as ex: msg = _("Failed to delete migration source vol %(vol)s: %(err)s") LOG.error(msg % {"vol": volume_id, "err": ex}) self.db.finish_volume_migration(ctxt, volume_id, new_volume_id) self.db.volume_destroy(ctxt, new_volume_id) updates = {"migration_status": None} if status_update: updates.update(status_update) self.db.volume_update(ctxt, volume_id, updates) return volume["id"] def migrate_volume(self, ctxt, volume_id, host, force_host_copy=False, new_type_id=None): """Migrate the volume to the specified host (called on source host).""" try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the migration status updated. utils.require_driver_initialized(self.driver) except exception.DriverNotInitialized: with excutils.save_and_reraise_exception(): self.db.volume_update(ctxt, volume_id, {"migration_status": "error"}) volume_ref = self.db.volume_get(ctxt, volume_id) model_update = None moved = False status_update = None if volume_ref["status"] == "retyping": status_update = {"status": self._get_original_status(volume_ref)} self.db.volume_update(ctxt, volume_ref["id"], {"migration_status": "migrating"}) if not force_host_copy and new_type_id is None: try: LOG.debug(_("volume %s: calling driver migrate_volume"), volume_ref["id"]) moved, model_update = self.driver.migrate_volume(ctxt, volume_ref, host) if moved: updates = {"host": host["host"], "migration_status": None} if status_update: updates.update(status_update) if model_update: updates.update(model_update) volume_ref = self.db.volume_update(ctxt, volume_ref["id"], updates) except Exception: with excutils.save_and_reraise_exception(): updates = {"migration_status": None} if status_update: updates.update(status_update) model_update = self.driver.create_export(ctxt, volume_ref) if model_update: updates.update(model_update) self.db.volume_update(ctxt, volume_ref["id"], updates) if not moved: try: self._migrate_volume_generic(ctxt, volume_ref, host, new_type_id) except Exception: with excutils.save_and_reraise_exception(): updates = {"migration_status": None} if status_update: updates.update(status_update) model_update = self.driver.create_export(ctxt, volume_ref) if model_update: updates.update(model_update) self.db.volume_update(ctxt, volume_ref["id"], updates) @periodic_task.periodic_task def _report_driver_status(self, context): LOG.info(_("Updating volume status")) if not self.driver.initialized: if self.driver.configuration.config_group is None: config_group = "" else: config_group = "(config name %s)" % self.driver.configuration.config_group LOG.warning( _( "Unable to update stats, %(driver_name)s " "-%(driver_version)s " "%(config_group)s driver is uninitialized." ) % { "driver_name": self.driver.__class__.__name__, "driver_version": self.driver.get_version(), "config_group": config_group, } ) else: volume_stats = self.driver.get_volume_stats(refresh=True) if volume_stats: # Append volume stats with 'allocated_capacity_gb' volume_stats.update(self.stats) # queue it to be sent to the Schedulers. self.update_service_capabilities(volume_stats) def publish_service_capabilities(self, context): """Collect driver status and then publish.""" self._report_driver_status(context) self._publish_service_capabilities(context) def notification(self, context, event): LOG.info(_("Notification {%s} received"), event) def _notify_about_volume_usage(self, context, volume, event_suffix, extra_usage_info=None): volume_utils.notify_about_volume_usage( context, volume, event_suffix, extra_usage_info=extra_usage_info, host=self.host ) def _notify_about_snapshot_usage(self, context, snapshot, event_suffix, extra_usage_info=None): volume_utils.notify_about_snapshot_usage( context, snapshot, event_suffix, extra_usage_info=extra_usage_info, host=self.host ) def extend_volume(self, context, volume_id, new_size): try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the volume status updated. utils.require_driver_initialized(self.driver) except exception.DriverNotInitialized: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {"status": "error_extending"}) volume = self.db.volume_get(context, volume_id) size_increase = (int(new_size)) - volume["size"] try: reservations = QUOTAS.reserve(context, gigabytes=+size_increase) except exception.OverQuota as exc: self.db.volume_update(context, volume["id"], {"status": "error_extending"}) overs = exc.kwargs["overs"] usages = exc.kwargs["usages"] quotas = exc.kwargs["quotas"] def _consumed(name): return usages[name]["reserved"] + usages[name]["in_use"] if "gigabytes" in overs: msg = _( "Quota exceeded for %(s_pid)s, " "tried to extend volume by " "%(s_size)sG, (%(d_consumed)dG of %(d_quota)dG " "already consumed)" ) LOG.error( msg % { "s_pid": context.project_id, "s_size": size_increase, "d_consumed": _consumed("gigabytes"), "d_quota": quotas["gigabytes"], } ) return self._notify_about_volume_usage(context, volume, "resize.start") try: LOG.info(_("volume %s: extending"), volume["id"]) self.driver.extend_volume(volume, new_size) LOG.info(_("volume %s: extended successfully"), volume["id"]) except Exception: LOG.exception(_("volume %s: Error trying to extend volume"), volume_id) try: self.db.volume_update(context, volume["id"], {"status": "error_extending"}) finally: QUOTAS.rollback(context, reservations) return QUOTAS.commit(context, reservations) self.db.volume_update(context, volume["id"], {"size": int(new_size), "status": "available"}) self.stats["allocated_capacity_gb"] += size_increase self._notify_about_volume_usage(context, volume, "resize.end", extra_usage_info={"size": int(new_size)}) def retype(self, ctxt, volume_id, new_type_id, host, migration_policy="never", reservations=None): def _retype_error(context, volume_id, old_reservations, new_reservations, status_update): try: self.db.volume_update(context, volume_id, status_update) finally: QUOTAS.rollback(context, old_reservations) QUOTAS.rollback(context, new_reservations) context = ctxt.elevated() volume_ref = self.db.volume_get(ctxt, volume_id) status_update = {"status": self._get_original_status(volume_ref)} if context.project_id != volume_ref["project_id"]: project_id = volume_ref["project_id"] else: project_id = context.project_id try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the volume status updated. utils.require_driver_initialized(self.driver) except exception.DriverNotInitialized: with excutils.save_and_reraise_exception(): # NOTE(flaper87): Other exceptions in this method don't # set the volume status to error. Should that be done # here? Setting the volume back to it's original status # for now. self.db.volume_update(context, volume_id, status_update) # Get old reservations try: reserve_opts = {"volumes": -1, "gigabytes": -volume_ref["size"]} QUOTAS.add_volume_type_opts(context, reserve_opts, volume_ref.get("volume_type_id")) old_reservations = QUOTAS.reserve(context, project_id=project_id, **reserve_opts) except Exception: old_reservations = None self.db.volume_update(context, volume_id, status_update) LOG.exception(_("Failed to update usages while retyping volume.")) raise exception.CinderException(_("Failed to get old volume type" " quota reservations")) # We already got the new reservations new_reservations = reservations # If volume types have the same contents, no need to do anything retyped = False diff, all_equal = volume_types.volume_types_diff(context, volume_ref.get("volume_type_id"), new_type_id) if all_equal: retyped = True # Call driver to try and change the type if not retyped: try: new_type = volume_types.get_volume_type(context, new_type_id) retyped = self.driver.retype(context, volume_ref, new_type, diff, host) if retyped: LOG.info(_("Volume %s: retyped succesfully"), volume_id) except Exception as ex: retyped = False LOG.error( _("Volume %s: driver error when trying to retype, " "falling back to generic mechanism."), volume_ref["id"], ) LOG.exception(ex) # We could not change the type, so we need to migrate the volume, where # the destination volume will be of the new type if not retyped: if migration_policy == "never": _retype_error(context, volume_id, old_reservations, new_reservations, status_update) msg = _("Retype requires migration but is not allowed.") raise exception.VolumeMigrationFailed(reason=msg) snaps = self.db.snapshot_get_all_for_volume(context, volume_ref["id"]) if snaps: _retype_error(context, volume_id, old_reservations, new_reservations, status_update) msg = _("Volume must not have snapshots.") LOG.error(msg) raise exception.InvalidVolume(reason=msg) self.db.volume_update(context, volume_ref["id"], {"migration_status": "starting"}) try: self.migrate_volume(context, volume_id, host, new_type_id=new_type_id) except Exception: with excutils.save_and_reraise_exception(): _retype_error(context, volume_id, old_reservations, new_reservations, status_update) self.db.volume_update( context, volume_id, {"volume_type_id": new_type_id, "host": host["host"], "status": status_update["status"]} ) if old_reservations: QUOTAS.commit(context, old_reservations, project_id=project_id) if new_reservations: QUOTAS.commit(context, new_reservations, project_id=project_id) self.publish_service_capabilities(context)
class VolumeManager(manager.SchedulerDependentManager): """Manages attachable block storage devices.""" RPC_API_VERSION = '1.11' def __init__(self, volume_driver=None, service_name=None, *args, **kwargs): """Load the driver from the one specified in args, or from flags.""" # update_service_capabilities needs service_name to be volume super(VolumeManager, self).__init__(service_name='volume', *args, **kwargs) self.configuration = Configuration(volume_manager_opts, config_group=service_name) self._tp = GreenPool() if not volume_driver: # Get from configuration, which will get the default # if its not using the multi backend volume_driver = self.configuration.volume_driver if volume_driver in MAPPING: LOG.warn(_("Driver path %s is deprecated, update your " "configuration to the new path."), volume_driver) volume_driver = MAPPING[volume_driver] if volume_driver == 'cinder.volume.drivers.lvm.ThinLVMVolumeDriver': # Deprecated in Havana # Not handled in MAPPING because it requires setting a conf option LOG.warn(_("ThinLVMVolumeDriver is deprecated, please configure " "LVMISCSIDriver and lvm_type=thin. Continuing with " "those settings.")) volume_driver = 'cinder.volume.drivers.lvm.LVMISCSIDriver' self.configuration.lvm_type = 'thin' self.driver = importutils.import_object( volume_driver, configuration=self.configuration, db=self.db) def _add_to_threadpool(self, func, *args, **kwargs): self._tp.spawn_n(func, *args, **kwargs) def init_host(self): """Do any initialization that needs to be run if this is a standalone service. """ ctxt = context.get_admin_context() LOG.info(_("Starting volume driver %(driver_name)s (%(version)s)") % {'driver_name': self.driver.__class__.__name__, 'version': self.driver.get_version()}) try: self.driver.do_setup(ctxt) self.driver.check_for_setup_error() except Exception as ex: LOG.error(_("Error encountered during " "initialization of driver: %(name)s") % {'name': self.driver.__class__.__name__}) LOG.exception(ex) # we don't want to continue since we failed # to initialize the driver correctly. return volumes = self.db.volume_get_all_by_host(ctxt, self.host) LOG.debug(_("Re-exporting %s volumes"), len(volumes)) try: for volume in volumes: if volume['status'] in ['available', 'in-use']: self.driver.ensure_export(ctxt, volume) elif volume['status'] == 'downloading': LOG.info(_("volume %s stuck in a downloading state"), volume['id']) self.driver.clear_download(ctxt, volume) self.db.volume_update(ctxt, volume['id'], {'status': 'error'}) else: LOG.info(_("volume %s: skipping export"), volume['id']) except Exception as ex: LOG.error(_("Error encountered during " "re-exporting phase of driver initialization: " " %(name)s") % {'name': self.driver.__class__.__name__}) LOG.exception(ex) return # at this point the driver is considered initialized. self.driver.set_initialized() LOG.debug(_('Resuming any in progress delete operations')) for volume in volumes: if volume['status'] == 'deleting': LOG.info(_('Resuming delete on volume: %s') % volume['id']) if CONF.volume_service_inithost_offload: # Offload all the pending volume delete operations to the # threadpool to prevent the main volume service thread # from being blocked. self._add_to_threadpool(self.delete_volume(ctxt, volume['id'])) else: # By default, delete volumes sequentially self.delete_volume(ctxt, volume['id']) # collect and publish service capabilities self.publish_service_capabilities(ctxt) @utils.require_driver_initialized def create_volume(self, context, volume_id, request_spec=None, filter_properties=None, allow_reschedule=True, snapshot_id=None, image_id=None, source_volid=None): """Creates and exports the volume.""" context_saved = context.deepcopy() context = context.elevated() if filter_properties is None: filter_properties = {} try: flow_engine = create_volume.get_manager_flow( context, self.db, self.driver, self.scheduler_rpcapi, self.host, volume_id, snapshot_id=snapshot_id, image_id=image_id, source_volid=source_volid, allow_reschedule=allow_reschedule, reschedule_context=context_saved, request_spec=request_spec, filter_properties=filter_properties) except Exception: raise exception.CinderException( _("Failed to create manager volume flow")) if snapshot_id is not None: # Make sure the snapshot is not deleted until we are done with it. locked_action = "%s-%s" % (snapshot_id, 'delete_snapshot') elif source_volid is not None: # Make sure the volume is not deleted until we are done with it. locked_action = "%s-%s" % (source_volid, 'delete_volume') else: locked_action = None def _run_flow(): # This code executes create volume flow. If something goes wrong, # flow reverts all job that was done and reraises an exception. # Otherwise, all data that was generated by flow becomes available # in flow engine's storage. flow_engine.run() @utils.synchronized(locked_action, external=True) def _run_flow_locked(): _run_flow() if locked_action is None: _run_flow() else: _run_flow_locked() # Fetch created volume from storage volume_ref = flow_engine.storage.fetch('volume') return volume_ref['id'] @utils.require_driver_initialized @locked_volume_operation def delete_volume(self, context, volume_id): """Deletes and unexports volume.""" context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) if context.project_id != volume_ref['project_id']: project_id = volume_ref['project_id'] else: project_id = context.project_id LOG.info(_("volume %s: deleting"), volume_ref['id']) if volume_ref['attach_status'] == "attached": # Volume is still attached, need to detach first raise exception.VolumeAttached(volume_id=volume_id) if volume_ref['host'] != self.host: raise exception.InvalidVolume( reason=_("volume is not local to this node")) self._notify_about_volume_usage(context, volume_ref, "delete.start") self._reset_stats() try: LOG.debug(_("volume %s: removing export"), volume_ref['id']) self.driver.remove_export(context, volume_ref) LOG.debug(_("volume %s: deleting"), volume_ref['id']) self.driver.delete_volume(volume_ref) except exception.VolumeIsBusy: LOG.error(_("Cannot delete volume %s: volume is busy"), volume_ref['id']) self.driver.ensure_export(context, volume_ref) self.db.volume_update(context, volume_ref['id'], {'status': 'available'}) return True except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_ref['id'], {'status': 'error_deleting'}) # If deleting the source volume in a migration, we want to skip quotas # and other database updates. if volume_ref['migration_status']: return True # Get reservations try: reserve_opts = {'volumes': -1, 'gigabytes': -volume_ref['size']} QUOTAS.add_volume_type_opts(context, reserve_opts, volume_ref.get('volume_type_id')) reservations = QUOTAS.reserve(context, project_id=project_id, **reserve_opts) except Exception: reservations = None LOG.exception(_("Failed to update usages deleting volume")) # Delete glance metadata if it exists try: self.db.volume_glance_metadata_delete_by_volume(context, volume_id) LOG.debug(_("volume %s: glance metadata deleted"), volume_ref['id']) except exception.GlanceMetadataNotFound: LOG.debug(_("no glance metadata found for volume %s"), volume_ref['id']) self.db.volume_destroy(context, volume_id) LOG.info(_("volume %s: deleted successfully"), volume_ref['id']) self._notify_about_volume_usage(context, volume_ref, "delete.end") # Commit the reservations if reservations: QUOTAS.commit(context, reservations, project_id=project_id) self.publish_service_capabilities(context) return True @utils.require_driver_initialized def create_snapshot(self, context, volume_id, snapshot_id): """Creates and exports the snapshot.""" caller_context = context context = context.elevated() snapshot_ref = self.db.snapshot_get(context, snapshot_id) LOG.info(_("snapshot %s: creating"), snapshot_ref['id']) self._notify_about_snapshot_usage( context, snapshot_ref, "create.start") try: LOG.debug(_("snapshot %(snap_id)s: creating"), {'snap_id': snapshot_ref['id']}) # Pass context so that drivers that want to use it, can, # but it is not a requirement for all drivers. snapshot_ref['context'] = caller_context model_update = self.driver.create_snapshot(snapshot_ref) if model_update: self.db.snapshot_update(context, snapshot_ref['id'], model_update) except Exception: with excutils.save_and_reraise_exception(): self.db.snapshot_update(context, snapshot_ref['id'], {'status': 'error'}) self.db.snapshot_update(context, snapshot_ref['id'], {'status': 'available', 'progress': '100%'}) vol_ref = self.db.volume_get(context, volume_id) if vol_ref.bootable: try: self.db.volume_glance_metadata_copy_to_snapshot( context, snapshot_ref['id'], volume_id) except exception.CinderException as ex: LOG.exception(_("Failed updating %(snapshot_id)s" " metadata using the provided volumes" " %(volume_id)s metadata") % {'volume_id': volume_id, 'snapshot_id': snapshot_id}) raise exception.MetadataCopyFailure(reason=ex) LOG.info(_("snapshot %s: created successfully"), snapshot_ref['id']) self._notify_about_snapshot_usage(context, snapshot_ref, "create.end") return snapshot_id @utils.require_driver_initialized @locked_snapshot_operation def delete_snapshot(self, context, snapshot_id): """Deletes and unexports snapshot.""" caller_context = context context = context.elevated() snapshot_ref = self.db.snapshot_get(context, snapshot_id) project_id = snapshot_ref['project_id'] LOG.info(_("snapshot %s: deleting"), snapshot_ref['id']) self._notify_about_snapshot_usage( context, snapshot_ref, "delete.start") try: LOG.debug(_("snapshot %s: deleting"), snapshot_ref['id']) # Pass context so that drivers that want to use it, can, # but it is not a requirement for all drivers. snapshot_ref['context'] = caller_context self.driver.delete_snapshot(snapshot_ref) except exception.SnapshotIsBusy: LOG.error(_("Cannot delete snapshot %s: snapshot is busy"), snapshot_ref['id']) self.db.snapshot_update(context, snapshot_ref['id'], {'status': 'available'}) return True except Exception: with excutils.save_and_reraise_exception(): self.db.snapshot_update(context, snapshot_ref['id'], {'status': 'error_deleting'}) # Get reservations try: if CONF.no_snapshot_gb_quota: reserve_opts = {'snapshots': -1} else: reserve_opts = { 'snapshots': -1, 'gigabytes': -snapshot_ref['volume_size'], } volume_ref = self.db.volume_get(context, snapshot_ref['volume_id']) QUOTAS.add_volume_type_opts(context, reserve_opts, volume_ref.get('volume_type_id')) reservations = QUOTAS.reserve(context, project_id=project_id, **reserve_opts) except Exception: reservations = None LOG.exception(_("Failed to update usages deleting snapshot")) self.db.volume_glance_metadata_delete_by_snapshot(context, snapshot_id) self.db.snapshot_destroy(context, snapshot_id) LOG.info(_("snapshot %s: deleted successfully"), snapshot_ref['id']) self._notify_about_snapshot_usage(context, snapshot_ref, "delete.end") # Commit the reservations if reservations: QUOTAS.commit(context, reservations, project_id=project_id) return True @utils.require_driver_initialized def attach_volume(self, context, volume_id, instance_uuid, host_name, mountpoint, mode): """Updates db to show volume is attached""" @utils.synchronized(volume_id, external=True) def do_attach(): # check the volume status before attaching volume = self.db.volume_get(context, volume_id) volume_metadata = self.db.volume_admin_metadata_get( context.elevated(), volume_id) if volume['status'] == 'attaching': if (volume['instance_uuid'] and volume['instance_uuid'] != instance_uuid): msg = _("being attached by another instance") raise exception.InvalidVolume(reason=msg) if (volume['attached_host'] and volume['attached_host'] != host_name): msg = _("being attached by another host") raise exception.InvalidVolume(reason=msg) if (volume_metadata.get('attached_mode') and volume_metadata.get('attached_mode') != mode): msg = _("being attached by different mode") raise exception.InvalidVolume(reason=msg) elif volume['status'] != "available": msg = _("status must be available") raise exception.InvalidVolume(reason=msg) # TODO(jdg): attach_time column is currently varchar # we should update this to a date-time object # also consider adding detach_time? self._notify_about_volume_usage(context, volume, "attach.start") self.db.volume_update(context, volume_id, {"instance_uuid": instance_uuid, "attached_host": host_name, "status": "attaching", "attach_time": timeutils.strtime()}) self.db.volume_admin_metadata_update(context.elevated(), volume_id, {"attached_mode": mode}, False) if instance_uuid and not uuidutils.is_uuid_like(instance_uuid): self.db.volume_update(context, volume_id, {'status': 'error_attaching'}) raise exception.InvalidUUID(uuid=instance_uuid) host_name_sanitized = utils.sanitize_hostname( host_name) if host_name else None volume = self.db.volume_get(context, volume_id) if volume_metadata.get('readonly') == 'True' and mode != 'ro': self.db.volume_update(context, volume_id, {'status': 'error_attaching'}) raise exception.InvalidVolumeAttachMode(mode=mode, volume_id=volume_id) try: self.driver.attach_volume(context, volume, instance_uuid, host_name_sanitized, mountpoint) except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {'status': 'error_attaching'}) volume = self.db.volume_attached(context.elevated(), volume_id, instance_uuid, host_name_sanitized, mountpoint) self._notify_about_volume_usage(context, volume, "attach.end") return do_attach() @utils.require_driver_initialized def detach_volume(self, context, volume_id): """Updates db to show volume is detached""" # TODO(vish): refactor this into a more general "unreserve" # TODO(sleepsonthefloor): Is this 'elevated' appropriate? volume = self.db.volume_get(context, volume_id) self._notify_about_volume_usage(context, volume, "detach.start") try: self.driver.detach_volume(context, volume) except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {'status': 'error_detaching'}) self.db.volume_detached(context.elevated(), volume_id) self.db.volume_admin_metadata_delete(context.elevated(), volume_id, 'attached_mode') # Check for https://bugs.launchpad.net/cinder/+bug/1065702 volume = self.db.volume_get(context, volume_id) if (volume['provider_location'] and volume['name'] not in volume['provider_location']): self.driver.ensure_export(context, volume) self._notify_about_volume_usage(context, volume, "detach.end") @utils.require_driver_initialized def copy_volume_to_image(self, context, volume_id, image_meta): """Uploads the specified volume to Glance. image_meta is a dictionary containing the following keys: 'id', 'container_format', 'disk_format' """ payload = {'volume_id': volume_id, 'image_id': image_meta['id']} try: volume = self.db.volume_get(context, volume_id) self.driver.ensure_export(context.elevated(), volume) image_service, image_id = \ glance.get_remote_image_service(context, image_meta['id']) self.driver.copy_volume_to_image(context, volume, image_service, image_meta) LOG.debug(_("Uploaded volume %(volume_id)s to " "image (%(image_id)s) successfully"), {'volume_id': volume_id, 'image_id': image_id}) except Exception as error: with excutils.save_and_reraise_exception(): payload['message'] = unicode(error) finally: if (volume['instance_uuid'] is None and volume['attached_host'] is None): self.db.volume_update(context, volume_id, {'status': 'available'}) else: self.db.volume_update(context, volume_id, {'status': 'in-use'}) @utils.require_driver_initialized def initialize_connection(self, context, volume_id, connector): """Prepare volume for connection from host represented by connector. This method calls the driver initialize_connection and returns it to the caller. The connector parameter is a dictionary with information about the host that will connect to the volume in the following format:: { 'ip': ip, 'initiator': initiator, } ip: the ip address of the connecting machine initiator: the iscsi initiator name of the connecting machine. This can be None if the connecting machine does not support iscsi connections. driver is responsible for doing any necessary security setup and returning a connection_info dictionary in the following format:: { 'driver_volume_type': driver_volume_type, 'data': data, } driver_volume_type: a string to identify the type of volume. This can be used by the calling code to determine the strategy for connecting to the volume. This could be 'iscsi', 'rbd', 'sheepdog', etc. data: this is the data that the calling code will use to connect to the volume. Keep in mind that this will be serialized to json in various places, so it should not contain any non-json data types. """ volume = self.db.volume_get(context, volume_id) self.driver.validate_connector(connector) conn_info = self.driver.initialize_connection(volume, connector) # Add qos_specs to connection info typeid = volume['volume_type_id'] specs = {} if typeid: res = volume_types.get_volume_type_qos_specs(typeid) specs = res['qos_specs'] # Don't pass qos_spec as empty dict qos_spec = dict(qos_spec=specs if specs else None) conn_info['data'].update(qos_spec) # Add access_mode to connection info volume_metadata = self.db.volume_admin_metadata_get(context.elevated(), volume_id) if conn_info['data'].get('access_mode') is None: access_mode = volume_metadata.get('attached_mode') if access_mode is None: # NOTE(zhiyan): client didn't call 'os-attach' before access_mode = ('ro' if volume_metadata.get('readonly') == 'True' else 'rw') conn_info['data']['access_mode'] = access_mode return conn_info @utils.require_driver_initialized def terminate_connection(self, context, volume_id, connector, force=False): """Cleanup connection from host represented by connector. The format of connector is the same as for initialize_connection. """ volume_ref = self.db.volume_get(context, volume_id) self.driver.terminate_connection(volume_ref, connector, force=force) @utils.require_driver_initialized def accept_transfer(self, context, volume_id, new_user, new_project): # NOTE(jdg): need elevated context as we haven't "given" the vol # yet volume_ref = self.db.volume_get(context.elevated(), volume_id) self.driver.accept_transfer(context, volume_ref, new_user, new_project) def _migrate_volume_generic(self, ctxt, volume, host): rpcapi = volume_rpcapi.VolumeAPI() # Create new volume on remote host new_vol_values = {} for k, v in volume.iteritems(): new_vol_values[k] = v del new_vol_values['id'] del new_vol_values['_name_id'] # We don't copy volume_type because the db sets that according to # volume_type_id, which we do copy del new_vol_values['volume_type'] new_vol_values['host'] = host['host'] new_vol_values['status'] = 'creating' new_vol_values['migration_status'] = 'target:%s' % volume['id'] new_vol_values['attach_status'] = 'detached' new_volume = self.db.volume_create(ctxt, new_vol_values) rpcapi.create_volume(ctxt, new_volume, host['host'], None, None, allow_reschedule=False) # Wait for new_volume to become ready starttime = time.time() deadline = starttime + CONF.migration_create_volume_timeout_secs new_volume = self.db.volume_get(ctxt, new_volume['id']) tries = 0 while new_volume['status'] != 'available': tries = tries + 1 now = time.time() if new_volume['status'] == 'error': msg = _("failed to create new_volume on destination host") raise exception.VolumeMigrationFailed(reason=msg) elif now > deadline: msg = _("timeout creating new_volume on destination host") raise exception.VolumeMigrationFailed(reason=msg) else: time.sleep(tries ** 2) new_volume = self.db.volume_get(ctxt, new_volume['id']) # Copy the source volume to the destination volume try: if volume['status'] == 'available': self.driver.copy_volume_data(ctxt, volume, new_volume, remote='dest') # The above call is synchronous so we complete the migration self.migrate_volume_completion(ctxt, volume['id'], new_volume['id'], error=False) else: nova_api = compute.API() # This is an async call to Nova, which will call the completion # when it's done nova_api.update_server_volume(ctxt, volume['instance_uuid'], volume['id'], new_volume['id']) except Exception: with excutils.save_and_reraise_exception(): msg = _("Failed to copy volume %(vol1)s to %(vol2)s") LOG.error(msg % {'vol1': volume['id'], 'vol2': new_volume['id']}) volume = self.db.volume_get(ctxt, volume['id']) # If we're in the completing phase don't delete the target # because we may have already deleted the source! if volume['migration_status'] == 'migrating': rpcapi.delete_volume(ctxt, new_volume) new_volume['migration_status'] = None def migrate_volume_completion(self, ctxt, volume_id, new_volume_id, error=False): msg = _("migrate_volume_completion: completing migration for " "volume %(vol1)s (temporary volume %(vol2)s") LOG.debug(msg % {'vol1': volume_id, 'vol2': new_volume_id}) volume = self.db.volume_get(ctxt, volume_id) new_volume = self.db.volume_get(ctxt, new_volume_id) rpcapi = volume_rpcapi.VolumeAPI() if error: msg = _("migrate_volume_completion is cleaning up an error " "for volume %(vol1)s (temporary volume %(vol2)s") LOG.info(msg % {'vol1': volume['id'], 'vol2': new_volume['id']}) new_volume['migration_status'] = None rpcapi.delete_volume(ctxt, new_volume) self.db.volume_update(ctxt, volume_id, {'migration_status': None}) return volume_id self.db.volume_update(ctxt, volume_id, {'migration_status': 'completing'}) # Delete the source volume (if it fails, don't fail the migration) try: self.delete_volume(ctxt, volume_id) except Exception as ex: msg = _("Failed to delete migration source vol %(vol)s: %(err)s") LOG.error(msg % {'vol': volume_id, 'err': ex}) self.db.finish_volume_migration(ctxt, volume_id, new_volume_id) self.db.volume_destroy(ctxt, new_volume_id) self.db.volume_update(ctxt, volume_id, {'migration_status': None}) return volume['id'] @utils.require_driver_initialized def migrate_volume(self, ctxt, volume_id, host, force_host_copy=False): """Migrate the volume to the specified host (called on source host).""" volume_ref = self.db.volume_get(ctxt, volume_id) model_update = None moved = False self.db.volume_update(ctxt, volume_ref['id'], {'migration_status': 'migrating'}) if not force_host_copy: try: LOG.debug(_("volume %s: calling driver migrate_volume"), volume_ref['id']) moved, model_update = self.driver.migrate_volume(ctxt, volume_ref, host) if moved: updates = {'host': host['host'], 'migration_status': None} if model_update: updates.update(model_update) volume_ref = self.db.volume_update(ctxt, volume_ref['id'], updates) except Exception: with excutils.save_and_reraise_exception(): updates = {'migration_status': None} model_update = self.driver.create_export(ctxt, volume_ref) if model_update: updates.update(model_update) self.db.volume_update(ctxt, volume_ref['id'], updates) if not moved: try: self._migrate_volume_generic(ctxt, volume_ref, host) except Exception: with excutils.save_and_reraise_exception(): updates = {'migration_status': None} model_update = self.driver.create_export(ctxt, volume_ref) if model_update: updates.update(model_update) self.db.volume_update(ctxt, volume_ref['id'], updates) @periodic_task.periodic_task def _report_driver_status(self, context): LOG.info(_("Updating volume status")) if not self.driver.initialized: if self.driver.configuration.config_group is None: config_group = '' else: config_group = ('(config name %s)' % self.driver.configuration.config_group) LOG.warning(_('Unable to update stats, %(driver_name)s ' '-%(driver_version)s ' '%(config_group)s driver is uninitialized.') % {'driver_name': self.driver.__class__.__name__, 'driver_version': self.driver.get_version(), 'config_group': config_group}) else: volume_stats = self.driver.get_volume_stats(refresh=True) if volume_stats: # This will grab info about the host and queue it # to be sent to the Schedulers. self.update_service_capabilities(volume_stats) def publish_service_capabilities(self, context): """Collect driver status and then publish.""" self._report_driver_status(context) self._publish_service_capabilities(context) def _reset_stats(self): LOG.info(_("Clear capabilities")) self._last_volume_stats = [] def notification(self, context, event): LOG.info(_("Notification {%s} received"), event) self._reset_stats() def _notify_about_volume_usage(self, context, volume, event_suffix, extra_usage_info=None): volume_utils.notify_about_volume_usage( context, volume, event_suffix, extra_usage_info=extra_usage_info, host=self.host) def _notify_about_snapshot_usage(self, context, snapshot, event_suffix, extra_usage_info=None): volume_utils.notify_about_snapshot_usage( context, snapshot, event_suffix, extra_usage_info=extra_usage_info, host=self.host) @utils.require_driver_initialized def extend_volume(self, context, volume_id, new_size): volume = self.db.volume_get(context, volume_id) size_increase = (int(new_size)) - volume['size'] try: reservations = QUOTAS.reserve(context, gigabytes=+size_increase) except exception.OverQuota as exc: self.db.volume_update(context, volume['id'], {'status': 'error_extending'}) overs = exc.kwargs['overs'] usages = exc.kwargs['usages'] quotas = exc.kwargs['quotas'] def _consumed(name): return (usages[name]['reserved'] + usages[name]['in_use']) if 'gigabytes' in overs: msg = _("Quota exceeded for %(s_pid)s, " "tried to extend volume by " "%(s_size)sG, (%(d_consumed)dG of %(d_quota)dG " "already consumed)") LOG.error(msg % {'s_pid': context.project_id, 's_size': size_increase, 'd_consumed': _consumed('gigabytes'), 'd_quota': quotas['gigabytes']}) return self._notify_about_volume_usage(context, volume, "resize.start") try: LOG.info(_("volume %s: extending"), volume['id']) self.driver.extend_volume(volume, new_size) LOG.info(_("volume %s: extended successfully"), volume['id']) except Exception: LOG.exception(_("volume %s: Error trying to extend volume"), volume_id) try: self.db.volume_update(context, volume['id'], {'status': 'error_extending'}) finally: QUOTAS.rollback(context, reservations) return QUOTAS.commit(context, reservations) self.db.volume_update(context, volume['id'], {'size': int(new_size), 'status': 'available'}) self._notify_about_volume_usage( context, volume, "resize.end", extra_usage_info={'size': int(new_size)})
def run_once(self, *args, **kwargs): """ Executes a single pass, looking for objects to expire. :param args: Extra args to fulfill the Daemon interface; this daemon has no additional args. :param kwargs: Extra keyword args to fulfill the Daemon interface; this daemon accepts processes and process keyword args. These will override the values from the config file if provided. """ # This if-clause will be removed when general task queue feature is # implemented. if not self.dequeue_from_legacy: self.logger.info('This node is not configured to dequeue tasks ' 'from the legacy queue. This node will ' 'not process any expiration tasks. At least ' 'one node in your cluster must be configured ' 'with dequeue_from_legacy == true.') return self.get_process_values(kwargs) pool = GreenPool(self.concurrency) self.report_first_time = self.report_last_time = time() self.report_objects = 0 try: self.logger.debug('Run begin') task_account_container_list_to_delete = list() for task_account, my_index, divisor in \ self.iter_task_accounts_to_expire(): container_count, obj_count = \ self.swift.get_account_info(task_account) # the task account is skipped if there are no task container if not container_count: continue self.logger.info(_( 'Pass beginning for task account %(account)s; ' '%(container_count)s possible containers; ' '%(obj_count)s possible objects') % { 'account': task_account, 'container_count': container_count, 'obj_count': obj_count}) task_account_container_list = \ [(task_account, task_container) for task_container in self.iter_task_containers_to_expire(task_account)] task_account_container_list_to_delete.extend( task_account_container_list) # delete_task_iter is a generator to yield a dict of # task_account, task_container, task_object, delete_timestamp, # target_path to handle delete actual object and pop the task # from the queue. delete_task_iter = \ self.round_robin_order(self.iter_task_to_expire( task_account_container_list, my_index, divisor)) for delete_task in delete_task_iter: pool.spawn_n(self.delete_object, **delete_task) pool.waitall() for task_account, task_container in \ task_account_container_list_to_delete: try: self.swift.delete_container( task_account, task_container, acceptable_statuses=(2, HTTP_NOT_FOUND, HTTP_CONFLICT)) except (Exception, Timeout) as err: self.logger.exception( _('Exception while deleting container %(account)s ' '%(container)s %(err)s') % { 'account': task_account, 'container': task_container, 'err': str(err)}) self.logger.debug('Run end') self.report(final=True) except (Exception, Timeout): self.logger.exception(_('Unhandled exception'))
def _start_consume(self): greenpool = GreenPool(5) greenpool.spawn_n(self._consume_stream, self.process.stdout) greenpool.spawn_n(self._consume_stream, self.process.stderr) return greenpool
class CinderBackupProxy(manager.SchedulerDependentManager): """Manages attachable block storage devices.""" RPC_API_VERSION = '1.18' target = messaging.Target(version=RPC_API_VERSION) VOLUME_NAME_MAX_LEN = 255 VOLUME_UUID_MAX_LEN = 36 BACKUP_NAME_MAX_LEN = 255 BACKUP_UUID_MAX_LEN = 36 def __init__(self, service_name=None, *args, **kwargs): """Load the specified in args, or flags.""" # update_service_capabilities needs service_name to be volume super(CinderBackupProxy, self).__init__(service_name='backup', *args, **kwargs) self.configuration = Configuration(volume_backup_opts, config_group=service_name) self._tp = GreenPool() self.volume_api = volume.API() self._last_info_volume_state_heal = 0 self._change_since_time = None self.volumes_mapping_cache = {'backups': {}} self.init_flag = False self.backup_cache = [] self.tenant_id = self._get_tenant_id() self.adminCinderClient = self._get_cascaded_cinder_client() def _init_volume_mapping_cache(self,context): try: backups = self.db.backup_get_all(context) for backup in backups: backup_id = backup['id'] status = backup['status'] try: cascaded_backup_id =self._get_cascaded_backup_id(backup_id) except Exception as ex: continue if cascaded_backup_id == '' or status == 'error': continue self.volumes_mapping_cache['backups'][backup_id] = cascaded_backup_id LOG.info(_("cascade info: init volume mapping cache is %s"), self.volumes_mapping_cache) except Exception as ex: LOG.error(_("Failed init volumes mapping cache")) LOG.exception(ex) def _gen_ccding_backup_name(self, backup_id): return "backup" + "@" + backup_id def _get_cinder_cascaded_admin_client(self): try: kwargs = {'username': cfg.CONF.cinder_username, 'password': cfg.CONF.admin_password, 'tenant_name': CONF.cinder_tenant_name, 'auth_url': cfg.CONF.keystone_auth_url, 'insecure': True } keystoneclient = kc.Client(**kwargs) cinderclient = cinder_client.Client( username=cfg.CONF.cinder_username, auth_url=cfg.CONF.keystone_auth_url, insecure=True) cinderclient.client.auth_token = keystoneclient.auth_ref.auth_token diction = {'project_id': cfg.CONF.cinder_tenant_id} cinderclient.client.management_url = \ cfg.CONF.cascaded_cinder_url % diction return cinderclient except keystone_exception.Unauthorized: with excutils.save_and_reraise_exception(): LOG.error(_('Token unauthorized failed for keystoneclient ' 'constructed when get cascaded admin client')) except cinder_exception.Unauthorized: with excutils.save_and_reraise_exception(): LOG.error(_('Token unauthorized failed for cascaded ' 'cinderClient constructed')) except Exception: with excutils.save_and_reraise_exception(): LOG.error(_('Failed to get cinder python client.')) def _add_to_threadpool(self, func, *args, **kwargs): self._tp.spawn_n(func, *args, **kwargs) @property def initialized(self): return self.init_flag def init_host(self): ctxt = context.get_admin_context() self._init_volume_mapping_cache(ctxt) LOG.info(_("Cleaning up incomplete backup operations.")) # TODO(smulcahy) implement full resume of backup and restore # operations on restart (rather than simply resetting) backups = self.db.backup_get_all_by_host(ctxt, self.host) for backup in backups: if backup['status'] == 'creating' or backup['status'] == 'restoring': backup_info = {'status':backup['status'], 'id':backup['id']} self.backup_cache.append(backup_info) # TODO: this won't work because under this context, you have # no project id '''if backup['status'] == 'deleting': LOG.info(_('Resuming delete on backup: %s.') % backup['id']) self.delete_backup(ctxt, backup['id'])''' self.init_flag = True def create_backup(self, context, backup_id): """Create volume backups using configured backup service.""" backup = self.db.backup_get(context, backup_id) volume_id = backup['volume_id'] display_description = backup['display_description'] container = backup['container'] display_name = self._gen_ccding_backup_name(backup_id) availability_zone = cfg.CONF.storage_availability_zone # Because volume could be available or in-use initial_vol_status = self.db.volume_get(context, volume_id)['status'] self.db.volume_update(context, volume_id, {'status': 'backing-up'}) '''if volume status is in-use, it must have been checked with force flag in cascading api layer''' force = False if initial_vol_status == 'in-use': force = True LOG.info(_('cascade info: Create backup started, backup: %(backup_id)s ' 'volume: %(volume_id)s.') % {'backup_id': backup_id, 'volume_id': volume_id}) volume = self.db.volume_get(context, volume_id) expected_status = 'backing-up' actual_status = volume['status'] if actual_status != expected_status: err = _('Create backup aborted, expected volume status ' '%(expected_status)s but got %(actual_status)s.') % { 'expected_status': expected_status, 'actual_status': actual_status, } self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': err}) raise exception.InvalidVolume(reason=err) expected_status = 'creating' actual_status = backup['status'] if actual_status != expected_status: err = _('Create backup aborted, expected backup status ' '%(expected_status)s but got %(actual_status)s.') % { 'expected_status': expected_status, 'actual_status': actual_status, } self.db.volume_update(context, volume_id, {'status': initial_vol_status}) self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': err}) raise exception.InvalidBackup(reason=err) cascaded_snapshot_id='' query_status = "error" try: cascaded_volume_id = self._query_cascaded_vol_id(context,volume_id) LOG.info(_("begin to create backup,cascaded volume : %s"), cascaded_volume_id) if container: try: cascaded_snapshot_id = self._get_cascaded_snapshot_id(context,container) except Exception as err: cascaded_snapshot_id = '' LOG.info(_("the container is not snapshot :%s"), container) if cascaded_snapshot_id: LOG.info(_("the container is snapshot :%s"), container) snapshot_ref = self.db.snapshot_get(context, container) update_volume_id = snapshot_ref['volume_id'] container = cascaded_snapshot_id self.db.backup_update(context, backup_id, {'volume_id': update_volume_id}) cinderClient = self ._get_cascaded_cinder_client(context) bodyResponse = cinderClient.backups.create( volume_id=cascaded_volume_id, container=container, name=display_name, description=display_description, force=force) LOG.info(_("cascade ino: create backup while response is:%s"), bodyResponse._info) self.volumes_mapping_cache['backups'][backup_id] = \ bodyResponse._info['id'] # use service metadata to record cascading to cascaded backup id # mapping, to support cross az backup restore metadata = "mapping_uuid:" + bodyResponse._info['id'] + ";" tmp_metadata = None while True: time.sleep(CONF.volume_sync_interval) queryResponse = \ cinderClient.backups.get(bodyResponse._info['id']) query_status = queryResponse._info['status'] if query_status != 'creating': tmp_metadata = queryResponse._info.get('service_metadata','') self.db.backup_update(context, backup['id'], {'status': query_status}) self.db.volume_update(context, volume_id, {'status': initial_vol_status}) break else: continue except Exception as err: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {'status': initial_vol_status}) self.db.backup_update(context, backup['id'], {'status': 'error', 'fail_reason': unicode(err)}) return if tmp_metadata: metadata = metadata + tmp_metadata self.db.backup_update(context, backup_id, {'status': query_status, 'size': volume['size'], 'availability_zone': availability_zone, 'service_metadata': metadata}) LOG.info(_('Create backup finished. backup: %s.'), backup_id) def _get_cascaded_backup_id(self, backup_id): count = 0 display_name =self._gen_ccding_backup_name(backup_id) try: sopt={ "name":display_name } cascaded_backups = self.adminCinderClient.backups.list(search_opts=sopt) except cinder_exception.Unauthorized: count = count + 1 self.adminCinderClient = self._get_cascaded_cinder_client() if count < 2: LOG.info(_('To try again for get_cascaded_backup_id()')) self._get_cascaded_backup_id(backup_id) if cascaded_backups: cascaded_backup_id = getattr(cascaded_backups[-1], '_info')['id'] else: err = _('the backup %s is not exist ') %display_name raise exception.InvalidBackup(reason=err) return cascaded_backup_id def _get_cascaded_snapshot_id(self, context, snapshot_id): metadata = self.db.snapshot_metadata_get(context, snapshot_id) cascaded_snapshot_id = metadata['mapping_uuid'] if cascaded_snapshot_id: LOG.info(_("cascade ino: cascaded_snapshot_id is:%s"), cascaded_snapshot_id) return cascaded_snapshot_id def _clean_up_fake_resource(self, context, fake_backup_id, fake_source_volume_id): cinderClient = self._get_cascaded_cinder_client(context) cinderClient.backups.delete(fake_backup_id) cinderClient.volumes.delete(fake_source_volume_id) def restore_backup(self, context, backup_id, volume_id): """Restore volume backups from configured backup service.""" LOG.info(_('Restore backup started, backup: %(backup_id)s ' 'volume: %(volume_id)s.') % {'backup_id': backup_id, 'volume_id': volume_id}) backup = self.db.backup_get(context, backup_id) volume = self.db.volume_get(context, volume_id) availability_zone = cfg.CONF.storage_availability_zone expected_status = 'restoring-backup' actual_status = volume['status'] if actual_status != expected_status: err = (_('Restore backup aborted, expected volume status ' '%(expected_status)s but got %(actual_status)s.') % {'expected_status': expected_status, 'actual_status': actual_status}) self.db.backup_update(context, backup_id, {'status': 'available'}) raise exception.InvalidVolume(reason=err) expected_status = 'restoring' actual_status = backup['status'] if actual_status != expected_status: err = (_('Restore backup aborted: expected backup status ' '%(expected_status)s but got %(actual_status)s.') % {'expected_status': expected_status, 'actual_status': actual_status}) self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': err}) self.db.volume_update(context, volume_id, {'status': 'error'}) raise exception.InvalidBackup(reason=err) if volume['size'] > backup['size']: LOG.info(_('Volume: %(vol_id)s, size: %(vol_size)d is ' 'larger than backup: %(backup_id)s, ' 'size: %(backup_size)d, continuing with restore.'), {'vol_id': volume['id'], 'vol_size': volume['size'], 'backup_id': backup['id'], 'backup_size': backup['size']}) try: cinderClient = self._get_cascaded_cinder_client(context) cascaded_volume_id = self._query_cascaded_vol_id(context, volume_id) # the backup to be restored may be cross-az, so get cascaded backup id # not from cache (since cache is built from cinder client of its own # region), but retrieve it from service meta data LOG.info(_("backup az:(backup_az)%s, conf az:%(conf_az)s") % {'backup_az': backup['availability_zone'], 'conf_az': availability_zone}) fake_description = "" fake_source_volume_id = None fake_backup_id = None if backup['availability_zone'] != availability_zone: volumeResponse = cinderClient.volumes.create( volume['size'], name=volume['display_name'] + "-fake", description=volume['display_description'], user_id=context.user_id, project_id=context.project_id, availability_zone=availability_zone, metadata={'cross_az': ""}) fake_source_volume_id = volumeResponse._info['id'] time.sleep(30) # retrieve cascaded backup id md_set = backup['service_metadata'].split(';') cascaded_backup_id = None if len(md_set) > 1 and 'mapping_uuid' in md_set[0]: mapping_set = md_set[0].split(':') cascaded_backup_id = mapping_set[1] # save original backup id cascaded_source_backup_id = cascaded_backup_id # retrieve the original cascaded_source_volume_id cascading_source_volume_id = backup['volume_id'] cascaded_source_volume_id = self._query_cascaded_vol_id( context, cascading_source_volume_id) LOG.info(_("cascaded_source_backup_id:%(cascaded_source_backup_id)s," "cascaded_source_volume_id:%(cascaded_source_volume_id)s" % {'cascaded_source_backup_id': cascaded_source_backup_id, 'cascaded_source_volume_id': cascaded_source_volume_id})) # compose display description for cascaded volume driver mapping to # original source backup id and original source volume_id fake_description = "cross_az:" + cascaded_source_backup_id + ":" + \ cascaded_source_volume_id backup_bodyResponse = cinderClient.backups.create( volume_id=fake_source_volume_id, container=backup['container'], name=backup['display_name'] + "-fake", description=fake_description) # set cascaded_backup_id as the faked one, which will help call # into our volume driver's restore function fake_backup_id = backup_bodyResponse._info['id'] cascaded_backup_id = backup_bodyResponse._info['id'] LOG.info(_("update cacaded_backup_id to created one:%s"), cascaded_backup_id) LOG.info(_("restore, cascaded_backup_id:%(cascaded_backup_id)s, " "cascaded_volume_id:%(cascaded_volume_id)s, " "description:%(description)s") % {'cascaded_backup_id': cascaded_backup_id, 'cascaded_volume_id': cascaded_volume_id, 'description': fake_description}) bodyResponse = cinderClient.restores.restore( backup_id=cascaded_backup_id, volume_id=cascaded_volume_id) LOG.info(_("cascade info: restore backup while response is:%s"), bodyResponse._info) while True: time.sleep(CONF.volume_sync_interval) queryResponse = \ cinderClient.backups.get(cascaded_backup_id) query_status = queryResponse._info['status'] if query_status != 'restoring': self.db.volume_update(context, volume_id, {'status': 'available'}) self.db.backup_update(context, backup_id, {'status': query_status}) LOG.info(_("get backup:%(backup)s status:%(status)s" % {'backup': cascaded_backup_id, 'status': query_status})) if fake_backup_id and fake_source_volume_id: LOG.info(_("cleanup fake backup:%(backup)s," "fake source volume id:%(volume)" % {'backup': fake_backup_id, 'volume': fake_source_volume_id})) # TODO: check fake_source_volume_id status issue and clean it else: continue except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {'status': 'error_restoring'}) self.db.backup_update(context, backup_id, {'status': 'available'}) LOG.info(_('Restore backup finished, backup %(backup_id)s restored' ' to volume %(volume_id)s.') % {'backup_id': backup_id, 'volume_id': volume_id}) def _query_cascaded_vol_id(self,ctxt,volume_id=None): volume = self.db.volume_get(ctxt, volume_id) volume_metadata = dict((item['key'], item['value']) for item in volume['volume_metadata']) mapping_uuid = volume_metadata.get('mapping_uuid', None) return mapping_uuid def _delete_backup_cascaded(self, context, backup_id): try: cascaded_backup_id = \ self.volumes_mapping_cache['backups'].get(backup_id, '') LOG.info(_("cascade ino: delete cascaded backup :%s"), cascaded_backup_id) cinderClient = self._get_cascaded_cinder_client(context) cinderClient.backups.get(cascaded_backup_id) resp = cinderClient.backups.delete(cascaded_backup_id) self.volumes_mapping_cache['backups'].pop(backup_id, '') LOG.info(_("delete cascaded backup %s successfully. resp :%s"), cascaded_backup_id, resp) return except cinder_exception.NotFound: self.volumes_mapping_cache['backups'].pop(backup_id, '') LOG.info(_("delete cascaded backup %s successfully."), cascaded_backup_id) return except Exception: with excutils.save_and_reraise_exception(): self.db.backup_update(context, backup_id, {'status': 'error_deleting'}) LOG.error(_("failed to delete cascaded backup %s"), cascaded_backup_id) @locked_backup_operation def delete_backup(self, context, backup_id): """Delete volume backup from configured backup service.""" LOG.info(_('cascade info:delete backup started, backup: %s.'), backup_id) backup = self.db.backup_get(context, backup_id) expected_status = 'deleting' actual_status = backup['status'] if actual_status != expected_status: err = _('Delete_backup aborted, expected backup status ' '%(expected_status)s but got %(actual_status)s.') \ % {'expected_status': expected_status, 'actual_status': actual_status} self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': err}) raise exception.InvalidBackup(reason=err) try: self._delete_backup_cascaded(context,backup_id) except Exception as err: with excutils.save_and_reraise_exception(): self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': unicode(err)}) # Get reservations try: reserve_opts = { 'backups': -1, 'backup_gigabytes': -backup['size'], } reservations = QUOTAS.reserve(context, project_id=backup['project_id'], **reserve_opts) except Exception: reservations = None LOG.exception(_("Failed to update usages deleting backup")) context = context.elevated() self.db.backup_destroy(context, backup_id) # Commit the reservations if reservations: QUOTAS.commit(context, reservations, project_id=backup['project_id']) LOG.info(_('Delete backup finished, backup %s deleted.'), backup_id) def export_record(self, context, backup_id): """Export all volume backup metadata details to allow clean import. Export backup metadata so it could be re-imported into the database without any prerequisite in the backup database. :param context: running context :param backup_id: backup id to export :returns: backup_record - a description of how to import the backup :returns: contains 'backup_url' - how to import the backup, and :returns: 'backup_service' describing the needed driver. :raises: InvalidBackup """ LOG.info(_('Export record started, backup: %s.'), backup_id) backup = self.db.backup_get(context, backup_id) expected_status = 'available' actual_status = backup['status'] if actual_status != expected_status: err = (_('Export backup aborted, expected backup status ' '%(expected_status)s but got %(actual_status)s.') % {'expected_status': expected_status, 'actual_status': actual_status}) raise exception.InvalidBackup(reason=err) backup_record = {} # Call driver to create backup description string try: cinderClient = self._get_cascaded_cinder_client(context) cascaded_backup_id = \ self.volumes_mapping_cache['backups'].get(backup_id, '') LOG.info(_("cascade ino: export cascade backup :%s"), cascaded_backup_id) bodyResponse = cinderClient.backups.export_record(cascaded_backup_id) backup_record['backup_url'] = bodyResponse['backup_url'] backup_record['backup_service'] = bodyResponse['backup_service'] except Exception as err: msg = unicode(err) raise exception.InvalidBackup(reason=msg) LOG.info(_('Export record finished, backup %s exported.'), cascaded_backup_id) return backup_record def import_record(self, context, backup_id, backup_service, backup_url, backup_hosts): """Import all volume backup metadata details to the backup db. :param context: running context :param backup_id: The new backup id for the import :param backup_service: The needed backup driver for import :param backup_url: An identifier string to locate the backup :param backup_hosts: Potential hosts to execute the import :raises: InvalidBackup :raises: ServiceNotFound """ LOG.info(_('Import record started, backup_url: %s.'), backup_url) # Can we import this backup? try: cinderClient = self._get_cascaded_cinder_client(context) bodyResponse = cinderClient.backups.import_record(backup_service,backup_url) except Exception as err: msg = unicode(err) self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': msg}) raise exception.InvalidBackup(reason=msg) backup_update = {} backup_update['status'] = 'available' backup_update['host'] = self.host self.db.backup_update(context, backup_id, backup_update) # Verify backup LOG.info(_('Import record id %s metadata from driver ' 'finished.') % backup_id) @periodic_task.periodic_task(spacing=CONF.volume_sync_interval, run_immediately=True) def _deal_backup_status(self,context): if not self.init_flag: LOG.debug(_('cinder backup proxy is not ready')) return for backup in self.backup_cache: try: cascaded_backup_id = \ self.volumes_mapping_cache['backups'].get(backup['id'], None) if not cascaded_backup_id: self.backup_cache.pop() continue cinderClient = self._get_cinder_cascaded_admin_client() queryResponse = cinderClient.backups.get(cascaded_backup_id) query_status = queryResponse._info['status'] if query_status != backup['status']: metadata = queryResponse._info.get('service_metadata','') self.db.backup_update(context, backup['id'], {'status': query_status}) self.db.volume_update(context, backup['volume_id'], {'status': 'available'}) self.backup_cache.pop() except Exception: pass def _get_tenant_id(self): tenant_id = None try: kwargs = {'username': CONF.cinder_username, 'password': CONF.admin_password, 'tenant_name': CONF.cinder_tenant_name, 'auth_url': CONF.keystone_auth_url, 'insecure': True } keystoneclient = kc.Client(**kwargs) tenant_id = keystoneclient.tenants.find(name=CONF.cinder_tenant_name).to_dict().get('id') LOG.debug("_get_tenant_id tenant_id: %s" %str(tenant_id)) except keystone_exception.Unauthorized: with excutils.save_and_reraise_exception(): LOG.error('_get_tenant_id Unauthorized') except Exception: with excutils.save_and_reraise_exception(): LOG.error('_get_tenant_id raise Exception') return tenant_id def _get_management_url(self, kc, **kwargs): return kc.service_catalog.url_for(**kwargs) def _get_cascaded_cinder_client(self, context=None): try: if context is None: cinderclient = cinder_client.Client( auth_url=CONF.keystone_auth_url, region_name=CONF.cascaded_region_name, tenant_id=self.tenant_id, api_key=CONF.admin_password, username=CONF.cinder_username, insecure=True, timeout=30, retries=3) else: ctx_dict = context.to_dict() kwargs = { 'auth_url': CONF.keystone_auth_url, 'tenant_name': CONF.cinder_tenant_name, 'username': CONF.cinder_username, 'password': CONF.admin_password, 'insecure': True } keystoneclient = kc.Client(**kwargs) management_url = self._get_management_url(keystoneclient, service_type='volumev2', attr='region', endpoint_type='publicURL', filter_value=CONF.cascaded_region_name) LOG.info("before replace: management_url:%s", management_url) url = management_url.rpartition("/")[0] management_url = url+ '/' + ctx_dict.get("project_id") LOG.info("after replace: management_url:%s", management_url) cinderclient = cinder_client.Client( username=ctx_dict.get('user_id'), auth_url=cfg.CONF.keystone_auth_url, insecure=True, timeout=30, retries=3) cinderclient.client.auth_token = ctx_dict.get('auth_token') cinderclient.client.management_url = management_url LOG.info(_("cascade info: os_region_name:%s"), CONF.cascaded_region_name) return cinderclient except keystone_exception.Unauthorized: with excutils.save_and_reraise_exception(): LOG.error(_('Token unauthorized failed for keystoneclient ' 'constructed when get cascaded admin client')) except cinder_exception.Unauthorized: with excutils.save_and_reraise_exception(): LOG.error(_('Token unauthorized failed for cascaded ' 'cinderClient constructed')) except Exception: with excutils.save_and_reraise_exception(): LOG.error(_('Failed to get cinder python client.'))
class BanResource(BaseResource): def __init__(self): super(BanResource, self).__init__() self.user_manager = UserManager(environ.env) self.executor = GreenPool() self.request = request self.env = environ.env def do_post(self): try: json = self._validate_params() self.schedule_execution(json) return ok() except Exception as e: logger.error('could not ban user: %s' % str(e)) logger.exception(traceback.format_exc()) self.env.capture_exception(sys.exc_info()) return fail(str(e)) def schedule_execution(self, json: dict): try: # avoid hanging clients self.executor.spawn_n(self._do_post, json) except Exception as e: logger.error('could not schedule ban request: %s' % str(e)) logger.exception(e) self.env.capture_exception(sys.exc_info()) def _validate_params(self): is_valid, msg, json = self.validate_json(self.request, silent=False) if not is_valid: raise RuntimeError('invalid json: %s' % msg) if json is None: raise RuntimeError('no json in request') if not isinstance(json, dict): raise RuntimeError('need a dict of user-room keys') for user_id, ban_info in json.items(): try: target_type = ban_info['type'] except KeyError: raise KeyError( 'missing target type for user id %s and request %s' % (user_id, ban_info)) try: ban_info['target'] except KeyError: if target_type != 'global': raise KeyError( 'missing target id for user id %s and request %s' % (user_id, ban_info)) try: ban_info['duration'] except KeyError: raise KeyError( 'missing ban duration for user id %s and request %s' % (user_id, ban_info)) ban_info.get('reason') ban_info.get('admin_id') return json @timeit(logger, 'on_rest_ban') def _do_post(self, json: dict): logger.debug('POST request: %s' % str(json)) for user_id, ban_info in json.items(): try: self.ban_user(user_id, ban_info) except Exception as e: self.env.capture_exception(sys.exc_info()) logger.error('could not ban user %s: %s' % (user_id, str(e))) def ban_user(self, user_id: str, ban_info: dict): target_type = ban_info.get('type', '') target_id = ban_info.get('target', '') duration = ban_info.get('duration', '') reason = ban_info.get('reason', '') banner_id = ban_info.get('admin_id', '') try: user_name = ban_info['name'] user_name = utils.b64d(user_name) except KeyError: logger.warning( 'no name specified in ban info, if we have to create the user it will get the ID as name' ) user_name = user_id try: self.user_manager.ban_user(user_id, target_id, duration, target_type, reason=reason, banner_id=banner_id, user_name=user_name) except ValueError as e: logger.error('invalid ban duration "%s" for user %s: %s' % (duration, user_id, str(e))) self.env.capture_exception(sys.exc_info()) except NoSuchUserException as e: logger.error('no such user %s: %s' % (user_id, str(e))) self.env.capture_exception(sys.exc_info()) except UnknownBanTypeException as e: logger.error('unknown ban type "%s" for user %s: %s' % (target_type, user_id, str(e))) self.env.capture_exception(sys.exc_info()) except Exception as e: logger.error('could not ban user %s: %s' % (user_id, str(e))) logger.error(traceback.format_exc()) self.env.capture_exception(sys.exc_info())
class Checker(object): def __init__(self, namespace, concurrency=50, error_file=None): self.pool = GreenPool(concurrency) self.error_file = error_file if self.error_file: f = open(self.error_file, 'a') self.error_writer = csv.writer(f, delimiter=' ') conf = {'namespace': namespace} self.account_client = AccountClient(conf) self.container_client = ContainerClient(conf) self.blob_client = BlobClient() self.accounts_checked = 0 self.containers_checked = 0 self.objects_checked = 0 self.chunks_checked = 0 self.account_not_found = 0 self.container_not_found = 0 self.object_not_found = 0 self.chunk_not_found = 0 self.account_exceptions = 0 self.container_exceptions = 0 self.object_exceptions = 0 self.chunk_exceptions = 0 self.list_cache = {} self.running = {} def write_error(self, target): error = [target.account] if target.container: error.append(target.container) if target.obj: error.append(target.obj) if target.chunk: error.append(target.chunk) self.error_writer.writerow(error) def check_chunk(self, target): chunk = target.chunk obj_listing = self.check_obj(target) error = False if chunk not in obj_listing: print(' Chunk %s missing in object listing' % target) error = True # checksum = None else: # TODO check checksum match # checksum = obj_listing[chunk]['hash'] pass try: self.blob_client.chunk_head(chunk) except exc.NotFound as e: self.chunk_not_found += 1 error = True print(' Not found chunk "%s": %s' % (target, str(e))) except Exception as e: self.chunk_exceptions += 1 error = True print(' Exception chunk "%s": %s' % (target, str(e))) if error and self.error_file: self.write_error(target) self.chunks_checked += 1 def check_obj(self, target, recurse=False): account = target.account container = target.container obj = target.obj if (account, container, obj) in self.running: self.running[(account, container, obj)].wait() if (account, container, obj) in self.list_cache: return self.list_cache[(account, container, obj)] self.running[(account, container, obj)] = Event() print('Checking object "%s"' % target) container_listing = self.check_container(target) error = False if obj not in container_listing: print(' Object %s missing in container listing' % target) error = True # checksum = None else: # TODO check checksum match # checksum = container_listing[obj]['hash'] pass results = [] try: _, resp = self.container_client.content_show(acct=account, ref=container, path=obj) except exc.NotFound as e: self.object_not_found += 1 error = True print(' Not found object "%s": %s' % (target, str(e))) except Exception as e: self.object_exceptions += 1 error = True print(' Exception object "%s": %s' % (target, str(e))) else: results = resp chunk_listing = dict() for chunk in results: chunk_listing[chunk['url']] = chunk self.objects_checked += 1 self.list_cache[(account, container, obj)] = chunk_listing self.running[(account, container, obj)].send(True) del self.running[(account, container, obj)] if recurse: for chunk in chunk_listing: t = target.copy() t.chunk = chunk self.pool.spawn_n(self.check_chunk, t) if error and self.error_file: self.write_error(target) return chunk_listing def check_container(self, target, recurse=False): account = target.account container = target.container if (account, container) in self.running: self.running[(account, container)].wait() if (account, container) in self.list_cache: return self.list_cache[(account, container)] self.running[(account, container)] = Event() print('Checking container "%s"' % target) account_listing = self.check_account(target) error = False if container not in account_listing: error = True print(' Container %s missing in account listing' % target) marker = None results = [] while True: try: resp = self.container_client.container_list(acct=account, ref=container, marker=marker) except exc.NotFound as e: self.container_not_found += 1 error = True print(' Not found container "%s": %s' % (target, str(e))) break except Exception as e: self.container_exceptions += 1 error = True print(' Exception container "%s": %s' % (target, str(e))) break if resp['objects']: marker = resp['objects'][-1]['name'] else: break results.extend(resp['objects']) container_listing = dict() for obj in results: container_listing[obj['name']] = obj self.containers_checked += 1 self.list_cache[(account, container)] = container_listing self.running[(account, container)].send(True) del self.running[(account, container)] if recurse: for obj in container_listing: t = target.copy() t.obj = obj self.pool.spawn_n(self.check_obj, t, True) if error and self.error_file: self.write_error(target) return container_listing def check_account(self, target, recurse=False): account = target.account if account in self.running: self.running[account].wait() if account in self.list_cache: return self.list_cache[account] self.running[account] = Event() print('Checking account "%s"' % target) error = False marker = None results = [] while True: try: resp = self.account_client.containers_list(account, marker=marker) except Exception as e: self.account_exceptions += 1 error = True print(' Exception account "%s": %s' % (target, str(e))) break if resp['listing']: marker = resp['listing'][-1][0] else: break results.extend(resp['listing']) containers = dict() for e in results: containers[e[0]] = (e[1], e[2]) self.list_cache[account] = containers self.running[account].send(True) del self.running[account] self.accounts_checked += 1 if recurse: for container in containers: t = target.copy() t.container = container self.pool.spawn_n(self.check_container, t, True) if error and self.error_file: self.write_error(target) return containers def check(self, target): if target.chunk and target.obj and target.container: self.pool.spawn_n(self.check_chunk, target) elif target.obj and target.container: self.pool.spawn_n(self.check_obj, target, True) elif target.container: self.pool.spawn_n(self.check_container, target, True) else: self.pool.spawn_n(self.check_account, target, True) def wait(self): self.pool.waitall() def report(self): def _report_stat(name, stat): print("{0:18}: {1}".format(name, stat)) print() print('Report') _report_stat("Accounts checked", self.accounts_checked) if self.account_not_found: _report_stat("Missing accounts", self.account_not_found) if self.account_exceptions: _report_stat("Exceptions", self.account_not_found) print() _report_stat("Containers checked", self.containers_checked) if self.container_not_found: _report_stat("Missing containers", self.container_not_found) if self.container_exceptions: _report_stat("Exceptions", self.container_exceptions) print() _report_stat("Objects checked", self.objects_checked) if self.object_not_found: _report_stat("Missing objects", self.object_not_found) if self.object_exceptions: _report_stat("Exceptions", self.object_exceptions) print() _report_stat("Chunks checked", self.chunks_checked) if self.chunk_not_found: _report_stat("Missing chunks", self.chunk_not_found) if self.chunk_exceptions: _report_stat("Exceptions", self.chunk_exceptions)
class CinderProxy(manager.SchedulerDependentManager): """Manages attachable block storage devices.""" RPC_API_VERSION = '1.16' target = messaging.Target(version=RPC_API_VERSION) def __init__(self, service_name=None, *args, **kwargs): """Load the specified in args, or flags.""" # update_service_capabilities needs service_name to be volume super(CinderProxy, self).__init__(service_name='volume', *args, **kwargs) self.configuration = Configuration(volume_manager_opts, config_group=service_name) self._tp = GreenPool() self.volume_api = volume.API() self._last_info_volume_state_heal = 0 self._change_since_time = None self.volumes_mapping_cache = {'volumes': {}, 'snapshots': {}} self._init_volume_mapping_cache() self.image_service = glance.get_default_image_service() def _init_volume_mapping_cache(self): cinderClient = self._get_cinder_cascaded_admin_client() try: search_op = {'all_tenants': True} volumes = cinderClient.volumes.list(search_opts=search_op) for volume in volumes: if 'logicalVolumeId' in volume._info['metadata']: volumeId = volume._info['metadata']['logicalVolumeId'] physicalVolumeId = volume._info['id'] self.volumes_mapping_cache['volumes'][volumeId] = \ physicalVolumeId snapshots = \ cinderClient.volume_snapshots.list(search_opts=search_op) for snapshot in snapshots: if 'logicalSnapshotId' in snapshot._info['metadata']: snapshotId = \ snapshot._info['metadata']['logicalSnapshotId'] physicalSnapshotId = snapshot._info['id'] self.volumes_mapping_cache['snapshots'][snapshotId] = \ physicalSnapshotId LOG.info(_("Cascade info: cinder proxy: init volumes mapping" "cache:%s"), self.volumes_mapping_cache) except Exception as ex: LOG.error(_("Failed init volumes mapping cache")) LOG.exception(ex) def _heal_volume_mapping_cache(self, volumeId, physicalVolumeId, action): if action == 'add': self.volumes_mapping_cache['volumes'][volumeId] = physicalVolumeId LOG.info(_("Cascade info: volume mapping cache add record. " "volumeId:%s,physicalVolumeId:%s"), (volumeId, physicalVolumeId)) return True elif action == 'remove': if volumeId in self.volumes_mapping_cache['volumes']: self.volumes_mapping_cache['volumes'].pop(volumeId) LOG.info(_("Casecade info: volume mapping cache remove record." " volumeId:%s, physicalVolumeId:%s"), (volumeId, physicalVolumeId)) return True def _heal_snapshot_mapping_cache(self, snapshotId, physicalSnapshotId, action): if action == 'add': self.volumes_mapping_cache['snapshots'][snapshotId] = \ physicalSnapshotId LOG.info(_("Cascade info: snapshots mapping cache add record. " "snapshotId:%s, physicalSnapshotId:%s"), (snapshotId, physicalSnapshotId)) return True elif action == 'remove': if snapshotId in self.volumes_mapping_cache['snapshots']: self.volumes_mapping_cache['snapshots'].pop(snapshotId) LOG.info(_("Casecade info: volume snapshot mapping cache" "remove snapshotId:%s,physicalSnapshotId:%s"), (snapshotId, physicalSnapshotId)) return True def _get_cascaded_volume_id(self, volume_id): physical_volume_id = None if volume_id in self.volumes_mapping_cache['volumes']: physical_volume_id = \ self.volumes_mapping_cache['volumes'].get(volume_id) LOG.debug(_('get cascade volume,volume id:%s,physicalVolumeId:%s'), (volume_id, physical_volume_id)) if physical_volume_id is None: LOG.error(_('can not find volume %s in volumes_mapping_cache %s.'), volume_id, self.volumes_mapping_cache) return physical_volume_id def _get_cascaded_snapshot_id(self, snapshot_id): physical_snapshot_id = None if snapshot_id in self.volumes_mapping_cache['snapshots']: physical_snapshot_id = \ self.volumes_mapping_cache['snapshots'].get('snapshot_id') LOG.debug(_("get cascade volume,snapshot_id:%s," "physicalSnapshotId:%s"), (snapshot_id, physical_snapshot_id)) if physical_snapshot_id is None: LOG.error(_('not find snapshot %s in volumes_mapping_cache %s'), snapshot_id, self.volumes_mapping_cache) return physical_snapshot_id def _get_cinder_cascaded_admin_client(self): try: kwargs = {'username': cfg.CONF.cinder_username, 'password': cfg.CONF.cinder_password, 'tenant_name': cfg.CONF.cinder_tenant_name, 'auth_url': cfg.CONF.keystone_auth_url } client_v2 = kc.Client(**kwargs) sCatalog = getattr(client_v2, 'auth_ref').get('serviceCatalog') compat_catalog = { 'access': {'serviceCatalog': sCatalog} } sc = service_catalog.ServiceCatalog(compat_catalog) url = sc.url_for(attr='region', filter_value=cfg.CONF.cascaded_region_name, service_type='volume', service_name='cinder', endpoint_type='publicURL') cinderclient = cinder_client.Client( username=cfg.CONF.cinder_username, api_key=cfg.CONF.cinder_password, tenant_id=cfg.CONF.cinder_tenant_name, auth_url=cfg.CONF.keystone_auth_url) cinderclient.client.auth_token = client_v2.auth_ref.auth_token cinderclient.client.management_url = url return cinderclient except Exception: with excutils.save_and_reraise_exception(): LOG.error(_('Failed to get cinder python client.')) def _get_cinder_cascaded_user_client(self, context): try: ctx_dict = context.to_dict() cinderclient = cinder_client.Client( username=ctx_dict.get('user_id'), api_key=ctx_dict.get('auth_token'), project_id=ctx_dict.get('project_name'), auth_url=cfg.CONF.keystone_auth_url) cinderclient.client.auth_token = ctx_dict.get('auth_token') cinderclient.client.management_url = \ cfg.CONF.cascaded_cinder_url % ctx_dict return cinderclient except Exception: with excutils.save_and_reraise_exception(): LOG.error(_('Failed to get cinder python client.')) def _get_image_cascaded(self, context, image_id, cascaded_glance_url): try: # direct_url is returned by v2 api client = glance.GlanceClientWrapper( context, netloc=cfg.CONF.cascading_glance_url, use_ssl=False, version="2") image_meta = client.call(context, 'get', image_id) except Exception: glance._reraise_translated_image_exception(image_id) if not self.image_service._is_image_available(context, image_meta): raise exception.ImageNotFound(image_id=image_id) locations = getattr(image_meta, 'locations', None) LOG.debug(_("Cascade info: image glance get_image_cascaded," "locations:%s"), locations) LOG.debug(_("Cascade info: image glance get_image_cascaded," "cascaded_glance_url:%s"), cascaded_glance_url) cascaded_image_id = None for loc in locations: image_url = loc.get('url') LOG.debug(_("Cascade info: image glance get_image_cascaded," "image_url:%s"), image_url) if cascaded_glance_url in image_url: (cascaded_image_id, glance_netloc, use_ssl) = \ glance._parse_image_ref(image_url) LOG.debug(_("Cascade info : Result :image glance " "get_image_cascaded,%s") % cascaded_image_id) break if cascaded_image_id is None: raise exception.CinderException( _("Cascade exception: Cascaded image for image %s not exist ") % image_id) return cascaded_image_id def _add_to_threadpool(self, func, *args, **kwargs): self._tp.spawn_n(func, *args, **kwargs) def init_host(self): """Do any initialization that needs to be run if this is a standalone service. """ ctxt = context.get_admin_context() volumes = self.db.volume_get_all_by_host(ctxt, self.host) LOG.debug(_("Re-exporting %s volumes"), len(volumes)) LOG.debug(_('Resuming any in progress delete operations')) for volume in volumes: if volume['status'] == 'deleting': LOG.info(_('Resuming delete on volume: %s') % volume['id']) if CONF.volume_service_inithost_offload: # Offload all the pending volume delete operations to the # threadpool to prevent the main volume service thread # from being blocked. self._add_to_threadpool(self.delete_volume(ctxt, volume['id'])) else: # By default, delete volumes sequentially self.delete_volume(ctxt, volume['id']) # collect and publish service capabilities self.publish_service_capabilities(ctxt) def create_volume(self, context, volume_id, request_spec=None, filter_properties=None, allow_reschedule=True, snapshot_id=None, image_id=None, source_volid=None): """Creates and exports the volume.""" ctx_dict = context.__dict__ try: volume_properties = request_spec.get('volume_properties') size = volume_properties.get('size') display_name = volume_properties.get('display_name') display_description = volume_properties.get('display_description') volume_type_id = volume_properties.get('volume_type_id') user_id = ctx_dict.get('user_id') project_id = ctx_dict.get('project_id') cascaded_snapshot_id = None if snapshot_id is not None: snapshot_ref = self.db.snapshot_get(context, snapshot_id) cascaded_snapshot_id = snapshot_ref['mapping_uuid'] LOG.info(_('Cascade info: create volume from snapshot, ' 'cascade id:%s'), cascaded_snapshot_id) cascaded_source_volid = None if source_volid is not None: vol_ref = self.db.volume_get(context, source_volid) cascaded_source_volid = vol_ref['mapping_uuid'] LOG.info(_('Cascade info: create volume from source volume, ' 'cascade id:%s'), cascaded_source_volid) cascaded_volume_type = None if volume_type_id is not None: volume_type_ref = \ self.db.volume_type_get(context, volume_type_id) cascaded_volume_type = volume_type_ref['name'] LOG.info(_('Cascade info: create volume use volume type, ' 'cascade name:%s'), cascaded_volume_type) metadata = volume_properties.get('metadata') if metadata is None: metadata = {} metadata['logicalVolumeId'] = volume_id cascaded_image_id = None if image_id is not None: if cfg.CONF.glance_cascading_flag: cascaded_image_id = self._get_image_cascaded( context, image_id, cfg.CONF.cascaded_glance_url) else: cascaded_image_id = image_id LOG.info(_("Cascade info: create volume use image, " "cascaded image id is %s:"), cascaded_image_id) availability_zone = cfg.CONF.cascaded_available_zone LOG.info(_('Cascade info: create volume with available zone:%s'), availability_zone) cinderClient = self._get_cinder_cascaded_user_client(context) bodyResponse = cinderClient.volumes.create( size=size, snapshot_id=cascaded_snapshot_id, source_volid=cascaded_source_volid, name=display_name, description=display_description, volume_type=cascaded_volume_type, user_id=user_id, project_id=project_id, availability_zone=availability_zone, metadata=metadata, imageRef=cascaded_image_id) if 'logicalVolumeId' in metadata: metadata.pop('logicalVolumeId') metadata['mapping_uuid'] = bodyResponse._info['id'] self.db.volume_metadata_update(context, volume_id, metadata, True) if bodyResponse._info['status'] == 'creating': self._heal_volume_mapping_cache(volume_id, bodyResponse._info['id'], 'add') self.db.volume_update( context, volume_id, {'mapping_uuid': bodyResponse._info['id']}) except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {'status': 'error'}) return volume_id @periodic_task.periodic_task(spacing=CONF.volume_sync_interval, run_immediately=True) def _heal_volume_status(self, context): TIME_SHIFT_TOLERANCE = 3 heal_interval = CONF.volume_sync_interval if not heal_interval: return curr_time = time.time() LOG.info(_('Cascade info: last volume update time:%s'), self._last_info_volume_state_heal) LOG.info(_('Cascade info: heal interval:%s'), heal_interval) LOG.info(_('Cascade info: curr_time:%s'), curr_time) if self._last_info_volume_state_heal + heal_interval > curr_time: return self._last_info_volume_state_heal = curr_time cinderClient = self._get_cinder_cascaded_admin_client() try: if self._change_since_time is None: search_opt = {'all_tenants': True} volumes = cinderClient.volumes.list(search_opts=search_opt) volumetypes = cinderClient.volume_types.list() LOG.info(_('Cascade info: change since time is none,' 'volumes:%s'), volumes) else: change_since_isotime = \ timeutils.parse_isotime(self._change_since_time) changesine_timestamp = change_since_isotime - \ datetime.timedelta(seconds=TIME_SHIFT_TOLERANCE) timestr = time.mktime(changesine_timestamp.timetuple()) new_change_since_isotime = \ timeutils.iso8601_from_timestamp(timestr) search_op = {'all_tenants': True, 'changes-since': new_change_since_isotime} volumes = cinderClient.volumes.list(search_opts=search_op) volumetypes = cinderClient.volume_types.list() LOG.info(_('Cascade info: search time is not none,' 'volumes:%s'), volumes) self._change_since_time = timeutils.isotime() if len(volumes) > 0: LOG.debug(_('Updated the volumes %s'), volumes) for volume in volumes: volume_id = volume._info['metadata']['logicalVolumeId'] volume_status = volume._info['status'] if volume_status == "in-use": self.db.volume_update(context, volume_id, {'status': volume._info['status'], 'attach_status': 'attached', 'attach_time': timeutils.strtime() }) elif volume_status == "available": self.db.volume_update(context, volume_id, {'status': volume._info['status'], 'attach_status': 'detached', 'instance_uuid': None, 'attached_host': None, 'mountpoint': None, 'attach_time': None }) else: self.db.volume_update(context, volume_id, {'status': volume._info['status']}) LOG.info(_('Cascade info: Updated the volume %s status from' 'cinder-proxy'), volume_id) vol_types = self.db.volume_type_get_all(context, inactive=False) for volumetype in volumetypes: volume_type_name = volumetype._info['name'] if volume_type_name not in vol_types.keys(): extra_specs = volumetype._info['extra_specs'] self.db.volume_type_create( context, dict(name=volume_type_name, extra_specs=extra_specs)) except Exception: with excutils.save_and_reraise_exception(): LOG.error(_('Failed to sys volume status to db.')) @locked_volume_operation def delete_volume(self, context, volume_id, unmanage_only=False): """Deletes and unexports volume.""" context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) if context.project_id != volume_ref['project_id']: project_id = volume_ref['project_id'] else: project_id = context.project_id LOG.info(_("volume %s: deleting"), volume_ref['id']) if volume_ref['attach_status'] == "attached": # Volume is still attached, need to detach first raise exception.VolumeAttached(volume_id=volume_id) if volume_ref['host'] != self.host: raise exception.InvalidVolume( reason=_("volume is not local to this node")) self._notify_about_volume_usage(context, volume_ref, "delete.start") self._reset_stats() try: self._delete_cascaded_volume(context, volume_id) except Exception: LOG.exception(_("Failed to deleting volume")) # Get reservations try: reserve_opts = {'volumes': -1, 'gigabytes': -volume_ref['size']} QUOTAS.add_volume_type_opts(context, reserve_opts, volume_ref.get('volume_type_id')) reservations = QUOTAS.reserve(context, project_id=project_id, **reserve_opts) except Exception: reservations = None LOG.exception(_("Failed to update usages deleting volume")) # Delete glance metadata if it exists try: self.db.volume_glance_metadata_delete_by_volume(context, volume_id) LOG.debug(_("volume %s: glance metadata deleted"), volume_ref['id']) except exception.GlanceMetadataNotFound: LOG.debug(_("no glance metadata found for volume %s"), volume_ref['id']) self.db.volume_destroy(context, volume_id) LOG.info(_("volume %s: deleted successfully"), volume_ref['id']) self._notify_about_volume_usage(context, volume_ref, "delete.end") # Commit the reservations if reservations: QUOTAS.commit(context, reservations, project_id=project_id) self.publish_service_capabilities(context) return True def _delete_cascaded_volume(self, context, volume_id): try: vol_ref = self.db.volume_get(context, volume_id) casecaded_volume_id = vol_ref['mapping_uuid'] LOG.info(_('Cascade info: prepare to delete cascaded volume %s.'), casecaded_volume_id) cinderClient = self._get_cinder_cascaded_user_client(context) cinderClient.volumes.delete(volume=casecaded_volume_id) LOG.info(_('Cascade info: finished to delete cascade volume %s'), casecaded_volume_id) # self._heal_volume_mapping_cache(volume_id,casecade_volume_id,s'remove') except Exception: with excutils.save_and_reraise_exception(): LOG.error(_('Cascade info: failed to delete cascaded' ' volume %s'), casecaded_volume_id) def create_snapshot(self, context, volume_id, snapshot_id): """Creates and exports the snapshot.""" context = context.elevated() snapshot_ref = self.db.snapshot_get(context, snapshot_id) display_name = snapshot_ref['display_name'] display_description = snapshot_ref['display_description'] LOG.info(_("snapshot %s: creating"), snapshot_ref['id']) self._notify_about_snapshot_usage( context, snapshot_ref, "create.start") vol_ref = self.db.volume_get(context, volume_id) LOG.info(_("Cascade info: create snapshot while cascade id is:%s"), vol_ref['mapping_uuid']) try: vol_ref = self.db.volume_get(context, volume_id) casecaded_volume_id = vol_ref['mapping_uuid'] cinderClient = self._get_cinder_cascaded_user_client(context) bodyResponse = cinderClient.volume_snapshots.create( volume_id=casecaded_volume_id, force=False, name=display_name, description=display_description) LOG.info(_("Cascade info: create snapshot while response is:%s"), bodyResponse._info) if bodyResponse._info['status'] == 'creating': self._heal_snapshot_mapping_cache(snapshot_id, bodyResponse._info['id'], "add") self.db.snapshot_update( context, snapshot_ref['id'], {'mapping_uuid': bodyResponse._info['id']}) except Exception: with excutils.save_and_reraise_exception(): self.db.snapshot_update(context, snapshot_ref['id'], {'status': 'error'}) return self.db.snapshot_update(context, snapshot_ref['id'], {'status': 'available', 'progress': '100%'}) # vol_ref = self.db.volume_get(context, volume_id) if vol_ref.bootable: try: self.db.volume_glance_metadata_copy_to_snapshot( context, snapshot_ref['id'], volume_id) except exception.CinderException as ex: LOG.exception(_("Failed updating %(snapshot_id)s" " metadata using the provided volumes" " %(volume_id)s metadata") % {'volume_id': volume_id, 'snapshot_id': snapshot_id}) raise exception.MetadataCopyFailure(reason=ex) LOG.info(_("Cascade info: snapshot %s, created successfully"), snapshot_ref['id']) self._notify_about_snapshot_usage(context, snapshot_ref, "create.end") return snapshot_id @locked_snapshot_operation def delete_snapshot(self, context, snapshot_id): """Deletes and unexports snapshot.""" caller_context = context context = context.elevated() snapshot_ref = self.db.snapshot_get(context, snapshot_id) project_id = snapshot_ref['project_id'] LOG.info(_("snapshot %s: deleting"), snapshot_ref['id']) self._notify_about_snapshot_usage( context, snapshot_ref, "delete.start") try: LOG.debug(_("snapshot %s: deleting"), snapshot_ref['id']) # Pass context so that drivers that want to use it, can, # but it is not a requirement for all drivers. snapshot_ref['context'] = caller_context self._delete_snapshot_cascaded(context, snapshot_id) except exception.SnapshotIsBusy: LOG.error(_("Cannot delete snapshot %s: snapshot is busy"), snapshot_ref['id']) self.db.snapshot_update(context, snapshot_ref['id'], {'status': 'available'}) return True except Exception: with excutils.save_and_reraise_exception(): self.db.snapshot_update(context, snapshot_ref['id'], {'status': 'error_deleting'}) # Get reservations try: if CONF.no_snapshot_gb_quota: reserve_opts = {'snapshots': -1} else: reserve_opts = { 'snapshots': -1, 'gigabytes': -snapshot_ref['volume_size'], } volume_ref = self.db.volume_get(context, snapshot_ref['volume_id']) QUOTAS.add_volume_type_opts(context, reserve_opts, volume_ref.get('volume_type_id')) reservations = QUOTAS.reserve(context, project_id=project_id, **reserve_opts) except Exception: reservations = None LOG.exception(_("Failed to update usages deleting snapshot")) self.db.volume_glance_metadata_delete_by_snapshot(context, snapshot_id) self.db.snapshot_destroy(context, snapshot_id) LOG.info(_("snapshot %s: deleted successfully"), snapshot_ref['id']) self._notify_about_snapshot_usage(context, snapshot_ref, "delete.end") # Commit the reservations if reservations: QUOTAS.commit(context, reservations, project_id=project_id) return True def _delete_snapshot_cascaded(self, context, snapshot_id): try: snapshot_ref = self.db.snapshot_get(context, snapshot_id) cascaded_snapshot_id = snapshot_ref['mapping_uuid'] LOG.info(_("Cascade info: delete casecade snapshot:%s"), cascaded_snapshot_id) cinderClient = self._get_cinder_cascaded_user_client(context) cinderClient.volume_snapshots.delete(cascaded_snapshot_id) LOG.info(_("delete casecade snapshot %s successfully."), cascaded_snapshot_id) except Exception: with excutils.save_and_reraise_exception(): LOG.error(_("failed to delete cascade snapshot %s"), cascaded_snapshot_id) def attach_volume(self, context, volume_id, instance_uuid, host_name, mountpoint, mode): """Updates db to show volume is attached""" @utils.synchronized(volume_id, external=True) def do_attach(): # check the volume status before attaching volume = self.db.volume_get(context, volume_id) volume_metadata = self.db.volume_admin_metadata_get( context.elevated(), volume_id) if volume['status'] == 'attaching': if (volume['instance_uuid'] and volume['instance_uuid'] != instance_uuid): msg = _("being attached by another instance") raise exception.InvalidVolume(reason=msg) if (volume['attached_host'] and volume['attached_host'] != host_name): msg = _("being attached by another host") raise exception.InvalidVolume(reason=msg) if (volume_metadata.get('attached_mode') and volume_metadata.get('attached_mode') != mode): msg = _("being attached by different mode") raise exception.InvalidVolume(reason=msg) elif volume['status'] != "available": msg = _("status must be available") raise exception.InvalidVolume(reason=msg) # TODO(jdg): attach_time column is currently varchar # we should update this to a date-time object # also consider adding detach_time? self.db.volume_update(context, volume_id, {"instance_uuid": instance_uuid, "mountpoint": mountpoint, "attached_host": host_name }) self.db.volume_admin_metadata_update(context.elevated(), volume_id, {"attached_mode": mode}, False) return do_attach() @locked_volume_operation def detach_volume(self, context, volume_id): """Updates db to show volume is detached""" # TODO(vish): refactor this into a more general "unreserve" # TODO(sleepsonthefloor): Is this 'elevated' appropriate? # self.db.volume_detached(context.elevated(), volume_id) self.db.volume_admin_metadata_delete(context.elevated(), volume_id, 'attached_mode') def copy_volume_to_image(self, context, volume_id, image_meta): """Uploads the specified volume to Glance. image_meta is a dictionary containing the following keys: 'id', 'container_format', 'disk_format' """ LOG.info(_("cascade info, copy volume to image, image_meta is:%s"), image_meta) force = image_meta.get('force', False) image_name = image_meta.get("name") container_format = image_meta.get("container_format") disk_format = image_meta.get("disk_format") vol_ref = self.db.volume_get(context, volume_id) casecaded_volume_id = vol_ref['mapping_uuid'] cinderClient = self._get_cinder_cascaded_user_client(context) resp = cinderClient.volumes.upload_to_image( volume=casecaded_volume_id, force=force, image_name=image_name, container_format=container_format, disk_format=disk_format) if cfg.CONF.glance_cascading_flag: cascaded_image_id = resp[1]['os-volume_upload_image']['image_id'] LOG.debug(_('Cascade info:upload volume to image,get cascaded ' 'image id is %s'), cascaded_image_id) url = '%s/v2/images/%s' % (cfg.CONF.cascaded_glance_url, cascaded_image_id) locations = [{ 'url': url, 'metadata': {'image_id': str(cascaded_image_id), 'image_from': 'volume' } }] image_service, image_id = \ glance.get_remote_image_service(context, image_meta['id']) LOG.debug(_("Cascade info: image service:%s"), image_service) glanceClient = glance.GlanceClientWrapper( context, netloc=cfg.CONF.cascading_glance_url, use_ssl=False, version="2") glanceClient.call(context, 'update', image_id, remove_props=None, locations=locations) LOG.debug(_('Cascade info:upload volume to image,finish update' 'image %s locations %s.'), (image_id, locations)) def accept_transfer(self, context, volume_id, new_user, new_project): # NOTE(jdg): need elevated context as we haven't "given" the vol # yet return def _migrate_volume_generic(self, ctxt, volume, host): rpcapi = volume_rpcapi.VolumeAPI() # Create new volume on remote host new_vol_values = {} for k, v in volume.iteritems(): new_vol_values[k] = v del new_vol_values['id'] del new_vol_values['_name_id'] # We don't copy volume_type because the db sets that according to # volume_type_id, which we do copy del new_vol_values['volume_type'] new_vol_values['host'] = host['host'] new_vol_values['status'] = 'creating' new_vol_values['migration_status'] = 'target:%s' % volume['id'] new_vol_values['attach_status'] = 'detached' new_volume = self.db.volume_create(ctxt, new_vol_values) rpcapi.create_volume(ctxt, new_volume, host['host'], None, None, allow_reschedule=False) # Wait for new_volume to become ready starttime = time.time() deadline = starttime + CONF.migration_create_volume_timeout_secs new_volume = self.db.volume_get(ctxt, new_volume['id']) tries = 0 while new_volume['status'] != 'available': tries = tries + 1 now = time.time() if new_volume['status'] == 'error': msg = _("failed to create new_volume on destination host") raise exception.VolumeMigrationFailed(reason=msg) elif now > deadline: msg = _("timeout creating new_volume on destination host") raise exception.VolumeMigrationFailed(reason=msg) else: time.sleep(tries ** 2) new_volume = self.db.volume_get(ctxt, new_volume['id']) # Copy the source volume to the destination volume try: if volume['status'] == 'available': # The above call is synchronous so we complete the migration self.migrate_volume_completion(ctxt, volume['id'], new_volume['id'], error=False) else: nova_api = compute.API() # This is an async call to Nova, which will call the completion # when it's done nova_api.update_server_volume(ctxt, volume['instance_uuid'], volume['id'], new_volume['id']) except Exception: with excutils.save_and_reraise_exception(): msg = _("Failed to copy volume %(vol1)s to %(vol2)s") LOG.error(msg % {'vol1': volume['id'], 'vol2': new_volume['id']}) volume = self.db.volume_get(ctxt, volume['id']) # If we're in the completing phase don't delete the target # because we may have already deleted the source! if volume['migration_status'] == 'migrating': rpcapi.delete_volume(ctxt, new_volume) new_volume['migration_status'] = None def migrate_volume_completion(self, ctxt, volume_id, new_volume_id, error=False): volume = self.db.volume_get(ctxt, volume_id) new_volume = self.db.volume_get(ctxt, new_volume_id) rpcapi = volume_rpcapi.VolumeAPI() if error: new_volume['migration_status'] = None rpcapi.delete_volume(ctxt, new_volume) self.db.volume_update(ctxt, volume_id, {'migration_status': None}) return volume_id self.db.volume_update(ctxt, volume_id, {'migration_status': 'completing'}) # Delete the source volume (if it fails, don't fail the migration) try: self.delete_volume(ctxt, volume_id) except Exception as ex: msg = _("Failed to delete migration source vol %(vol)s: %(err)s") LOG.error(msg % {'vol': volume_id, 'err': ex}) self.db.finish_volume_migration(ctxt, volume_id, new_volume_id) self.db.volume_destroy(ctxt, new_volume_id) self.db.volume_update(ctxt, volume_id, {'migration_status': None}) return volume['id'] def migrate_volume(self, ctxt, volume_id, host, force_host_copy=False): """Migrate the volume to the specified host (called on source host).""" return @periodic_task.periodic_task def _report_driver_status(self, context): LOG.info(_("Updating fake volume status")) fake_location_info = 'LVMVolumeDriver:Huawei:cinder-volumes:default:0' volume_stats = {'QoS_support': False, 'location_info': fake_location_info, 'volume_backend_name': 'LVM_iSCSI', 'free_capacity_gb': 1024, 'driver_version': '2.0.0', 'total_capacity_gb': 1024, 'reserved_percentage': 0, 'vendor_name': 'Open Source', 'storage_protocol': 'iSCSI' } self.update_service_capabilities(volume_stats) def publish_service_capabilities(self, context): """Collect driver status and then publish.""" self._report_driver_status(context) self._publish_service_capabilities(context) def _reset_stats(self): LOG.info(_("Clear capabilities")) self._last_volume_stats = [] def notification(self, context, event): LOG.info(_("Notification {%s} received"), event) self._reset_stats() def _notify_about_volume_usage(self, context, volume, event_suffix, extra_usage_info=None): volume_utils.notify_about_volume_usage( context, volume, event_suffix, extra_usage_info=extra_usage_info, host=self.host) def _notify_about_snapshot_usage(self, context, snapshot, event_suffix, extra_usage_info=None): volume_utils.notify_about_snapshot_usage( context, snapshot, event_suffix, extra_usage_info=extra_usage_info, host=self.host) def extend_volume(self, context, volume_id, new_size, reservations): volume = self.db.volume_get(context, volume_id) self._notify_about_volume_usage(context, volume, "resize.start") try: LOG.info(_("volume %s: extending"), volume['id']) cinderClient = self._get_cinder_cascaded_user_client(context) vol_ref = self.db.volume_get(context, volume_id) cascaded_volume_id = vol_ref['mapping_uuid'] LOG.info(_("Cascade info: extend volume cascade volume id is:%s"), cascaded_volume_id) cinderClient.volumes.extend(cascaded_volume_id, new_size) LOG.info(_("Cascade info: volume %s: extended successfully"), volume['id']) except Exception: LOG.exception(_("volume %s: Error trying to extend volume"), volume_id) try: self.db.volume_update(context, volume['id'], {'status': 'error_extending'}) finally: QUOTAS.rollback(context, reservations) return QUOTAS.commit(context, reservations) self.db.volume_update(context, volume['id'], {'size': int(new_size), 'status': 'extending'}) self._notify_about_volume_usage( context, volume, "resize.end", extra_usage_info={'size': int(new_size)})
class VolumeManager(manager.SchedulerDependentManager): """Manages attachable block storage devices.""" RPC_API_VERSION = '1.11' def __init__(self, volume_driver=None, service_name=None, *args, **kwargs): """Load the driver from the one specified in args, or from flags.""" # update_service_capabilities needs service_name to be volume super(VolumeManager, self).__init__(service_name='volume', *args, **kwargs) self.configuration = Configuration(volume_manager_opts, config_group=service_name) self._tp = GreenPool() if not volume_driver: # Get from configuration, which will get the default # if its not using the multi backend volume_driver = self.configuration.volume_driver if volume_driver in MAPPING: LOG.warn( _("Driver path %s is deprecated, update your " "configuration to the new path."), volume_driver) volume_driver = MAPPING[volume_driver] if volume_driver == 'cinder.volume.drivers.lvm.ThinLVMVolumeDriver': # Deprecated in Havana # Not handled in MAPPING because it requires setting a conf option LOG.warn( _("ThinLVMVolumeDriver is deprecated, please configure " "LVMISCSIDriver and lvm_type=thin. Continuing with " "those settings.")) volume_driver = 'cinder.volume.drivers.lvm.LVMISCSIDriver' self.configuration.lvm_type = 'thin' self.driver = importutils.import_object( volume_driver, configuration=self.configuration, db=self.db) def _add_to_threadpool(self, func, *args, **kwargs): self._tp.spawn_n(func, *args, **kwargs) def init_host(self): """Do any initialization that needs to be run if this is a standalone service. """ ctxt = context.get_admin_context() LOG.info( _("Starting volume driver %(driver_name)s (%(version)s)") % { 'driver_name': self.driver.__class__.__name__, 'version': self.driver.get_version() }) try: self.driver.do_setup(ctxt) self.driver.check_for_setup_error() except Exception as ex: LOG.error( _("Error encountered during " "initialization of driver: %(name)s") % {'name': self.driver.__class__.__name__}) LOG.exception(ex) # we don't want to continue since we failed # to initialize the driver correctly. return # at this point the driver is considered initailized. # next re-initialize exports and clean up volumes that # should be deleted. self.driver.set_initialized() volumes = self.db.volume_get_all_by_host(ctxt, self.host) LOG.debug(_("Re-exporting %s volumes"), len(volumes)) for volume in volumes: if volume['status'] in ['available', 'in-use']: self.driver.ensure_export(ctxt, volume) elif volume['status'] == 'downloading': LOG.info(_("volume %s stuck in a downloading state"), volume['id']) self.driver.clear_download(ctxt, volume) self.db.volume_update(ctxt, volume['id'], {'status': 'error'}) else: LOG.info(_("volume %s: skipping export"), volume['id']) LOG.debug(_('Resuming any in progress delete operations')) for volume in volumes: if volume['status'] == 'deleting': LOG.info(_('Resuming delete on volume: %s') % volume['id']) if CONF.volume_service_inithost_offload: # Offload all the pending volume delete operations to the # threadpool to prevent the main volume service thread # from being blocked. self._add_to_threadpool( self.delete_volume(ctxt, volume['id'])) else: # By default, delete volumes sequentially self.delete_volume(ctxt, volume['id']) # collect and publish service capabilities self.publish_service_capabilities(ctxt) @utils.require_driver_initialized def create_volume(self, context, volume_id, request_spec=None, filter_properties=None, allow_reschedule=True, snapshot_id=None, image_id=None, source_volid=None): """Creates and exports the volume.""" flow = create_volume.get_manager_flow( self.db, self.driver, self.scheduler_rpcapi, self.host, volume_id, request_spec=request_spec, filter_properties=filter_properties, allow_reschedule=allow_reschedule, snapshot_id=snapshot_id, image_id=image_id, source_volid=source_volid, reschedule_context=context.deepcopy()) assert flow, _('Manager volume flow not retrieved') flow.run(context.elevated()) if flow.state != states.SUCCESS: raise exception.CinderException( _("Failed to successfully complete" " manager volume workflow")) self._reset_stats() return volume_id @utils.require_driver_initialized def delete_volume(self, context, volume_id): """Deletes and unexports volume.""" context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) if context.project_id != volume_ref['project_id']: project_id = volume_ref['project_id'] else: project_id = context.project_id LOG.info(_("volume %s: deleting"), volume_ref['id']) if volume_ref['attach_status'] == "attached": # Volume is still attached, need to detach first raise exception.VolumeAttached(volume_id=volume_id) if volume_ref['host'] != self.host: raise exception.InvalidVolume( reason=_("volume is not local to this node")) self._notify_about_volume_usage(context, volume_ref, "delete.start") self._reset_stats() try: LOG.debug(_("volume %s: removing export"), volume_ref['id']) self.driver.remove_export(context, volume_ref) LOG.debug(_("volume %s: deleting"), volume_ref['id']) self.driver.delete_volume(volume_ref) except exception.VolumeIsBusy: LOG.error(_("Cannot delete volume %s: volume is busy"), volume_ref['id']) self.driver.ensure_export(context, volume_ref) self.db.volume_update(context, volume_ref['id'], {'status': 'available'}) return True except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_ref['id'], {'status': 'error_deleting'}) # If deleting the source volume in a migration, we want to skip quotas # and other database updates. if volume_ref['migration_status']: return True # Get reservations try: reserve_opts = {'volumes': -1, 'gigabytes': -volume_ref['size']} QUOTAS.add_volume_type_opts(context, reserve_opts, volume_ref.get('volume_type_id')) reservations = QUOTAS.reserve(context, project_id=project_id, **reserve_opts) except Exception: reservations = None LOG.exception(_("Failed to update usages deleting volume")) # Delete glance metadata if it exists try: self.db.volume_glance_metadata_delete_by_volume(context, volume_id) LOG.debug(_("volume %s: glance metadata deleted"), volume_ref['id']) except exception.GlanceMetadataNotFound: LOG.debug(_("no glance metadata found for volume %s"), volume_ref['id']) self.db.volume_destroy(context, volume_id) LOG.info(_("volume %s: deleted successfully"), volume_ref['id']) self._notify_about_volume_usage(context, volume_ref, "delete.end") # Commit the reservations if reservations: QUOTAS.commit(context, reservations, project_id=project_id) self.publish_service_capabilities(context) return True @utils.require_driver_initialized def create_snapshot(self, context, volume_id, snapshot_id): """Creates and exports the snapshot.""" caller_context = context context = context.elevated() snapshot_ref = self.db.snapshot_get(context, snapshot_id) LOG.info(_("snapshot %s: creating"), snapshot_ref['id']) self._notify_about_snapshot_usage(context, snapshot_ref, "create.start") try: LOG.debug(_("snapshot %(snap_id)s: creating"), {'snap_id': snapshot_ref['id']}) # Pass context so that drivers that want to use it, can, # but it is not a requirement for all drivers. snapshot_ref['context'] = caller_context model_update = self.driver.create_snapshot(snapshot_ref) if model_update: self.db.snapshot_update(context, snapshot_ref['id'], model_update) except Exception: with excutils.save_and_reraise_exception(): self.db.snapshot_update(context, snapshot_ref['id'], {'status': 'error'}) self.db.snapshot_update(context, snapshot_ref['id'], { 'status': 'available', 'progress': '100%' }) vol_ref = self.db.volume_get(context, volume_id) if vol_ref.bootable: try: self.db.volume_glance_metadata_copy_to_snapshot( context, snapshot_ref['id'], volume_id) except exception.CinderException as ex: LOG.exception( _("Failed updating %(snapshot_id)s" " metadata using the provided volumes" " %(volume_id)s metadata") % { 'volume_id': volume_id, 'snapshot_id': snapshot_id }) raise exception.MetadataCopyFailure(reason=ex) LOG.info(_("snapshot %s: created successfully"), snapshot_ref['id']) self._notify_about_snapshot_usage(context, snapshot_ref, "create.end") return snapshot_id @utils.require_driver_initialized def delete_snapshot(self, context, snapshot_id): """Deletes and unexports snapshot.""" caller_context = context context = context.elevated() snapshot_ref = self.db.snapshot_get(context, snapshot_id) project_id = snapshot_ref['project_id'] LOG.info(_("snapshot %s: deleting"), snapshot_ref['id']) self._notify_about_snapshot_usage(context, snapshot_ref, "delete.start") try: LOG.debug(_("snapshot %s: deleting"), snapshot_ref['id']) # Pass context so that drivers that want to use it, can, # but it is not a requirement for all drivers. snapshot_ref['context'] = caller_context self.driver.delete_snapshot(snapshot_ref) except exception.SnapshotIsBusy: LOG.error(_("Cannot delete snapshot %s: snapshot is busy"), snapshot_ref['id']) self.db.snapshot_update(context, snapshot_ref['id'], {'status': 'available'}) return True except Exception: with excutils.save_and_reraise_exception(): self.db.snapshot_update(context, snapshot_ref['id'], {'status': 'error_deleting'}) # Get reservations try: if CONF.no_snapshot_gb_quota: reserve_opts = {'snapshots': -1} else: reserve_opts = { 'snapshots': -1, 'gigabytes': -snapshot_ref['volume_size'], } volume_ref = self.db.volume_get(context, snapshot_ref['volume_id']) QUOTAS.add_volume_type_opts(context, reserve_opts, volume_ref.get('volume_type_id')) reservations = QUOTAS.reserve(context, project_id=project_id, **reserve_opts) except Exception: reservations = None LOG.exception(_("Failed to update usages deleting snapshot")) self.db.volume_glance_metadata_delete_by_snapshot(context, snapshot_id) self.db.snapshot_destroy(context, snapshot_id) LOG.info(_("snapshot %s: deleted successfully"), snapshot_ref['id']) self._notify_about_snapshot_usage(context, snapshot_ref, "delete.end") # Commit the reservations if reservations: QUOTAS.commit(context, reservations, project_id=project_id) return True @utils.require_driver_initialized def attach_volume(self, context, volume_id, instance_uuid, host_name, mountpoint, mode): """Updates db to show volume is attached""" @utils.synchronized(volume_id, external=True) def do_attach(): # check the volume status before attaching volume = self.db.volume_get(context, volume_id) volume_metadata = self.db.volume_admin_metadata_get( context.elevated(), volume_id) if volume['status'] == 'attaching': if (volume['instance_uuid'] and volume['instance_uuid'] != instance_uuid): msg = _("being attached by another instance") raise exception.InvalidVolume(reason=msg) if (volume['attached_host'] and volume['attached_host'] != host_name): msg = _("being attached by another host") raise exception.InvalidVolume(reason=msg) if (volume_metadata.get('attached_mode') and volume_metadata.get('attached_mode') != mode): msg = _("being attached by different mode") raise exception.InvalidVolume(reason=msg) elif volume['status'] != "available": msg = _("status must be available") raise exception.InvalidVolume(reason=msg) # TODO(jdg): attach_time column is currently varchar # we should update this to a date-time object # also consider adding detach_time? self.db.volume_update( context, volume_id, { "instance_uuid": instance_uuid, "attached_host": host_name, "status": "attaching", "attach_time": timeutils.strtime() }) self.db.volume_admin_metadata_update(context.elevated(), volume_id, {"attached_mode": mode}, False) if instance_uuid and not uuidutils.is_uuid_like(instance_uuid): self.db.volume_update(context, volume_id, {'status': 'error_attaching'}) raise exception.InvalidUUID(uuid=instance_uuid) host_name_sanitized = utils.sanitize_hostname( host_name) if host_name else None volume = self.db.volume_get(context, volume_id) if volume_metadata.get('readonly') == 'True' and mode != 'ro': self.db.volume_update(context, volume_id, {'status': 'error_attaching'}) raise exception.InvalidVolumeAttachMode(mode=mode, volume_id=volume_id) try: self.driver.attach_volume(context, volume, instance_uuid, host_name_sanitized, mountpoint) except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {'status': 'error_attaching'}) self.db.volume_attached(context.elevated(), volume_id, instance_uuid, host_name_sanitized, mountpoint) return do_attach() @utils.require_driver_initialized def detach_volume(self, context, volume_id): """Updates db to show volume is detached""" # TODO(vish): refactor this into a more general "unreserve" # TODO(sleepsonthefloor): Is this 'elevated' appropriate? volume = self.db.volume_get(context, volume_id) try: self.driver.detach_volume(context, volume) except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {'status': 'error_detaching'}) self.db.volume_detached(context.elevated(), volume_id) self.db.volume_admin_metadata_delete(context.elevated(), volume_id, 'attached_mode') # Check for https://bugs.launchpad.net/cinder/+bug/1065702 volume = self.db.volume_get(context, volume_id) if (volume['provider_location'] and volume['name'] not in volume['provider_location']): self.driver.ensure_export(context, volume) @utils.require_driver_initialized def copy_volume_to_image(self, context, volume_id, image_meta): """Uploads the specified volume to Glance. image_meta is a dictionary containing the following keys: 'id', 'container_format', 'disk_format' """ payload = {'volume_id': volume_id, 'image_id': image_meta['id']} try: volume = self.db.volume_get(context, volume_id) self.driver.ensure_export(context.elevated(), volume) image_service, image_id = \ glance.get_remote_image_service(context, image_meta['id']) self.driver.copy_volume_to_image(context, volume, image_service, image_meta) LOG.debug( _("Uploaded volume %(volume_id)s to " "image (%(image_id)s) successfully"), { 'volume_id': volume_id, 'image_id': image_id }) except Exception as error: with excutils.save_and_reraise_exception(): payload['message'] = unicode(error) finally: if (volume['instance_uuid'] is None and volume['attached_host'] is None): self.db.volume_update(context, volume_id, {'status': 'available'}) else: self.db.volume_update(context, volume_id, {'status': 'in-use'}) @utils.require_driver_initialized def initialize_connection(self, context, volume_id, connector): """Prepare volume for connection from host represented by connector. This method calls the driver initialize_connection and returns it to the caller. The connector parameter is a dictionary with information about the host that will connect to the volume in the following format:: { 'ip': ip, 'initiator': initiator, } ip: the ip address of the connecting machine initiator: the iscsi initiator name of the connecting machine. This can be None if the connecting machine does not support iscsi connections. driver is responsible for doing any necessary security setup and returning a connection_info dictionary in the following format:: { 'driver_volume_type': driver_volume_type, 'data': data, } driver_volume_type: a string to identify the type of volume. This can be used by the calling code to determine the strategy for connecting to the volume. This could be 'iscsi', 'rbd', 'sheepdog', etc. data: this is the data that the calling code will use to connect to the volume. Keep in mind that this will be serialized to json in various places, so it should not contain any non-json data types. """ volume = self.db.volume_get(context, volume_id) self.driver.validate_connector(connector) conn_info = self.driver.initialize_connection(volume, connector) # Add qos_specs to connection info typeid = volume['volume_type_id'] specs = {} if typeid: res = volume_types.get_volume_type_qos_specs(typeid) specs = res['qos_specs'] # Don't pass qos_spec as empty dict qos_spec = dict(qos_spec=specs if specs else None) conn_info['data'].update(qos_spec) # Add access_mode to connection info volume_metadata = self.db.volume_admin_metadata_get( context.elevated(), volume_id) if conn_info['data'].get('access_mode') is None: access_mode = volume_metadata.get('attached_mode') if access_mode is None: # NOTE(zhiyan): client didn't call 'os-attach' before access_mode = ('ro' if volume_metadata.get('readonly') == 'True' else 'rw') conn_info['data']['access_mode'] = access_mode return conn_info @utils.require_driver_initialized def terminate_connection(self, context, volume_id, connector, force=False): """Cleanup connection from host represented by connector. The format of connector is the same as for initialize_connection. """ volume_ref = self.db.volume_get(context, volume_id) self.driver.terminate_connection(volume_ref, connector, force=force) @utils.require_driver_initialized def accept_transfer(self, context, volume_id, new_user, new_project): # NOTE(jdg): need elevated context as we haven't "given" the vol # yet volume_ref = self.db.volume_get(context.elevated(), volume_id) self.driver.accept_transfer(context, volume_ref, new_user, new_project) def _migrate_volume_generic(self, ctxt, volume, host): rpcapi = volume_rpcapi.VolumeAPI() # Create new volume on remote host new_vol_values = {} for k, v in volume.iteritems(): new_vol_values[k] = v del new_vol_values['id'] del new_vol_values['_name_id'] # We don't copy volume_type because the db sets that according to # volume_type_id, which we do copy del new_vol_values['volume_type'] new_vol_values['host'] = host['host'] new_vol_values['status'] = 'creating' new_vol_values['migration_status'] = 'target:%s' % volume['id'] new_vol_values['attach_status'] = 'detached' new_volume = self.db.volume_create(ctxt, new_vol_values) rpcapi.create_volume(ctxt, new_volume, host['host'], None, None, allow_reschedule=False) # Wait for new_volume to become ready starttime = time.time() deadline = starttime + CONF.migration_create_volume_timeout_secs new_volume = self.db.volume_get(ctxt, new_volume['id']) tries = 0 while new_volume['status'] != 'available': tries = tries + 1 now = time.time() if new_volume['status'] == 'error': msg = _("failed to create new_volume on destination host") raise exception.VolumeMigrationFailed(reason=msg) elif now > deadline: msg = _("timeout creating new_volume on destination host") raise exception.VolumeMigrationFailed(reason=msg) else: time.sleep(tries**2) new_volume = self.db.volume_get(ctxt, new_volume['id']) # Copy the source volume to the destination volume try: if volume['status'] == 'available': self.driver.copy_volume_data(ctxt, volume, new_volume, remote='dest') # The above call is synchronous so we complete the migration self.migrate_volume_completion(ctxt, volume['id'], new_volume['id'], error=False) else: nova_api = compute.API() # This is an async call to Nova, which will call the completion # when it's done nova_api.update_server_volume(ctxt, volume['instance_uuid'], volume['id'], new_volume['id']) except Exception: with excutils.save_and_reraise_exception(): msg = _("Failed to copy volume %(vol1)s to %(vol2)s") LOG.error(msg % { 'vol1': volume['id'], 'vol2': new_volume['id'] }) volume = self.db.volume_get(ctxt, volume['id']) # If we're in the completing phase don't delete the target # because we may have already deleted the source! if volume['migration_status'] == 'migrating': rpcapi.delete_volume(ctxt, new_volume) new_volume['migration_status'] = None def migrate_volume_completion(self, ctxt, volume_id, new_volume_id, error=False): volume = self.db.volume_get(ctxt, volume_id) new_volume = self.db.volume_get(ctxt, new_volume_id) rpcapi = volume_rpcapi.VolumeAPI() if error: new_volume['migration_status'] = None rpcapi.delete_volume(ctxt, new_volume) self.db.volume_update(ctxt, volume_id, {'migration_status': None}) return volume_id self.db.volume_update(ctxt, volume_id, {'migration_status': 'completing'}) # Delete the source volume (if it fails, don't fail the migration) try: self.delete_volume(ctxt, volume_id) except Exception as ex: msg = _("Failed to delete migration source vol %(vol)s: %(err)s") LOG.error(msg % {'vol': volume_id, 'err': ex}) self.db.finish_volume_migration(ctxt, volume_id, new_volume_id) self.db.volume_destroy(ctxt, new_volume_id) self.db.volume_update(ctxt, volume_id, {'migration_status': None}) return volume['id'] @utils.require_driver_initialized def migrate_volume(self, ctxt, volume_id, host, force_host_copy=False): """Migrate the volume to the specified host (called on source host).""" volume_ref = self.db.volume_get(ctxt, volume_id) model_update = None moved = False self.db.volume_update(ctxt, volume_ref['id'], {'migration_status': 'migrating'}) if not force_host_copy: try: LOG.debug(_("volume %s: calling driver migrate_volume"), volume_ref['id']) moved, model_update = self.driver.migrate_volume( ctxt, volume_ref, host) if moved: updates = {'host': host['host'], 'migration_status': None} if model_update: updates.update(model_update) volume_ref = self.db.volume_update(ctxt, volume_ref['id'], updates) except Exception: with excutils.save_and_reraise_exception(): updates = {'migration_status': None} model_update = self.driver.create_export(ctxt, volume_ref) if model_update: updates.update(model_update) self.db.volume_update(ctxt, volume_ref['id'], updates) if not moved: try: self._migrate_volume_generic(ctxt, volume_ref, host) except Exception: with excutils.save_and_reraise_exception(): updates = {'migration_status': None} model_update = self.driver.create_export(ctxt, volume_ref) if model_update: updates.update(model_update) self.db.volume_update(ctxt, volume_ref['id'], updates) @periodic_task.periodic_task def _report_driver_status(self, context): LOG.info(_("Updating volume status")) if not self.driver.initialized: LOG.warning(_('Unable to update stats, driver is ' 'uninitialized')) else: volume_stats = self.driver.get_volume_stats(refresh=True) if volume_stats: # This will grab info about the host and queue it # to be sent to the Schedulers. self.update_service_capabilities(volume_stats) def publish_service_capabilities(self, context): """Collect driver status and then publish.""" self._report_driver_status(context) self._publish_service_capabilities(context) def _reset_stats(self): LOG.info(_("Clear capabilities")) self._last_volume_stats = [] def notification(self, context, event): LOG.info(_("Notification {%s} received"), event) self._reset_stats() def _notify_about_volume_usage(self, context, volume, event_suffix, extra_usage_info=None): volume_utils.notify_about_volume_usage( context, volume, event_suffix, extra_usage_info=extra_usage_info, host=self.host) def _notify_about_snapshot_usage(self, context, snapshot, event_suffix, extra_usage_info=None): volume_utils.notify_about_snapshot_usage( context, snapshot, event_suffix, extra_usage_info=extra_usage_info, host=self.host) @utils.require_driver_initialized def extend_volume(self, context, volume_id, new_size): volume = self.db.volume_get(context, volume_id) size_increase = (int(new_size)) - volume['size'] try: reservations = QUOTAS.reserve(context, gigabytes=+size_increase) except exception.OverQuota as exc: self.db.volume_update(context, volume['id'], {'status': 'error_extending'}) overs = exc.kwargs['overs'] usages = exc.kwargs['usages'] quotas = exc.kwargs['quotas'] def _consumed(name): return (usages[name]['reserved'] + usages[name]['in_use']) if 'gigabytes' in overs: msg = _("Quota exceeded for %(s_pid)s, " "tried to extend volume by " "%(s_size)sG, (%(d_consumed)dG of %(d_quota)dG " "already consumed)") LOG.error( msg % { 's_pid': context.project_id, 's_size': size_increase, 'd_consumed': _consumed('gigabytes'), 'd_quota': quotas['gigabytes'] }) return self._notify_about_volume_usage(context, volume, "resize.start") try: LOG.info(_("volume %s: extending"), volume['id']) self.driver.extend_volume(volume, new_size) LOG.info(_("volume %s: extended successfully"), volume['id']) except Exception: LOG.exception(_("volume %s: Error trying to extend volume"), volume_id) try: self.db.volume_update(context, volume['id'], {'status': 'error_extending'}) finally: QUOTAS.rollback(context, reservations) return QUOTAS.commit(context, reservations) self.db.volume_update(context, volume['id'], { 'size': int(new_size), 'status': 'available' }) self._notify_about_volume_usage( context, volume, "resize.end", extra_usage_info={'size': int(new_size)})
def run_once(self, *args, **kwargs): """ Executes a single pass, looking for objects to expire. :param args: Extra args to fulfill the Daemon interface; this daemon has no additional args. :param kwargs: Extra keyword args to fulfill the Daemon interface; this daemon accepts processes and process keyword args. These will override the values from the config file if provided. """ # This if-clause will be removed when general task queue feature is # implemented. if not self.dequeue_from_legacy: self.logger.info('This node is not configured to dequeue tasks ' 'from the legacy queue. This node will ' 'not process any expiration tasks. At least ' 'one node in your cluster must be configured ' 'with dequeue_from_legacy == true.') return self.get_process_values(kwargs) pool = GreenPool(self.concurrency) self.report_first_time = self.report_last_time = time() self.report_objects = 0 try: self.logger.debug('Run begin') task_account_container_list_to_delete = list() for task_account, my_index, divisor in \ self.iter_task_accounts_to_expire(): container_count, obj_count = \ self.swift.get_account_info(task_account) # the task account is skipped if there are no task container if not container_count: continue self.logger.info( _('Pass beginning for task account %(account)s; ' '%(container_count)s possible containers; ' '%(obj_count)s possible objects') % { 'account': task_account, 'container_count': container_count, 'obj_count': obj_count }) task_account_container_list = \ [(task_account, task_container) for task_container in self.iter_task_containers_to_expire(task_account)] task_account_container_list_to_delete.extend( task_account_container_list) # delete_task_iter is a generator to yield a dict of # task_account, task_container, task_object, delete_timestamp, # target_path to handle delete actual object and pop the task # from the queue. delete_task_iter = \ self.round_robin_order(self.iter_task_to_expire( task_account_container_list, my_index, divisor)) for delete_task in delete_task_iter: pool.spawn_n(self.delete_object, **delete_task) pool.waitall() for task_account, task_container in \ task_account_container_list_to_delete: try: self.swift.delete_container( task_account, task_container, acceptable_statuses=(2, HTTP_NOT_FOUND, HTTP_CONFLICT)) except (Exception, Timeout) as err: self.logger.exception( _('Exception while deleting container %(account)s ' '%(container)s %(err)s') % { 'account': task_account, 'container': task_container, 'err': str(err) }) self.logger.debug('Run end') self.report(final=True) except (Exception, Timeout): self.logger.exception(_('Unhandled exception'))
class CinderBackupProxy(manager.SchedulerDependentManager): """Manages attachable block storage devices.""" RPC_API_VERSION = '1.18' target = messaging.Target(version=RPC_API_VERSION) VOLUME_NAME_MAX_LEN = 255 VOLUME_UUID_MAX_LEN = 36 BACKUP_NAME_MAX_LEN = 255 BACKUP_UUID_MAX_LEN = 36 def __init__(self, service_name=None, *args, **kwargs): """Load the specified in args, or flags.""" # update_service_capabilities needs service_name to be volume super(CinderBackupProxy, self).__init__(service_name='backup', *args, **kwargs) self.configuration = Configuration(volume_backup_opts, config_group=service_name) self._tp = GreenPool() self.volume_api = volume.API() self._last_info_volume_state_heal = 0 self._change_since_time = None self.volumes_mapping_cache = {'backups': {}} self.init_flag = False self.backup_cache = [] self.tenant_id = self._get_tenant_id() self.adminCinderClient = self._get_cascaded_cinder_client() def _init_volume_mapping_cache(self,context): try: backups = self.db.backup_get_all(context) for backup in backups: backup_id = backup['id'] status = backup['status'] try: cascaded_backup_id =self._get_cascaded_backup_id(backup_id) except Exception as ex: continue if cascaded_backup_id == '' or status == 'error': continue self.volumes_mapping_cache['backups'][backup_id] = cascaded_backup_id LOG.info(_("cascade info: init volume mapping cache is %s"), self.volumes_mapping_cache) except Exception as ex: LOG.error(_("Failed init volumes mapping cache")) LOG.exception(ex) def _gen_ccding_backup_name(self, backup_id): return "backup" + "@" + backup_id def _get_cinder_cascaded_admin_client(self): try: kwargs = {'username': cfg.CONF.cinder_username, 'password': cfg.CONF.admin_password, 'tenant_name': CONF.cinder_tenant_name, 'auth_url': cfg.CONF.keystone_auth_url, 'insecure': True } keystoneclient = kc.Client(**kwargs) cinderclient = cinder_client.Client( username=cfg.CONF.cinder_username, auth_url=cfg.CONF.keystone_auth_url, insecure=True) cinderclient.client.auth_token = keystoneclient.auth_ref.auth_token diction = {'project_id': cfg.CONF.cinder_tenant_id} cinderclient.client.management_url = \ cfg.CONF.cascaded_cinder_url % diction return cinderclient except keystone_exception.Unauthorized: with excutils.save_and_reraise_exception(): LOG.error(_('Token unauthorized failed for keystoneclient ' 'constructed when get cascaded admin client')) except cinder_exception.Unauthorized: with excutils.save_and_reraise_exception(): LOG.error(_('Token unauthorized failed for cascaded ' 'cinderClient constructed')) except Exception: with excutils.save_and_reraise_exception(): LOG.error(_('Failed to get cinder python client.')) def _add_to_threadpool(self, func, *args, **kwargs): self._tp.spawn_n(func, *args, **kwargs) @property def initialized(self): return self.init_flag def init_host(self): ctxt = context.get_admin_context() self._init_volume_mapping_cache(ctxt) LOG.info(_("Cleaning up incomplete backup operations.")) # TODO(smulcahy) implement full resume of backup and restore # operations on restart (rather than simply resetting) backups = self.db.backup_get_all_by_host(ctxt, self.host) for backup in backups: if backup['status'] == 'creating' or backup['status'] == 'restoring': backup_info = {'status':backup['status'], 'id':backup['id']} self.backup_cache.append(backup_info) # TODO: this won't work because under this context, you have # no project id '''if backup['status'] == 'deleting': LOG.info(_('Resuming delete on backup: %s.') % backup['id']) self.delete_backup(ctxt, backup['id'])''' self.init_flag = True def create_backup(self, context, backup_id): """Create volume backups using configured backup service.""" backup = self.db.backup_get(context, backup_id) volume_id = backup['volume_id'] display_description = backup['display_description'] container = backup['container'] display_name = self._gen_ccding_backup_name(backup_id) # code begin by luobin availability_zone = cfg.CONF.storage_availability_zone # Because volume could be available or in-use initial_vol_status = self.db.volume_get(context, volume_id)['status'] self.db.volume_update(context, volume_id, {'status': 'backing-up'}) '''if volume status is in-use, it must have been checked with force flag in cascading api layer''' force = False if initial_vol_status == 'in-use': force = True # code begin by luobin LOG.info(_('cascade info: Create backup started, backup: %(backup_id)s ' 'volume: %(volume_id)s.') % {'backup_id': backup_id, 'volume_id': volume_id}) volume = self.db.volume_get(context, volume_id) expected_status = 'backing-up' actual_status = volume['status'] if actual_status != expected_status: err = _('Create backup aborted, expected volume status ' '%(expected_status)s but got %(actual_status)s.') % { 'expected_status': expected_status, 'actual_status': actual_status, } self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': err}) raise exception.InvalidVolume(reason=err) expected_status = 'creating' actual_status = backup['status'] if actual_status != expected_status: err = _('Create backup aborted, expected backup status ' '%(expected_status)s but got %(actual_status)s.') % { 'expected_status': expected_status, 'actual_status': actual_status, } self.db.volume_update(context, volume_id, {'status': initial_vol_status}) self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': err}) raise exception.InvalidBackup(reason=err) cascaded_snapshot_id='' query_status = "error" try: cascaded_volume_id = self._query_cascaded_vol_id(context,volume_id) LOG.info(_("begin to create backup,cascaded volume : %s"), cascaded_volume_id) if container: try: cascaded_snapshot_id = self._get_cascaded_snapshot_id(context,container) except Exception as err: cascaded_snapshot_id = '' LOG.info(_("the container is not snapshot :%s"), container) if cascaded_snapshot_id: LOG.info(_("the container is snapshot :%s"), container) snapshot_ref = self.db.snapshot_get(context, container) update_volume_id = snapshot_ref['volume_id'] container = cascaded_snapshot_id self.db.backup_update(context, backup_id, {'volume_id': update_volume_id}) cinderClient = self ._get_cascaded_cinder_client(context) bodyResponse = cinderClient.backups.create( volume_id=cascaded_volume_id, container=container, name=display_name, description=display_description, force=force) LOG.info(_("cascade ino: create backup while response is:%s"), bodyResponse._info) self.volumes_mapping_cache['backups'][backup_id] = \ bodyResponse._info['id'] # code begin by luobin # use service metadata to record cascading to cascaded backup id # mapping, to support cross az backup restore metadata = "mapping_uuid:" + bodyResponse._info['id'] + ";" tmp_metadata = None while True: time.sleep(CONF.volume_sync_interval) queryResponse = \ cinderClient.backups.get(bodyResponse._info['id']) query_status = queryResponse._info['status'] if query_status != 'creating': tmp_metadata = queryResponse._info.get('service_metadata','') self.db.backup_update(context, backup['id'], {'status': query_status}) self.db.volume_update(context, volume_id, {'status': initial_vol_status}) break else: continue except Exception as err: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {'status': initial_vol_status}) self.db.backup_update(context, backup['id'], {'status': 'error', 'fail_reason': unicode(err)}) return if tmp_metadata: metadata = metadata + tmp_metadata self.db.backup_update(context, backup_id, {'status': query_status, 'size': volume['size'], 'availability_zone': availability_zone, 'service_metadata': metadata}) # code end by luobin LOG.info(_('Create backup finished. backup: %s.'), backup_id) def _get_cascaded_backup_id(self, backup_id): count = 0 display_name =self._gen_ccding_backup_name(backup_id) try: sopt={ "name":display_name } cascaded_backups = self.adminCinderClient.backups.list(search_opts=sopt) except cinder_exception.Unauthorized: count = count + 1 self.adminCinderClient = self._get_cascaded_cinder_client() if count < 2: LOG.info(_('To try again for get_cascaded_backup_id()')) self._get_cascaded_backup_id(backup_id) if cascaded_backups: cascaded_backup_id = getattr(cascaded_backups[-1], '_info')['id'] else: err = _('the backup %s is not exist ') %display_name raise exception.InvalidBackup(reason=err) return cascaded_backup_id def _get_cascaded_snapshot_id(self, context, snapshot_id): metadata = self.db.snapshot_metadata_get(context, snapshot_id) cascaded_snapshot_id = metadata['mapping_uuid'] if cascaded_snapshot_id: LOG.info(_("cascade ino: cascaded_snapshot_id is:%s"), cascaded_snapshot_id) return cascaded_snapshot_id # code begin by luobin def _clean_up_fake_resource(self, cinderClient, fake_backup_id, fake_source_volume_id): cinderClient.backups.delete(fake_backup_id) cinderClient.volumes.delete(fake_source_volume_id) # code end by luobin def restore_backup(self, context, backup_id, volume_id): """Restore volume backups from configured backup service.""" LOG.info(_('Restore backup started, backup: %(backup_id)s ' 'volume: %(volume_id)s.') % {'backup_id': backup_id, 'volume_id': volume_id}) backup = self.db.backup_get(context, backup_id) volume = self.db.volume_get(context, volume_id) availability_zone = cfg.CONF.storage_availability_zone expected_status = 'restoring-backup' actual_status = volume['status'] if actual_status != expected_status: err = (_('Restore backup aborted, expected volume status ' '%(expected_status)s but got %(actual_status)s.') % {'expected_status': expected_status, 'actual_status': actual_status}) self.db.backup_update(context, backup_id, {'status': 'available'}) raise exception.InvalidVolume(reason=err) expected_status = 'restoring' actual_status = backup['status'] if actual_status != expected_status: err = (_('Restore backup aborted: expected backup status ' '%(expected_status)s but got %(actual_status)s.') % {'expected_status': expected_status, 'actual_status': actual_status}) self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': err}) self.db.volume_update(context, volume_id, {'status': 'error'}) raise exception.InvalidBackup(reason=err) if volume['size'] > backup['size']: LOG.info(_('Volume: %(vol_id)s, size: %(vol_size)d is ' 'larger than backup: %(backup_id)s, ' 'size: %(backup_size)d, continuing with restore.'), {'vol_id': volume['id'], 'vol_size': volume['size'], 'backup_id': backup['id'], 'backup_size': backup['size']}) try: cinderClient = self._get_cascaded_cinder_client(context) cascaded_volume_id = self._query_cascaded_vol_id(context, volume_id) # code begin by luobin # the backup to be restored may be cross-az, so get cascaded backup id # not from cache (since cache is built from cinder client of its own # region), but retrieve it from service meta data LOG.info(_("backup az:(backup_az)%s, conf az:%(conf_az)s") % {'backup_az': backup['availability_zone'], 'conf_az': availability_zone}) fake_description = "" fake_source_volume_id = None fake_backup_id = None cascaded_backup_id = None # retrieve cascaded backup id md_set = backup['service_metadata'].split(';') if len(md_set) > 1 and 'mapping_uuid' in md_set[0]: mapping_set = md_set[0].split(':') cascaded_backup_id = mapping_set[1] if backup['availability_zone'] != availability_zone: cascading_volume_type = self.db.volume_type_get( context, volume['volume_type_id']) cascading_volume_type_name = cascading_volume_type['name'] names = cascading_volume_type_name.split('@') cascaded_volume_type_name = names[0] + '@' + availability_zone LOG.info(_("cascaded vol type:%(cascaded_volume_type_name)s") % {'cascaded_volume_type_name': cascaded_volume_type_name}) volumeResponse = cinderClient.volumes.create( volume['size'], name=volume['display_name'] + "-fake", description=volume['display_description'], user_id=context.user_id, project_id=context.project_id, availability_zone=availability_zone, volume_type=cascaded_volume_type_name, metadata={'cross_az': "yes"}) fake_source_volume_id = volumeResponse._info['id'] time.sleep(30) # save original backup id cascaded_source_backup_id = cascaded_backup_id # retrieve the original cascaded_source_volume_id cascading_source_volume_id = backup['volume_id'] cascaded_source_volume_id = self._query_cascaded_vol_id( context, cascading_source_volume_id) LOG.info(_("cascaded_source_backup_id:%(cascaded_source_backup_id)s," "cascaded_source_volume_id:%(cascaded_source_volume_id)s" % {'cascaded_source_backup_id': cascaded_source_backup_id, 'cascaded_source_volume_id': cascaded_source_volume_id})) # compose display description for cascaded volume driver mapping to # original source backup id and original source volume_id fake_description = "cross_az:" + cascaded_source_backup_id + ":" + \ cascaded_source_volume_id backup_bodyResponse = cinderClient.backups.create( volume_id=fake_source_volume_id, container=backup['container'], name=backup['display_name'] + "-fake", description=fake_description) # set cascaded_backup_id as the faked one, which will help call # into our volume driver's restore function fake_backup_id = backup_bodyResponse._info['id'] cascaded_backup_id = backup_bodyResponse._info['id'] LOG.info(_("update cacaded_backup_id to created one:%s"), cascaded_backup_id) LOG.info(_("restore, cascaded_backup_id:%(cascaded_backup_id)s, " "cascaded_volume_id:%(cascaded_volume_id)s, " "description:%(description)s") % {'cascaded_backup_id': cascaded_backup_id, 'cascaded_volume_id': cascaded_volume_id, 'description': fake_description}) bodyResponse = cinderClient.restores.restore( backup_id=cascaded_backup_id, volume_id=cascaded_volume_id) LOG.info(_("cascade info: restore backup while response is:%s"), bodyResponse._info) while True: time.sleep(CONF.volume_sync_interval) queryResponse = \ cinderClient.backups.get(cascaded_backup_id) query_status = queryResponse._info['status'] if query_status != 'restoring': self.db.volume_update(context, volume_id, {'status': 'available'}) self.db.backup_update(context, backup_id, {'status': query_status}) LOG.info(_("get backup:%(backup)s status:%(status)s" % {'backup': cascaded_backup_id, 'status': query_status})) if fake_backup_id and fake_source_volume_id: LOG.info(_("cleanup fake backup:%(backup)s," "fake source volume id:%(volume)s") % {'backup': fake_backup_id, 'volume': fake_source_volume_id}) cinderClient.backups.delete(fake_backup_id) cinderClient.volumes.delete(fake_source_volume_id) # TODO: note, this is a walkaround since target cced volume will be # TODO: changed with its logicalVolumeId to source ccing volume id # TODO: and thus may fail to flush status to correct ccing volume time.sleep(CONF.volume_sync_interval) # code end by luobin self.db.volume_update(context, volume_id, {'status': 'available'}) self.db.backup_update(context, backup_id, {'status': query_status}) break else: continue except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {'status': 'error_restoring'}) self.db.backup_update(context, backup_id, {'status': 'available'}) LOG.info(_('Restore backup finished, backup %(backup_id)s restored' ' to volume %(volume_id)s.') % {'backup_id': backup_id, 'volume_id': volume_id}) def _query_cascaded_vol_id(self,ctxt,volume_id=None): volume = self.db.volume_get(ctxt, volume_id) volume_metadata = dict((item['key'], item['value']) for item in volume['volume_metadata']) mapping_uuid = volume_metadata.get('mapping_uuid', None) return mapping_uuid def _delete_backup_cascaded(self, context, backup_id): try: cascaded_backup_id = \ self.volumes_mapping_cache['backups'].get(backup_id, '') LOG.info(_("cascade ino: delete cascaded backup :%s"), cascaded_backup_id) cinderClient = self._get_cascaded_cinder_client(context) cinderClient.backups.get(cascaded_backup_id) resp = cinderClient.backups.delete(cascaded_backup_id) self.volumes_mapping_cache['backups'].pop(backup_id, '') LOG.info(_("delete cascaded backup %s successfully. resp :%s"), cascaded_backup_id, resp) return except cinder_exception.NotFound: self.volumes_mapping_cache['backups'].pop(backup_id, '') LOG.info(_("delete cascaded backup %s successfully."), cascaded_backup_id) return except Exception: with excutils.save_and_reraise_exception(): self.db.backup_update(context, backup_id, {'status': 'error_deleting'}) LOG.error(_("failed to delete cascaded backup %s"), cascaded_backup_id) @locked_backup_operation def delete_backup(self, context, backup_id): """Delete volume backup from configured backup service.""" LOG.info(_('cascade info:delete backup started, backup: %s.'), backup_id) backup = self.db.backup_get(context, backup_id) expected_status = 'deleting' actual_status = backup['status'] if actual_status != expected_status: err = _('Delete_backup aborted, expected backup status ' '%(expected_status)s but got %(actual_status)s.') \ % {'expected_status': expected_status, 'actual_status': actual_status} self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': err}) raise exception.InvalidBackup(reason=err) try: self._delete_backup_cascaded(context,backup_id) except Exception as err: with excutils.save_and_reraise_exception(): self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': unicode(err)}) # Get reservations try: reserve_opts = { 'backups': -1, 'backup_gigabytes': -backup['size'], } reservations = QUOTAS.reserve(context, project_id=backup['project_id'], **reserve_opts) except Exception: reservations = None LOG.exception(_("Failed to update usages deleting backup")) context = context.elevated() self.db.backup_destroy(context, backup_id) # Commit the reservations if reservations: QUOTAS.commit(context, reservations, project_id=backup['project_id']) LOG.info(_('Delete backup finished, backup %s deleted.'), backup_id) def export_record(self, context, backup_id): """Export all volume backup metadata details to allow clean import. Export backup metadata so it could be re-imported into the database without any prerequisite in the backup database. :param context: running context :param backup_id: backup id to export :returns: backup_record - a description of how to import the backup :returns: contains 'backup_url' - how to import the backup, and :returns: 'backup_service' describing the needed driver. :raises: InvalidBackup """ LOG.info(_('Export record started, backup: %s.'), backup_id) backup = self.db.backup_get(context, backup_id) expected_status = 'available' actual_status = backup['status'] if actual_status != expected_status: err = (_('Export backup aborted, expected backup status ' '%(expected_status)s but got %(actual_status)s.') % {'expected_status': expected_status, 'actual_status': actual_status}) raise exception.InvalidBackup(reason=err) backup_record = {} # Call driver to create backup description string try: cinderClient = self._get_cascaded_cinder_client(context) cascaded_backup_id = \ self.volumes_mapping_cache['backups'].get(backup_id, '') LOG.info(_("cascade ino: export cascade backup :%s"), cascaded_backup_id) bodyResponse = cinderClient.backups.export_record(cascaded_backup_id) backup_record['backup_url'] = bodyResponse['backup_url'] backup_record['backup_service'] = bodyResponse['backup_service'] except Exception as err: msg = unicode(err) raise exception.InvalidBackup(reason=msg) LOG.info(_('Export record finished, backup %s exported.'), cascaded_backup_id) return backup_record def import_record(self, context, backup_id, backup_service, backup_url, backup_hosts): """Import all volume backup metadata details to the backup db. :param context: running context :param backup_id: The new backup id for the import :param backup_service: The needed backup driver for import :param backup_url: An identifier string to locate the backup :param backup_hosts: Potential hosts to execute the import :raises: InvalidBackup :raises: ServiceNotFound """ LOG.info(_('Import record started, backup_url: %s.'), backup_url) # Can we import this backup? try: cinderClient = self._get_cascaded_cinder_client(context) bodyResponse = cinderClient.backups.import_record(backup_service,backup_url) except Exception as err: msg = unicode(err) self.db.backup_update(context, backup_id, {'status': 'error', 'fail_reason': msg}) raise exception.InvalidBackup(reason=msg) backup_update = {} backup_update['status'] = 'available' backup_update['host'] = self.host self.db.backup_update(context, backup_id, backup_update) # Verify backup LOG.info(_('Import record id %s metadata from driver ' 'finished.') % backup_id) @periodic_task.periodic_task(spacing=CONF.volume_sync_interval, run_immediately=True) def _deal_backup_status(self,context): if not self.init_flag: LOG.debug(_('cinder backup proxy is not ready')) return for backup in self.backup_cache: try: cascaded_backup_id = \ self.volumes_mapping_cache['backups'].get(backup['id'], None) if not cascaded_backup_id: self.backup_cache.pop() continue cinderClient = self._get_cinder_cascaded_admin_client() queryResponse = cinderClient.backups.get(cascaded_backup_id) query_status = queryResponse._info['status'] if query_status != backup['status']: metadata = queryResponse._info.get('service_metadata','') self.db.backup_update(context, backup['id'], {'status': query_status}) self.db.volume_update(context, backup['volume_id'], {'status': 'available'}) self.backup_cache.pop() except Exception: pass def _get_tenant_id(self): tenant_id = None try: kwargs = {'username': CONF.cinder_username, 'password': CONF.admin_password, 'tenant_name': CONF.cinder_tenant_name, 'auth_url': CONF.keystone_auth_url, 'insecure': True } keystoneclient = kc.Client(**kwargs) tenant_id = keystoneclient.tenants.find(name=CONF.cinder_tenant_name).to_dict().get('id') LOG.debug("_get_tenant_id tenant_id: %s" %str(tenant_id)) except keystone_exception.Unauthorized: with excutils.save_and_reraise_exception(): LOG.error('_get_tenant_id Unauthorized') except Exception: with excutils.save_and_reraise_exception(): LOG.error('_get_tenant_id raise Exception') return tenant_id def _get_management_url(self, kc, **kwargs): return kc.service_catalog.url_for(**kwargs) def _get_cascaded_cinder_client(self, context=None): try: if context is None: cinderclient = cinder_client.Client( auth_url=CONF.keystone_auth_url, region_name=CONF.cascaded_region_name, tenant_id=self.tenant_id, api_key=CONF.admin_password, username=CONF.cinder_username, insecure=True, timeout=30, retries=3) else: ctx_dict = context.to_dict() kwargs = { 'auth_url': CONF.keystone_auth_url, 'tenant_name': CONF.cinder_tenant_name, 'username': CONF.cinder_username, 'password': CONF.admin_password, 'insecure': True } keystoneclient = kc.Client(**kwargs) management_url = self._get_management_url(keystoneclient, service_type='volumev2', attr='region', endpoint_type='publicURL', filter_value=CONF.cascaded_region_name) LOG.info("before replace: management_url:%s", management_url) url = management_url.rpartition("/")[0] management_url = url+ '/' + ctx_dict.get("project_id") LOG.info("after replace: management_url:%s", management_url) cinderclient = cinder_client.Client( username=ctx_dict.get('user_id'), auth_url=cfg.CONF.keystone_auth_url, insecure=True, timeout=30, retries=3) cinderclient.client.auth_token = ctx_dict.get('auth_token') cinderclient.client.management_url = management_url LOG.info(_("cascade info: os_region_name:%s"), CONF.cascaded_region_name) return cinderclient except keystone_exception.Unauthorized: with excutils.save_and_reraise_exception(): LOG.error(_('Token unauthorized failed for keystoneclient ' 'constructed when get cascaded admin client')) except cinder_exception.Unauthorized: with excutils.save_and_reraise_exception(): LOG.error(_('Token unauthorized failed for cascaded ' 'cinderClient constructed')) except Exception: with excutils.save_and_reraise_exception(): LOG.error(_('Failed to get cinder python client.'))