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