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