# -*- coding: utf-8 -*- # pylint: disable=C0302 # pylint: disable=too-many-branches # pylint: disable=too-many-lines from __future__ import absolute_import import json import re from copy import deepcopy import cherrypy import rados import rbd from .. import mgr from ..exceptions import DashboardException from ..rest_client import RequestException from ..security import Scope from ..services.iscsi_cli import IscsiGatewaysConfig from ..services.iscsi_client import IscsiClient from ..services.iscsi_config import IscsiGatewayDoesNotExist from ..services.rbd import format_bitmask from ..services.tcmu_service import TcmuService from ..tools import TaskManager, str_to_bool from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \ ReadPermission, RESTController, Task, UIRouter, UpdatePermission try: from typing import Any, Dict, List, no_type_check except ImportError: no_type_check = object() # Just for type checking ISCSI_SCHEMA = { 'user': (str, 'username'), 'password': (str, 'password'), 'mutual_user': (str, ''), 'mutual_password': (str, '') } @UIRouter('/iscsi', Scope.ISCSI) class IscsiUi(BaseController): REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION = 10 REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION = 11 @Endpoint() @ReadPermission @no_type_check def status(self): status = {'available': False} try: gateway = get_available_gateway() except DashboardException as e: status['message'] = str(e) return status try: config = IscsiClient.instance(gateway_name=gateway).get_config() if config['version'] < IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION or \ config['version'] > IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION: status['message'] = 'Unsupported `ceph-iscsi` config version. ' \ 'Expected >= {} and <= {} but found' \ ' {}.'.format(IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION, IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION, config['version']) return status status['available'] = True except RequestException as e: if e.content: try: content = json.loads(e.content) content_message = content.get('message') except ValueError: content_message = e.content if content_message: status['message'] = content_message return status @Endpoint() @ReadPermission def version(self): gateway = get_available_gateway() config = IscsiClient.instance(gateway_name=gateway).get_config() return { 'ceph_iscsi_config_version': config['version'] } @Endpoint() @ReadPermission def settings(self): gateway = get_available_gateway() settings = IscsiClient.instance(gateway_name=gateway).get_settings() if 'target_controls_limits' in settings: target_default_controls = settings['target_default_controls'] for ctrl_k, ctrl_v in target_default_controls.items(): limits = settings['target_controls_limits'].get(ctrl_k, {}) if 'type' not in limits: # default limits['type'] = 'int' # backward compatibility if target_default_controls[ctrl_k] in ['Yes', 'No']: limits['type'] = 'bool' target_default_controls[ctrl_k] = str_to_bool(ctrl_v) settings['target_controls_limits'][ctrl_k] = limits if 'disk_controls_limits' in settings: for backstore, disk_controls_limits in settings['disk_controls_limits'].items(): disk_default_controls = settings['disk_default_controls'][backstore] for ctrl_k, ctrl_v in disk_default_controls.items(): limits = disk_controls_limits.get(ctrl_k, {}) if 'type' not in limits: # default limits['type'] = 'int' settings['disk_controls_limits'][backstore][ctrl_k] = limits return settings @Endpoint() @ReadPermission def portals(self): portals = [] gateways_config = IscsiGatewaysConfig.get_gateways_config() for name in gateways_config['gateways']: try: ip_addresses = IscsiClient.instance(gateway_name=name).get_ip_addresses() portals.append({'name': name, 'ip_addresses': ip_addresses['data']}) except RequestException: pass return sorted(portals, key=lambda p: '{}.{}'.format(p['name'], p['ip_addresses'])) @Endpoint() @ReadPermission def overview(self): result_gateways = [] result_images = [] gateways_names = IscsiGatewaysConfig.get_gateways_config()['gateways'].keys() config = None for gateway_name in gateways_names: try: config = IscsiClient.instance(gateway_name=gateway_name).get_config() break except RequestException: pass # Gateways info for gateway_name in gateways_names: gateway = { 'name': gateway_name, 'state': '', 'num_targets': 'n/a', 'num_sessions': 'n/a' } try: IscsiClient.instance(gateway_name=gateway_name).ping() gateway['state'] = 'up' if config: gateway['num_sessions'] = 0 if gateway_name in config['gateways']: gatewayinfo = IscsiClient.instance( gateway_name=gateway_name).get_gatewayinfo() gateway['num_sessions'] = gatewayinfo['num_sessions'] except RequestException: gateway['state'] = 'down' if config: gateway['num_targets'] = len([target for _, target in config['targets'].items() if gateway_name in target['portals']]) result_gateways.append(gateway) # Images info if config: tcmu_info = TcmuService.get_iscsi_info() for _, disk_config in config['disks'].items(): image = { 'pool': disk_config['pool'], 'image': disk_config['image'], 'backstore': disk_config['backstore'], 'optimized_since': None, 'stats': None, 'stats_history': None } tcmu_image_info = TcmuService.get_image_info(image['pool'], image['image'], tcmu_info) if tcmu_image_info: if 'optimized_since' in tcmu_image_info: image['optimized_since'] = tcmu_image_info['optimized_since'] if 'stats' in tcmu_image_info: image['stats'] = tcmu_image_info['stats'] if 'stats_history' in tcmu_image_info: image['stats_history'] = tcmu_image_info['stats_history'] result_images.append(image) return { 'gateways': sorted(result_gateways, key=lambda g: g['name']), 'images': sorted(result_images, key=lambda i: '{}/{}'.format(i['pool'], i['image'])) } @APIRouter('/iscsi', Scope.ISCSI) @APIDoc("Iscsi Management API", "Iscsi") class Iscsi(BaseController): @Endpoint('GET', 'discoveryauth') @ReadPermission @EndpointDoc("Get Iscsi discoveryauth Details", responses={'200': [ISCSI_SCHEMA]}) def get_discoveryauth(self): gateway = get_available_gateway() return self._get_discoveryauth(gateway) @Endpoint('PUT', 'discoveryauth', query_params=['user', 'password', 'mutual_user', 'mutual_password']) @UpdatePermission @EndpointDoc("Set Iscsi discoveryauth", parameters={ 'user': (str, 'Username'), 'password': (str, 'Password'), 'mutual_user': (str, 'Mutual UserName'), 'mutual_password': (str, 'Mutual Password'), }) def set_discoveryauth(self, user, password, mutual_user, mutual_password): validate_auth({ 'user': user, 'password': password, 'mutual_user': mutual_user, 'mutual_password': mutual_password }) gateway = get_available_gateway() config = IscsiClient.instance(gateway_name=gateway).get_config() gateway_names = list(config['gateways'].keys()) validate_rest_api(gateway_names) IscsiClient.instance(gateway_name=gateway).update_discoveryauth(user, password, mutual_user, mutual_password) return self._get_discoveryauth(gateway) def _get_discoveryauth(self, gateway): config = IscsiClient.instance(gateway_name=gateway).get_config() user = config['discovery_auth']['username'] password = config['discovery_auth']['password'] mutual_user = config['discovery_auth']['mutual_username'] mutual_password = config['discovery_auth']['mutual_password'] return { 'user': user, 'password': password, 'mutual_user': mutual_user, 'mutual_password': mutual_password } def iscsi_target_task(name, metadata, wait_for=2.0): return Task("iscsi/target/{}".format(name), metadata, wait_for) @APIRouter('/iscsi/target', Scope.ISCSI) @APIDoc("Get Iscsi Target Details", "IscsiTarget") class IscsiTarget(RESTController): def list(self): gateway = get_available_gateway() config = IscsiClient.instance(gateway_name=gateway).get_config() targets = [] for target_iqn in config['targets'].keys(): target = IscsiTarget._config_to_target(target_iqn, config) IscsiTarget._set_info(target) targets.append(target) return targets def get(self, target_iqn): gateway = get_available_gateway() config = IscsiClient.instance(gateway_name=gateway).get_config() if target_iqn not in config['targets']: raise cherrypy.HTTPError(404) target = IscsiTarget._config_to_target(target_iqn, config) IscsiTarget._set_info(target) return target @iscsi_target_task('delete', {'target_iqn': '{target_iqn}'}) def delete(self, target_iqn): gateway = get_available_gateway() config = IscsiClient.instance(gateway_name=gateway).get_config() if target_iqn not in config['targets']: raise DashboardException(msg='Target does not exist', code='target_does_not_exist', component='iscsi') portal_names = list(config['targets'][target_iqn]['portals'].keys()) validate_rest_api(portal_names) if portal_names: portal_name = portal_names[0] target_info = IscsiClient.instance(gateway_name=portal_name).get_targetinfo(target_iqn) if target_info['num_sessions'] > 0: raise DashboardException(msg='Target has active sessions', code='target_has_active_sessions', component='iscsi') IscsiTarget._delete(target_iqn, config, 0, 100) @iscsi_target_task('create', {'target_iqn': '{target_iqn}'}) def create(self, target_iqn=None, target_controls=None, acl_enabled=None, auth=None, portals=None, disks=None, clients=None, groups=None): target_controls = target_controls or {} portals = portals or [] disks = disks or [] clients = clients or [] groups = groups or [] validate_auth(auth) for client in clients: validate_auth(client['auth']) gateway = get_available_gateway() config = IscsiClient.instance(gateway_name=gateway).get_config() if target_iqn in config['targets']: raise DashboardException(msg='Target already exists', code='target_already_exists', component='iscsi') settings = IscsiClient.instance(gateway_name=gateway).get_settings() IscsiTarget._validate(target_iqn, target_controls, portals, disks, groups, settings) IscsiTarget._create(target_iqn, target_controls, acl_enabled, auth, portals, disks, clients, groups, 0, 100, config, settings) @iscsi_target_task('edit', {'target_iqn': '{target_iqn}'}) def set(self, target_iqn, new_target_iqn=None, target_controls=None, acl_enabled=None, auth=None, portals=None, disks=None, clients=None, groups=None): target_controls = target_controls or {} portals = IscsiTarget._sorted_portals(portals) disks = IscsiTarget._sorted_disks(disks) clients = IscsiTarget._sorted_clients(clients) groups = IscsiTarget._sorted_groups(groups) validate_auth(auth) for client in clients: validate_auth(client['auth']) gateway = get_available_gateway() config = IscsiClient.instance(gateway_name=gateway).get_config() if target_iqn not in config['targets']: raise DashboardException(msg='Target does not exist', code='target_does_not_exist', component='iscsi') if target_iqn != new_target_iqn and new_target_iqn in config['targets']: raise DashboardException(msg='Target IQN already in use', code='target_iqn_already_in_use', component='iscsi') settings = IscsiClient.instance(gateway_name=gateway).get_settings() new_portal_names = {p['host'] for p in portals} old_portal_names = set(config['targets'][target_iqn]['portals'].keys()) deleted_portal_names = list(old_portal_names - new_portal_names) validate_rest_api(deleted_portal_names) IscsiTarget._validate(new_target_iqn, target_controls, portals, disks, groups, settings) IscsiTarget._validate_delete(gateway, target_iqn, config, new_target_iqn, target_controls, disks, clients, groups) config = IscsiTarget._delete(target_iqn, config, 0, 50, new_target_iqn, target_controls, portals, disks, clients, groups) IscsiTarget._create(new_target_iqn, target_controls, acl_enabled, auth, portals, disks, clients, groups, 50, 100, config, settings) @staticmethod def _delete(target_iqn, config, task_progress_begin, task_progress_end, new_target_iqn=None, new_target_controls=None, new_portals=None, new_disks=None, new_clients=None, new_groups=None): new_target_controls = new_target_controls or {} new_portals = new_portals or [] new_disks = new_disks or [] new_clients = new_clients or [] new_groups = new_groups or [] TaskManager.current_task().set_progress(task_progress_begin) target_config = config['targets'][target_iqn] if not target_config['portals'].keys(): raise DashboardException(msg="Cannot delete a target that doesn't contain any portal", code='cannot_delete_target_without_portals', component='iscsi') target = IscsiTarget._config_to_target(target_iqn, config) n_groups = len(target_config['groups']) n_clients = len(target_config['clients']) n_target_disks = len(target_config['disks']) task_progress_steps = n_groups + n_clients + n_target_disks task_progress_inc = 0 if task_progress_steps != 0: task_progress_inc = int((task_progress_end - task_progress_begin) / task_progress_steps) gateway_name = list(target_config['portals'].keys())[0] deleted_groups = [] for group_id in list(target_config['groups'].keys()): if IscsiTarget._group_deletion_required(target, new_target_iqn, new_target_controls, new_groups, group_id): deleted_groups.append(group_id) IscsiClient.instance(gateway_name=gateway_name).delete_group(target_iqn, group_id) else: group = IscsiTarget._get_group(new_groups, group_id) old_group_disks = set(target_config['groups'][group_id]['disks'].keys()) new_group_disks = {'{}/{}'.format(x['pool'], x['image']) for x in group['disks']} local_deleted_disks = list(old_group_disks - new_group_disks) old_group_members = set(target_config['groups'][group_id]['members']) new_group_members = set(group['members']) local_deleted_members = list(old_group_members - new_group_members) if local_deleted_disks or local_deleted_members: IscsiClient.instance(gateway_name=gateway_name).update_group( target_iqn, group_id, local_deleted_members, local_deleted_disks) TaskManager.current_task().inc_progress(task_progress_inc) deleted_clients = [] deleted_client_luns = [] for client_iqn, client_config in target_config['clients'].items(): if IscsiTarget._client_deletion_required(target, new_target_iqn, new_target_controls, new_clients, client_iqn): deleted_clients.append(client_iqn) IscsiClient.instance(gateway_name=gateway_name).delete_client(target_iqn, client_iqn) else: for image_id in list(client_config.get('luns', {}).keys()): if IscsiTarget._client_lun_deletion_required(target, client_iqn, image_id, new_clients, new_groups): deleted_client_luns.append((client_iqn, image_id)) IscsiClient.instance(gateway_name=gateway_name).delete_client_lun( target_iqn, client_iqn, image_id) TaskManager.current_task().inc_progress(task_progress_inc) for image_id in target_config['disks']: if IscsiTarget._target_lun_deletion_required(target, new_target_iqn, new_target_controls, new_disks, image_id): all_clients = target_config['clients'].keys() not_deleted_clients = [c for c in all_clients if c not in deleted_clients and not IscsiTarget._client_in_group(target['groups'], c) and not IscsiTarget._client_in_group(new_groups, c)] for client_iqn in not_deleted_clients: client_image_ids = target_config['clients'][client_iqn]['luns'].keys() for client_image_id in client_image_ids: if image_id == client_image_id and \ (client_iqn, client_image_id) not in deleted_client_luns: IscsiClient.instance(gateway_name=gateway_name).delete_client_lun( target_iqn, client_iqn, client_image_id) IscsiClient.instance(gateway_name=gateway_name).delete_target_lun(target_iqn, image_id) pool, image = image_id.split('/', 1) IscsiClient.instance(gateway_name=gateway_name).delete_disk(pool, image) TaskManager.current_task().inc_progress(task_progress_inc) old_portals_by_host = IscsiTarget._get_portals_by_host(target['portals']) new_portals_by_host = IscsiTarget._get_portals_by_host(new_portals) for old_portal_host, old_portal_ip_list in old_portals_by_host.items(): if IscsiTarget._target_portal_deletion_required(old_portal_host, old_portal_ip_list, new_portals_by_host): IscsiClient.instance(gateway_name=gateway_name).delete_gateway(target_iqn, old_portal_host) if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls): IscsiClient.instance(gateway_name=gateway_name).delete_target(target_iqn) TaskManager.current_task().set_progress(task_progress_end) return IscsiClient.instance(gateway_name=gateway_name).get_config() @staticmethod def _get_group(groups, group_id): for group in groups: if group['group_id'] == group_id: return group return None @staticmethod def _group_deletion_required(target, new_target_iqn, new_target_controls, new_groups, group_id): if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls): return True new_group = IscsiTarget._get_group(new_groups, group_id) if not new_group: return True return False @staticmethod def _get_client(clients, client_iqn): for client in clients: if client['client_iqn'] == client_iqn: return client return None @staticmethod def _client_deletion_required(target, new_target_iqn, new_target_controls, new_clients, client_iqn): if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls): return True new_client = IscsiTarget._get_client(new_clients, client_iqn) if not new_client: return True return False @staticmethod def _client_in_group(groups, client_iqn): for group in groups: if client_iqn in group['members']: return True return False @staticmethod def _client_lun_deletion_required(target, client_iqn, image_id, new_clients, new_groups): new_client = IscsiTarget._get_client(new_clients, client_iqn) if not new_client: return True # Disks inherited from groups must be considered was_in_group = IscsiTarget._client_in_group(target['groups'], client_iqn) is_in_group = IscsiTarget._client_in_group(new_groups, client_iqn) if not was_in_group and is_in_group: return True if is_in_group: return False new_lun = IscsiTarget._get_disk(new_client.get('luns', []), image_id) if not new_lun: return True old_client = IscsiTarget._get_client(target['clients'], client_iqn) if not old_client: return False old_lun = IscsiTarget._get_disk(old_client.get('luns', []), image_id) return new_lun != old_lun @staticmethod def _get_disk(disks, image_id): for disk in disks: if '{}/{}'.format(disk['pool'], disk['image']) == image_id: return disk return None @staticmethod def _target_lun_deletion_required(target, new_target_iqn, new_target_controls, new_disks, image_id): if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls): return True new_disk = IscsiTarget._get_disk(new_disks, image_id) if not new_disk: return True old_disk = IscsiTarget._get_disk(target['disks'], image_id) new_disk_without_controls = deepcopy(new_disk) new_disk_without_controls.pop('controls') old_disk_without_controls = deepcopy(old_disk) old_disk_without_controls.pop('controls') if new_disk_without_controls != old_disk_without_controls: return True return False @staticmethod def _target_portal_deletion_required(old_portal_host, old_portal_ip_list, new_portals_by_host): if old_portal_host not in new_portals_by_host: return True if sorted(old_portal_ip_list) != sorted(new_portals_by_host[old_portal_host]): return True return False @staticmethod def _target_deletion_required(target, new_target_iqn, new_target_controls): gateway = get_available_gateway() settings = IscsiClient.instance(gateway_name=gateway).get_settings() if target['target_iqn'] != new_target_iqn: return True if settings['api_version'] < 2 and target['target_controls'] != new_target_controls: return True return False @staticmethod def _validate(target_iqn, target_controls, portals, disks, groups, settings): if not target_iqn: raise DashboardException(msg='Target IQN is required', code='target_iqn_required', component='iscsi') minimum_gateways = max(1, settings['config']['minimum_gateways']) portals_by_host = IscsiTarget._get_portals_by_host(portals) if len(portals_by_host.keys()) < minimum_gateways: if minimum_gateways == 1: msg = 'At least one portal is required' else: msg = 'At least {} portals are required'.format(minimum_gateways) raise DashboardException(msg=msg, code='portals_required', component='iscsi') # 'target_controls_limits' was introduced in ceph-iscsi > 3.2 # When using an older `ceph-iscsi` version these validations will # NOT be executed beforehand if 'target_controls_limits' in settings: for target_control_name, target_control_value in target_controls.items(): limits = settings['target_controls_limits'].get(target_control_name) if limits is not None: min_value = limits.get('min') if min_value is not None and target_control_value < min_value: raise DashboardException(msg='Target control {} must be >= ' '{}'.format(target_control_name, min_value), code='target_control_invalid_min', component='iscsi') max_value = limits.get('max') if max_value is not None and target_control_value > max_value: raise DashboardException(msg='Target control {} must be <= ' '{}'.format(target_control_name, max_value), code='target_control_invalid_max', component='iscsi') portal_names = [p['host'] for p in portals] validate_rest_api(portal_names) for disk in disks: pool = disk['pool'] image = disk['image'] backstore = disk['backstore'] required_rbd_features = settings['required_rbd_features'][backstore] unsupported_rbd_features = settings['unsupported_rbd_features'][backstore] IscsiTarget._validate_image(pool, image, backstore, required_rbd_features, unsupported_rbd_features) # 'disk_controls_limits' was introduced in ceph-iscsi > 3.2 # When using an older `ceph-iscsi` version these validations will # NOT be executed beforehand if 'disk_controls_limits' in settings: for disk_control_name, disk_control_value in disk['controls'].items(): limits = settings['disk_controls_limits'][backstore].get(disk_control_name) if limits is not None: min_value = limits.get('min') if min_value is not None and disk_control_value < min_value: raise DashboardException(msg='Disk control {} must be >= ' '{}'.format(disk_control_name, min_value), code='disk_control_invalid_min', component='iscsi') max_value = limits.get('max') if max_value is not None and disk_control_value > max_value: raise DashboardException(msg='Disk control {} must be <= ' '{}'.format(disk_control_name, max_value), code='disk_control_invalid_max', component='iscsi') initiators = [] # type: List[Any] for group in groups: initiators = initiators + group['members'] if len(initiators) != len(set(initiators)): raise DashboardException(msg='Each initiator can only be part of 1 group at a time', code='initiator_in_multiple_groups', component='iscsi') @staticmethod def _validate_image(pool, image, backstore, required_rbd_features, unsupported_rbd_features): try: ioctx = mgr.rados.open_ioctx(pool) try: with rbd.Image(ioctx, image) as img: if img.features() & required_rbd_features != required_rbd_features: raise DashboardException(msg='Image {} cannot be exported using {} ' 'backstore because required features are ' 'missing (required features are ' '{})'.format(image, backstore, format_bitmask( required_rbd_features)), code='image_missing_required_features', component='iscsi') if img.features() & unsupported_rbd_features != 0: raise DashboardException(msg='Image {} cannot be exported using {} ' 'backstore because it contains unsupported ' 'features (' '{})'.format(image, backstore, format_bitmask( unsupported_rbd_features)), code='image_contains_unsupported_features', component='iscsi') except rbd.ImageNotFound: raise DashboardException(msg='Image {} does not exist'.format(image), code='image_does_not_exist', component='iscsi') except rados.ObjectNotFound: raise DashboardException(msg='Pool {} does not exist'.format(pool), code='pool_does_not_exist', component='iscsi') @staticmethod def _validate_delete(gateway, target_iqn, config, new_target_iqn=None, new_target_controls=None, new_disks=None, new_clients=None, new_groups=None): new_target_controls = new_target_controls or {} new_disks = new_disks or [] new_clients = new_clients or [] new_groups = new_groups or [] target_config = config['targets'][target_iqn] target = IscsiTarget._config_to_target(target_iqn, config) for client_iqn in list(target_config['clients'].keys()): if IscsiTarget._client_deletion_required(target, new_target_iqn, new_target_controls, new_clients, client_iqn): client_info = IscsiClient.instance(gateway_name=gateway).get_clientinfo(target_iqn, client_iqn) if client_info.get('state', {}).get('LOGGED_IN', []): raise DashboardException(msg="Client '{}' cannot be deleted until it's logged " "out".format(client_iqn), code='client_logged_in', component='iscsi') @staticmethod def _update_targetauth(config, target_iqn, auth, gateway_name): # Target level authentication was introduced in ceph-iscsi config v11 if config['version'] > 10: user = auth['user'] password = auth['password'] mutual_user = auth['mutual_user'] mutual_password = auth['mutual_password'] IscsiClient.instance(gateway_name=gateway_name).update_targetauth(target_iqn, user, password, mutual_user, mutual_password) @staticmethod def _update_targetacl(target_config, target_iqn, acl_enabled, gateway_name): if not target_config or target_config['acl_enabled'] != acl_enabled: targetauth_action = ('enable_acl' if acl_enabled else 'disable_acl') IscsiClient.instance(gateway_name=gateway_name).update_targetacl(target_iqn, targetauth_action) @staticmethod def _is_auth_equal(auth_config, auth): return auth['user'] == auth_config['username'] and \ auth['password'] == auth_config['password'] and \ auth['mutual_user'] == auth_config['mutual_username'] and \ auth['mutual_password'] == auth_config['mutual_password'] @staticmethod def _create(target_iqn, target_controls, acl_enabled, auth, portals, disks, clients, groups, task_progress_begin, task_progress_end, config, settings): target_config = config['targets'].get(target_iqn, None) TaskManager.current_task().set_progress(task_progress_begin) portals_by_host = IscsiTarget._get_portals_by_host(portals) n_hosts = len(portals_by_host) n_disks = len(disks) n_clients = len(clients) n_groups = len(groups) task_progress_steps = n_hosts + n_disks + n_clients + n_groups task_progress_inc = 0 if task_progress_steps != 0: task_progress_inc = int((task_progress_end - task_progress_begin) / task_progress_steps) try: gateway_name = portals[0]['host'] if not target_config: IscsiClient.instance(gateway_name=gateway_name).create_target(target_iqn, target_controls) for host, ip_list in portals_by_host.items(): if not target_config or host not in target_config['portals']: IscsiClient.instance(gateway_name=gateway_name).create_gateway(target_iqn, host, ip_list) TaskManager.current_task().inc_progress(task_progress_inc) if not target_config or \ acl_enabled != target_config['acl_enabled'] or \ not IscsiTarget._is_auth_equal(target_config['auth'], auth): if acl_enabled: IscsiTarget._update_targetauth(config, target_iqn, auth, gateway_name) IscsiTarget._update_targetacl(target_config, target_iqn, acl_enabled, gateway_name) else: IscsiTarget._update_targetacl(target_config, target_iqn, acl_enabled, gateway_name) IscsiTarget._update_targetauth(config, target_iqn, auth, gateway_name) for disk in disks: pool = disk['pool'] image = disk['image'] image_id = '{}/{}'.format(pool, image) backstore = disk['backstore'] wwn = disk.get('wwn') lun = disk.get('lun') if image_id not in config['disks']: IscsiClient.instance(gateway_name=gateway_name).create_disk(pool, image, backstore, wwn) if not target_config or image_id not in target_config['disks']: IscsiClient.instance(gateway_name=gateway_name).create_target_lun(target_iqn, image_id, lun) controls = disk['controls'] d_conf_controls = {} if image_id in config['disks']: d_conf_controls = config['disks'][image_id]['controls'] disk_default_controls = settings['disk_default_controls'][backstore] for old_control in d_conf_controls.keys(): # If control was removed, restore the default value if old_control not in controls: controls[old_control] = disk_default_controls[old_control] if (image_id not in config['disks'] or d_conf_controls != controls) and controls: IscsiClient.instance(gateway_name=gateway_name).reconfigure_disk(pool, image, controls) TaskManager.current_task().inc_progress(task_progress_inc) for client in clients: client_iqn = client['client_iqn'] if not target_config or client_iqn not in target_config['clients']: IscsiClient.instance(gateway_name=gateway_name).create_client(target_iqn, client_iqn) if not target_config or client_iqn not in target_config['clients'] or \ not IscsiTarget._is_auth_equal(target_config['clients'][client_iqn]['auth'], client['auth']): user = client['auth']['user'] password = client['auth']['password'] m_user = client['auth']['mutual_user'] m_password = client['auth']['mutual_password'] IscsiClient.instance(gateway_name=gateway_name).create_client_auth( target_iqn, client_iqn, user, password, m_user, m_password) for lun in client['luns']: pool = lun['pool'] image = lun['image'] image_id = '{}/{}'.format(pool, image) # Disks inherited from groups must be considered group_disks = [] for group in groups: if client_iqn in group['members']: group_disks = ['{}/{}'.format(x['pool'], x['image']) for x in group['disks']] if not target_config or client_iqn not in target_config['clients'] or \ (image_id not in target_config['clients'][client_iqn]['luns'] and image_id not in group_disks): IscsiClient.instance(gateway_name=gateway_name).create_client_lun( target_iqn, client_iqn, image_id) TaskManager.current_task().inc_progress(task_progress_inc) for group in groups: group_id = group['group_id'] members = group['members'] image_ids = [] for disk in group['disks']: image_ids.append('{}/{}'.format(disk['pool'], disk['image'])) if target_config and group_id in target_config['groups']: old_members = target_config['groups'][group_id]['members'] old_disks = target_config['groups'][group_id]['disks'].keys() if not target_config or group_id not in target_config['groups'] or \ list(set(group['members']) - set(old_members)) or \ list(set(image_ids) - set(old_disks)): IscsiClient.instance(gateway_name=gateway_name).create_group( target_iqn, group_id, members, image_ids) TaskManager.current_task().inc_progress(task_progress_inc) if target_controls: if not target_config or target_controls != target_config['controls']: IscsiClient.instance(gateway_name=gateway_name).reconfigure_target( target_iqn, target_controls) TaskManager.current_task().set_progress(task_progress_end) except RequestException as e: if e.content: content = json.loads(e.content) content_message = content.get('message') if content_message: raise DashboardException(msg=content_message, component='iscsi') raise DashboardException(e=e, component='iscsi') @staticmethod def _config_to_target(target_iqn, config): target_config = config['targets'][target_iqn] portals = [] for host, portal_config in target_config['portals'].items(): for portal_ip in portal_config['portal_ip_addresses']: portal = { 'host': host, 'ip': portal_ip } portals.append(portal) portals = IscsiTarget._sorted_portals(portals) disks = [] for target_disk in target_config['disks']: disk_config = config['disks'][target_disk] disk = { 'pool': disk_config['pool'], 'image': disk_config['image'], 'controls': disk_config['controls'], 'backstore': disk_config['backstore'], 'wwn': disk_config['wwn'] } # lun_id was introduced in ceph-iscsi config v11 if config['version'] > 10: disk['lun'] = target_config['disks'][target_disk]['lun_id'] disks.append(disk) disks = IscsiTarget._sorted_disks(disks) clients = [] for client_iqn, client_config in target_config['clients'].items(): luns = [] for client_lun in client_config['luns'].keys(): pool, image = client_lun.split('/', 1) lun = { 'pool': pool, 'image': image } luns.append(lun) user = client_config['auth']['username'] password = client_config['auth']['password'] mutual_user = client_config['auth']['mutual_username'] mutual_password = client_config['auth']['mutual_password'] client = { 'client_iqn': client_iqn, 'luns': luns, 'auth': { 'user': user, 'password': password, 'mutual_user': mutual_user, 'mutual_password': mutual_password } } clients.append(client) clients = IscsiTarget._sorted_clients(clients) groups = [] for group_id, group_config in target_config['groups'].items(): group_disks = [] for group_disk_key, _ in group_config['disks'].items(): pool, image = group_disk_key.split('/', 1) group_disk = { 'pool': pool, 'image': image } group_disks.append(group_disk) group = { 'group_id': group_id, 'disks': group_disks, 'members': group_config['members'], } groups.append(group) groups = IscsiTarget._sorted_groups(groups) target_controls = target_config['controls'] acl_enabled = target_config['acl_enabled'] target = { 'target_iqn': target_iqn, 'portals': portals, 'disks': disks, 'clients': clients, 'groups': groups, 'target_controls': target_controls, 'acl_enabled': acl_enabled } # Target level authentication was introduced in ceph-iscsi config v11 if config['version'] > 10: target_user = target_config['auth']['username'] target_password = target_config['auth']['password'] target_mutual_user = target_config['auth']['mutual_username'] target_mutual_password = target_config['auth']['mutual_password'] target['auth'] = { 'user': target_user, 'password': target_password, 'mutual_user': target_mutual_user, 'mutual_password': target_mutual_password } return target @staticmethod def _is_executing(target_iqn): executing_tasks, _ = TaskManager.list() for t in executing_tasks: if t.name.startswith('iscsi/target') and t.metadata.get('target_iqn') == target_iqn: return True return False @staticmethod def _set_info(target): if not target['portals']: return target_iqn = target['target_iqn'] # During task execution, additional info is not available if IscsiTarget._is_executing(target_iqn): return # If any portal is down, additional info is not available for portal in target['portals']: try: IscsiClient.instance(gateway_name=portal['host']).ping() except (IscsiGatewayDoesNotExist, RequestException): return gateway_name = target['portals'][0]['host'] try: target_info = IscsiClient.instance(gateway_name=gateway_name).get_targetinfo( target_iqn) target['info'] = target_info for client in target['clients']: client_iqn = client['client_iqn'] client_info = IscsiClient.instance(gateway_name=gateway_name).get_clientinfo( target_iqn, client_iqn) client['info'] = client_info except RequestException as e: # Target/Client has been removed in the meanwhile (e.g. using gwcli) if e.status_code != 404: raise e @staticmethod def _sorted_portals(portals): portals = portals or [] return sorted(portals, key=lambda p: '{}.{}'.format(p['host'], p['ip'])) @staticmethod def _sorted_disks(disks): disks = disks or [] return sorted(disks, key=lambda d: '{}.{}'.format(d['pool'], d['image'])) @staticmethod def _sorted_clients(clients): clients = clients or [] for client in clients: client['luns'] = sorted(client['luns'], key=lambda d: '{}.{}'.format(d['pool'], d['image'])) return sorted(clients, key=lambda c: c['client_iqn']) @staticmethod def _sorted_groups(groups): groups = groups or [] for group in groups: group['disks'] = sorted(group['disks'], key=lambda d: '{}.{}'.format(d['pool'], d['image'])) group['members'] = sorted(group['members']) return sorted(groups, key=lambda g: g['group_id']) @staticmethod def _get_portals_by_host(portals): # type: (List[dict]) -> Dict[str, List[str]] portals_by_host = {} # type: Dict[str, List[str]] for portal in portals: host = portal['host'] ip = portal['ip'] if host not in portals_by_host: portals_by_host[host] = [] portals_by_host[host].append(ip) return portals_by_host def get_available_gateway(): gateways = IscsiGatewaysConfig.get_gateways_config()['gateways'] if not gateways: raise DashboardException(msg='There are no gateways defined', code='no_gateways_defined', component='iscsi') for gateway in gateways: try: IscsiClient.instance(gateway_name=gateway).ping() return gateway except RequestException: pass raise DashboardException(msg='There are no gateways available', code='no_gateways_available', component='iscsi') def validate_rest_api(gateways): for gateway in gateways: try: IscsiClient.instance(gateway_name=gateway).ping() except RequestException: raise DashboardException(msg='iSCSI REST Api not available for gateway ' '{}'.format(gateway), code='ceph_iscsi_rest_api_not_available_for_gateway', component='iscsi') def validate_auth(auth): username_regex = re.compile(r'^[\w\.:@_-]{8,64}$') password_regex = re.compile(r'^[\w@\-_\/]{12,16}$') result = True if auth['user'] or auth['password']: result = bool(username_regex.match(auth['user'])) and \ bool(password_regex.match(auth['password'])) if auth['mutual_user'] or auth['mutual_password']: result = result and bool(username_regex.match(auth['mutual_user'])) and \ bool(password_regex.match(auth['mutual_password'])) and auth['user'] if not result: raise DashboardException(msg='Bad authentication', code='target_bad_auth', component='iscsi')