def ui_command_create(self, client_iqn): """ Clients may be created using the 'create' sub-command. The initial definition will be added to each gateway without any authentication set. Once a client is created the admin is automatically placed in the context of the new client definition for auth and disk configuration operations. e.g. > create <client_iqn> """ self.logger.debug("CMD: ../hosts/ create {}".format(client_iqn)) cli_seed = {"luns": {}, "auth": {}} # is the IQN usable? try: client_iqn, iqn_type = normalize_wwn(['iqn'], client_iqn) except RTSLibError: self.logger.error("IQN name '{}' is not valid for " "iSCSI".format(client_iqn)) return target_iqn = self.parent.name # Issue the API call to create the client client_api = ('{}://localhost:{}/api/' 'client/{}/{}'.format(self.http_mode, settings.config.api_port, target_iqn, client_iqn)) self.logger.debug("Client CREATE for {}".format(client_iqn)) api = APIRequest(client_api) api.put() if api.response.status_code == 200: Client(self, client_iqn, cli_seed) self.config = get_config() self.logger.debug("- Client '{}' added".format(client_iqn)) self.logger.info('ok') else: self.logger.error("Failed: {}".format(response_message(api.response, self.logger))) return # switch the current directory to the new client for auth or disk # definitions as part of the users workflow return self.ui_command_cd(client_iqn)
def ui_command_create(self, target_iqn): """ Create an iSCSI target. This target is defined across all gateway nodes, providing the client with a single 'image' for iscsi discovery. Only ONE iSCSI target is supported, at this time. """ self.logger.debug("CMD: /iscsi create {}".format(target_iqn)) defined_targets = [tgt.name for tgt in self.children] if len(defined_targets) > 0: self.logger.error("Only ONE iscsi target image is supported") return # We need LIO to be empty, so check there aren't any targets defined local_lio = root.RTSRoot() current_target_names = [tgt.wwn for tgt in local_lio.targets] if current_target_names: self.logger.error("Local LIO instance already has LIO configured " "with a target - unable to continue") return # OK - this request is valid, but is the IQN usable? if not valid_iqn(target_iqn): self.logger.error("IQN name '{}' is not valid for " "iSCSI".format(target_iqn)) return # 'safe' to continue with the definition self.logger.debug("Create an iscsi target definition in the UI") local_api = ('{}://localhost:{}/api/' 'target/{}'.format(self.http_mode, settings.config.api_port, target_iqn)) api = APIRequest(local_api) api.put() if api.response.status_code == 200: self.logger.info('ok') # create the target entry in the UI tree Target(target_iqn, self) else: self.logger.error("Failed to create the target on the local node") raise GatewayAPIError("iSCSI target creation failed - " "{}".format( response_message(api.response, self.logger)))
def ui_command_auth(self, action=None): """ Disable/enable ACL authentication or clear CHAP settings for all clients on the target. - disable_acl ... Disable initiator name based ACL authentication. - enable_acl .... Enable initiator name based ACL authentication. - nochap ........ Remove chap authentication for all clients across all gateways. Initiator name based authentication will then be used. e.g. auth disable_acl """ if not action: self.logger.error( "Missing auth argument. Use 'auth nochap|disable_acl|enable_acl'" ) return if action not in ['nochap', 'enable_acl', 'disable_acl']: self.logger.error( "Invalid auth argument. Use 'auth nochap|disable_acl|enable_acl'" ) return if action == 'nochap': for client in self.children: client.set_auth(action, None, None, None) else: target_iqn = self.parent.name api_vars = {'action': action} targetauth_api = ('{}://localhost:{}/api/' 'targetauth/{}'.format(self.http_mode, settings.config.api_port, target_iqn)) api = APIRequest(targetauth_api, data=api_vars) api.put() if api.response.status_code == 200: self.config = get_config() self.logger.info('ok') else: self.logger.error("Failed to {}: " "{}".format( action, response_message(api.response, self.logger))) return
def snapshot(self, action, name): self.logger.debug("CMD: /disks/{} snapshot action={} " "name={}".format(self.image_id, action, name)) valid_actions = ['create', 'delete', 'rollback'] if action not in valid_actions: self.logger.error("you can only create, delete, or rollback - " "{} is invalid ".format(action)) return if action == 'create': if name in self.snapshot_names: self.logger.error("Snapshot {} already exists".format(name)) return if not valid_snapshot_name(name): self.logger.error( "Snapshot {} contains invalid characters".format(name)) return else: if name not in self.snapshot_names: self.logger.error("Snapshot {} does not exist".format(name)) return if action == 'rollback': self.logger.warning("Please be patient, rollback might take time") self.logger.debug("Issuing snapshot {} request".format(action)) disk_api = ('{}://localhost:{}/api/' 'disksnap/{}/{}'.format(self.http_mode, settings.config.api_port, self.image_id, name)) if action == 'delete': api = APIRequest(disk_api) api.delete() else: api_vars = {'mode': action} api = APIRequest(disk_api, data=api_vars) api.put() if api.response.status_code == 200: if action == 'create' or action == 'delete': self._refresh_config() self.logger.info('ok') else: self.logger.error("Failed to {} snapshot: " "{}".format( action, response_message(api.response, self.logger)))
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 set_auth(self, chap=None): self.logger.debug("CMD: ../hosts/<client_iqn> auth *") if not chap: self.logger.error("To set or reset authentication, specify either " "chap=<user>/<password> or nochap") return if chap == 'nochap': chap = '' else: # string could have been supplied as chap=user/password or # simply user/password - either way all we see is user/password if '/' not in chap: self.logger.error( "CHAP format is invalid - must be a <username>/<password> " "format. Use 'help auth' to show the correct syntax and " "supported characters") return self.logger.debug("CHAP to be set to '{}' for '{}'".format( chap, self.client_iqn)) api_vars = {"chap": chap} clientauth_api = ('{}://localhost:{}/api/' 'clientauth/{}'.format(self.http_mode, settings.config.api_port, self.client_iqn)) api = APIRequest(clientauth_api, data=api_vars) api.put() if api.response.status_code == 200: self.logger.debug("- client credentials updated") if chap != '': self.auth['chap'] = chap else: self.auth['chap'] = "None" self.logger.info('ok') else: self.logger.error("Failed to update the client's auth" " :{}".format( response_message(api.response, self.logger))) return
def ui_command_auth(self, nochap=False, chap=None): """ Client authentication can be set to use CHAP by supplying the a string of the form <username>/<password> e.g. auth nochap | chap=username/password username ... The username is freeform, but would typically be the host's shortname or iqn password ... the password must be between 12-16 chars in length containing alphanumeric characters, plus the following special characters @,_,- Specifying 'nochap' will remove chap authentication for the client across all gateways. """ self.logger.debug("CMD: ../hosts/<client_iqn> auth *") if nochap: chap = '' self.logger.debug("Client '{}' AUTH update".format(self.client_iqn)) api_vars = {"chap": chap} clientauth_api = ('{}://127.0.0.1:{}/api/' 'clientauth/{}'.format(self.http_mode, settings.config.api_port, self.client_iqn)) api = APIRequest(clientauth_api, data=api_vars) api.put() if api.response.status_code == 200: self.logger.debug("- client credentials updated") self.auth['chap'] = chap self.logger.info('ok') else: self.logger.error("Failed to update the client's auth" " :{}".format(api.response.json()['message'])) return
def ui_command_create(self, group_name): """ Create a host group definition. Group names can be use up to 32 alphanumeric characters, including '_', '-' and '@'. Note that once a group is created it can not be renamed. """ self.logger.debug("CMD: ../host-groups/ create {}".format(group_name)) if group_name in self.groups: self.logger.error("Group {} already defined".format(group_name)) return grp_regex = re.compile(r"^[\w\@\-\_]{{1,{}}}$".format( HostGroups.group_name_length)) if not grp_regex.search(group_name): self.logger.error("Invalid group name - max of {} chars of " "alphanumeric and -,_,@ " "characters".format( HostGroups.group_name_length)) return target_iqn = self.parent.name # this is a new group group_api = ('{}://{}:{}/api/hostgroup/' '{}/{}'.format(self.http_mode, "localhost", settings.config.api_port, target_iqn, group_name)) api = APIRequest(group_api) api.put() if api.response.status_code != 200: self.logger.error("Failed : " "{}".format( response_message(api.response, self.logger))) return self.logger.debug('Adding group to the UI') HostGroup(self, group_name) self.logger.info('ok') # Switch to the new group return self.ui_command_cd(group_name)
def ui_command_discovery_auth(self, chap=None, chap_mutual=None): """ Discovery authentication can be set to use CHAP/CHAP_MUTUAL by supplying strings of the form <username>/<password> Specifying 'nochap' will remove discovery authentication. e.g. auth chap=username/password chap_mutual=username/password """ self.logger.warn("discovery chap={}, chap_mutual={}".format(chap, chap_mutual)) self.logger.debug("CMD: /iscsi discovery_auth") if not chap: self.logger.error("To set or reset discovery authentication, specify either " "chap=<user>/<password> [chap_mutual]=<user>/<password> or nochap") return if chap == 'nochap': chap = '' if not chap_mutual: chap_mutual = '' self.logger.debug("discovery auth to be set to chap='{}', chap_mutual='{}'".format( chap, chap_mutual)) api_vars = {"chap": chap, "chap_mutual": chap_mutual} discoveryauth_api = ('{}://localhost:{}/api/' 'discoveryauth'.format(self.http_mode, settings.config.api_port)) api = APIRequest(discoveryauth_api, data=api_vars) api.put() if api.response.status_code == 200: self._set_auth(chap, chap_mutual) self.logger.info('ok') else: self.logger.error("Error: {}".format(response_message(api.response, self.logger))) return
def ui_command_create(self, target_iqn): """ Create an iSCSI target. This target is defined across all gateway nodes, providing the client with a single 'image' for iscsi discovery. """ self.logger.debug("CMD: /iscsi create {}".format(target_iqn)) # is the IQN usable? try: target_iqn, iqn_type = normalize_wwn(['iqn'], target_iqn) except RTSLibError: self.logger.error("IQN name '{}' is not valid for " "iSCSI".format(target_iqn)) return # 'safe' to continue with the definition self.logger.debug("Create an iscsi target definition in the UI") local_api = ('{}://localhost:{}/api/' 'target/{}'.format(self.http_mode, settings.config.api_port, target_iqn)) api = APIRequest(local_api) api.put() if api.response.status_code == 200: self.logger.info('ok') # create the target entry in the UI tree target_exists = len([ target for target in self.children if target.name == target_iqn ]) > 0 if not target_exists: Target(target_iqn, self) else: self.logger.error("Failed to create the target on the local node") raise GatewayAPIError("iSCSI target creation failed - " "{}".format( response_message(api.response, self.logger)))
def ui_command_create(self, client_iqn): """ Clients may be created using the 'create' sub-command. The initial definition will be added to each gateway without any authentication set. Once a client is created the admin is automatically placed in the context of the new client definition for auth and disk configuration operations. e.g. > create <client_iqn> """ self.logger.debug("CMD: ../hosts/ create {}".format(client_iqn)) cli_seed = {"luns": {}, "auth": {}} # Issue the API call to create the client client_api = ('{}://127.0.0.1:{}/api/' 'client/{}'.format(self.http_mode, settings.config.api_port, client_iqn)) self.logger.debug("Client CREATE for {}".format(client_iqn)) api = APIRequest(client_api) api.put() if api.response.status_code == 200: Client(self, client_iqn, cli_seed) self.logger.debug("- Client '{}' added".format(client_iqn)) self.logger.info('ok') else: self.logger.error("Failed: {}".format( api.response.json()['message'])) return # switch the current directory to the new client for auth or disk # definitions as part of the users workflow return self.ui_command_cd(client_iqn)
def reconfigure(self, attribute, value): controls = {attribute: value} controls_json = json.dumps(controls) ui_root = self.get_ui_root() disk = ui_root.disks.disk_lookup[self.image_id] if not disk.owner: self.logger.error( "Cannot reconfigure until disk assigned to target") return local_gw = this_host() # Issue the api request for reconfigure disk_api = ('{}://localhost:{}/api/' 'disk/{}'.format(self.http_mode, settings.config.api_port, self.image_id)) api_vars = { 'pool': self.pool, 'owner': local_gw, 'controls': controls_json, 'mode': 'reconfigure' } self.logger.debug("Issuing reconfigure request: attribute={}, " "value={}".format(attribute, value)) api = APIRequest(disk_api, data=api_vars) api.put() if api.response.status_code == 200: self.logger.info('ok') self._refresh_config() else: self.logger.error("Failed to reconfigure : " "{}".format( response_message(api.response, self.logger)))
def ui_command_disk(self, action='add', disk=None, size=None): """ Disks can be added or removed from the client one at a time using the 'disk' sub-command. Note that if the disk does not currently exist in the configuration, the cli will attempt to create it for you. e.g. disk add <pool_name.image_name> <size> disk remove <pool_name.image_name> Adding a disk will result in the disk occupying the client's next available lun id. Once allocated removing a LUN will not change the LUN id associations for the client. Note that if the client is a member of a host group, disk management *must* be performed at the group level. Attempting to add/remove disks at the client level will fail. """ self.logger.debug("CMD: ../hosts/<client_iqn> disk action={}" " disk={}".format(action, disk)) valid_actions = ['add', 'remove'] if not disk: self.logger.critical("You must supply a disk name to add/remove " "for this client") return if action not in valid_actions: self.logger.error("you can only add and remove disks - {} is " "invalid ".format(action)) return lun_list = [(lun.rbd_name, lun.lun_id) for lun in self.children] current_luns = Client.get_srtd_names(lun_list) if action == 'add': if disk not in current_luns: ui_root = self.get_ui_root() valid_disk_names = [ defined_disk.image_id for defined_disk in ui_root.disks.children ] else: # disk provided is already mapped, so remind the user self.logger.error("Disk {} already mapped".format(disk)) return else: valid_disk_names = current_luns if disk not in valid_disk_names: # if this is an add operation, we can create the disk on-the-fly # for the admin if action == 'add': ui_root = self.get_ui_root() ui_disks = ui_root.disks if not size: self.logger.error("To auto-define the disk to the client" " you must provide a disk size") return # a disk given here would be of the form pool.image try: pool, image = disk.split('.') except ValueError: self.logger.error( "Invalid format. Use pool_name.disk_name") return rc = ui_disks.create_disk(pool=pool, image=image, size=size) if rc == 0: self.logger.debug("disk auto-define successful") else: self.logger.error("disk auto-define failed({}), try " "using the /disks create " "command".format(rc)) return else: self.logger.error("disk '{}' is not mapped to this " "client ".format(disk)) return # At this point we are either in add/remove mode, with a valid disk # to act upon self.logger.debug("Client '{}' update - {} disk " "{}".format(self.client_iqn, action, disk)) api_vars = {"disk": disk} clientlun_api = ('{}://localhost:{}/api/' 'clientlun/{}'.format(self.http_mode, settings.config.api_port, self.client_iqn)) api = APIRequest(clientlun_api, data=api_vars) if action == 'add': api.put() else: api.delete() if api.response.status_code == 200: self.logger.debug("disk mapping updated successfully") if action == 'add': # The addition of the lun will get a lun id assigned so # we need to query the api server to get the new configuration # to be able to set the local cli entry correctly get_api_vars = {"disk": disk} clientlun_api = clientlun_api.replace('/clientlun/', '/_clientlun/') self.logger.debug("Querying API to get mapped LUN information") api = APIRequest(clientlun_api, data=get_api_vars) api.get() if api.response.status_code == 200: try: lun_dict = api.response.json()['message'] except Exception: self.logger.error("Malformed REST API response") return # now update the UI lun_id = lun_dict[disk]['lun_id'] self.add_lun(disk, lun_id) else: self.logger.error("Query for disk '{}' meta data " "failed".format(disk)) return else: # this was a remove request, so simply delete the child # MappedLun object corresponding to this rbd name mlun = [lun for lun in self.children if lun.rbd_name == disk][0] self.remove_lun(mlun) self.logger.debug("configuration update successful") self.logger.info('ok') else: # the request to add/remove the disk for the client failed self.logger.error("disk {} for '{}' against {} failed" "\n{}".format( action, disk, self.client_iqn, response_message(api.response, self.logger))) return
def all_disk(image_id): """ Coordinate the create/delete of rbd images across the gateway nodes The "all_" method calls the corresponding disk api entrypoints across each gateway. Processing is done serially: creation is done locally first, then other gateways - whereas, rbd deletion is performed first against remote gateways and then the local machine is used to perform the actual rbd delete. :param image_id: (str) rbd image name of the format pool.image **RESTRICTED** """ http_mode = 'https' if settings.config.api_secure else 'http' local_gw = this_host() logger.debug("this host is {}".format(local_gw)) gateways = [ key for key in config.config['gateways'] if isinstance(config.config['gateways'][key], dict) ] logger.debug("other gateways - {}".format(gateways)) gateways.remove(local_gw) logger.debug("other gw's {}".format(gateways)) if request.method == 'PUT': pool = request.form.get('pool') size = request.form.get('size') mode = request.form.get('mode') pool, image_name = image_id.split('.') disk_usable = valid_disk(pool=pool, image=image_name, size=size, mode=mode) if disk_usable != 'ok': return jsonify(message=disk_usable), 400 # make call to local api server first! disk_api = '{}://127.0.0.1:{}/api/disk/{}'.format( http_mode, settings.config.api_port, image_id) api_vars = { 'pool': pool, 'size': size, 'owner': local_gw, 'mode': mode } logger.debug("Issuing disk request to the local API " "for {}".format(image_id)) api = APIRequest(disk_api, data=api_vars) api.put() if api.response.status_code == 200: logger.info("LUN is ready on this host") for gw in gateways: logger.debug("Adding {} to gw {}".format(image_id, gw)) disk_api = '{}://{}:{}/api/disk/{}'.format( http_mode, gw, settings.config.api_port, image_id) api = APIRequest(disk_api, data=api_vars) api.put() if api.response.status_code == 200: logger.info("LUN is ready on {}".format(gw)) else: return jsonify(message=api.response.json()['message']), 500 else: logger.error(api.response.json()['message']) return jsonify(message=api.response.json()['message']), 500 logger.info("LUN defined to all gateways for {}".format(image_id)) return jsonify(message="ok"), 200 else: # this is a DELETE request pool_name, image_name = image_id.split('.') disk_usable = valid_disk(mode='delete', pool=pool_name, image=image_name) if disk_usable != 'ok': return jsonify(message=disk_usable), 400 api_vars = {'purge_host': local_gw} # process other gateways first for gw_name in gateways: disk_api = '{}://{}:{}/api/disk/{}'.format( http_mode, gw_name, settings.config.api_port, image_id) logger.debug("removing '{}' from {}".format(image_id, gw_name)) api = APIRequest(disk_api, data=api_vars) api.delete() if api.response.status_code == 200: logger.debug("{} removed from {}".format(image_id, gw_name)) elif api.response.status_code == 400: # 400 means the rbd is still allocated to a client msg = api.response.json()['message'] logger.error(msg) return jsonify(message=msg), 400 else: # delete failed - don't know why, pass the error to the # admin and abort msg = api.response.json()['message'] return jsonify(message=msg), 500 # at this point the remote gateways are cleaned up, now perform the # purge on the local host which will also purge the rbd disk_api = '{}://127.0.0.1:{}/api/disk/{}'.format( http_mode, settings.config.api_port, image_id) logger.debug("- removing '{}' from the local " "machine, deleting the rbd".format(image_id)) api = APIRequest(disk_api, data=api_vars) api.delete() if api.response.status_code == 200: logger.debug("- rbd {} deleted".format(image_id)) return jsonify(message="ok"), 200 else: return jsonify(message="failed to delete rbd " "{}".format(image_id)), 500
def all_client_auth(client_iqn): """ Coordinate client authentication changes across each gateway node The following parameters are needed to manage client auth :param client_iqn: (str) client IQN name :param chap: (str) chap string of the form user/password or '' **RESTRICTED** """ http_mode = 'https' if settings.config.api_secure else 'http' local_gw = this_host() logger.debug("this host is {}".format(local_gw)) gateways = [ key for key in config.config['gateways'] if isinstance(config.config['gateways'][key], dict) ] logger.debug("other gateways - {}".format(gateways)) gateways.remove(local_gw) lun_list = config.config['clients'][client_iqn]['luns'].keys() image_list = ','.join(lun_list) chap = request.form.get('chap') client_usable = valid_client(mode='auth', client_iqn=client_iqn, chap=chap) if client_usable != 'ok': logger.error("BAD auth request from {}".format(request.remote_addr)) return jsonify(message=client_usable), 400 api_vars = { "committing_host": local_gw, "image_list": image_list, "chap": chap } clientauth_api = '{}://127.0.0.1:{}/api/clientauth/{}'.format( http_mode, settings.config.api_port, client_iqn) logger.debug("Issuing client update to local gw for {}".format(client_iqn)) api = APIRequest(clientauth_api, data=api_vars) api.put() if api.response.status_code == 200: logger.debug("Client update succeeded on local LIO") for gw in gateways: clientauth_api = '{}://{}:{}/api/clientauth/{}'.format( http_mode, gw, settings.config.api_port, client_iqn) logger.debug("updating client {} on {}".format(client_iqn, gw)) api = APIRequest(clientauth_api, data=api_vars) api.put() if api.response.status_code == 200: logger.info("client update successful on {}".format(gw)) continue else: return jsonify(message="client update failed on " "{}".format(gw)), \ api.response.status_code logger.info("All gateways updated") return jsonify(message="ok"), 200 else: # the local update failed, so abort further updates return jsonify(message="Client updated failed on local " "LIO instance"), api.response.status_code
def create_disk(self, pool=None, image=None, size=None, count=1, parent=None, create_image=True, backstore=None): rc = 0 if not parent: parent = self local_gw = this_host() disk_key = "{}/{}".format(pool, image) if not self._valid_pool(pool): return self.logger.debug("Creating/mapping disk {}/{}".format(pool, image)) # make call to local api server's disk endpoint disk_api = '{}://localhost:{}/api/disk/{}'.format( self.http_mode, settings.config.api_port, disk_key) api_vars = { 'pool': pool, 'owner': local_gw, 'count': count, 'mode': 'create', 'create_image': 'true' if create_image else 'false', 'backstore': backstore } if size: api_vars['size'] = size.upper() self.logger.debug("Issuing disk create request") api = APIRequest(disk_api, data=api_vars) api.put() if api.response.status_code == 200: # rbd create and map successful across all gateways so request # it's details and add to the UI self.logger.debug("- LUN(s) ready on all gateways") self.logger.info("ok") self.logger.debug("Updating UI for the new disk(s)") for n in range(1, (int(count) + 1), 1): if int(count) > 1: disk_key = "{}/{}{}".format(pool, image, n) else: disk_key = "{}/{}".format(pool, image) disk_api = ('{}://localhost:{}/api/disk/' '{}'.format(self.http_mode, settings.config.api_port, disk_key)) api = APIRequest(disk_api) api.get() if api.response.status_code == 200: try: image_config = api.response.json() except Exception: raise GatewayAPIError("Malformed REST API response") disk_pool = None for current_disk_pool in self.children: if current_disk_pool.name == pool: disk_pool = current_disk_pool break if disk_pool: Disk(disk_pool, disk_key, image_config) else: DiskPool(parent, pool, [image_config]) self.logger.debug("{} added to the UI".format(disk_key)) else: raise GatewayAPIError( "Unable to retrieve disk details " "for '{}' from the API".format(disk_key)) ceph_pools = self.parent.ceph.cluster.pools ceph_pools.refresh() else: self.logger.error("Failed : {}".format( response_message(api.response, self.logger))) rc = 8 return rc
def create_disk(self, pool=None, image=None, size=None, count=1, parent=None): rc = 0 if not parent: parent = self local_gw = this_host() disk_key = "{}.{}".format(pool, image) if not self._valid_pool(pool): return self.logger.debug("Creating/mapping disk {}/{}".format(pool, image)) # make call to local api server's disk endpoint disk_api = '{}://127.0.0.1:{}/api/disk/{}'.format( self.http_mode, settings.config.api_port, disk_key) api_vars = { 'pool': pool, 'size': size.upper(), 'owner': local_gw, 'count': count, 'mode': 'create' } self.logger.debug("Issuing disk create request") api = APIRequest(disk_api, data=api_vars) api.put() if api.response.status_code == 200: # rbd create and map successful across all gateways so request # it's details and add to the UI self.logger.debug("- LUN(s) ready on all gateways") self.logger.info("ok") ceph_pools = self.parent.ceph.local_ceph.pools ceph_pools.refresh() self.logger.debug("Updating UI for the new disk(s)") for n in range(1, (int(count) + 1), 1): if count > 1: disk_key = "{}.{}{}".format(pool, image, n) else: disk_key = "{}.{}".format(pool, image) disk_api = ('{}://127.0.0.1:{}/api/disk/' '{}'.format(self.http_mode, settings.config.api_port, disk_key)) api = APIRequest(disk_api) api.get() if api.response.status_code == 200: image_config = api.response.json() Disk(parent, disk_key, image_config) self.logger.debug("{} added to the UI".format(disk_key)) else: raise GatewayAPIError( "Unable to retrieve disk details " "for '{}' from the API".format(disk_key)) else: self.logger.error("Failed : {}".format( api.response.json()['message'])) rc = 8 return rc
def all_client_luns(client_iqn): """ Coordinate the addition(PUT) and removal(DELETE) of a disk from a client :param client_iqn: (str) IQN of the client :param disk: (str) rbd image name of the format pool.image **RESTRICTED** """ http_mode = 'https' if settings.config.api_secure else 'http' local_gw = this_host() logger.debug("this host is {}".format(local_gw)) gateways = [ key for key in config.config['gateways'] if isinstance(config.config['gateways'][key], dict) ] logger.debug("other gateways - {}".format(gateways)) gateways.remove(local_gw) disk = request.form.get('disk') lun_list = config.config['clients'][client_iqn]['luns'].keys() if request.method == 'PUT': lun_list.append(disk) else: # this is a delete request if disk in lun_list: lun_list.remove(disk) else: return jsonify(message="disk not mapped to client"), 400 chap_obj = CHAP(config.config['clients'][client_iqn]['auth']['chap']) chap = "{}/{}".format(chap_obj.user, chap_obj.password) image_list = ','.join(lun_list) client_usable = valid_client(mode='disk', client_iqn=client_iqn, image_list=image_list) if client_usable != 'ok': logger.error("Bad disk request for client {} : " "{}".format(client_iqn, client_usable)) return jsonify(message=client_usable), 400 # committing host is the local LIO node api_vars = { "committing_host": local_gw, "image_list": image_list, "chap": chap } clientlun_api = '{}://127.0.0.1:{}/api/clientlun/{}'.format( http_mode, settings.config.api_port, client_iqn) api = APIRequest(clientlun_api, data=api_vars) api.put() if api.response.status_code == 200: logger.info("disk mapping update for {} successful".format(client_iqn)) for gw in gateways: clientlun_api = '{}://{}:{}/api/clientlun/{}'.format( http_mode, gw, settings.config.api_port, client_iqn) logger.debug("Updating disk map for {} on GW {}".format( client_iqn, gw)) api = APIRequest(clientlun_api, data=api_vars) api.put() if api.response.status_code == 200: logger.debug("gateway '{}' updated".format(gw)) continue else: logger.error("disk mapping update on {} failed".format(gw)) return jsonify(message="disk map updated failed on " "{}".format(gw)), \ api.response.status_code return jsonify(message="ok"), 200 else: # disk map update failed at the first hurdle! logger.error("disk map update failed on the local LIO instance") return jsonify(message="failed to update local LIO instance"), \ api.response.status_code
def all_client(client_iqn): """ Handle the client create/delete actions across gateways :param client_iqn: (str) IQN of the client to create or delete **RESTRICTED** Examples: curl --insecure --user admin:admin -X PUT https://192.168.122.69:5001/api/all_client/iqn.1994-05.com.redhat:myhost4 curl --insecure --user admin:admin -X DELETE https://192.168.122.69:5001/api/all_client/iqn.1994-05.com.redhat:myhost4 """ method = {"PUT": 'create', "DELETE": 'delete'} http_mode = 'https' if settings.config.api_secure else 'http' local_gw = this_host() logger.debug("this host is {}".format(local_gw)) gateways = [ key for key in config.config['gateways'] if isinstance(config.config['gateways'][key], dict) ] logger.debug("other gateways - {}".format(gateways)) gateways.remove(local_gw) # committing host is the node responsible for updating the config object api_vars = {"committing_host": local_gw} # validate the PUT/DELETE request first client_usable = valid_client(mode=method[request.method], client_iqn=client_iqn) if client_usable != 'ok': return jsonify(message=client_usable), 400 if request.method == 'PUT': client_api = '{}://127.0.0.1:{}/api/client/{}'.format( http_mode, settings.config.api_port, client_iqn) logger.debug("Processing client CREATE for {}".format(client_iqn)) api = APIRequest(client_api, data=api_vars) api.put() if api.response.status_code == 200: logger.info("Client {} added to local LIO".format(client_iqn)) for gw in gateways: client_api = '{}://{}:{}/api/client/{}'.format( http_mode, gw, settings.config.api_port, client_iqn) logger.debug("sending request to {} to create {}".format( gw, client_iqn)) api = APIRequest(client_api, data=api_vars) api.put() if api.response.status_code == 200: logger.info("Client '{}' added to {}".format( client_iqn, gw)) continue else: # client create failed against the remote LIO instance msg = api.response.json()['message'] logger.error("Client create for {} failed on {} " ": {}".format(client_iqn, gw, msg)) return jsonify(message=msg), 500 # all gateways processed return a success state to the caller return jsonify(message='ok'), 200 else: # client create failed against the local LIO instance msg = api.response.json()['message'] logger.error("Client create on local LIO instance failed " "for {} : {}".format(client_iqn, msg)) return jsonify(message=msg), 500 else: # DELETE client request # Process flow: remote gateways > local > delete config object entry for gw in gateways: client_api = '{}://{}:{}/api/client/{}'.format( http_mode, gw, settings.config.api_port, client_iqn) logger.info("- removing '{}' from {}".format(client_iqn, gw)) api = APIRequest(client_api, data=api_vars) api.delete() if api.response.status_code == 200: logger.info("- '{}' removed".format(client_iqn)) continue elif api.response.status_code == 400: logger.error("- '{}' is in use on {}".format(client_iqn, gw)) return jsonify(message="Client in use"), 400 else: msg = api.response.json()['message'] logger.error("Failed to remove {} from {}".format( client_iqn, gw)) return jsonify(message="failed to remove client '{}' on " "{}".format(client_iqn, msg)), 500 # At this point the other gateways have removed the client, so # remove from the local LIO instance client_api = '{}://127.0.0.1:{}/api/client/{}'.format( http_mode, settings.config.api_port, client_iqn) api = APIRequest(client_api, data=api_vars) api.delete() if api.response.status_code == 200: logger.info("successfully removed '{}'".format(client_iqn)) return jsonify(message="ok"), 200 else: return jsonify(message="Unable to delete {} from local LIO " "instance".format(client_iqn)), \ api.response.status_code
def all_gateway(gateway_name=None): """ Define iscsi gateway(s) across node(s), adding TPGs, disks and clients The call requires the following variables to be set; :param gateway_name: (str) gateway name :param ip_address: (str) ipv4 dotted quad for the address iSCSI should use :param nosync: (bool) whether to sync the LIO objects to the new gateway **RESTRICTED** """ ip_address = request.form.get('ip_address') nosync = request.form.get('nosync', False) # first confirm that the request is actually valid, if not return a 400 # error with the error description current_config = config.config gateway_usable = valid_gateway(gateway_name, ip_address, current_config) if gateway_usable != 'ok': return jsonify(message=gateway_usable), 400 resp_text = "Gateway added" # Assume the best! http_mode = 'https' if settings.config.api_secure else 'http' current_disks = config.config['disks'] current_clients = config.config['clients'] target_iqn = config.config['gateways'].get('iqn') total_objects = (len(current_disks.keys()) + len(current_clients.keys())) if total_objects == 0: nosync = True gateway_ip_list = config.config['gateways'].get('ip_list', []) gateway_ip_list.append(ip_address) first_gateway = (len(gateway_ip_list) == 1) for endpoint in gateway_ip_list: if first_gateway: endpoint = '127.0.0.1' logger.debug("Processing GW endpoint {} for {}".format( endpoint, gateway_name)) api_endpoint = '{}://{}:{}/api'.format(http_mode, endpoint, settings.config.api_port) gw_rqst = api_endpoint + '/gateway/{}'.format(gateway_name) gw_vars = { "target_iqn": target_iqn, "gateway_ip_list": ",".join(gateway_ip_list), "mode": "target" } logger.debug("Calling API at {} with {}".format(gw_rqst, gw_vars)) api = APIRequest(gw_rqst, data=gw_vars) api.put() if api.response.status_code != 200: # GW creation failed msg = api.response.json()['message'] logger.error("Failed to create gateway {}: {}".format( gateway_name, msg)) return jsonify(message="Failed to create gateway"), 500 # for the new gateway, when sync is selected we need to run the # disk api to register all the rbd's to that gateway if endpoint == ip_address and not nosync: for disk_key in current_disks: this_disk = current_disks[disk_key] lun_rqst = api_endpoint + '/disk/{}'.format(disk_key) lun_vars = { "pool": this_disk['pool'], "size": "0G", "owner": this_disk['owner'], "mode": "sync" } api = APIRequest(lun_rqst, data=lun_vars) api.put() if api.response.status_code != 200: msg = api.response.json()['message'] logger.error("Failed to add disk {} to {} new " "tpg : {}".format(disk_key, endpoint, msg)) return jsonify(message="Failed to add disk"), 500 resp_text += ", {} disks added".format(len(current_disks)) # Adding a gateway introduces a new tpg - each tpg MUST have the # luns defined so a RTPG call can be responded to correctly, so # we need to sync the disks to the new tpg's if len(current_disks.keys()) > 0: if endpoint != ip_address or not nosync: gw_vars['mode'] = 'map' api = APIRequest(gw_rqst, data=gw_vars) api.put() if api.response.status_code != 200: # GW creation failed - if the failure was severe you'll # see a json issue here. msg = api.response.json()['message'] logger.error("Failed to map existing disks to new" " tpg on {} - ".format(endpoint)) return jsonify(message="Failed to map disk"), 500 if endpoint == ip_address and not nosync: for client_iqn in current_clients: this_client = current_clients[client_iqn] client_luns = this_client['luns'] lun_list = [(disk, client_luns[disk]['lun_id']) for disk in client_luns] srtd_list = Client.get_srtd_names(lun_list) # client_iqn, image_list, chap, committing_host client_vars = { 'chap': this_client['auth']['chap'], 'image_list': ','.join(srtd_list), 'committing_host': local_gw } api = APIRequest(api_endpoint + "/client/{}".format(client_iqn), data=client_vars) api.put() if api.response.status_code != 200: msg = api.response.json()['message'] logger.error("Problem adding client {} - " "{}".format( client_iqn, api.response.json()['message'])) return jsonify(message="Failed to add client"), 500 resp_text += ", {} clients defined".format( len(current_clients)) return jsonify(message=resp_text), 200
def ui_command_auth(self, username=None, password=None, mutual_username=None, mutual_password=None): """ Target authentication can be set to use CHAP/CHAP_MUTUAL by supplying username, password, mutual_username, mutual_password e.g. auth username=<user> password=<pass> mutual_username=<m_user> mutual_password=<m_pass> username / mutual_username ... the username is 8-64 character string. Each character may either be an alphanumeric or use one of the following special characters .,:,-,@. Consider using the hosts 'shortname' or the initiators IQN value as the username password / mutual_password ... the password must be between 12-16 chars in length containing alphanumeric characters, plus the following special characters @,_,-,/ """ self.logger.debug("CMD: /iscsi-targets/<target_iqn> auth *") if not username: self.logger.error( "To set authentication, specify " "username=<user> password=<password> " "[mutual_username]=<user> [mutual_password]=<password> " "or nochap") return if username == 'nochap': username = '' password = '' mutual_username = '' mutual_password = '' self.logger.debug( "auth to be set to username='******', password='******', mutual_username='******', " "mutual_password='******'".format(username, password, mutual_username, mutual_password)) target_iqn = self.name api_vars = { "username": username, "password": password, "mutual_username": mutual_username, "mutual_password": mutual_password } targetauth_api = ('{}://localhost:{}/api/' 'targetauth/{}'.format(self.http_mode, settings.config.api_port, target_iqn)) api = APIRequest(targetauth_api, data=api_vars) api.put() if api.response.status_code == 200: self.logger.debug("- target credentials updated") self.auth['username'] = username self.auth['password'] = password self.auth['mutual_username'] = mutual_username self.auth['mutual_password'] = mutual_password self.logger.info('ok') else: self.logger.error("Failed to update target auth: " "{}".format( response_message(api.response, self.logger))) return
def ui_command_discovery_auth(self, username=None, password=None, mutual_username=None, mutual_password=None): """ Discovery authentication can be set to use CHAP/CHAP_MUTUAL by supplying username, password, mutual_username, mutual_password Specifying 'nochap' will remove discovery authentication. e.g. auth username=<user> password=<pass> mutual_username=<m_user> mutual_password=<m_pass> """ self.logger.warn( "discovery username={}, password={}, mutual_username={}, " "mutual_password={}".format(username, password, mutual_username, mutual_password)) self.logger.debug("CMD: /iscsi discovery_auth") if not username: self.logger.error( "To set or reset discovery authentication, specify either " "username=<user> password=<password> [mutual_username]=<user> " "[mutual_password]=<password> or nochap") return if username == 'nochap': username = '' password = '' mutual_username = '' mutual_password = '' self.logger.debug( "discovery auth to be set to username='******', password='******', " "mutual_username='******', mutual_password='******'".format( username, password, mutual_username, mutual_password)) api_vars = { "username": username, "password": password, "mutual_username": mutual_username, "mutual_password": mutual_password } discoveryauth_api = ('{}://localhost:{}/api/' 'discoveryauth'.format(self.http_mode, settings.config.api_port)) api = APIRequest(discoveryauth_api, data=api_vars) api.put() if api.response.status_code == 200: self._set_auth(username, password, mutual_username, mutual_password) self.logger.info('ok') else: self.logger.error("Error: {}".format( response_message(api.response, self.logger))) return
def ui_command_host(self, action, client_iqn): """ use the 'host' sub-command to add and remove hosts from a host group. Adding a host will automatically map the host group's disks to that specific host. Removing a host however, does not change the hosts disk masking - it simply removes the host from group. e.g. host add|remove iqn.1994-05.com.redhat:rh7-client """ if action not in HostGroup.valid_actions: self.logger.error("Invalid request - must be " "host add|remove <client_iqn>") return target_iqn = self.parent.parent.name # basic checks client_group = self._get_client_group(target_iqn) client_map = client_group.client_map if client_iqn not in client_map: self.logger.error("'{}' is not managed by a " "group".format(client_iqn)) return current_group = client_map[client_iqn].group_name if action == 'add' and current_group: self.logger.error("'{}' already belongs to " "'{}'".format(client_iqn, current_group)) return elif action == 'remove' and current_group != self.name: self.logger.error("'{}' does not belong to this " "group".format(client_iqn)) return # Basic checks passed, hand-off to the API now group_api = ('{}://{}:{}/api/hostgroup/' '{}/{}'.format(self.http_mode, "localhost", settings.config.api_port, target_iqn, self.name)) api_vars = {"action": action, "members": client_iqn} api = APIRequest(group_api, data=api_vars) api.put() self.logger.debug("- api call responded " "{}".format(api.response.status_code)) if api.response.status_code != 200: self.logger.error("Failed :" "{}".format( response_message(api.response, self.logger))) return # group updated, so update the UI self.logger.debug("Updating the UI") if action == 'add': HostGroupMember(self, 'host', client_iqn) self.update_clients_UI([client_iqn], target_iqn) elif action == 'remove': child = [ child for child in self.children if child.name == client_iqn ][0] self.delete(child) self.logger.info('ok')
def ui_command_create(self, gateway_name, ip_address, nosync=False, skipchecks='false'): """ Define a gateway to the gateway group for this iscsi target. The first host added should be the gateway running the command gateway_name ... should resolve to the hostname of the gateway ip_address ..... is the IPv4/IPv6 address of the interface the iscsi portal should use nosync ......... by default new gateways are sync'd with the existing configuration by cli. By specifying nosync the sync step is bypassed - so the new gateway will need to have it's rbd-target-gw daemon restarted to apply the current configuration (default = False) skipchecks ..... set this to true to force gateway validity checks to be bypassed(default = False). This is a developer option ONLY. Skipping these checks has the potential to result in an unstable configuration. """ ip_address = normalize_ip_address(ip_address) self.logger.debug("CMD: ../gateways/ create {} {} " "nosync={} skipchecks={}".format(gateway_name, ip_address, nosync, skipchecks)) local_gw = this_host() current_gateways = [tgt.name for tgt in self.children] if gateway_name != local_gw and len(current_gateways) == 0: # the first gateway defined must be the local machine. By doing # this the initial create uses localhost, and places it's portal IP # in the gateway ip list. Once the gateway ip list is defined, the # api server can resolve against the gateways - until the list is # defined only a request from localhost is acceptable to the api self.logger.error("The first gateway defined must be the local " "machine") return if skipchecks not in ['true', 'false']: self.logger.error("skipchecks must be either true or false") return if local_gw in current_gateways: current_gateways.remove(local_gw) config = self.parent.parent.parent._get_config() if not config: self.logger.error("Unable to refresh local config" " over API - sync aborted, restart rbd-target-gw" " on {} to sync".format(gateway_name)) target_iqn = self.parent.name target_config = config['targets'][target_iqn] if nosync: sync_text = "sync skipped" else: sync_text = ("sync'ing {} disk(s) and " "{} client(s)".format(len(target_config['disks']), len(target_config['clients']))) if skipchecks == 'true': self.logger.warning("OS version/package checks have been bypassed") self.logger.info("Adding gateway, {}".format(sync_text)) gw_api = '{}://{}:{}/api'.format(self.http_mode, "localhost", settings.config.api_port) gw_rqst = gw_api + '/gateway/{}/{}'.format(target_iqn, gateway_name) gw_vars = {"nosync": nosync, "skipchecks": skipchecks, "ip_address": ip_address} api = APIRequest(gw_rqst, data=gw_vars) api.put() msg = response_message(api.response, self.logger) if api.response.status_code != 200: self.logger.error("Failed : {}".format(msg)) return self.logger.debug("{}".format(msg)) self.logger.debug("Adding gw to UI") # Target created OK, get the details back from the gateway and # add to the UI. We have to use the new gateway to ensure what # we get back is current (the other gateways will lag until they see # epoch xattr change on the config object) new_gw_endpoint = ('{}://{}:{}/' 'api'.format(self.http_mode, gateway_name, settings.config.api_port)) config = self.parent.parent.parent._get_config(endpoint=new_gw_endpoint) target_config = config['targets'][target_iqn] portal_config = target_config['portals'][gateway_name] Gateway(self, gateway_name, portal_config) self.logger.info('ok')
def ui_command_auth(self, chap=None): """ Client authentication can be set to use CHAP by supplying the a string of the form <username>/<password> e.g. auth chap=username/password | nochap username ... the username is 8-64 character string. Each character may either be an alphanumeric or use one of the following special characters .,:,-,@. Consider using the hosts 'shortname' or the initiators IQN value as the username password ... the password must be between 12-16 chars in length containing alphanumeric characters, plus the following special characters @,_,- WARNING: Using unsupported special characters may result in truncation, resulting in failed logins. Specifying 'nochap' will remove chap authentication for the client across all gateways. """ self.logger.debug("CMD: ../hosts/<client_iqn> auth *") if not chap: self.logger.error("To set or reset authentication, specify either " "chap=<user>/<password> or nochap") return if chap == 'nochap': chap = '' else: # string could have been supplied as chap=user/password or # simply user/password - either way all we see is user/password if '/' not in chap: self.logger.error( "CHAP format is invalid - must be a <username>/<password> " "format. Use 'help auth' to show the correct syntax and " "supported characters") return self.logger.debug( "CHAP to be set to '{}' for '{}'".format(chap, self.client_iqn)) api_vars = {"chap": chap} clientauth_api = ('{}://127.0.0.1:{}/api/' 'clientauth/{}'.format(self.http_mode, settings.config.api_port, self.client_iqn)) api = APIRequest(clientauth_api, data=api_vars) api.put() if api.response.status_code == 200: self.logger.debug("- client credentials updated") self.auth['chap'] = chap self.logger.info('ok') else: self.logger.error("Failed to update the client's auth" " :{}".format(response_message(api.response, self.logger))) return
def ui_command_disk(self, action, disk_name): """ use the 'disk' sub-command to add or remove a disk from a specific host group. Removing disks should be done with care, as the remove operation will be executed across all hosts defined to the host group. e.g. disk add|remove rbd.disk_1 """ if action not in HostGroup.valid_actions: self.logger.error("Invalid request - must be " "disk add|remove <disk_image>") return target_iqn = self.parent.parent.name # simple sanity checks # 1. does the disk exist in the configuration ui_root = self.get_ui_root() all_pools = ui_root.disks.children all_disks = [] for current_pool in all_pools: for current_disk in current_pool.children: all_disks.append(current_disk) if disk_name not in [disk.image_id for disk in all_disks]: self.logger.error("Disk '{}' is not defined within the " "configuration".format(disk_name)) return # 2. For an 'add' request, the disk must not already be in the host # group. Whereas, for a remove request the disk must exist. if action == 'add': if disk_name in self.disks: self.logger.error("'{}' is already defined to this " "host-group".format(disk_name)) return else: if disk_name not in self.disks: self.logger.error("'{}' is not a member of this " "group".format(disk_name)) return mapped_disks = [ mapped_disk.name for mapped_disk in self.parent.parent.target_disks.children ] if disk_name not in mapped_disks: rc = self.parent.parent.target_disks.add_disk(disk_name, None) if rc == 0: self.logger.debug("disk auto-map successful") else: self.logger.error("disk auto-map failed({}), try " "using the /iscsi-target/<iqn>/disks add " "command".format(rc)) return # Basic checks passed, hand-off to the API group_api = ('{}://{}:{}/api/hostgroup/' '{}/{}'.format(self.http_mode, "localhost", settings.config.api_port, target_iqn, self.name)) api_vars = {"action": action, "disks": disk_name} api = APIRequest(group_api, data=api_vars) api.put() self.logger.debug("- api call responded {}".format( api.response.status_code)) if api.response.status_code != 200: self.logger.error("Failed: " "{}".format( response_message(api.response, self.logger))) return # group updated, so update the host-groups UI elements self.logger.debug("Updating the UI") if action == 'add': HostGroupMember(self, 'disk', disk_name) elif action == 'remove': child = [ child for child in self.children if child.name == disk_name ][0] self.delete(child) self.update_clients_UI(self.members, target_iqn) self.logger.info('ok')
def set_auth(self, username=None, password=None, mutual_username=None, mutual_password=None): self.logger.debug("username={}, password={}, mutual_username={}, " "mutual_password={}".format(username, password, mutual_username, mutual_password)) self.logger.debug("CMD: ../hosts/<client_iqn> auth *") if not username: self.logger.error( "To set or reset authentication, specify either " "username=<user> password=<password> " "[mutual_username]=<user> [mutual_password]=<password> " "or nochap") return if username == 'nochap': username = '' password = '' mutual_username = '' mutual_password = '' self.logger.debug( "auth to be set to username='******', password='******', mutual_username='******', " "mutual_password='******' for '{}'".format(username, password, mutual_username, mutual_password, self.client_iqn)) target_iqn = self.parent.parent.name api_vars = { "username": username, "password": password, "mutual_username": mutual_username, "mutual_password": mutual_password } clientauth_api = ('{}://localhost:{}/api/' 'clientauth/{}/{}'.format(self.http_mode, settings.config.api_port, target_iqn, self.client_iqn)) api = APIRequest(clientauth_api, data=api_vars) api.put() if api.response.status_code == 200: self.logger.debug("- client credentials updated") self.auth['username'] = username self.auth['password'] = password self.auth['mutual_username'] = mutual_username self.auth['mutual_password'] = mutual_password self.logger.info('ok') else: self.logger.error("Failed to update the client's auth: " "{}".format( response_message(api.response, self.logger))) return
def ui_command_create(self, gateway_name, ip_address, nosync=False): """ Define a gateway to the gateway group for this iscsi target. The first host added should be the gateway running the command gateway_name ... should resolve to the hostname of the gateway ip_address ..... is the IP v4 address of the interface the iscsi portal should use nosync ......... by default new gateways are sync'd with the existing configuration by cli. By specifying nosync the sync step is bypassed - so the new gateway will need to have it's rbd-target-gw daemon restarted to apply the current configuration """ self.logger.debug("CMD: ../gateways/ create {} {} " "nosync={}".format(gateway_name, ip_address, nosync)) local_gw = this_host() current_gateways = [tgt.name for tgt in self.children] if gateway_name != local_gw and len(current_gateways) == 0: # the first gateway defined must be the local machine. By doing # this the initial create uses 127.0.0.1, and places it's portal IP # in the gateway ip list. Once the gateway ip list is defined, the # api server can resolve against the gateways - until the list is # defined only a request from 127.0.0.1 is acceptable to the api self.logger.error("The first gateway defined must be the local " "machine") return if local_gw in current_gateways: current_gateways.remove(local_gw) config = self.parent.parent.parent._get_config() if not config: self.logger.error("Unable to refresh local config" " over API - sync aborted, restart rbd-target-gw" " on {} to sync".format(gateway_name)) if nosync: sync_text = "sync skipped" else: sync_text = ("sync'ing {} disk(s) and " "{} client(s)".format(len(config['disks']), len(config['clients']))) self.logger.info("Adding gateway, {}".format(sync_text)) gw_api = '{}://{}:{}/api'.format(self.http_mode, "127.0.0.1", settings.config.api_port) gw_rqst = gw_api + '/gateway/{}'.format(gateway_name) gw_vars = {"nosync": nosync, "ip_address": ip_address} api = APIRequest(gw_rqst, data=gw_vars) api.put() if api.response.status_code != 200: msg = api.response.json()['message'] self.logger.error("Failed : {}".format(msg)) return self.logger.debug("{}".format(api.response.json()['message'])) self.logger.debug("Adding gw to UI") # Target created OK, get the details back from the gateway and # add to the UI. We have to use the new gateway to ensure what # we get back is current (the other gateways will lag until they see # epoch xattr change on the config object) new_gw_endpoint = ('{}://{}:{}/' 'api'.format(self.http_mode, gateway_name, settings.config.api_port)) config = self.parent.parent.parent._get_config( endpoint=new_gw_endpoint) gw_config = config['gateways'][gateway_name] Gateway(self, gateway_name, gw_config) self.logger.info('ok')
def ui_command_disk(self, action='add', disk=None, size=None): """ Disks can be added or removed from the client one at a time using the disk sub-command. Note that the disk MUST already be defined within the configuration > disk add|remove <disk_name> Adding a disk will result in the disk occupying the client's next available lun id. Removing a disk will preserve existing lun id allocations """ self.logger.debug("CMD: ../hosts/<client_iqn> disk action={}" " disk={}".format(action, disk)) valid_actions = ['add', 'remove'] if not disk: self.logger.critical("You must supply a disk name to add/remove " "for this client") return if action not in valid_actions: self.logger.error("you can only add and remove disks - {} is " "invalid ".format(action)) return lun_list = [(lun.rbd_name, lun.lun_id) for lun in self.children] current_luns = Client.get_srtd_names(lun_list) if action == 'add': if disk not in current_luns: ui_root = self.get_ui_root() valid_disk_names = [defined_disk.image_id for defined_disk in ui_root.disks.children] else: # disk provided is already mapped, so remind the user self.logger.error("Disk {} already mapped".format(disk)) return else: valid_disk_names = current_luns if disk not in valid_disk_names: # if this is an add operation, we can create the disk on-the-fly # for the admin if action == 'add': ui_root = self.get_ui_root() ui_disks = ui_root.disks if not size: self.logger.error("To auto-define the disk to the client" " you must provide a disk size") return # a disk given here would be of the form pool.image pool, image = disk.split('.') rc = ui_disks.create_disk(pool=pool, image=image, size=size) if rc == 0: self.logger.debug("disk auto-define successful") else: self.logger.error("disk auto-define failed({}), try " "using the /disks create " "command".format(rc)) return else: self.logger.error("disk '{}' is not mapped to this " "client ".format(disk)) return # At this point we are either in add/remove mode, with a valid disk # to act upon self.logger.debug("Client '{}' update - {} disk " "{}".format(self.client_iqn, action, disk)) api_vars = {"disk": disk} # if action == 'add': # current_luns.append(disk) # else: # current_luns.remove(disk) # # image_list = ','.join(current_luns) # # api_vars = {"image_list": image_list, # "chap": self.auth.get('chap', '')} clientlun_api = '{}://127.0.0.1:{}/api/all_clientlun/{}'.format( self.http_mode, settings.config.api_port, self.client_iqn) api = APIRequest(clientlun_api, data=api_vars) if action == 'add': api.put() else: api.delete() if api.response.status_code == 200: self.logger.debug("disk mapping updated successfully") if action == 'add': # The addition of the lun will get a lun id assigned so # we need to query the api server to get the new configuration # to be able to set the local cli entry correctly get_api_vars = {"disk": disk} clientlun_api = clientlun_api.replace('/all_clientlun/', '/clientlun/') self.logger.debug("Querying API to get mapped LUN information") api = APIRequest(clientlun_api, data=get_api_vars) api.get() if api.response.status_code == 200: lun_dict = api.response.json()['message'] lun_id = lun_dict[disk]['lun_id'] MappedLun(self, disk, lun_id) # update the objects lun list (so ui info cmd picks # up the change self.luns[disk] = {'lun_id': lun_id} self.parent.update_lun_map('add', disk, self.client_iqn) active_maps = len(self.parent.lun_map[disk]) - 1 if active_maps > 0: self.logger.warning("Warning: '{}' mapped to {} other " "client(s)".format(disk, active_maps)) else: self.logger.error("Query for disk '{}' meta data " "failed".format(disk)) return else: # this was a remove request, so simply delete the child # MappedLun object corresponding to this rbd name mlun = [lun for lun in self.children if lun.rbd_name == disk][0] self.remove_child(mlun) del self.luns[disk] self.parent.update_lun_map('remove', disk, self.client_iqn) self.logger.debug("configuration update successful") self.logger.info('ok') else: # the request to add/remove the disk for the client failed self.logger.error("disk {} for '{}' against {} " "failed\n{}".format(action, disk, self.client_iqn, api.response.json()['message'])) return