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)
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']))
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()
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
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()
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()
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
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
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)))
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', {})
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'
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'
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'
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
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')