mirror of
https://github.com/ceph/ceph
synced 2024-12-22 03:22:00 +00:00
Merge pull request #25638 from ricardoasmarques/iscsi-management-w
mgr/dashboard: iSCSI management API Reviewed-by: Tiago Melo <tmelo@suse.com>
This commit is contained in:
commit
d239c2a8b4
@ -258,6 +258,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 <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
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
538
src/pybind/mgr/dashboard/controllers/iscsi.py
Normal file
538
src/pybind/mgr/dashboard/controllers/iscsi.py
Normal file
@ -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
|
@ -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;
|
||||
}
|
||||
|
@ -5387,6 +5387,13 @@
|
||||
<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>
|
||||
|
@ -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
|
||||
|
131
src/pybind/mgr/dashboard/services/iscsi_cli.py
Normal file
131
src/pybind/mgr/dashboard/services/iscsi_cli.py
Normal file
@ -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: '<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)
|
165
src/pybind/mgr/dashboard/services/iscsi_client.py
Normal file
165
src/pybind/mgr/dashboard/services/iscsi_client.py
Normal file
@ -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()
|
55
src/pybind/mgr/dashboard/services/orchestrator.py
Normal file
55
src/pybind/mgr/dashboard/services/orchestrator.py
Normal file
@ -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)
|
@ -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 \
|
||||
|
508
src/pybind/mgr/dashboard/tests/test_iscsi.py
Normal file
508
src/pybind/mgr/dashboard/tests/test_iscsi.py
Normal file
@ -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]}
|
Loading…
Reference in New Issue
Block a user