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 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 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='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 <disk_name> <size> disk remove <disk_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 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} clientlun_api = ('{}://127.0.0.1:{}/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: 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 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") 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: try: image_config = api.response.json() except: raise GatewayAPIError("Malformed REST API response") 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)) ceph_pools = self.parent.ceph.local_ceph.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, create_image=True, backstore=None, wwn=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, 'wwn': wwn} 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 ui_command_delete(self, gateway_name, confirm=None): """ Delete a gateway from the group. This will stop and delete the target running on the gateway. If this is the last gateway the target is mapped to all objects added to it will be removed, and confirm=True is required. """ self.logger.debug("CMD: ../gateways/ delete {} confirm {}".format( gateway_name, confirm)) self.logger.info("Deleting gateway, {}".format(gateway_name)) confirm = self.ui_eval_param(confirm, 'bool', False) 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-api on {0} to " "sync".format(gateway_name)) return target_iqn = self.parent.name gw_cnt = len(config['targets'][target_iqn]['portals']) if gw_cnt == 0: self.logger.error("Target is not mapped to any gateways.") return if gw_cnt == 1: if not confirm: self.logger.error("Deleting the last gateway will remove all " "objects on this target. Use confirm=true") return gw_api = '{}://{}:{}/api'.format(self.http_mode, "localhost", settings.config.api_port) gw_rqst = gw_api + '/gateway/{}/{}'.format(target_iqn, gateway_name) if confirm: gw_vars = {"force": 'true'} else: gw_vars = {"force": 'false'} api = APIRequest(gw_rqst, data=gw_vars) api.delete() msg = response_message(api.response, self.logger) if api.response.status_code != 200: if "unavailable:" + gateway_name in msg: self.logger.error( "Could not contact {}. If the gateway is " "permanently down. Use confirm=true to " "force removal. WARNING: Forcing removal of " "a gateway that can still be reached by an " "initiator may result in data corruption.".format( gateway_name)) else: self.logger.error("Failed : {}".format(msg)) return self.logger.debug("{}".format(msg)) self.logger.debug("Removing gw from UI") self.thread_lock.acquire() gw_object = self.get_child(gateway_name) self.remove_child(gw_object) self.thread_lock.release() config = self.parent.parent.parent._get_config() if not config: self.logger.error("Could not refresh display. Restart gwcli.") return elif not config['targets'][target_iqn]['portals']: # no more gws so everything but the target is dropped. disks_object = self.parent.get_child("disks") disks_object.reset() hosts_grp_object = self.parent.get_child("host-groups") hosts_grp_object.reset() hosts_object = self.parent.get_child("hosts") hosts_object.reset()
def ui_command_create(self, gateway_name, ip_addresses, 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_addresses ... are the IPv4/IPv6 addresses of the interfaces the iSCSI portals 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-api 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_addresses = [ normalize_ip_address(ip_address) for ip_address in ip_addresses.split(',') ] self.logger.debug("CMD: ../gateways/ create {} {} " "nosync={} skipchecks={}".format( gateway_name, ip_addresses, 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-api" " on {0} to sync".format(gateway_name)) return 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") # Check if we can get hostname from # the new gw endpoint new_gw_endpoint = ('{}://{}:{}/' 'api'.format(self.http_mode, gateway_name, settings.config.api_port)) api = APIRequest('{}/sysinfo/hostname'.format(new_gw_endpoint)) api.get() if api.response.status_code != 200: msg = response_message(api.response, self.logger) self.logger.error("Get gateway hostname failed : {}\n" "Please check api_host setting and make sure " "host {} IP is listening on port {}" "".format(msg, gateway_name, settings.config.api_port)) return gateway_hostname = api.response.json()['data'] 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": ','.join(ip_addresses) } 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) config = self.parent.parent.parent._get_config( endpoint=new_gw_endpoint) if not config: self.logger.error( "Unable to refresh local config" " over API - sync aborted, restart rbd-target-api" " on {0} to sync".format(gateway_name)) return target_config = config['targets'][target_iqn] portal_config = target_config['portals'][gateway_hostname] Gateway(self, gateway_hostname, portal_config) 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_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)))
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 # simple sanity checks # 1. does the disk exist in the configuration ui_root = self.get_ui_root() if disk_name not in [disk.name for disk in ui_root.disks.children]: 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 # Basic checks passed, hand-off to the API group_api = ('{}://{}:{}/api/hostgroup/' '{}'.format(self.http_mode, "127.0.0.1", settings.config.api_port, 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) self.logger.info('ok')
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 # basic checks client_group = self._get_client_group() 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, "127.0.0.1", settings.config.api_port, 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]) elif action == 'remove': child = [ child for child in self.children if child.name == client_iqn ][0] self.delete(child) self.logger.info('ok')
def set_auth(self, chap=None, chap_mutual=None): self.logger.warn("chap={}, chap_mutual={}".format(chap, chap_mutual)) self.logger.debug("CMD: ../hosts/<client_iqn> auth *") if not chap: self.logger.error("To set or reset authentication, specify either " "chap=<user>/<password> [chap_mutual]=<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 if not chap_mutual: chap_mutual = '' else: if '/' not in chap_mutual: self.logger.error( "CHAP_MUTUAL format is invalid - must be a <username>/<password> " "format. Use 'help auth' to show the correct syntax and " "supported characters") return self.logger.debug("auth to be set to chap='{}', chap_mutual='{}' for '{}'".format( chap, chap_mutual, self.client_iqn)) target_iqn = self.parent.parent.name api_vars = {"chap": chap, "chap_mutual": chap_mutual} 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") if chap != '': self.auth['chap'] = chap else: self.auth['chap'] = "None" if chap_mutual != '': self.auth['chap_mutual'] = chap_mutual else: self.auth['chap_mutual'] = "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_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
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 + '/all_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')