Beispiel #1
0
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()
Beispiel #2
0
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()
Beispiel #3
0
    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'))
Beispiel #4
0
    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'))
Beispiel #5
0
    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'))
Beispiel #7
0
    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'))
Beispiel #8
0
    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)
Beispiel #10
0
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()
Beispiel #11
0
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()
Beispiel #12
0
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)
Beispiel #13
0
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)
Beispiel #14
0
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)})
Beispiel #15
0
    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'))
Beispiel #16
0
 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.'))
Beispiel #18
0
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())
Beispiel #19
0
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)
Beispiel #20
0
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)})
Beispiel #21
0
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)})
Beispiel #22
0
    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'))
Beispiel #23
0
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.'))