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