Exemple #1
0
    def create(self):
        """
        Create an rbd image compatible with exporting through LIO to multiple clients
        :return: status code and msg
        """

        size_bytes = convert_2_bytes(self.size)

        # build the required feature settings into an int
        feature_int = 0
        for feature in RBDDev.rbd_feature_list:
            feature_int += getattr(rbd, feature)

        with rados.Rados(conffile=settings.config.cephconf) as cluster:
            with cluster.open_ioctx(self.pool) as ioctx:
                rbd_inst = rbd.RBD()
                try:
                    rbd_inst.create(ioctx,
                                    self.image,
                                    size_bytes,
                                    features=feature_int,
                                    old_format=False)
                except (rbd.ImageExists, rbd.InvalidArgument) as err:
                    self.error = True
                    self.error_msg = "Failed to create rbd image {} in pool {} : {}".format(
                        self.image, self.pool, err)
Exemple #2
0
    def resize(self, size=None):
        """
        Perform the resize operation, and sync the disk size across each of the
        gateways
        :param size: (int) new size for the rbd image
        :return:
        """
        # resize is actually managed by the same lun and api endpoint as
        # create so this logic is very similar to a 'create' request

        # if not size:
        #     self.logger.error("Specify a size value (current size "
        #                       "is {})".format(self.size_h))
        #     return
        #
        size_rqst = size.upper()
        # if not valid_size(size_rqst):
        #     self.logger.error("Size parameter value is not valid syntax "
        #                       "(must be of the form 100G, or 1T)")
        #     return
        #
        # new_size = convert_2_bytes(size_rqst)
        # if self.size >= new_size:
        #     # current size is larger, so nothing to do
        #     self.logger.error("New size isn't larger than the current "
        #                       "image size, ignoring request")
        #     return

        # At this point the size request needs to be honoured
        self.logger.debug("Resizing {} to {}".format(self.image_id, size_rqst))

        local_gw = this_host()

        # Issue the api request for the resize
        disk_api = '{}://127.0.0.1:{}/api/all_disk/{}'.format(
            self.http_mode, settings.config.api_port, self.image_id)

        api_vars = {
            'pool': self.pool,
            'size': size_rqst,
            'owner': local_gw,
            'mode': 'resize'
        }

        self.logger.debug("Issuing resize request")
        api = APIRequest(disk_api, data=api_vars)
        api.put()

        if api.response.status_code == 200:
            # at this point the resize request was successful, so we need to
            # update the ceph pool meta data (%commit etc)
            self._update_pool()
            self.size_h = size_rqst
            self.size = convert_2_bytes(size_rqst)

            self.logger.info('ok')

        else:
            self.logger.error("Failed to resize : "
                              "{}".format(api.response.json()['message']))
Exemple #3
0
    def __init__(self, logger, pool, image, size, allocating_host, backstore):
        self.logger = logger
        self.image = image
        self.pool = pool
        self.pool_id = 0
        self.size_bytes = convert_2_bytes(size)
        self.config_key = '{}.{}'.format(self.pool, self.image)

        # the allocating host could be fqdn or shortname - but the config
        # only uses shortname so it needs to be converted to shortname format
        self.allocating_host = allocating_host.split('.')[0]
        self.backstore = backstore

        self.error = False
        self.error_msg = ''
        self.num_changes = 0

        try:
            super(LUN, self).__init__('disks', self.config_key, logger,
                                      LUN.SETTINGS[self.backstore])
        except CephiSCSIError as err:
            self.error = True
            self.error_msg = err

        self._validate_request()
Exemple #4
0
    def rbd_size(self):
        """
        Confirm that the existing rbd image size, matches the requirement passed in the ansible
        config file - if the required size is > than current, resize the rbd image to match
        :return: boolean value reflecting whether the rbd image was resized
        """

        with rados.Rados(conffile=settings.config.cephconf) as cluster:
            with cluster.open_ioctx(self.pool) as ioctx:
                with rbd.Image(ioctx, self.image) as rbd_image:

                    # get the current size in bytes
                    current_bytes = rbd_image.size()  # bytes
                    target_bytes = convert_2_bytes(self.size)

                    if target_bytes > current_bytes:

                        # resize method, doesn't document potential exceptions
                        # so using a generic catch all (Yuk!)
                        try:
                            rbd_image.resize(target_bytes)
                        except:
                            self.error = True
                            self.error_msg = "rbd image resize failed for {}".format(
                                self.image)
                        else:
                            self.changed = True
Exemple #5
0
    def __init__(self, logger, pool, image, size, allocating_host):
        self.logger = logger
        self.image = image
        self.pool = pool
        self.pool_id = 0
        self.size = size
        self.size_bytes = convert_2_bytes(size)
        self.config_key = '{}.{}'.format(self.pool, self.image)
        self.controls = {}

        # the allocating host could be fqdn or shortname - but the config
        # only uses shortname so it needs to be converted to shortname format
        self.allocating_host = allocating_host.split('.')[0]

        self.owner = ''  # gateway that owns the preferred path for this LUN
        self.error = False
        self.error_msg = ''
        self.num_changes = 0

        self.config = Config(logger)
        if self.config.error:
            self.error = self.config.error
            self.error_msg = self.config.error_msg
            return

        self._validate_request()
        if self.config_key in self.config.config['disks']:
            self.controls = self.config.config['disks'][self.config_key].get(
                'controls', {}).copy()
Exemple #6
0
    def __init__(self, logger, pool, image, size, allocating_host, backstore,
                 backstore_object_name):
        self.logger = logger
        self.image = image
        self.pool = pool
        self.pool_id = 0
        self.size_bytes = convert_2_bytes(size)
        self.config_key = '{}/{}'.format(self.pool, self.image)

        self.allocating_host = allocating_host
        self.backstore = backstore
        self.backstore_object_name = backstore_object_name

        self.error = False
        self.error_msg = ''
        self.num_changes = 0

        try:
            super(LUN, self).__init__('disks', self.config_key, logger,
                                      LUN.SETTINGS[self.backstore])
        except CephiSCSIError as err:
            self.error = True
            self.error_msg = err

        self._validate_request()
Exemple #7
0
 def __init__(self, image, size, pool='rbd'):
     self.image = image
     self.size_bytes = convert_2_bytes(size)
     self.pool = pool
     self.pool_id = get_pool_id(pool_name=self.pool)
     self.error = False
     self.error_msg = ''
     self.changed = False
Exemple #8
0
 def __init__(self, image, size, backstore, pool=None):
     self.image = image
     self.size_bytes = convert_2_bytes(size)
     self.backstore = backstore
     if pool is None:
         pool = settings.config.pool
     self.pool = pool
     self.pool_id = get_pool_id(pool_name=self.pool)
     self.error = False
     self.error_msg = ''
     self.changed = False
Exemple #9
0
    def resize(self, size):
        """
        Perform the resize operation, and sync the disk size across each of the
        gateways
        :param size: (int) new size for the rbd image
        :return:
        """
        # resize is actually managed by the same lun and api endpoint as
        # create so this logic is very similar to a 'create' request

        size_rqst = size.upper()
        if not valid_size(size_rqst):
            self.logger.error("Size is invalid")
            return

        # At this point the size request needs to be honoured
        self.logger.debug("Resizing {} to {}".format(self.image_id, size_rqst))

        local_gw = this_host()

        # Issue the api request for the resize
        disk_api = ('{}://localhost:{}/api/'
                    'disk/{}'.format(self.http_mode, settings.config.api_port,
                                     self.image_id))

        api_vars = {
            'pool': self.pool,
            'size': size_rqst,
            'owner': local_gw,
            'mode': 'resize'
        }

        self.logger.debug("Issuing resize request")
        api = APIRequest(disk_api, data=api_vars)
        api.put()

        if api.response.status_code == 200:
            # at this point the resize request was successful, so we need to
            # update the ceph pool meta data (%commit etc)
            self._update_pool()
            self.size_h = size_rqst
            self.size = convert_2_bytes(size_rqst)

            self.logger.info('ok')

        else:
            self.logger.error("Failed to resize : "
                              "{}".format(
                                  response_message(api.response, self.logger)))
Exemple #10
0
    def __init__(self, logger, pool, image, size, config):
        self.logger = logger
        self.image = image
        self.pool = pool
        self.pool_id = 0
        self.size = size
        self.size_bytes = convert_2_bytes(size)
        self.config_key = '{}.{}'.format(self.pool, self.image)
        self.controls = {}

        self.owner = ''  # gateway that owns the preferred path for this LUN
        self.error = False
        self.error_msg = ''
        self.num_changes = 0

        self.config = config

        self._validate_request()
        if self.config_key in self.config.config['disks']:
            self.controls = self.config.config['disks'][self.config_key].get(
                'controls', {})
Exemple #11
0
def valid_disk(**kwargs):
    """
    determine whether the given image info is valid for a disk operation

    :param image_id: (str) <pool>.<image> format
    :return: (str) either 'ok' or an error description
    """

    mode_vars = {"create": ['pool', 'image', 'size', 'count'],
                 "resize": ['pool', 'image', 'size'],
                 "delete": ['pool', 'image']}

    config = get_config()
    if not config:
        return "Unable to query the local API for the current config"

    if 'mode' in kwargs.keys():
        mode = kwargs['mode']
    else:
        mode = None

    if mode in mode_vars:
        if not all(x in kwargs for x in mode_vars[mode]):
            return ("{} request must contain the following "
                    "variables: ".format(mode,
                                         ','.join(mode_vars[mode])))
    else:
        return "disk operation mode '{}' is invalid".format(mode)

    disk_key = "{}.{}".format(kwargs['pool'], kwargs['image'])

    if mode in ['create', 'resize']:

        if not valid_size(kwargs['size']):
            return "Size is invalid"

        elif kwargs['pool'] not in rados_pools():
            return "pool name is invalid"

    if mode == 'create':

        if kwargs['count'].isdigit():
            if not 1 <= int(kwargs['count']) <= 10:
                return "invalid count specified, must be an integer (1-10)"
        else:
            return "invalid count specified, must be an integer (1-10)"

        if kwargs['count'] == '1':
            new_disks = {disk_key}
        else:
            limit = int(kwargs['count']) + 1
            new_disks = set(['{}{}'.format(disk_key, ctr)
                             for ctr in range(1, limit)])

        if any(new_disk in config['disks'] for new_disk in new_disks):
            return ("at least one rbd image(s) with that name/prefix is "
                    "already defined")

        gateways_defined = len([key for key in config['gateways']
                               if isinstance(config['gateways'][key],
                                             dict)])
        if gateways_defined < settings.config.minimum_gateways:
            return ("disks can not be added until at least {} gateways "
                    "are defined".format(settings.config.minimum_gateways))


    if mode in ["resize", "delete"]:
        # disk must exist in the config
        if disk_key not in config['disks']:
            return ("rbd {}/{} is not defined to the "
                    "configuration".format(kwargs['pool'],
                                           kwargs['image']))


    if mode == 'resize':

        size = kwargs['size'].upper()
        current_size = rbd_size(kwargs['pool'], kwargs['image'])
        if convert_2_bytes(size) <= current_size:
            return ("resize value must be larger than the "
                    "current size ({}/{})".format(human_size(current_size),
                                                  current_size))

    if mode == 'delete':

        # disk must *not* be allocated to a client in the config
        allocation_list = []
        for client_iqn in config['clients']:
            client_metadata = config['clients'][client_iqn]
            if disk_key in client_metadata['luns']:
                allocation_list.append(client_iqn)

        if allocation_list:
            return ("Unable to delete {}. Allocated "
                    "to: {}".format(disk_key,
                                    ','.join(allocation_list)))

    return 'ok'
Exemple #12
0
    def valid_disk(ceph_iscsi_config, logger, **kwargs):
        """
        determine whether the given image info is valid for a disk operation

        :param ceph_iscsi_config: Config object
        :param logger: logger object
        :param image_id: (str) <pool>.<image> format
        :return: (str) either 'ok' or an error description
        """

        # create can also pass optional controls dict
        mode_vars = {
            "create": ['pool', 'image', 'size', 'count'],
            "resize": ['pool', 'image', 'size'],
            "reconfigure": ['pool', 'image', 'controls'],
            "delete": ['pool', 'image']
        }

        if 'mode' in kwargs.keys():
            mode = kwargs['mode']
        else:
            mode = None

        backstore = kwargs['backstore']
        if backstore not in LUN.BACKSTORES:
            return "Invalid '{}' backstore - Supported backstores: " \
                   "{}".format(backstore, ','.join(LUN.BACKSTORES))

        if mode in mode_vars:
            if not all(x in kwargs for x in mode_vars[mode]):
                return ("{} request must contain the following "
                        "variables: ".format(mode, ','.join(mode_vars[mode])))
        else:
            return "disk operation mode '{}' is invalid".format(mode)

        config = ceph_iscsi_config.config

        disk_key = "{}.{}".format(kwargs['pool'], kwargs['image'])

        if mode in ['create', 'resize']:

            if kwargs['pool'] not in get_pools():
                return "pool name is invalid"

        if mode == 'create':
            if kwargs['size'] and not valid_size(kwargs['size']):
                return "Size is invalid"

            if len(config['disks']) >= 256:
                return "Disk limit of 256 reached."

            disk_regex = re.compile(r"^[a-zA-Z0-9\-_]+$")
            if not disk_regex.search(kwargs['pool']):
                return "Invalid pool name (use alphanumeric, '_', or '-' characters)"
            if not disk_regex.search(kwargs['image']):
                return "Invalid image name (use alphanumeric, '_', or '-' characters)"

            if kwargs['count'].isdigit():
                if not 1 <= int(kwargs['count']) <= 10:
                    return "invalid count specified, must be an integer (1-10)"
            else:
                return "invalid count specified, must be an integer (1-10)"

            if kwargs['count'] == '1':
                new_disks = {disk_key}
            else:
                limit = int(kwargs['count']) + 1
                new_disks = set(
                    ['{}{}'.format(disk_key, ctr) for ctr in range(1, limit)])

            if any(new_disk in config['disks'] for new_disk in new_disks):
                return ("at least one rbd image(s) with that name/prefix is "
                        "already defined")

        if mode in ["resize", "delete", "reconfigure"]:
            # disk must exist in the config
            if disk_key not in config['disks']:
                return ("rbd {}/{} is not defined to the "
                        "configuration".format(kwargs['pool'],
                                               kwargs['image']))

        if mode == 'resize':

            if not valid_size(kwargs['size']):
                return "Size is invalid"

            size = kwargs['size'].upper()
            current_size = get_rbd_size(kwargs['pool'], kwargs['image'])
            if convert_2_bytes(size) <= current_size:
                return ("resize value must be larger than the "
                        "current size ({}/{})".format(human_size(current_size),
                                                      current_size))

        if mode in ['create', 'reconfigure']:

            try:
                settings.Settings.normalize_controls(kwargs['controls'],
                                                     LUN.SETTINGS[backstore])
            except ValueError as err:
                return (err)

        if mode == 'delete':

            # disk must *not* be allocated to a client in the config
            mapped_list = []
            allocation_list = []
            for target_iqn, target in config['targets'].items():
                if disk_key in target['disks']:
                    mapped_list.append(target_iqn)
                for client_iqn in target['clients']:
                    client_metadata = target['clients'][client_iqn]
                    if disk_key in client_metadata['luns']:
                        allocation_list.append(client_iqn)

            if allocation_list:
                return ("Unable to delete {}. Allocated "
                        "to: {}".format(disk_key, ','.join(allocation_list)))

            if mapped_list:
                return ("Unable to delete {}. Mapped "
                        "to: {}".format(disk_key, ','.join(mapped_list)))

        return 'ok'
Exemple #13
0
    def valid_disk(ceph_iscsi_config, logger, **kwargs):
        """
        determine whether the given image info is valid for a disk operation

        :param ceph_iscsi_config: Config object
        :param logger: logger object
        :param image_id: (str) <pool>.<image> format
        :return: (str) either 'ok' or an error description
        """

        # create can also pass optional controls dict
        mode_vars = {
            "create": ['pool', 'image', 'size', 'count'],
            "resize": ['pool', 'image', 'size'],
            "reconfigure": ['pool', 'image', 'controls'],
            "delete": ['pool', 'image']
        }

        if 'mode' in kwargs.keys():
            mode = kwargs['mode']
        else:
            mode = None

        if mode in mode_vars:
            if not all(x in kwargs for x in mode_vars[mode]):
                return ("{} request must contain the following "
                        "variables: ".format(mode, ','.join(mode_vars[mode])))
        else:
            return "disk operation mode '{}' is invalid".format(mode)

        config = ceph_iscsi_config.config

        disk_key = "{}.{}".format(kwargs['pool'], kwargs['image'])

        if mode in ['create', 'resize']:

            if not valid_size(kwargs['size']):
                return "Size is invalid"

            elif kwargs['pool'] not in get_pools():
                return "pool name is invalid"

        if mode == 'create':
            if len(config['disks']) >= 256:
                return "Disk limit of 256 reached."

            disk_regex = re.compile(r"^[a-zA-Z0-9\-_]+$")
            if not disk_regex.search(kwargs['pool']):
                return "Invalid pool name (use alphanumeric, '_', or '-' characters)"
            if not disk_regex.search(kwargs['image']):
                return "Invalid image name (use alphanumeric, '_', or '-' characters)"

            if kwargs['count'].isdigit():
                if not 1 <= int(kwargs['count']) <= 10:
                    return "invalid count specified, must be an integer (1-10)"
            else:
                return "invalid count specified, must be an integer (1-10)"

            if kwargs['count'] == '1':
                new_disks = {disk_key}
            else:
                limit = int(kwargs['count']) + 1
                new_disks = set(
                    ['{}{}'.format(disk_key, ctr) for ctr in range(1, limit)])

            if any(new_disk in config['disks'] for new_disk in new_disks):
                return ("at least one rbd image(s) with that name/prefix is "
                        "already defined")

            gateways_defined = len([
                key for key in config['gateways']
                if isinstance(config['gateways'][key], dict)
            ])
            if gateways_defined < settings.config.minimum_gateways:
                return ("disks can not be added until at least {} gateways "
                        "are defined".format(settings.config.minimum_gateways))

        if mode in ["resize", "delete", "reconfigure"]:
            # disk must exist in the config
            if disk_key not in config['disks']:
                return ("rbd {}/{} is not defined to the "
                        "configuration".format(kwargs['pool'],
                                               kwargs['image']))

        if mode == 'resize':

            size = kwargs['size'].upper()
            current_size = get_rbd_size(kwargs['pool'], kwargs['image'])
            if convert_2_bytes(size) <= current_size:
                return ("resize value must be larger than the "
                        "current size ({}/{})".format(human_size(current_size),
                                                      current_size))

        if mode in ['create', 'reconfigure']:

            try:
                settings.Settings.normalize_controls(kwargs['controls'],
                                                     LUN.SETTINGS)
            except ValueError as err:
                return (err)

        if mode == 'delete':

            # disk must *not* be allocated to a client in the config
            allocation_list = []
            for client_iqn in config['clients']:
                client_metadata = config['clients'][client_iqn]
                if disk_key in client_metadata['luns']:
                    allocation_list.append(client_iqn)

            if allocation_list:
                return ("Unable to delete {}. Allocated "
                        "to: {}".format(disk_key, ','.join(allocation_list)))

            try:
                with rados.Rados(conffile=settings.config.cephconf) as cluster:
                    with cluster.open_ioctx(kwargs['pool']) as ioctx:
                        with rbd.Image(ioctx, kwargs['image']) as rbd_image:
                            if list(rbd_image.list_snaps()):
                                return ("Unable to delete {}. Snapshots must "
                                        "be removed first.".format(disk_key))
            except rbd.ImageNotFound:
                pass
            except Exception as e:
                return "Unable to query {}: {}".format(disk_key, e)

        return 'ok'
Exemple #14
0
    def dm_size_ok(self, rbd_object):
        """
        Check that the dm device matches the request. if the size request is lower than
        current size, just return since resizing down is not support and problematic
        for client filesystems anyway
        :return boolean indicating whether the size matches
        """

        target_bytes = convert_2_bytes(self.size)
        if rbd_object.size_bytes > target_bytes:
            return True

        tmr = 0
        size_ok = False
        rbd_size_ok = False
        dm_path_found = False

        # we have to wait for the rbd size to match, since the rbd could have been
        # resized on another gateway host when this is called from Ansible
        while tmr < settings.config.time_out:
            if rbd_object.size_bytes == target_bytes:
                rbd_size_ok = True
                break
            sleep(settings.config.loop_delay)
            tmr += settings.config.loop_delay

        # since the size matches underneath device mapper, now we ensure the size
        # matches with device mapper - if not issue a resize map request
        if rbd_size_ok:

            # find the dm-X device
            dm_devices = glob.glob('/sys/class/block/dm-*/')
            # convert the full dm_device path to just the name (last component of path
            dm_name = os.path.basename(self.dm_device)

            for dm_dev in dm_devices:
                if fread(os.path.join(dm_dev, 'dm/name')) == dm_name:
                    dm_path_found = True
                    break

            if dm_path_found:

                # size is in sectors, so read it and * 512 = bytes
                dm_size_bytes = int(fread(os.path.join(dm_dev, 'size'))) * 512
                if dm_size_bytes != target_bytes:

                    self.logger.info(
                        "Issuing a resize map for {}".format(dm_name))
                    response = shellcommand(
                        'multipathd resize map {}'.format(dm_name))

                    self.logger.debug("resize result : {}".format(response))
                    dm_size_bytes = int(fread(os.path.join(dm_dev,
                                                           'size'))) * 512

                    if response.lower().startswith(
                            'ok') and dm_size_bytes == target_bytes:
                        size_ok = True
                    else:
                        self.logger.critical(
                            "multipathd resize map for {} failed".format(
                                dm_name))
                else:
                    # size matches
                    size_ok = True
            else:
                self.logger.critical(
                    "Unable to locate a dm-X device for this rbd image - {}".
                    format(self.image))

        return size_ok
Exemple #15
0
    def ui_command_resize(self, size=None):
        """
        The resize command allows you to increase the size of an
        existing rbd image. Attempting to decrease the size of an
        rbd will be ignored.

        size: new size including unit suffix e.g. 300G

        """

        # resize is actually managed by the same lun and api endpoint as
        # create so this logic is very similar to a 'create' request

        if not size:
            self.logger.error(
                "Specify a size value (current size is {})".format(
                    self.size_h))
            return

        size_rqst = size.upper()
        if not valid_size(size_rqst):
            self.logger.error(
                "Size parameter value is not valid syntax (must be of the form 100G, or 1T)"
            )
            return

        new_size = convert_2_bytes(size_rqst)
        if self.size >= new_size:
            # current size is larger, so nothing to do
            self.logger.error(
                "New size isn't larger than the current image size, ignoring request"
            )
            return

        # At this point the size request needs to be honoured
        self.logger.debug("Resizing {} to {}".format(self.image_id, size_rqst))

        local_gw = this_host()
        other_gateways = get_other_gateways(self.parent.parent.target.children)

        # make call to local api server first!
        disk_api = '{}://127.0.0.1:{}/api/disk/{}'.format(
            self.http_mode, settings.config.api_port, self.image_id)

        api_vars = {
            'pool': self.pool,
            'size': size_rqst,
            'owner': local_gw,
            'mode': 'resize'
        }

        self.logger.debug("Processing local LIO instance")
        response = put(disk_api,
                       data=api_vars,
                       auth=(settings.config.api_user,
                             settings.config.api_password),
                       verify=settings.config.api_ssl_verify)

        if response.status_code == 200:
            # rbd resize request successful, so update the local information
            self.logger.debug("- LUN resize complete")
            self.get_meta_data()

            self.logger.debug("Processing other gateways")
            for gw in other_gateways:
                disk_api = '{}://{}:{}/api/disk/{}'.format(
                    self.http_mode, gw, settings.config.api_port,
                    self.image_id)

                response = put(disk_api,
                               data=api_vars,
                               auth=(settings.config.api_user,
                                     settings.config.api_password),
                               verify=settings.config.api_ssl_verify)

                if response.status_code == 200:
                    self.logger.debug(
                        "- LUN resize registered on {}".format(gw))
                else:
                    raise GatewayAPIError(response.text)

        else:
            raise GatewayAPIError(response.text)

        self.logger.info('ok')