The default value is 45 seconds.
+Enabling iSCSI Management
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The Ceph Manager Dashboard can manage iSCSI targets using the REST API provided
+by the `rbd-target-api` service of the `ceph-iscsi <https://github.com/ceph/ceph-iscsi>`_
+project. Please make sure that it's installed and enabled on the iSCSI gateways.
+
+The available iSCSI gateways must be defined using the following commands::
+
+ $ ceph dashboard iscsi-gateway-list
+ $ ceph dashboard iscsi-gateway-add <gateway_name> <scheme>://<username>:<password>@<host>[:port]
+ $ ceph dashboard iscsi-gateway-rm <gateway_name>
+
Enabling the Embedding of Grafana Dashboards
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
--- /dev/null
+# -*- coding: utf-8 -*-
+# pylint: disable=too-many-branches
+
+from __future__ import absolute_import
+
+from copy import deepcopy
+import json
+import cherrypy
+
+import rados
+import rbd
+
+from . import ApiController, UiApiController, RESTController, BaseController, Endpoint,\
+ ReadPermission, Task
+from .. import mgr
+from ..rest_client import RequestException
+from ..security import Scope
+from ..services.iscsi_client import IscsiClient
+from ..services.iscsi_cli import IscsiGatewaysConfig
+from ..exceptions import DashboardException
+from ..tools import TaskManager
+
+
+@UiApiController('/iscsi', Scope.ISCSI)
+class Iscsi(BaseController):
+
+ @Endpoint()
+ @ReadPermission
+ def status(self):
+ status = {'available': False}
+ if not IscsiGatewaysConfig.get_gateways_config()['gateways']:
+ status['message'] = 'There are no gateways defined'
+ return status
+ try:
+ IscsiClient.instance().get_config()
+ status['available'] = True
+ except RequestException as e:
+ if e.content:
+ content = json.loads(e.content)
+ content_message = content.get('message')
+ if content_message:
+ status['message'] = content_message
+ return status
+
+ @Endpoint()
+ @ReadPermission
+ def settings(self):
+ return IscsiClient.instance().get_settings()
+
+ @Endpoint()
+ @ReadPermission
+ def portals(self):
+ portals = []
+ gateways_config = IscsiGatewaysConfig.get_gateways_config()
+ for name in gateways_config['gateways'].keys():
+ ip_addresses = IscsiClient.instance(gateway_name=name).get_ip_addresses()
+ portals.append({'name': name, 'ip_addresses': ip_addresses['data']})
+ return sorted(portals, key=lambda p: '{}.{}'.format(p['name'], p['ip_addresses']))
+
+
+def iscsi_target_task(name, metadata, wait_for=2.0):
+ return Task("iscsi/target/{}".format(name), metadata, wait_for)
+
+
+@ApiController('/iscsi/target', Scope.ISCSI)
+class IscsiTarget(RESTController):
+
+ def list(self):
+ config = IscsiClient.instance().get_config()
+ targets = []
+ for target_iqn in config['targets'].keys():
+ target = IscsiTarget._config_to_target(target_iqn, config)
+ targets.append(target)
+ return targets
+
+ def get(self, target_iqn):
+ config = IscsiClient.instance().get_config()
+ if target_iqn not in config['targets']:
+ raise cherrypy.HTTPError(404)
+ return IscsiTarget._config_to_target(target_iqn, config)
+
+ @iscsi_target_task('delete', {'target_iqn': '{target_iqn}'})
+ def delete(self, target_iqn):
+ config = IscsiClient.instance().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 not in config['targets']:
+ raise DashboardException(msg='Target does not exist',
+ code='target_does_not_exist',
+ 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,
+ 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 []
+
+ config = IscsiClient.instance().get_config()
+ if target_iqn in config['targets']:
+ raise DashboardException(msg='Target already exists',
+ code='target_already_exists',
+ component='iscsi')
+ IscsiTarget._validate(target_iqn, portals, disks)
+ IscsiTarget._create(target_iqn, target_controls, portals, disks, clients, groups, 0, 100,
+ config)
+
+ @iscsi_target_task('edit', {'target_iqn': '{target_iqn}'})
+ def set(self, target_iqn, new_target_iqn=None, target_controls=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)
+
+ config = IscsiClient.instance().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')
+ IscsiTarget._validate(new_target_iqn, portals, disks)
+ config = IscsiTarget._delete(target_iqn, config, 0, 50, new_target_iqn, target_controls,
+ portals, disks, clients, groups)
+ IscsiTarget._create(new_target_iqn, target_controls, portals, disks, clients, groups,
+ 50, 100, config)
+
+ @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_portals, new_groups, group_id, new_clients,
+ new_disks):
+ deleted_groups.append(group_id)
+ IscsiClient.instance(gateway_name=gateway_name).delete_group(target_iqn,
+ group_id)
+ TaskManager.current_task().inc_progress(task_progress_inc)
+ for client_iqn in list(target_config['clients'].keys()):
+ if IscsiTarget._client_deletion_required(target, new_target_iqn, new_target_controls,
+ new_portals, new_clients, client_iqn,
+ new_groups, deleted_groups):
+ IscsiClient.instance(gateway_name=gateway_name).delete_client(target_iqn,
+ client_iqn)
+ 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_portals,
+ new_disks, image_id):
+ IscsiClient.instance(gateway_name=gateway_name).delete_target_lun(target_iqn,
+ image_id)
+ IscsiClient.instance(gateway_name=gateway_name).delete_disk(image_id)
+ TaskManager.current_task().inc_progress(task_progress_inc)
+ if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls,
+ new_portals):
+ 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_portals,
+ new_groups, group_id, new_clients, new_disks):
+ if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls,
+ new_portals):
+ return True
+ new_group = IscsiTarget._get_group(new_groups, group_id)
+ if not new_group:
+ return True
+ old_group = IscsiTarget._get_group(target['groups'], group_id)
+ if new_group != old_group:
+ return True
+ # Check if any client inside this group has changed
+ for client_iqn in new_group['members']:
+ if IscsiTarget._client_deletion_required(target, new_target_iqn, new_target_controls,
+ new_portals, new_clients, client_iqn,
+ new_groups, []):
+ return True
+ # Check if any disk inside this group has changed
+ for disk in new_group['disks']:
+ image_id = '{}.{}'.format(disk['pool'], disk['image'])
+ if IscsiTarget._target_lun_deletion_required(target, new_target_iqn,
+ new_target_controls, new_portals,
+ new_disks, image_id):
+ 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_portals,
+ new_clients, client_iqn, new_groups, deleted_groups):
+ if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls,
+ new_portals):
+ return True
+ new_client = deepcopy(IscsiTarget._get_client(new_clients, client_iqn))
+ if not new_client:
+ return True
+ # Disks inherited from groups must be considered
+ for group in new_groups:
+ if client_iqn in group['members']:
+ new_client['luns'] += group['disks']
+ old_client = IscsiTarget._get_client(target['clients'], client_iqn)
+ if new_client != old_client:
+ return True
+ # Check if client belongs to a groups that has been deleted
+ for group in target['groups']:
+ if group['group_id'] in deleted_groups and client_iqn in group['members']:
+ return True
+ return False
+
+ @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_portals,
+ new_disks, image_id):
+ if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls,
+ new_portals):
+ 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)
+ if new_disk != old_disk:
+ return True
+ return False
+
+ @staticmethod
+ def _target_deletion_required(target, new_target_iqn, new_target_controls, new_portals):
+ if target['target_iqn'] != new_target_iqn:
+ return True
+ if target['target_controls'] != new_target_controls:
+ return True
+ if target['portals'] != new_portals:
+ return True
+ return False
+
+ @staticmethod
+ def _validate(target_iqn, portals, disks):
+ if not target_iqn:
+ raise DashboardException(msg='Target IQN is required',
+ code='target_iqn_required',
+ component='iscsi')
+
+ settings = IscsiClient.instance().get_settings()
+ 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')
+
+ for portal in portals:
+ gateway_name = portal['host']
+ try:
+ IscsiClient.instance(gateway_name=gateway_name).ping()
+ except RequestException:
+ raise DashboardException(msg='iSCSI REST Api not available for gateway '
+ '{}'.format(gateway_name),
+ code='ceph_iscsi_rest_api_not_available_for_gateway',
+ component='iscsi')
+
+ for disk in disks:
+ pool = disk['pool']
+ image = disk['image']
+ IscsiTarget._validate_image_exists(pool, image)
+
+ @staticmethod
+ def _validate_image_exists(pool, image):
+ try:
+ ioctx = mgr.rados.open_ioctx(pool)
+ try:
+ rbd.Image(ioctx, image)
+ 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 _create(target_iqn, target_controls,
+ portals, disks, clients, groups,
+ task_progress_begin, task_progress_end, config):
+ 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():
+ IscsiClient.instance(gateway_name=gateway_name).create_gateway(target_iqn,
+ host,
+ ip_list)
+ TaskManager.current_task().inc_progress(task_progress_inc)
+ for disk in disks:
+ pool = disk['pool']
+ image = disk['image']
+ image_id = '{}.{}'.format(pool, image)
+ if image_id not in config['disks']:
+ IscsiClient.instance(gateway_name=gateway_name).create_disk(image_id)
+ 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)
+ controls = disk['controls']
+ if controls:
+ IscsiClient.instance(gateway_name=gateway_name).reconfigure_disk(image_id,
+ 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)
+ for lun in client['luns']:
+ pool = lun['pool']
+ image = lun['image']
+ image_id = '{}.{}'.format(pool, image)
+ IscsiClient.instance(gateway_name=gateway_name).create_client_lun(
+ target_iqn, client_iqn, image_id)
+ user = client['auth']['user']
+ password = client['auth']['password']
+ chap = '{}/{}'.format(user, password) if user and password else ''
+ m_user = client['auth']['mutual_user']
+ m_password = client['auth']['mutual_password']
+ m_chap = '{}/{}'.format(m_user, m_password) if m_user and m_password else ''
+ IscsiClient.instance(gateway_name=gateway_name).create_client_auth(
+ target_iqn, client_iqn, chap, m_chap)
+ 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 not target_config or group_id not in target_config['groups']:
+ 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 in target_config['portals'].keys():
+ ips = IscsiClient.instance(gateway_name=host).get_ip_addresses()['data']
+ portal_ips = [ip for ip in ips if ip in target_config['ip_list']]
+ for portal_ip in portal_ips:
+ 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'],
+ }
+ 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 = None
+ password = None
+ if '/' in client_config['auth']['chap']:
+ user, password = client_config['auth']['chap'].split('/', 1)
+ mutual_user = None
+ mutual_password = None
+ if '/' in client_config['auth']['chap_mutual']:
+ mutual_user, mutual_password = client_config['auth']['chap_mutual'].split('/', 1)
+ 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']
+ for key, value in target_controls.items():
+ if isinstance(value, bool):
+ target_controls[key] = 'Yes' if value else 'No'
+ target = {
+ 'target_iqn': target_iqn,
+ 'portals': portals,
+ 'disks': disks,
+ 'clients': clients,
+ 'groups': groups,
+ 'target_controls': target_controls,
+ }
+ return target
+
+ @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):
+ portals_by_host = {}
+ 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
this.commonOperations.delete,
this.rbd_mirroring.pool_peer,
(metadata) => ({})
+ ),
+ // iSCSI target tasks
+ 'iscsi/target/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+ this.iscsiTarget(metadata)
+ ),
+ 'iscsi/target/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+ this.iscsiTarget(metadata)
+ ),
+ 'iscsi/target/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+ this.iscsiTarget(metadata)
)
};
return this.i18n(`erasure code profile '{{name}}'`, { name: metadata.name });
}
+ iscsiTarget(metadata) {
+ return this.i18n(`target '{{target_iqn}}'`, { target_iqn: metadata.target_iqn });
+ }
+
_getTaskTitle(task: Task) {
return this.messages[task.name] || this.defaultMessage;
}
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
+ <trans-unit id="369462e5e018360e0600bb570866201ad5c3c8a8" datatype="html">
+ <source>target '<x id="INTERPOLATION" equiv-text="{{target_iqn}}"/>'</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/task-message.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
</body>
</file>
</xliff>
from mgr_module import MgrModule, MgrStandbyModule
+# Imports required for CLI commands registration
+# pylint: disable=unused-import
+from .services import iscsi_cli
+
try:
import cherrypy
from cherrypy._cptools import HandlerWrapperTool
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import errno
+import json
+
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse
+
+from mgr_module import CLIReadCommand, CLIWriteCommand
+
+from .orchestrator import OrchClient
+from .. import mgr
+
+
+class IscsiGatewayAlreadyExists(Exception):
+ def __init__(self, gateway_name):
+ super(IscsiGatewayAlreadyExists, self).__init__(
+ "iSCSI gateway '{}' already exists".format(gateway_name))
+
+
+class IscsiGatewayDoesNotExist(Exception):
+ def __init__(self, hostname):
+ super(IscsiGatewayDoesNotExist, self).__init__(
+ "iSCSI gateway '{}' does not exist".format(hostname))
+
+
+class InvalidServiceUrl(Exception):
+ def __init__(self, service_url):
+ super(InvalidServiceUrl, self).__init__(
+ "Invalid service URL '{}'. "
+ "Valid format: '<scheme>://<username>:<password>@<host>[:port]'.".format(service_url))
+
+
+class ManagedByOrchestratorException(Exception):
+ def __init__(self):
+ super(ManagedByOrchestratorException, self).__init__(
+ "iSCSI configuration is managed by the orchestrator")
+
+
+_ISCSI_STORE_KEY = "_iscsi_config"
+
+
+class IscsiGatewaysConfig(object):
+ @classmethod
+ def _load_config(cls):
+ if OrchClient.instance().available():
+ raise ManagedByOrchestratorException()
+ json_db = mgr.get_store(_ISCSI_STORE_KEY,
+ '{"gateways": {}}')
+ return json.loads(json_db)
+
+ @classmethod
+ def _save_config(cls, config):
+ mgr.set_store(_ISCSI_STORE_KEY, json.dumps(config))
+
+ @classmethod
+ def add_gateway(cls, name, service_url):
+ config = cls._load_config()
+ if name in config:
+ raise IscsiGatewayAlreadyExists(name)
+ url = urlparse(service_url)
+ if not url.scheme or not url.hostname or not url.username or not url.password:
+ raise InvalidServiceUrl(service_url)
+ config['gateways'][name] = {'service_url': service_url}
+ cls._save_config(config)
+
+ @classmethod
+ def remove_gateway(cls, name):
+ config = cls._load_config()
+ if name not in config['gateways']:
+ raise IscsiGatewayDoesNotExist(name)
+
+ del config['gateways'][name]
+ cls._save_config(config)
+
+ @classmethod
+ def get_gateways_config(cls):
+ try:
+ config = cls._load_config()
+ except ManagedByOrchestratorException:
+ config = {'gateways': {}}
+ instances = OrchClient.instance().list_service_info("iscsi")
+ for instance in instances:
+ config['gateways'][instance.nodename] = {
+ 'service_url': instance.service_url
+ }
+ return config
+
+ @classmethod
+ def get_gateway_config(cls, name):
+ config = IscsiGatewaysConfig.get_gateways_config()
+ if name not in config['gateways']:
+ raise IscsiGatewayDoesNotExist(name)
+ return config['gateways'][name]
+
+
+@CLIReadCommand('dashboard iscsi-gateway-list', desc='List iSCSI gateways')
+def list_iscsi_gateways(_):
+ return 0, json.dumps(IscsiGatewaysConfig.get_gateways_config()), ''
+
+
+@CLIWriteCommand('dashboard iscsi-gateway-add',
+ 'name=name,type=CephString '
+ 'name=service_url,type=CephString',
+ 'Add iSCSI gateway configuration')
+def add_iscsi_gateway(_, name, service_url):
+ try:
+ IscsiGatewaysConfig.add_gateway(name, service_url)
+ return 0, 'Success', ''
+ except IscsiGatewayAlreadyExists as ex:
+ return -errno.EEXIST, '', str(ex)
+ except InvalidServiceUrl as ex:
+ return -errno.EINVAL, '', str(ex)
+ except ManagedByOrchestratorException as ex:
+ return -errno.EINVAL, '', str(ex)
+
+
+@CLIWriteCommand('dashboard iscsi-gateway-rm',
+ 'name=name,type=CephString',
+ 'Remove iSCSI gateway configuration')
+def remove_iscsi_gateway(_, name):
+ try:
+ IscsiGatewaysConfig.remove_gateway(name)
+ return 0, 'Success', ''
+ except IscsiGatewayDoesNotExist as ex:
+ return -errno.ENOENT, '', str(ex)
+ except ManagedByOrchestratorException as ex:
+ return -errno.EINVAL, '', str(ex)
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+
+from requests.auth import HTTPBasicAuth
+
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse
+
+from .iscsi_cli import IscsiGatewaysConfig
+from .. import logger
+from ..rest_client import RestClient
+
+
+class IscsiClient(RestClient):
+ _CLIENT_NAME = 'iscsi'
+ _instances = {}
+
+ service_url = None
+
+ @classmethod
+ def instance(cls, gateway_name=None):
+ if not gateway_name:
+ gateway_name = list(IscsiGatewaysConfig.get_gateways_config()['gateways'].keys())[0]
+ gateways_config = IscsiGatewaysConfig.get_gateway_config(gateway_name)
+ service_url = gateways_config['service_url']
+
+ instance = cls._instances.get(gateway_name)
+ if not instance or service_url != instance.service_url:
+ url = urlparse(service_url)
+ ssl = url.scheme == 'https'
+ host = url.hostname
+ port = url.port
+ username = url.username
+ password = url.password
+ if not port:
+ port = 443 if ssl else 80
+ auth = HTTPBasicAuth(username, password)
+ instance = IscsiClient(host, port, IscsiClient._CLIENT_NAME, ssl, auth)
+ instance.service_url = service_url
+ cls._instances[gateway_name] = instance
+
+ return instance
+
+ @RestClient.api_get('/api/_ping')
+ def ping(self, request=None):
+ return request()
+
+ @RestClient.api_get('/api/settings')
+ def get_settings(self, request=None):
+ return request()
+
+ @RestClient.api_get('/api/sysinfo/ip_addresses')
+ def get_ip_addresses(self, request=None):
+ return request()
+
+ @RestClient.api_get('/api/config')
+ def get_config(self, request=None):
+ return request()
+
+ @RestClient.api_put('/api/target/{target_iqn}')
+ def create_target(self, target_iqn, target_controls, request=None):
+ logger.debug("iSCSI: Creating target: %s", target_iqn)
+ return request({
+ 'controls': json.dumps(target_controls)
+ })
+
+ @RestClient.api_delete('/api/target/{target_iqn}')
+ def delete_target(self, target_iqn, request=None):
+ logger.debug("iSCSI: Deleting target: %s", target_iqn)
+ return request()
+
+ @RestClient.api_put('/api/target/{target_iqn}')
+ def reconfigure_target(self, target_iqn, target_controls, request=None):
+ logger.debug("iSCSI: Reconfiguring target: %s", target_iqn)
+ return request({
+ 'mode': 'reconfigure',
+ 'controls': json.dumps(target_controls)
+ })
+
+ @RestClient.api_put('/api/gateway/{target_iqn}/{gateway_name}')
+ def create_gateway(self, target_iqn, gateway_name, ip_address, request=None):
+ logger.debug("iSCSI: Creating gateway: %s/%s", target_iqn, gateway_name)
+ return request({
+ 'ip_address': ','.join(ip_address),
+ 'skipchecks': 'true'
+ })
+
+ @RestClient.api_put('/api/disk/{image_id}')
+ def create_disk(self, image_id, request=None):
+ logger.debug("iSCSI: Creating disk: %s", image_id)
+ return request({
+ 'mode': 'create'
+ })
+
+ @RestClient.api_delete('/api/disk/{image_id}')
+ def delete_disk(self, image_id, request=None):
+ logger.debug("iSCSI: Deleting disk: %s", image_id)
+ return request({
+ 'preserve_image': 'true'
+ })
+
+ @RestClient.api_put('/api/disk/{image_id}')
+ def reconfigure_disk(self, image_id, controls, request=None):
+ logger.debug("iSCSI: Reconfiguring disk: %s", image_id)
+ return request({
+ 'controls': json.dumps(controls),
+ 'mode': 'reconfigure'
+ })
+
+ @RestClient.api_put('/api/targetlun/{target_iqn}')
+ def create_target_lun(self, target_iqn, image_id, request=None):
+ logger.debug("iSCSI: Creating target lun: %s/%s", target_iqn, image_id)
+ return request({
+ 'disk': image_id
+ })
+
+ @RestClient.api_delete('/api/targetlun/{target_iqn}')
+ def delete_target_lun(self, target_iqn, image_id, request=None):
+ logger.debug("iSCSI: Deleting target lun: %s/%s", target_iqn, image_id)
+ return request({
+ 'disk': image_id
+ })
+
+ @RestClient.api_put('/api/client/{target_iqn}/{client_iqn}')
+ def create_client(self, target_iqn, client_iqn, request=None):
+ logger.debug("iSCSI: Creating client: %s/%s", target_iqn, client_iqn)
+ return request()
+
+ @RestClient.api_delete('/api/client/{target_iqn}/{client_iqn}')
+ def delete_client(self, target_iqn, client_iqn, request=None):
+ logger.debug("iSCSI: Deleting client: %s/%s", target_iqn, client_iqn)
+ return request()
+
+ @RestClient.api_put('/api/clientlun/{target_iqn}/{client_iqn}')
+ def create_client_lun(self, target_iqn, client_iqn, image_id, request=None):
+ logger.debug("iSCSI: Creating client lun: %s/%s", target_iqn, client_iqn)
+ return request({
+ 'disk': image_id
+ })
+
+ @RestClient.api_put('/api/clientauth/{target_iqn}/{client_iqn}')
+ def create_client_auth(self, target_iqn, client_iqn, chap, chap_mutual, request=None):
+ logger.debug("iSCSI: Creating client auth: %s/%s/%s/%s",
+ target_iqn, client_iqn, chap, chap_mutual)
+ return request({
+ 'chap': chap,
+ 'chap_mutual': chap_mutual
+ })
+
+ @RestClient.api_put('/api/hostgroup/{target_iqn}/{group_name}')
+ def create_group(self, target_iqn, group_name, members, image_ids, request=None):
+ logger.debug("iSCSI: Creating group: %s/%s", target_iqn, group_name)
+ return request({
+ 'members': ','.join(members),
+ 'disks': ','.join(image_ids)
+ })
+
+ @RestClient.api_delete('/api/hostgroup/{target_iqn}/{group_name}')
+ def delete_group(self, target_iqn, group_name, request=None):
+ logger.debug("iSCSI: Deleting group: %s/%s", target_iqn, group_name)
+ return request()
--- /dev/null
+import copy
+import mock
+
+from .helper import ControllerTestCase
+from .. import mgr
+from ..controllers.iscsi import IscsiTarget
+from ..services.iscsi_client import IscsiClient
+
+
+class IscsiTest(ControllerTestCase):
+
+ @classmethod
+ def setup_server(cls):
+ mgr.rados.side_effect = None
+ # pylint: disable=protected-access
+ IscsiTarget._cp_config['tools.authenticate.on'] = False
+ cls.setup_controllers([IscsiTarget])
+
+ def setUp(self):
+ # pylint: disable=protected-access
+ IscsiClientMock._instance = IscsiClientMock()
+ IscsiClient.instance = IscsiClientMock.instance
+
+ def test_list_empty(self):
+ self._get('/api/iscsi/target')
+ self.assertStatus(200)
+ self.assertJsonBody([])
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_list(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw1"
+ request = copy.deepcopy(iscsi_target_request)
+ request['target_iqn'] = target_iqn
+ self._post('/api/iscsi/target', request)
+ self.assertStatus(201)
+ self._get('/api/iscsi/target')
+ self.assertStatus(200)
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ self.assertJsonBody([response])
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_create(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw2"
+ request = copy.deepcopy(iscsi_target_request)
+ request['target_iqn'] = target_iqn
+ self._post('/api/iscsi/target', request)
+ self.assertStatus(201)
+ self._get('/api/iscsi/target/{}'.format(request['target_iqn']))
+ self.assertStatus(200)
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ self.assertJsonBody(response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_delete(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw3"
+ request = copy.deepcopy(iscsi_target_request)
+ request['target_iqn'] = target_iqn
+ self._post('/api/iscsi/target', request)
+ self.assertStatus(201)
+ self._delete('/api/iscsi/target/{}'.format(request['target_iqn']))
+ self.assertStatus(204)
+ self._get('/api/iscsi/target')
+ self.assertStatus(200)
+ self.assertJsonBody([])
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_add_client(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw4"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'].append(
+ {
+ "luns": [{"image": "lun1", "pool": "rbd"}],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client3",
+ "auth": {
+ "password": "myiscsipassword5",
+ "user": "myiscsiusername5",
+ "mutual_password": "myiscsipassword6",
+ "mutual_user": "myiscsiusername6"}
+ })
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'].append(
+ {
+ "luns": [{"image": "lun1", "pool": "rbd"}],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client3",
+ "auth": {
+ "password": "myiscsipassword5",
+ "user": "myiscsiusername5",
+ "mutual_password": "myiscsipassword6",
+ "mutual_user": "myiscsiusername6"}
+ })
+ self._update_iscsi_target(create_request, update_request, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_change_client_password(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw5"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'][0]['auth']['password'] = 'mynewiscsipassword'
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'][0]['auth']['password'] = 'mynewiscsipassword'
+ self._update_iscsi_target(create_request, update_request, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_rename_client(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw6"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'][0]['client_iqn'] = 'iqn.1994-05.com.redhat:rh7-client0'
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'][0]['client_iqn'] = 'iqn.1994-05.com.redhat:rh7-client0'
+ self._update_iscsi_target(create_request, update_request, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_add_disk(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw7"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['disks'].append(
+ {
+ "image": "lun3",
+ "pool": "rbd",
+ "controls": {}
+ })
+ update_request['clients'][0]['luns'].append({"image": "lun3", "pool": "rbd"})
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['disks'].append(
+ {
+ "image": "lun3",
+ "pool": "rbd",
+ "controls": {}
+ })
+ response['clients'][0]['luns'].append({"image": "lun3", "pool": "rbd"})
+ self._update_iscsi_target(create_request, update_request, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_change_disk_image(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw8"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['disks'][0]['image'] = 'lun0'
+ update_request['clients'][0]['luns'][0]['image'] = 'lun0'
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['disks'][0]['image'] = 'lun0'
+ response['clients'][0]['luns'][0]['image'] = 'lun0'
+ self._update_iscsi_target(create_request, update_request, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_change_disk_controls(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw9"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['disks'][0]['controls'] = {"qfull_timeout": 15}
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['disks'][0]['controls'] = {"qfull_timeout": 15}
+ self._update_iscsi_target(create_request, update_request, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_rename_target(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw10"
+ new_target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw11"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = new_target_iqn
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = new_target_iqn
+ self._update_iscsi_target(create_request, update_request, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_rename_group(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw12"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['groups'][0]['group_id'] = 'mygroup0'
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['groups'][0]['group_id'] = 'mygroup0'
+ self._update_iscsi_target(create_request, update_request, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_add_client_to_group(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw13"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['clients'].append(
+ {
+ "luns": [],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client3",
+ "auth": {
+ "password": None,
+ "user": None,
+ "mutual_password": None,
+ "mutual_user": None}
+ })
+ update_request['groups'][0]['members'].append('iqn.1994-05.com.redhat:rh7-client3')
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['clients'].append(
+ {
+ "luns": [],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client3",
+ "auth": {
+ "password": None,
+ "user": None,
+ "mutual_password": None,
+ "mutual_user": None}
+ })
+ response['groups'][0]['members'].append('iqn.1994-05.com.redhat:rh7-client3')
+ self._update_iscsi_target(create_request, update_request, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_remove_client_from_group(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw14"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['groups'][0]['members'].remove('iqn.1994-05.com.redhat:rh7-client2')
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['groups'][0]['members'].remove('iqn.1994-05.com.redhat:rh7-client2')
+ self._update_iscsi_target(create_request, update_request, response)
+
+ @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists')
+ def test_remove_groups(self, _validate_image_exists_mock):
+ target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw15"
+ create_request = copy.deepcopy(iscsi_target_request)
+ create_request['target_iqn'] = target_iqn
+ update_request = copy.deepcopy(create_request)
+ update_request['new_target_iqn'] = target_iqn
+ update_request['groups'] = []
+ response = copy.deepcopy(iscsi_target_response)
+ response['target_iqn'] = target_iqn
+ response['groups'] = []
+ self._update_iscsi_target(create_request, update_request, response)
+
+ def _update_iscsi_target(self, create_request, update_request, response):
+ self._post('/api/iscsi/target', create_request)
+ self.assertStatus(201)
+ self._put('/api/iscsi/target/{}'.format(create_request['target_iqn']), update_request)
+ self.assertStatus(200)
+ self._get('/api/iscsi/target/{}'.format(update_request['new_target_iqn']))
+ self.assertStatus(200)
+ self.assertJsonBody(response)
+
+
+iscsi_target_request = {
+ "target_iqn": "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw",
+ "portals": [
+ {"ip": "192.168.100.202", "host": "node2"},
+ {"ip": "10.0.2.15", "host": "node2"},
+ {"ip": "192.168.100.203", "host": "node3"}
+ ],
+ "disks": [
+ {"image": "lun1", "pool": "rbd", "controls": {"max_data_area_mb": 128}},
+ {"image": "lun2", "pool": "rbd", "controls": {"max_data_area_mb": 128}}
+ ],
+ "clients": [
+ {
+ "luns": [{"image": "lun1", "pool": "rbd"}],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client",
+ "auth": {
+ "password": "myiscsipassword1",
+ "user": "myiscsiusername1",
+ "mutual_password": "myiscsipassword2",
+ "mutual_user": "myiscsiusername2"}
+ },
+ {
+ "luns": [],
+ "client_iqn": "iqn.1994-05.com.redhat:rh7-client2",
+ "auth": {
+ "password": "myiscsipassword3",
+ "user": "myiscsiusername3",
+ "mutual_password": "myiscsipassword4",
+ "mutual_user": "myiscsiusername4"
+ }
+ }
+ ],
+ "target_controls": {},
+ "groups": [
+ {
+ "group_id": "mygroup",
+ "disks": [{"pool": "rbd", "image": "lun2"}],
+ "members": ["iqn.1994-05.com.redhat:rh7-client2"]
+ }
+ ]
+}
+
+iscsi_target_response = {
+ 'target_iqn': 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw',
+ 'portals': [
+ {'host': 'node2', 'ip': '10.0.2.15'},
+ {'host': 'node2', 'ip': '192.168.100.202'},
+ {'host': 'node3', 'ip': '192.168.100.203'}
+ ],
+ 'disks': [
+ {'pool': 'rbd', 'image': 'lun1', 'controls': {'max_data_area_mb': 128}},
+ {'pool': 'rbd', 'image': 'lun2', 'controls': {'max_data_area_mb': 128}}
+ ],
+ 'clients': [
+ {
+ 'client_iqn': 'iqn.1994-05.com.redhat:rh7-client',
+ 'luns': [{'pool': 'rbd', 'image': 'lun1'}],
+ 'auth': {
+ 'user': 'myiscsiusername1',
+ 'password': 'myiscsipassword1',
+ 'mutual_password': 'myiscsipassword2',
+ 'mutual_user': 'myiscsiusername2'
+ }
+ },
+ {
+ 'client_iqn': 'iqn.1994-05.com.redhat:rh7-client2',
+ 'luns': [],
+ 'auth': {
+ 'user': 'myiscsiusername3',
+ 'password': 'myiscsipassword3',
+ 'mutual_password': 'myiscsipassword4',
+ 'mutual_user': 'myiscsiusername4'
+ }
+ }
+ ],
+ 'groups': [
+ {
+ 'group_id': 'mygroup',
+ 'disks': [{'pool': 'rbd', 'image': 'lun2'}],
+ 'members': ['iqn.1994-05.com.redhat:rh7-client2']
+ }
+ ],
+ 'target_controls': {}
+}
+
+
+class IscsiClientMock(object):
+
+ _instance = None
+
+ def __init__(self):
+ self.gateway_name = None
+ self.config = {
+ "created": "2019/01/17 08:57:16",
+ "discovery_auth": {
+ "chap": "",
+ "chap_mutual": ""
+ },
+ "disks": {},
+ "epoch": 0,
+ "gateways": {},
+ "targets": {},
+ "updated": "",
+ "version": 4
+ }
+
+ @classmethod
+ def instance(cls, gateway_name=None):
+ cls._instance.gateway_name = gateway_name
+ # pylint: disable=unused-argument
+ return cls._instance
+
+ def ping(self):
+ return {
+ "message": "pong"
+ }
+
+ def get_settings(self):
+ return {
+ "config": {
+ "minimum_gateways": 2
+ },
+ "disk_default_controls": {
+ "hw_max_sectors": 1024,
+ "max_data_area_mb": 8,
+ "osd_op_timeout": 30,
+ "qfull_timeout": 5
+ },
+ "target_default_controls": {
+ "cmdsn_depth": 128,
+ "dataout_timeout": 20,
+ "first_burst_length": 262144,
+ "immediate_data": "Yes",
+ "initial_r2t": "Yes",
+ "max_burst_length": 524288,
+ "max_outstanding_r2t": 1,
+ "max_recv_data_segment_length": 262144,
+ "max_xmit_data_segment_length": 262144,
+ "nopin_response_timeout": 5,
+ "nopin_timeout": 5
+ }
+ }
+
+ def get_config(self):
+ return self.config
+
+ def create_target(self, target_iqn, target_controls):
+ self.config['targets'][target_iqn] = {
+ "clients": {},
+ "controls": target_controls,
+ "created": "2019/01/17 09:22:34",
+ "disks": [],
+ "groups": {},
+ "portals": {}
+ }
+
+ def create_gateway(self, target_iqn, gateway_name, ip_address):
+ target_config = self.config['targets'][target_iqn]
+ if 'ip_list' not in target_config:
+ target_config['ip_list'] = []
+ target_config['ip_list'] += ip_address
+ target_config['portals'][gateway_name] = {
+ "portal_ip_address": ip_address[0]
+ }
+
+ def create_disk(self, image_id):
+ pool, image = image_id.split('.')
+ self.config['disks'][image_id] = {
+ "pool": pool,
+ "image": image,
+ "controls": {}
+ }
+
+ def create_target_lun(self, target_iqn, image_id):
+ target_config = self.config['targets'][target_iqn]
+ target_config['disks'].append(image_id)
+ self.config['disks'][image_id]['owner'] = list(target_config['portals'].keys())[0]
+
+ def reconfigure_disk(self, image_id, controls):
+ self.config['disks'][image_id]['controls'] = controls
+
+ def create_client(self, target_iqn, client_iqn):
+ target_config = self.config['targets'][target_iqn]
+ target_config['clients'][client_iqn] = {
+ "auth": {
+ "chap": "",
+ "chap_mutual": ""
+ },
+ "group_name": "",
+ "luns": {}
+ }
+
+ def create_client_lun(self, target_iqn, client_iqn, image_id):
+ target_config = self.config['targets'][target_iqn]
+ target_config['clients'][client_iqn]['luns'][image_id] = {}
+
+ def create_client_auth(self, target_iqn, client_iqn, chap, chap_mutual):
+ target_config = self.config['targets'][target_iqn]
+ target_config['clients'][client_iqn]['auth']['chap'] = chap
+ target_config['clients'][client_iqn]['auth']['chap_mutual'] = chap_mutual
+
+ def create_group(self, target_iqn, group_name, members, image_ids):
+ target_config = self.config['targets'][target_iqn]
+ target_config['groups'][group_name] = {
+ "disks": {},
+ "members": []
+ }
+ for image_id in image_ids:
+ target_config['groups'][group_name]['disks'][image_id] = {}
+ target_config['groups'][group_name]['members'] = members
+
+ def delete_group(self, target_iqn, group_name):
+ target_config = self.config['targets'][target_iqn]
+ del target_config['groups'][group_name]
+
+ def delete_client(self, target_iqn, client_iqn):
+ target_config = self.config['targets'][target_iqn]
+ del target_config['clients'][client_iqn]
+
+ def delete_target_lun(self, target_iqn, image_id):
+ target_config = self.config['targets'][target_iqn]
+ target_config['disks'].remove(image_id)
+ del self.config['disks'][image_id]['owner']
+
+ def delete_disk(self, image_id):
+ del self.config['disks'][image_id]
+
+ def delete_target(self, target_iqn):
+ del self.config['targets'][target_iqn]
+
+ def get_ip_addresses(self):
+ ips = {
+ 'node1': ['192.168.100.201'],
+ 'node2': ['192.168.100.202', '10.0.2.15'],
+ 'node3': ['192.168.100.203']
+ }
+ return {'data': ips[self.gateway_name]}