]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/controllers/iscsi.py
import new upstream nautilus stable release 14.2.8
[ceph.git] / ceph / src / pybind / mgr / dashboard / controllers / iscsi.py
CommitLineData
11fdf7f2
TL
1# -*- coding: utf-8 -*-
2# pylint: disable=too-many-branches
3from __future__ import absolute_import
4
5from copy import deepcopy
6import json
7import cherrypy
8
9import rados
10import rbd
11
12from . import ApiController, UiApiController, RESTController, BaseController, Endpoint,\
13 ReadPermission, UpdatePermission, Task
14from .. import mgr
15from ..rest_client import RequestException
16from ..security import Scope
17from ..services.iscsi_client import IscsiClient
18from ..services.iscsi_cli import IscsiGatewaysConfig
92f5a8d4 19from ..services.iscsi_config import IscsiGatewayDoesNotExist
11fdf7f2
TL
20from ..services.rbd import format_bitmask
21from ..services.tcmu_service import TcmuService
22from ..exceptions import DashboardException
eafe8130 23from ..tools import str_to_bool, TaskManager
11fdf7f2
TL
24
25
26@UiApiController('/iscsi', Scope.ISCSI)
27class IscsiUi(BaseController):
28
eafe8130
TL
29 REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION = 10
30 REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION = 11
11fdf7f2
TL
31
32 @Endpoint()
33 @ReadPermission
34 def status(self):
35 status = {'available': False}
92f5a8d4
TL
36 try:
37 gateway = get_available_gateway()
38 except DashboardException as e:
39 status['message'] = str(e)
11fdf7f2
TL
40 return status
41 try:
92f5a8d4 42 config = IscsiClient.instance(gateway_name=gateway).get_config()
eafe8130
TL
43 if config['version'] < IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION or \
44 config['version'] > IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION:
45 status['message'] = 'Unsupported `ceph-iscsi` config version. ' \
46 'Expected >= {} and <= {} but found' \
47 ' {}.'.format(IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION,
48 IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION,
49 config['version'])
11fdf7f2
TL
50 return status
51 status['available'] = True
52 except RequestException as e:
53 if e.content:
81eedcae
TL
54 try:
55 content = json.loads(e.content)
56 content_message = content.get('message')
57 except ValueError:
58 content_message = e.content
11fdf7f2
TL
59 if content_message:
60 status['message'] = content_message
81eedcae 61
11fdf7f2
TL
62 return status
63
eafe8130
TL
64 @Endpoint()
65 @ReadPermission
66 def version(self):
92f5a8d4
TL
67 gateway = get_available_gateway()
68 config = IscsiClient.instance(gateway_name=gateway).get_config()
eafe8130 69 return {
92f5a8d4 70 'ceph_iscsi_config_version': config['version']
eafe8130
TL
71 }
72
11fdf7f2
TL
73 @Endpoint()
74 @ReadPermission
75 def settings(self):
92f5a8d4
TL
76 gateway = get_available_gateway()
77 settings = IscsiClient.instance(gateway_name=gateway).get_settings()
eafe8130
TL
78 if 'target_controls_limits' in settings:
79 target_default_controls = settings['target_default_controls']
80 for ctrl_k, ctrl_v in target_default_controls.items():
81 limits = settings['target_controls_limits'].get(ctrl_k, {})
82 if 'type' not in limits:
83 # default
84 limits['type'] = 'int'
85 # backward compatibility
86 if target_default_controls[ctrl_k] in ['Yes', 'No']:
87 limits['type'] = 'bool'
88 target_default_controls[ctrl_k] = str_to_bool(ctrl_v)
89 settings['target_controls_limits'][ctrl_k] = limits
90 if 'disk_controls_limits' in settings:
91 for backstore, disk_controls_limits in settings['disk_controls_limits'].items():
92 disk_default_controls = settings['disk_default_controls'][backstore]
93 for ctrl_k, ctrl_v in disk_default_controls.items():
94 limits = disk_controls_limits.get(ctrl_k, {})
95 if 'type' not in limits:
96 # default
97 limits['type'] = 'int'
98 settings['disk_controls_limits'][backstore][ctrl_k] = limits
99 return settings
11fdf7f2
TL
100
101 @Endpoint()
102 @ReadPermission
103 def portals(self):
104 portals = []
105 gateways_config = IscsiGatewaysConfig.get_gateways_config()
81eedcae 106 for name in gateways_config['gateways']:
92f5a8d4
TL
107 try:
108 ip_addresses = IscsiClient.instance(gateway_name=name).get_ip_addresses()
109 portals.append({'name': name, 'ip_addresses': ip_addresses['data']})
110 except RequestException:
111 pass
11fdf7f2
TL
112 return sorted(portals, key=lambda p: '{}.{}'.format(p['name'], p['ip_addresses']))
113
114 @Endpoint()
115 @ReadPermission
116 def overview(self):
117 result_gateways = []
118 result_images = []
119 gateways_names = IscsiGatewaysConfig.get_gateways_config()['gateways'].keys()
120 config = None
121 for gateway_name in gateways_names:
122 try:
123 config = IscsiClient.instance(gateway_name=gateway_name).get_config()
124 break
125 except RequestException:
126 pass
127
128 # Gateways info
129 for gateway_name in gateways_names:
130 gateway = {
131 'name': gateway_name,
132 'state': '',
133 'num_targets': 'n/a',
134 'num_sessions': 'n/a'
135 }
136 try:
137 IscsiClient.instance(gateway_name=gateway_name).ping()
138 gateway['state'] = 'up'
139 if config:
140 gateway['num_sessions'] = 0
141 if gateway_name in config['gateways']:
142 gatewayinfo = IscsiClient.instance(
143 gateway_name=gateway_name).get_gatewayinfo()
144 gateway['num_sessions'] = gatewayinfo['num_sessions']
145 except RequestException:
146 gateway['state'] = 'down'
147 if config:
148 gateway['num_targets'] = len([target for _, target in config['targets'].items()
149 if gateway_name in target['portals']])
150 result_gateways.append(gateway)
151
152 # Images info
153 if config:
154 tcmu_info = TcmuService.get_iscsi_info()
155 for _, disk_config in config['disks'].items():
156 image = {
157 'pool': disk_config['pool'],
158 'image': disk_config['image'],
159 'backstore': disk_config['backstore'],
160 'optimized_since': None,
161 'stats': None,
162 'stats_history': None
163 }
164 tcmu_image_info = TcmuService.get_image_info(image['pool'],
165 image['image'],
166 tcmu_info)
167 if tcmu_image_info:
168 if 'optimized_since' in tcmu_image_info:
169 image['optimized_since'] = tcmu_image_info['optimized_since']
170 if 'stats' in tcmu_image_info:
171 image['stats'] = tcmu_image_info['stats']
172 if 'stats_history' in tcmu_image_info:
173 image['stats_history'] = tcmu_image_info['stats_history']
174 result_images.append(image)
175
176 return {
177 'gateways': sorted(result_gateways, key=lambda g: g['name']),
178 'images': sorted(result_images, key=lambda i: '{}/{}'.format(i['pool'], i['image']))
179 }
180
181
182@ApiController('/iscsi', Scope.ISCSI)
183class Iscsi(BaseController):
184
185 @Endpoint('GET', 'discoveryauth')
81eedcae 186 @ReadPermission
11fdf7f2 187 def get_discoveryauth(self):
92f5a8d4
TL
188 gateway = get_available_gateway()
189 return self._get_discoveryauth(gateway)
11fdf7f2
TL
190
191 @Endpoint('PUT', 'discoveryauth')
192 @UpdatePermission
193 def set_discoveryauth(self, user, password, mutual_user, mutual_password):
92f5a8d4
TL
194 gateway = get_available_gateway()
195 config = IscsiClient.instance(gateway_name=gateway).get_config()
196 gateway_names = list(config['gateways'].keys())
197 validate_rest_api(gateway_names)
198 IscsiClient.instance(gateway_name=gateway).update_discoveryauth(user,
199 password,
200 mutual_user,
201 mutual_password)
202 return self._get_discoveryauth(gateway)
203
204 def _get_discoveryauth(self, gateway):
205 config = IscsiClient.instance(gateway_name=gateway).get_config()
11fdf7f2
TL
206 user = config['discovery_auth']['username']
207 password = config['discovery_auth']['password']
208 mutual_user = config['discovery_auth']['mutual_username']
209 mutual_password = config['discovery_auth']['mutual_password']
210 return {
211 'user': user,
212 'password': password,
213 'mutual_user': mutual_user,
214 'mutual_password': mutual_password
215 }
216
217
218def iscsi_target_task(name, metadata, wait_for=2.0):
219 return Task("iscsi/target/{}".format(name), metadata, wait_for)
220
221
222@ApiController('/iscsi/target', Scope.ISCSI)
223class IscsiTarget(RESTController):
224
225 def list(self):
92f5a8d4
TL
226 gateway = get_available_gateway()
227 config = IscsiClient.instance(gateway_name=gateway).get_config()
11fdf7f2
TL
228 targets = []
229 for target_iqn in config['targets'].keys():
230 target = IscsiTarget._config_to_target(target_iqn, config)
231 IscsiTarget._set_info(target)
232 targets.append(target)
233 return targets
234
235 def get(self, target_iqn):
92f5a8d4
TL
236 gateway = get_available_gateway()
237 config = IscsiClient.instance(gateway_name=gateway).get_config()
11fdf7f2
TL
238 if target_iqn not in config['targets']:
239 raise cherrypy.HTTPError(404)
240 target = IscsiTarget._config_to_target(target_iqn, config)
241 IscsiTarget._set_info(target)
242 return target
243
244 @iscsi_target_task('delete', {'target_iqn': '{target_iqn}'})
245 def delete(self, target_iqn):
92f5a8d4
TL
246 gateway = get_available_gateway()
247 config = IscsiClient.instance(gateway_name=gateway).get_config()
11fdf7f2
TL
248 if target_iqn not in config['targets']:
249 raise DashboardException(msg='Target does not exist',
250 code='target_does_not_exist',
251 component='iscsi')
252 if target_iqn not in config['targets']:
253 raise DashboardException(msg='Target does not exist',
254 code='target_does_not_exist',
255 component='iscsi')
92f5a8d4
TL
256 portal_names = list(config['targets'][target_iqn]['portals'].keys())
257 validate_rest_api(portal_names)
258 if portal_names:
259 portal_name = portal_names[0]
260 target_info = IscsiClient.instance(gateway_name=portal_name).get_targetinfo(target_iqn)
261 if target_info['num_sessions'] > 0:
262 raise DashboardException(msg='Target has active sessions',
263 code='target_has_active_sessions',
264 component='iscsi')
11fdf7f2
TL
265 IscsiTarget._delete(target_iqn, config, 0, 100)
266
267 @iscsi_target_task('create', {'target_iqn': '{target_iqn}'})
268 def create(self, target_iqn=None, target_controls=None, acl_enabled=None,
eafe8130 269 auth=None, portals=None, disks=None, clients=None, groups=None):
11fdf7f2
TL
270 target_controls = target_controls or {}
271 portals = portals or []
272 disks = disks or []
273 clients = clients or []
274 groups = groups or []
275
92f5a8d4
TL
276 gateway = get_available_gateway()
277 config = IscsiClient.instance(gateway_name=gateway).get_config()
11fdf7f2
TL
278 if target_iqn in config['targets']:
279 raise DashboardException(msg='Target already exists',
280 code='target_already_exists',
281 component='iscsi')
92f5a8d4 282 settings = IscsiClient.instance(gateway_name=gateway).get_settings()
eafe8130
TL
283 IscsiTarget._validate(target_iqn, target_controls, portals, disks, groups, settings)
284
285 IscsiTarget._create(target_iqn, target_controls, acl_enabled, auth, portals, disks,
286 clients, groups, 0, 100, config, settings)
11fdf7f2
TL
287
288 @iscsi_target_task('edit', {'target_iqn': '{target_iqn}'})
289 def set(self, target_iqn, new_target_iqn=None, target_controls=None, acl_enabled=None,
eafe8130 290 auth=None, portals=None, disks=None, clients=None, groups=None):
11fdf7f2
TL
291 target_controls = target_controls or {}
292 portals = IscsiTarget._sorted_portals(portals)
293 disks = IscsiTarget._sorted_disks(disks)
294 clients = IscsiTarget._sorted_clients(clients)
295 groups = IscsiTarget._sorted_groups(groups)
296
92f5a8d4
TL
297 gateway = get_available_gateway()
298 config = IscsiClient.instance(gateway_name=gateway).get_config()
11fdf7f2
TL
299 if target_iqn not in config['targets']:
300 raise DashboardException(msg='Target does not exist',
301 code='target_does_not_exist',
302 component='iscsi')
303 if target_iqn != new_target_iqn and new_target_iqn in config['targets']:
304 raise DashboardException(msg='Target IQN already in use',
305 code='target_iqn_already_in_use',
306 component='iscsi')
92f5a8d4
TL
307 settings = IscsiClient.instance(gateway_name=gateway).get_settings()
308 new_portal_names = {p['host'] for p in portals}
309 old_portal_names = set(config['targets'][target_iqn]['portals'].keys())
310 deleted_portal_names = list(old_portal_names - new_portal_names)
311 validate_rest_api(deleted_portal_names)
eafe8130 312 IscsiTarget._validate(new_target_iqn, target_controls, portals, disks, groups, settings)
11fdf7f2
TL
313 config = IscsiTarget._delete(target_iqn, config, 0, 50, new_target_iqn, target_controls,
314 portals, disks, clients, groups)
eafe8130
TL
315 IscsiTarget._create(new_target_iqn, target_controls, acl_enabled, auth, portals, disks,
316 clients, groups, 50, 100, config, settings)
11fdf7f2
TL
317
318 @staticmethod
319 def _delete(target_iqn, config, task_progress_begin, task_progress_end, new_target_iqn=None,
320 new_target_controls=None, new_portals=None, new_disks=None, new_clients=None,
321 new_groups=None):
322 new_target_controls = new_target_controls or {}
323 new_portals = new_portals or []
324 new_disks = new_disks or []
325 new_clients = new_clients or []
326 new_groups = new_groups or []
327
328 TaskManager.current_task().set_progress(task_progress_begin)
329 target_config = config['targets'][target_iqn]
330 if not target_config['portals'].keys():
331 raise DashboardException(msg="Cannot delete a target that doesn't contain any portal",
332 code='cannot_delete_target_without_portals',
333 component='iscsi')
334 target = IscsiTarget._config_to_target(target_iqn, config)
335 n_groups = len(target_config['groups'])
336 n_clients = len(target_config['clients'])
337 n_target_disks = len(target_config['disks'])
338 task_progress_steps = n_groups + n_clients + n_target_disks
339 task_progress_inc = 0
340 if task_progress_steps != 0:
341 task_progress_inc = int((task_progress_end - task_progress_begin) / task_progress_steps)
342 gateway_name = list(target_config['portals'].keys())[0]
343 deleted_groups = []
344 for group_id in list(target_config['groups'].keys()):
345 if IscsiTarget._group_deletion_required(target, new_target_iqn, new_target_controls,
494da23a 346 new_groups, group_id, new_clients,
11fdf7f2
TL
347 new_disks):
348 deleted_groups.append(group_id)
349 IscsiClient.instance(gateway_name=gateway_name).delete_group(target_iqn,
350 group_id)
351 TaskManager.current_task().inc_progress(task_progress_inc)
eafe8130 352 deleted_clients = []
11fdf7f2
TL
353 for client_iqn in list(target_config['clients'].keys()):
354 if IscsiTarget._client_deletion_required(target, new_target_iqn, new_target_controls,
494da23a 355 new_clients, client_iqn,
11fdf7f2 356 new_groups, deleted_groups):
eafe8130 357 deleted_clients.append(client_iqn)
11fdf7f2
TL
358 IscsiClient.instance(gateway_name=gateway_name).delete_client(target_iqn,
359 client_iqn)
360 TaskManager.current_task().inc_progress(task_progress_inc)
361 for image_id in target_config['disks']:
362 if IscsiTarget._target_lun_deletion_required(target, new_target_iqn,
494da23a 363 new_target_controls,
11fdf7f2 364 new_disks, image_id):
eafe8130
TL
365 all_clients = target_config['clients'].keys()
366 not_deleted_clients = [c for c in all_clients if c not in deleted_clients]
367 for client_iqn in not_deleted_clients:
368 client_image_ids = target_config['clients'][client_iqn]['luns'].keys()
369 for client_image_id in client_image_ids:
370 if image_id == client_image_id:
371 IscsiClient.instance(gateway_name=gateway_name).delete_client_lun(
372 target_iqn, client_iqn, client_image_id)
11fdf7f2
TL
373 IscsiClient.instance(gateway_name=gateway_name).delete_target_lun(target_iqn,
374 image_id)
375 pool, image = image_id.split('/', 1)
376 IscsiClient.instance(gateway_name=gateway_name).delete_disk(pool, image)
377 TaskManager.current_task().inc_progress(task_progress_inc)
494da23a
TL
378 old_portals_by_host = IscsiTarget._get_portals_by_host(target['portals'])
379 new_portals_by_host = IscsiTarget._get_portals_by_host(new_portals)
380 for old_portal_host, old_portal_ip_list in old_portals_by_host.items():
381 if IscsiTarget._target_portal_deletion_required(old_portal_host,
382 old_portal_ip_list,
383 new_portals_by_host):
384 IscsiClient.instance(gateway_name=gateway_name).delete_gateway(target_iqn,
385 old_portal_host)
386 if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
11fdf7f2
TL
387 IscsiClient.instance(gateway_name=gateway_name).delete_target(target_iqn)
388 TaskManager.current_task().set_progress(task_progress_end)
389 return IscsiClient.instance(gateway_name=gateway_name).get_config()
390
391 @staticmethod
392 def _get_group(groups, group_id):
393 for group in groups:
394 if group['group_id'] == group_id:
395 return group
396 return None
397
398 @staticmethod
494da23a 399 def _group_deletion_required(target, new_target_iqn, new_target_controls,
11fdf7f2 400 new_groups, group_id, new_clients, new_disks):
494da23a 401 if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
11fdf7f2
TL
402 return True
403 new_group = IscsiTarget._get_group(new_groups, group_id)
404 if not new_group:
405 return True
406 old_group = IscsiTarget._get_group(target['groups'], group_id)
407 if new_group != old_group:
408 return True
409 # Check if any client inside this group has changed
410 for client_iqn in new_group['members']:
411 if IscsiTarget._client_deletion_required(target, new_target_iqn, new_target_controls,
494da23a 412 new_clients, client_iqn,
11fdf7f2
TL
413 new_groups, []):
414 return True
415 # Check if any disk inside this group has changed
416 for disk in new_group['disks']:
417 image_id = '{}/{}'.format(disk['pool'], disk['image'])
418 if IscsiTarget._target_lun_deletion_required(target, new_target_iqn,
494da23a 419 new_target_controls,
11fdf7f2
TL
420 new_disks, image_id):
421 return True
422 return False
423
424 @staticmethod
425 def _get_client(clients, client_iqn):
426 for client in clients:
427 if client['client_iqn'] == client_iqn:
428 return client
429 return None
430
431 @staticmethod
494da23a 432 def _client_deletion_required(target, new_target_iqn, new_target_controls,
11fdf7f2 433 new_clients, client_iqn, new_groups, deleted_groups):
494da23a 434 if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
11fdf7f2
TL
435 return True
436 new_client = deepcopy(IscsiTarget._get_client(new_clients, client_iqn))
437 if not new_client:
438 return True
439 # Disks inherited from groups must be considered
440 for group in new_groups:
441 if client_iqn in group['members']:
442 new_client['luns'] += group['disks']
443 old_client = IscsiTarget._get_client(target['clients'], client_iqn)
444 if new_client != old_client:
445 return True
446 # Check if client belongs to a groups that has been deleted
447 for group in target['groups']:
448 if group['group_id'] in deleted_groups and client_iqn in group['members']:
449 return True
450 return False
451
452 @staticmethod
453 def _get_disk(disks, image_id):
454 for disk in disks:
455 if '{}/{}'.format(disk['pool'], disk['image']) == image_id:
456 return disk
457 return None
458
459 @staticmethod
494da23a 460 def _target_lun_deletion_required(target, new_target_iqn, new_target_controls,
11fdf7f2 461 new_disks, image_id):
494da23a 462 if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
11fdf7f2
TL
463 return True
464 new_disk = IscsiTarget._get_disk(new_disks, image_id)
465 if not new_disk:
466 return True
467 old_disk = IscsiTarget._get_disk(target['disks'], image_id)
eafe8130
TL
468 new_disk_without_controls = deepcopy(new_disk)
469 new_disk_without_controls.pop('controls')
470 old_disk_without_controls = deepcopy(old_disk)
471 old_disk_without_controls.pop('controls')
472 if new_disk_without_controls != old_disk_without_controls:
11fdf7f2
TL
473 return True
474 return False
475
476 @staticmethod
494da23a
TL
477 def _target_portal_deletion_required(old_portal_host, old_portal_ip_list, new_portals_by_host):
478 if old_portal_host not in new_portals_by_host:
479 return True
480 if sorted(old_portal_ip_list) != sorted(new_portals_by_host[old_portal_host]):
481 return True
482 return False
483
484 @staticmethod
485 def _target_deletion_required(target, new_target_iqn, new_target_controls):
11fdf7f2
TL
486 if target['target_iqn'] != new_target_iqn:
487 return True
488 if target['target_controls'] != new_target_controls:
489 return True
11fdf7f2
TL
490 return False
491
492 @staticmethod
eafe8130 493 def _validate(target_iqn, target_controls, portals, disks, groups, settings):
11fdf7f2
TL
494 if not target_iqn:
495 raise DashboardException(msg='Target IQN is required',
496 code='target_iqn_required',
497 component='iscsi')
498
11fdf7f2
TL
499 minimum_gateways = max(1, settings['config']['minimum_gateways'])
500 portals_by_host = IscsiTarget._get_portals_by_host(portals)
501 if len(portals_by_host.keys()) < minimum_gateways:
502 if minimum_gateways == 1:
503 msg = 'At least one portal is required'
504 else:
505 msg = 'At least {} portals are required'.format(minimum_gateways)
506 raise DashboardException(msg=msg,
507 code='portals_required',
508 component='iscsi')
509
eafe8130
TL
510 # 'target_controls_limits' was introduced in ceph-iscsi > 3.2
511 # When using an older `ceph-iscsi` version these validations will
512 # NOT be executed beforehand
513 if 'target_controls_limits' in settings:
514 for target_control_name, target_control_value in target_controls.items():
515 limits = settings['target_controls_limits'].get(target_control_name)
516 if limits is not None:
517 min_value = limits.get('min')
518 if min_value is not None and target_control_value < min_value:
519 raise DashboardException(msg='Target control {} must be >= '
520 '{}'.format(target_control_name, min_value),
521 code='target_control_invalid_min',
522 component='iscsi')
523 max_value = limits.get('max')
524 if max_value is not None and target_control_value > max_value:
525 raise DashboardException(msg='Target control {} must be <= '
526 '{}'.format(target_control_name, max_value),
527 code='target_control_invalid_max',
528 component='iscsi')
529
92f5a8d4
TL
530 portal_names = [p['host'] for p in portals]
531 validate_rest_api(portal_names)
11fdf7f2
TL
532
533 for disk in disks:
534 pool = disk['pool']
535 image = disk['image']
536 backstore = disk['backstore']
537 required_rbd_features = settings['required_rbd_features'][backstore]
81eedcae 538 unsupported_rbd_features = settings['unsupported_rbd_features'][backstore]
11fdf7f2 539 IscsiTarget._validate_image(pool, image, backstore, required_rbd_features,
81eedcae
TL
540 unsupported_rbd_features)
541
eafe8130
TL
542 # 'disk_controls_limits' was introduced in ceph-iscsi > 3.2
543 # When using an older `ceph-iscsi` version these validations will
544 # NOT be executed beforehand
545 if 'disk_controls_limits' in settings:
546 for disk_control_name, disk_control_value in disk['controls'].items():
547 limits = settings['disk_controls_limits'][backstore].get(disk_control_name)
548 if limits is not None:
549 min_value = limits.get('min')
550 if min_value is not None and disk_control_value < min_value:
551 raise DashboardException(msg='Disk control {} must be >= '
552 '{}'.format(disk_control_name, min_value),
553 code='disk_control_invalid_min',
554 component='iscsi')
555 max_value = limits.get('max')
556 if max_value is not None and disk_control_value > max_value:
557 raise DashboardException(msg='Disk control {} must be <= '
558 '{}'.format(disk_control_name, max_value),
559 code='disk_control_invalid_max',
560 component='iscsi')
561
81eedcae
TL
562 initiators = []
563 for group in groups:
564 initiators = initiators + group['members']
565 if len(initiators) != len(set(initiators)):
566 raise DashboardException(msg='Each initiator can only be part of 1 group at a time',
567 code='initiator_in_multiple_groups',
568 component='iscsi')
11fdf7f2
TL
569
570 @staticmethod
81eedcae 571 def _validate_image(pool, image, backstore, required_rbd_features, unsupported_rbd_features):
11fdf7f2
TL
572 try:
573 ioctx = mgr.rados.open_ioctx(pool)
574 try:
575 with rbd.Image(ioctx, image) as img:
576 if img.features() & required_rbd_features != required_rbd_features:
577 raise DashboardException(msg='Image {} cannot be exported using {} '
578 'backstore because required features are '
579 'missing (required features are '
580 '{})'.format(image,
581 backstore,
582 format_bitmask(
583 required_rbd_features)),
584 code='image_missing_required_features',
585 component='iscsi')
81eedcae 586 if img.features() & unsupported_rbd_features != 0:
11fdf7f2
TL
587 raise DashboardException(msg='Image {} cannot be exported using {} '
588 'backstore because it contains unsupported '
81eedcae 589 'features ('
11fdf7f2
TL
590 '{})'.format(image,
591 backstore,
592 format_bitmask(
81eedcae 593 unsupported_rbd_features)),
11fdf7f2
TL
594 code='image_contains_unsupported_features',
595 component='iscsi')
596
597 except rbd.ImageNotFound:
598 raise DashboardException(msg='Image {} does not exist'.format(image),
599 code='image_does_not_exist',
600 component='iscsi')
601 except rados.ObjectNotFound:
602 raise DashboardException(msg='Pool {} does not exist'.format(pool),
603 code='pool_does_not_exist',
604 component='iscsi')
605
eafe8130
TL
606 @staticmethod
607 def _update_targetauth(config, target_iqn, auth, gateway_name):
608 # Target level authentication was introduced in ceph-iscsi config v11
609 if config['version'] > 10:
610 user = auth['user']
611 password = auth['password']
612 mutual_user = auth['mutual_user']
613 mutual_password = auth['mutual_password']
614 IscsiClient.instance(gateway_name=gateway_name).update_targetauth(target_iqn,
615 user,
616 password,
617 mutual_user,
618 mutual_password)
619
620 @staticmethod
621 def _update_targetacl(target_config, target_iqn, acl_enabled, gateway_name):
622 if not target_config or target_config['acl_enabled'] != acl_enabled:
623 targetauth_action = ('enable_acl' if acl_enabled else 'disable_acl')
624 IscsiClient.instance(gateway_name=gateway_name).update_targetacl(target_iqn,
625 targetauth_action)
626
11fdf7f2
TL
627 @staticmethod
628 def _create(target_iqn, target_controls, acl_enabled,
eafe8130
TL
629 auth, portals, disks, clients, groups,
630 task_progress_begin, task_progress_end, config, settings):
11fdf7f2
TL
631 target_config = config['targets'].get(target_iqn, None)
632 TaskManager.current_task().set_progress(task_progress_begin)
633 portals_by_host = IscsiTarget._get_portals_by_host(portals)
634 n_hosts = len(portals_by_host)
635 n_disks = len(disks)
636 n_clients = len(clients)
637 n_groups = len(groups)
638 task_progress_steps = n_hosts + n_disks + n_clients + n_groups
639 task_progress_inc = 0
640 if task_progress_steps != 0:
641 task_progress_inc = int((task_progress_end - task_progress_begin) / task_progress_steps)
642 try:
643 gateway_name = portals[0]['host']
644 if not target_config:
645 IscsiClient.instance(gateway_name=gateway_name).create_target(target_iqn,
646 target_controls)
494da23a
TL
647 for host, ip_list in portals_by_host.items():
648 if not target_config or host not in target_config['portals']:
11fdf7f2
TL
649 IscsiClient.instance(gateway_name=gateway_name).create_gateway(target_iqn,
650 host,
651 ip_list)
494da23a 652 TaskManager.current_task().inc_progress(task_progress_inc)
eafe8130
TL
653
654 if acl_enabled:
655 IscsiTarget._update_targetauth(config, target_iqn, auth, gateway_name)
656 IscsiTarget._update_targetacl(target_config, target_iqn, acl_enabled, gateway_name)
657
658 else:
659 IscsiTarget._update_targetacl(target_config, target_iqn, acl_enabled, gateway_name)
660 IscsiTarget._update_targetauth(config, target_iqn, auth, gateway_name)
661
11fdf7f2
TL
662 for disk in disks:
663 pool = disk['pool']
664 image = disk['image']
665 image_id = '{}/{}'.format(pool, image)
eafe8130
TL
666 backstore = disk['backstore']
667 wwn = disk.get('wwn')
668 lun = disk.get('lun')
11fdf7f2 669 if image_id not in config['disks']:
11fdf7f2
TL
670 IscsiClient.instance(gateway_name=gateway_name).create_disk(pool,
671 image,
eafe8130
TL
672 backstore,
673 wwn)
11fdf7f2
TL
674 if not target_config or image_id not in target_config['disks']:
675 IscsiClient.instance(gateway_name=gateway_name).create_target_lun(target_iqn,
eafe8130
TL
676 image_id,
677 lun)
678
679 controls = disk['controls']
680 d_conf_controls = {}
681 if image_id in config['disks']:
682 d_conf_controls = config['disks'][image_id]['controls']
683 disk_default_controls = settings['disk_default_controls'][backstore]
684 for old_control in d_conf_controls.keys():
685 # If control was removed, restore the default value
686 if old_control not in controls:
687 controls[old_control] = disk_default_controls[old_control]
688
689 if (image_id not in config['disks'] or d_conf_controls != controls) and controls:
690 IscsiClient.instance(gateway_name=gateway_name).reconfigure_disk(pool,
691 image,
692 controls)
11fdf7f2
TL
693 TaskManager.current_task().inc_progress(task_progress_inc)
694 for client in clients:
695 client_iqn = client['client_iqn']
696 if not target_config or client_iqn not in target_config['clients']:
697 IscsiClient.instance(gateway_name=gateway_name).create_client(target_iqn,
698 client_iqn)
11fdf7f2
TL
699 user = client['auth']['user']
700 password = client['auth']['password']
701 m_user = client['auth']['mutual_user']
702 m_password = client['auth']['mutual_password']
703 IscsiClient.instance(gateway_name=gateway_name).create_client_auth(
704 target_iqn, client_iqn, user, password, m_user, m_password)
eafe8130
TL
705 for lun in client['luns']:
706 pool = lun['pool']
707 image = lun['image']
708 image_id = '{}/{}'.format(pool, image)
709 if not target_config or client_iqn not in target_config['clients'] or \
710 image_id not in target_config['clients'][client_iqn]['luns']:
711 IscsiClient.instance(gateway_name=gateway_name).create_client_lun(
712 target_iqn, client_iqn, image_id)
11fdf7f2
TL
713 TaskManager.current_task().inc_progress(task_progress_inc)
714 for group in groups:
715 group_id = group['group_id']
716 members = group['members']
717 image_ids = []
718 for disk in group['disks']:
719 image_ids.append('{}/{}'.format(disk['pool'], disk['image']))
720 if not target_config or group_id not in target_config['groups']:
721 IscsiClient.instance(gateway_name=gateway_name).create_group(
722 target_iqn, group_id, members, image_ids)
723 TaskManager.current_task().inc_progress(task_progress_inc)
724 if target_controls:
725 if not target_config or target_controls != target_config['controls']:
726 IscsiClient.instance(gateway_name=gateway_name).reconfigure_target(
727 target_iqn, target_controls)
728 TaskManager.current_task().set_progress(task_progress_end)
729 except RequestException as e:
730 if e.content:
731 content = json.loads(e.content)
732 content_message = content.get('message')
733 if content_message:
734 raise DashboardException(msg=content_message, component='iscsi')
735 raise DashboardException(e=e, component='iscsi')
736
737 @staticmethod
738 def _config_to_target(target_iqn, config):
739 target_config = config['targets'][target_iqn]
740 portals = []
494da23a
TL
741 for host, portal_config in target_config['portals'].items():
742 for portal_ip in portal_config['portal_ip_addresses']:
11fdf7f2
TL
743 portal = {
744 'host': host,
745 'ip': portal_ip
746 }
747 portals.append(portal)
748 portals = IscsiTarget._sorted_portals(portals)
749 disks = []
750 for target_disk in target_config['disks']:
751 disk_config = config['disks'][target_disk]
752 disk = {
753 'pool': disk_config['pool'],
754 'image': disk_config['image'],
755 'controls': disk_config['controls'],
eafe8130
TL
756 'backstore': disk_config['backstore'],
757 'wwn': disk_config['wwn']
11fdf7f2 758 }
eafe8130
TL
759 # lun_id was introduced in ceph-iscsi config v11
760 if config['version'] > 10:
761 disk['lun'] = target_config['disks'][target_disk]['lun_id']
11fdf7f2
TL
762 disks.append(disk)
763 disks = IscsiTarget._sorted_disks(disks)
764 clients = []
765 for client_iqn, client_config in target_config['clients'].items():
766 luns = []
767 for client_lun in client_config['luns'].keys():
768 pool, image = client_lun.split('/', 1)
769 lun = {
770 'pool': pool,
771 'image': image
772 }
773 luns.append(lun)
774 user = client_config['auth']['username']
775 password = client_config['auth']['password']
776 mutual_user = client_config['auth']['mutual_username']
777 mutual_password = client_config['auth']['mutual_password']
778 client = {
779 'client_iqn': client_iqn,
780 'luns': luns,
781 'auth': {
782 'user': user,
783 'password': password,
784 'mutual_user': mutual_user,
785 'mutual_password': mutual_password
786 }
787 }
788 clients.append(client)
789 clients = IscsiTarget._sorted_clients(clients)
790 groups = []
791 for group_id, group_config in target_config['groups'].items():
792 group_disks = []
793 for group_disk_key, _ in group_config['disks'].items():
794 pool, image = group_disk_key.split('/', 1)
795 group_disk = {
796 'pool': pool,
797 'image': image
798 }
799 group_disks.append(group_disk)
800 group = {
801 'group_id': group_id,
802 'disks': group_disks,
803 'members': group_config['members'],
804 }
805 groups.append(group)
806 groups = IscsiTarget._sorted_groups(groups)
807 target_controls = target_config['controls']
11fdf7f2
TL
808 acl_enabled = target_config['acl_enabled']
809 target = {
810 'target_iqn': target_iqn,
811 'portals': portals,
812 'disks': disks,
813 'clients': clients,
814 'groups': groups,
815 'target_controls': target_controls,
816 'acl_enabled': acl_enabled
817 }
eafe8130
TL
818 # Target level authentication was introduced in ceph-iscsi config v11
819 if config['version'] > 10:
820 target_user = target_config['auth']['username']
821 target_password = target_config['auth']['password']
822 target_mutual_user = target_config['auth']['mutual_username']
823 target_mutual_password = target_config['auth']['mutual_password']
824 target['auth'] = {
825 'user': target_user,
826 'password': target_password,
827 'mutual_user': target_mutual_user,
828 'mutual_password': target_mutual_password
829 }
11fdf7f2
TL
830 return target
831
eafe8130
TL
832 @staticmethod
833 def _is_executing(target_iqn):
834 executing_tasks, _ = TaskManager.list()
835 for t in executing_tasks:
836 if t.name.startswith('iscsi/target') and t.metadata.get('target_iqn') == target_iqn:
837 return True
838 return False
839
11fdf7f2
TL
840 @staticmethod
841 def _set_info(target):
842 if not target['portals']:
843 return
844 target_iqn = target['target_iqn']
eafe8130
TL
845 # During task execution, additional info is not available
846 if IscsiTarget._is_executing(target_iqn):
847 return
92f5a8d4
TL
848 # If any portal is down, additional info is not available
849 for portal in target['portals']:
850 try:
851 IscsiClient.instance(gateway_name=portal['host']).ping()
852 except (IscsiGatewayDoesNotExist, RequestException):
853 return
11fdf7f2 854 gateway_name = target['portals'][0]['host']
eafe8130
TL
855 try:
856 target_info = IscsiClient.instance(gateway_name=gateway_name).get_targetinfo(
857 target_iqn)
858 target['info'] = target_info
859 for client in target['clients']:
860 client_iqn = client['client_iqn']
861 client_info = IscsiClient.instance(gateway_name=gateway_name).get_clientinfo(
862 target_iqn, client_iqn)
863 client['info'] = client_info
864 except RequestException as e:
865 # Target/Client has been removed in the meanwhile (e.g. using gwcli)
866 if e.status_code != 404:
867 raise e
11fdf7f2
TL
868
869 @staticmethod
870 def _sorted_portals(portals):
871 portals = portals or []
872 return sorted(portals, key=lambda p: '{}.{}'.format(p['host'], p['ip']))
873
874 @staticmethod
875 def _sorted_disks(disks):
876 disks = disks or []
877 return sorted(disks, key=lambda d: '{}.{}'.format(d['pool'], d['image']))
878
879 @staticmethod
880 def _sorted_clients(clients):
881 clients = clients or []
882 for client in clients:
883 client['luns'] = sorted(client['luns'],
884 key=lambda d: '{}.{}'.format(d['pool'], d['image']))
885 return sorted(clients, key=lambda c: c['client_iqn'])
886
887 @staticmethod
888 def _sorted_groups(groups):
889 groups = groups or []
890 for group in groups:
891 group['disks'] = sorted(group['disks'],
892 key=lambda d: '{}.{}'.format(d['pool'], d['image']))
893 group['members'] = sorted(group['members'])
894 return sorted(groups, key=lambda g: g['group_id'])
895
896 @staticmethod
897 def _get_portals_by_host(portals):
898 portals_by_host = {}
899 for portal in portals:
900 host = portal['host']
901 ip = portal['ip']
902 if host not in portals_by_host:
903 portals_by_host[host] = []
904 portals_by_host[host].append(ip)
905 return portals_by_host
92f5a8d4
TL
906
907
908def get_available_gateway():
909 gateways = IscsiGatewaysConfig.get_gateways_config()['gateways']
910 if not gateways:
911 raise DashboardException(msg='There are no gateways defined',
912 code='no_gateways_defined',
913 component='iscsi')
914 for gateway in gateways:
915 try:
916 IscsiClient.instance(gateway_name=gateway).ping()
917 return gateway
918 except RequestException:
919 pass
920 raise DashboardException(msg='There are no gateways available',
921 code='no_gateways_available',
922 component='iscsi')
923
924
925def validate_rest_api(gateways):
926 for gateway in gateways:
927 try:
928 IscsiClient.instance(gateway_name=gateway).ping()
929 except RequestException:
930 raise DashboardException(msg='iSCSI REST Api not available for gateway '
931 '{}'.format(gateway),
932 code='ceph_iscsi_rest_api_not_available_for_gateway',
933 component='iscsi')