def _valid_request(self, pool, image, size): """ Validate the parameters of a create request :param pool: rados pool name :param image: rbd image name :param size: size of the rbd (unit suffixed e.g. 20G) :return: boolean, indicating whether the parameters may be used or not """ ui_root = self.get_ui_root() state = True discovered_pools = [ rados_pool.name for rados_pool in ui_root.ceph.local_ceph.pools.children ] existing_rbds = self.disk_info.keys() storage_key = "{}.{}".format(pool, image) if not size: self.logger.error("Size parameter is missing") state = False elif not valid_size(size): self.logger.error("Size is invalid") state = False elif pool not in discovered_pools: self.logger.error("pool name is invalid") state = False elif storage_key in existing_rbds: self.logger.error("image of that name already defined") state = False return state
def ansible_main(): # Define the fields needs to create/map rbd's the the host(s) # NB. features and state are reserved/unused fields = { "pool": {"required": False, "default": "rbd", "type": "str"}, "image": {"required": True, "type": "str"}, "size": {"required": True, "type": "str"}, "host": {"required": True, "type": "str"}, "features": {"required": False, "type": "str"}, "state": { "required": False, "default": "present", "choices": ['present', 'absent'], "type": "str" }, } # not supporting check mode currently module = AnsibleModule(argument_spec=fields, supports_check_mode=False) pool = module.params["pool"] image = module.params['image'] size = module.params['size'] allocating_host = module.params['host'] desired_state = module.params['state'] ################################################ # Validate the parameters passed from Ansible # ################################################ if not valid_size(size): logger.critical("image '{}' has an invalid size specification '{}' in the ansible configuration".format(image, size)) module.fail_json(msg="(main) Unable to use the size parameter '{}' for image '{}' from the playbook - " "must be a number suffixed by M, G or T".format(size, image)) # define a lun object and perform some initial parameter validation lun = LUN(logger, pool, image, size, allocating_host) if lun.error: module.fail_json(msg=lun.error_msg) logger.info("START - LUN configuration started for {}/{}".format(pool, image)) # attempt to create/allocate the LUN for LIO lun.manage(desired_state) if lun.error: module.fail_json(msg=lun.error_msg) if lun.num_changes == 0: logger.info("END - No changes needed") else: logger.info("END - {} configuration changes made".format(lun.num_changes)) module.exit_json(changed=(lun.num_changes > 0), meta={"msg": "Configuration updated"})
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 ui_command_create(self, pool=None, image=None, size=None, backstore=None, count=1): """ Create a RBD image and assign to the gateway(s). The create command supports two request formats; Long format : create pool=<name> image=<name> size=<size> Short format : create pool/image <size> e.g. create pool=rbd image=testimage size=100g create rbd.testimage 100g The syntax of each parameter is as follows; pool : Pool and image name may contain a-z, A-Z, 0-9, '_', or '-' image characters. size : integer, suffixed by the allocation unit - either m/M, g/G or t/T representing the MB/GB/TB [1] backstore : lio backstore count : integer (default is 1)[2]. If the request provides a count=<n> parameter the image name will be used as a prefix, and the count used as a suffix to create multiple images from the same request. e.g. create rbd.test 1g count=5 -> create 5 images called test1..test5 each of 1GB in size from the rbd pool Notes. 1) size does not support decimal representations 2) Using a count to create multiple images will lock the CLI until all images have been created """ # NB the text above is shown on a help create request in the CLI if pool and '/' in pool: # shorthand version of the command self.logger.debug("user provided pool/image format request") if image: if size: try: count = int(size) except ValueError: self.logger.error("Invalid count provided " "({} ?)".format(size)) return size = image pool, image = pool.split('/') else: # long format request if not pool or not image: self.logger.error("Invalid create: pool and image " "parameters are needed") return if size and not valid_size(size): self.logger.error("Invalid size requested. Must be an integer, " "suffixed by M, G or T. See help for more info") return if count: if not str(count).isdigit(): self.logger.error("invalid count format, must be an integer") return self.logger.debug("CMD: /disks/ create pool={} " "image={} size={} " "count={} ".format(pool, image, size, count)) self.create_disk(pool=pool, image=image, size=size, count=count, backstore=backstore)
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 ui_command_create(self, pool=None, image=None, size=None, count=1, max_data_area_mb=None): """ Create a LUN and assign to the gateway(s). The create command supports two request formats; Long format : create pool=<name> image=<name> size=<size> [max_data_area_mb=<buffer_size>] Short format : create pool.image <size> e.g. create pool=rbd image=testimage size=100g ring_buffer_size=16 create rbd.testimage 100g The syntax of each parameter is as follows; pool : Pool and image name may contain a-z, A-Z, 0-9 or '-' chars image size : integer, suffixed by the allocation unit - either m/M, g/G or t/T representing the MB/GB/TB [1] count : integer (default is 1)[2]. If the request provides a count=<n> parameter the image name will be used as a prefix, and the count used as a suffix to create multiple LUNs from the same request. e.g. create rbd.test 1g count=5 -> create 5 LUNs called test1..test5 each of 1GB in size from the rbd pool max_data_area_mb : integer, optional size of kernel data ring buffer (MiB). Notes. 1) size does not support decimal representations 2) Using a count to create multiple LUNs will lock the CLI until all LUNs have been created """ # NB the text above is shown on a help create request in the CLI if '.' in pool: # shorthand version of the command self.logger.debug("user provided pool.image format request") if image: if size: try: count = int(size) except ValueError: self.logger.error("Invalid count provided " "({} ?)".format(size)) return size = image else: self.logger.error("Shorthand command is create <pool>.<image>" " <size>") return pool, image = pool.split('.') else: # long format request if not pool or not image or not size: self.logger.error("Invalid create: pool, image and size " "parameters are needed") return if not valid_size(size): self.logger.error("Invalid size requested. Must be an integer, " "suffixed by M, G or T. See help for more info") return if count: if not str(count).isdigit(): self.logger.error("invalid count format, must be an integer") return if max_data_area_mb: if not str(max_data_area_mb).isdigit(): self.logger.error( "invalid max data area format, must be an integer in MiB") return self.logger.debug("CMD: /disks/ create pool={} " "image={} size={} count={} " "max_data_area_mb={}".format(pool, image, size, count, max_data_area_mb)) self.create_disk(pool=pool, image=image, size=size, count=count, max_data_area_mb=max_data_area_mb)
def ui_command_create(self, pool=None, image=None, size=None, count=1, **attributes): """ Create a LUN and assign to the gateway(s). The create command supports two request formats; Long format : create pool=<name> image=<name> size=<size> [attributes: max_data_area_mb=<buffer_size> osd_op_timeout=<seconds> ...] Short format : create pool.image <size> e.g. create pool=rbd image=testimage size=100g max_data_area_mb=16 create rbd.testimage 100g The syntax of each parameter is as follows; pool : Pool and image name may contain a-z, A-Z, 0-9, '_', or '-' image characters. size : integer, suffixed by the allocation unit - either m/M, g/G or t/T representing the MB/GB/TB [1] count : integer (default is 1)[2]. If the request provides a count=<n> parameter the image name will be used as a prefix, and the count used as a suffix to create multiple LUNs from the same request. e.g. create rbd.test 1g count=5 -> create 5 LUNs called test1..test5 each of 1GB in size from the rbd pool attributes : max_data_area_mb : integer, optional size of kernel data ring buffer (MiB). Default 8. qfull_timeout: integer, optional time in seconds to wait for ring space. Default 5. osd_op_timeout: integer, optional time in seconds to wait for a Ceph op. Default 30. hw_max_sectors: integer, optional in units of 512 byte sectors reported to initiators as max IO size. Default 1024. Notes. 1) size does not support decimal representations 2) Using a count to create multiple LUNs will lock the CLI until all LUNs have been created """ # NB the text above is shown on a help create request in the CLI if pool and '.' in pool: # shorthand version of the command self.logger.debug("user provided pool.image format request") if image: if size: try: count = int(size) except ValueError: self.logger.error("Invalid count provided " "({} ?)".format(size)) return size = image else: self.logger.error("Shorthand command is create <pool>.<image>" " <size>") return pool, image = pool.split('.') else: # long format request if not pool or not image or not size: self.logger.error("Invalid create: pool, image and size " "parameters are needed") return if not valid_size(size): self.logger.error("Invalid size requested. Must be an integer, " "suffixed by M, G or T. See help for more info") return if count: if not str(count).isdigit(): self.logger.error("invalid count format, must be an integer") return try: controls = settings.Settings.normalize_controls( attributes, LUN.SETTINGS) except ValueError as err: self.logger.error(err) return for k, v in controls.iteritems(): if not v: self.logger.error("Missing value for {}.".format(k)) return self.logger.debug("CMD: /disks/ create pool={} " "image={} size={} count={} " "controls={}".format(pool, image, size, count, attributes)) self.create_disk(pool=pool, image=image, size=size, count=count, controls=attributes)
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')
def ansible_main(): # Define the fields needs to create/map rbd's the the host(s) # NB. features and state are reserved/unused fields = { "pool": {"required": False, "default": "rbd", "type": "str"}, "image": {"required": True, "type": "str"}, "size": {"required": True, "type": "str"}, "host": {"required": True, "type": "str"}, "features": {"required": False, "type": "str"}, "state": { "required": False, "default": "present", "choices": ['present', 'absent'], "type": "str" }, } # not supporting check mode currently module = AnsibleModule(argument_spec=fields, supports_check_mode=False) pool = module.params["pool"] image = module.params['image'] size = module.params['size'] allocating_host = module.params['host'] desired_state = module.params['state'] ################################################ # Validate the parameters passed from Ansible # ################################################ if not valid_size(size): logger.critical("image '{}' has an invalid size specification '{}' " "in the ansible configuration".format(image, size)) module.fail_json(msg="(main) Unable to use the size parameter '{}' " "for image '{}' from the playbook - " "must be a number suffixed by M,G " "or T".format(size, image)) # define a lun object and perform some initial parameter validation lun = LUN(logger, pool, image, size, allocating_host) if lun.error: module.fail_json(msg=lun.error_msg) logger.info("START - LUN configuration started for {}/{}".format(pool, image)) # attempt to create/allocate the LUN for LIO lun.manage(desired_state) if lun.error: module.fail_json(msg=lun.error_msg) if lun.num_changes == 0: logger.info("END - No changes needed") else: logger.info("END - {} configuration changes " "made".format(lun.num_changes)) module.exit_json(changed=(lun.num_changes > 0), meta={"msg": "Configuration updated"})