From a2c7bf8dbacdebc97736b6e84da72ca5405f0aaf Mon Sep 17 00:00:00 2001 From: Ricardo Dias Date: Mon, 17 Dec 2018 09:30:33 +0000 Subject: [PATCH 1/2] mgr/dashboard: Orchestrator client service Signed-off-by: Ricardo Dias --- .../mgr/dashboard/services/orchestrator.py | 55 +++++++++++++++++++ src/pybind/mgr/dashboard/settings.py | 3 + 2 files changed, 58 insertions(+) create mode 100644 src/pybind/mgr/dashboard/services/orchestrator.py diff --git a/src/pybind/mgr/dashboard/services/orchestrator.py b/src/pybind/mgr/dashboard/services/orchestrator.py new file mode 100644 index 00000000000..bfcec2bcbc6 --- /dev/null +++ b/src/pybind/mgr/dashboard/services/orchestrator.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import time + +from .. import mgr, logger +from ..settings import Settings + + +class NoOrchesrtatorConfiguredException(Exception): + pass + + +class OrchClient(object): + _instance = None + + @classmethod + def instance(cls): + if cls._instance is None: + cls._instance = OrchClient() + return cls._instance + + def _call(self, method, *args, **kwargs): + if not Settings.ORCHESTRATOR_BACKEND: + raise NoOrchesrtatorConfiguredException() + return mgr.remote(Settings.ORCHESTRATOR_BACKEND, method, *args, + **kwargs) + + def _wait(self, completions): + while not self._call("wait", completions): + if any(c.should_wait for c in completions): + time.sleep(5) + else: + break + + def list_service_info(self, service_type): + completion = self._call("describe_service", service_type, None, None) + self._wait([completion]) + return completion.result + + def available(self): + if not Settings.ORCHESTRATOR_BACKEND: + return False + status, desc = self._call("available") + logger.info("[ORCH] is orchestrator available: %s, %s", status, desc) + return status + + def reload_service(self, service_type, service_ids): + if not isinstance(service_ids, list): + service_ids = [service_ids] + + completion_list = [self._call("update_stateless_service", service_type, + service_id, None) + for service_id in service_ids] + self._wait(completion_list) diff --git a/src/pybind/mgr/dashboard/settings.py b/src/pybind/mgr/dashboard/settings.py index 7c0ea15411a..73c490297e7 100644 --- a/src/pybind/mgr/dashboard/settings.py +++ b/src/pybind/mgr/dashboard/settings.py @@ -40,6 +40,9 @@ class Options(object): GRAFANA_API_USERNAME = ('admin', str) GRAFANA_API_PASSWORD = ('admin', str) + # Orchestrator settings + ORCHESTRATOR_BACKEND = ('', str) + @staticmethod def has_default_value(name): return getattr(Settings, name, None) is None or \ From b24a7afca2e2f6d7bffaca2a7eda7eee5b7a669e Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Wed, 24 Oct 2018 13:42:23 +0100 Subject: [PATCH 2/2] mgr/dashboard: iSCSI management API Fixes: https://tracker.ceph.com/issues/35903 Signed-off-by: Ricardo Marques --- doc/mgr/dashboard.rst | 13 + src/pybind/mgr/dashboard/controllers/iscsi.py | 538 ++++++++++++++++++ .../shared/services/task-message.service.ts | 14 + .../frontend/src/locale/messages.xlf | 7 + src/pybind/mgr/dashboard/module.py | 4 + .../mgr/dashboard/services/iscsi_cli.py | 131 +++++ .../mgr/dashboard/services/iscsi_client.py | 165 ++++++ src/pybind/mgr/dashboard/tests/test_iscsi.py | 508 +++++++++++++++++ 8 files changed, 1380 insertions(+) create mode 100644 src/pybind/mgr/dashboard/controllers/iscsi.py create mode 100644 src/pybind/mgr/dashboard/services/iscsi_cli.py create mode 100644 src/pybind/mgr/dashboard/services/iscsi_client.py create mode 100644 src/pybind/mgr/dashboard/tests/test_iscsi.py diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index 2048be3bcc3..e06f40e8ff5 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -249,6 +249,19 @@ into timeouts, then you can set the timeout value to your needs:: 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 `_ +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 ://:@[:port] + $ ceph dashboard iscsi-gateway-rm + Enabling the Embedding of Grafana Dashboards ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/pybind/mgr/dashboard/controllers/iscsi.py b/src/pybind/mgr/dashboard/controllers/iscsi.py new file mode 100644 index 00000000000..504aca8a5fc --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/iscsi.py @@ -0,0 +1,538 @@ +# -*- 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 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 209f5ca7b14..265ccdeec54 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -311,6 +311,16 @@ export class TaskMessageService { 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) ) }; @@ -332,6 +342,10 @@ export class TaskMessageService { 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; } diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf index d6c8808f42c..40e35a9bf4b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf +++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf @@ -5253,6 +5253,13 @@ 1 + + target '' + + src/app/shared/services/task-message.service.ts + 1 + + diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py index ac45f23bdee..f4671d38109 100644 --- a/src/pybind/mgr/dashboard/module.py +++ b/src/pybind/mgr/dashboard/module.py @@ -19,6 +19,10 @@ from OpenSSL import crypto 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 diff --git a/src/pybind/mgr/dashboard/services/iscsi_cli.py b/src/pybind/mgr/dashboard/services/iscsi_cli.py new file mode 100644 index 00000000000..c03d69f78e0 --- /dev/null +++ b/src/pybind/mgr/dashboard/services/iscsi_cli.py @@ -0,0 +1,131 @@ +# -*- 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: '://:@[: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) diff --git a/src/pybind/mgr/dashboard/services/iscsi_client.py b/src/pybind/mgr/dashboard/services/iscsi_client.py new file mode 100644 index 00000000000..41f2b64f309 --- /dev/null +++ b/src/pybind/mgr/dashboard/services/iscsi_client.py @@ -0,0 +1,165 @@ +# -*- 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() diff --git a/src/pybind/mgr/dashboard/tests/test_iscsi.py b/src/pybind/mgr/dashboard/tests/test_iscsi.py new file mode 100644 index 00000000000..9c5970775b8 --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/test_iscsi.py @@ -0,0 +1,508 @@ +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]}