]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/controllers/iscsi.py
import 15.2.9
[ceph.git] / ceph / src / pybind / mgr / dashboard / controllers / iscsi.py
CommitLineData
11fdf7f2
TL
1# -*- coding: utf-8 -*-
2# pylint: disable=too-many-branches
f6b5b4d7 3# pylint: disable=too-many-lines
11fdf7f2
TL
4from __future__ import absolute_import
5
6from copy import deepcopy
1911f103 7import re
11fdf7f2
TL
8import json
9import cherrypy
10
11import rados
12import rbd
13
14from . import ApiController, UiApiController, RESTController, BaseController, Endpoint,\
15 ReadPermission, UpdatePermission, Task
16from .. import mgr
17from ..rest_client import RequestException
18from ..security import Scope
19from ..services.iscsi_client import IscsiClient
20from ..services.iscsi_cli import IscsiGatewaysConfig
92f5a8d4 21from ..services.iscsi_config import IscsiGatewayDoesNotExist
11fdf7f2
TL
22from ..services.rbd import format_bitmask
23from ..services.tcmu_service import TcmuService
24from ..exceptions import DashboardException
eafe8130 25from ..tools import str_to_bool, TaskManager
11fdf7f2 26
9f95a23c
TL
27try:
28 from typing import Any, Dict, List, no_type_check
29except ImportError:
30 no_type_check = object() # Just for type checking
31
11fdf7f2
TL
32
33@UiApiController('/iscsi', Scope.ISCSI)
34class IscsiUi(BaseController):
35
eafe8130
TL
36 REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION = 10
37 REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION = 11
11fdf7f2
TL
38
39 @Endpoint()
40 @ReadPermission
9f95a23c 41 @no_type_check
11fdf7f2
TL
42 def status(self):
43 status = {'available': False}
92f5a8d4
TL
44 try:
45 gateway = get_available_gateway()
46 except DashboardException as e:
47 status['message'] = str(e)
11fdf7f2
TL
48 return status
49 try:
92f5a8d4 50 config = IscsiClient.instance(gateway_name=gateway).get_config()
eafe8130
TL
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'])
11fdf7f2
TL
58 return status
59 status['available'] = True
60 except RequestException as e:
61 if e.content:
81eedcae
TL
62 try:
63 content = json.loads(e.content)
64 content_message = content.get('message')
65 except ValueError:
66 content_message = e.content
11fdf7f2
TL
67 if content_message:
68 status['message'] = content_message
81eedcae 69
11fdf7f2
TL
70 return status
71
eafe8130
TL
72 @Endpoint()
73 @ReadPermission
74 def version(self):
92f5a8d4
TL
75 gateway = get_available_gateway()
76 config = IscsiClient.instance(gateway_name=gateway).get_config()
eafe8130 77 return {
92f5a8d4 78 'ceph_iscsi_config_version': config['version']
eafe8130
TL
79 }
80
11fdf7f2
TL
81 @Endpoint()
82 @ReadPermission
83 def settings(self):
92f5a8d4
TL
84 gateway = get_available_gateway()
85 settings = IscsiClient.instance(gateway_name=gateway).get_settings()
eafe8130
TL
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
11fdf7f2
TL
108
109 @Endpoint()
110 @ReadPermission
111 def portals(self):
112 portals = []
113 gateways_config = IscsiGatewaysConfig.get_gateways_config()
81eedcae 114 for name in gateways_config['gateways']:
92f5a8d4
TL
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
11fdf7f2
TL
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)
191class Iscsi(BaseController):
192
193 @Endpoint('GET', 'discoveryauth')
81eedcae 194 @ReadPermission
11fdf7f2 195 def get_discoveryauth(self):
92f5a8d4
TL
196 gateway = get_available_gateway()
197 return self._get_discoveryauth(gateway)
11fdf7f2
TL
198
199 @Endpoint('PUT', 'discoveryauth')
200 @UpdatePermission
201 def set_discoveryauth(self, user, password, mutual_user, mutual_password):
1911f103
TL
202 validate_auth({
203 'user': user,
204 'password': password,
205 'mutual_user': mutual_user,
206 'mutual_password': mutual_password
207 })
208
92f5a8d4
TL
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()
11fdf7f2
TL
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
233def 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)
238class IscsiTarget(RESTController):
239
240 def list(self):
92f5a8d4
TL
241 gateway = get_available_gateway()
242 config = IscsiClient.instance(gateway_name=gateway).get_config()
11fdf7f2
TL
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):
92f5a8d4
TL
251 gateway = get_available_gateway()
252 config = IscsiClient.instance(gateway_name=gateway).get_config()
11fdf7f2
TL
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):
92f5a8d4
TL
261 gateway = get_available_gateway()
262 config = IscsiClient.instance(gateway_name=gateway).get_config()
11fdf7f2
TL
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')
92f5a8d4
TL
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')
11fdf7f2
TL
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,
eafe8130 280 auth=None, portals=None, disks=None, clients=None, groups=None):
11fdf7f2
TL
281 target_controls = target_controls or {}
282 portals = portals or []
283 disks = disks or []
284 clients = clients or []
285 groups = groups or []
286
1911f103
TL
287 validate_auth(auth)
288 for client in clients:
289 validate_auth(client['auth'])
290
92f5a8d4
TL
291 gateway = get_available_gateway()
292 config = IscsiClient.instance(gateway_name=gateway).get_config()
11fdf7f2
TL
293 if target_iqn in config['targets']:
294 raise DashboardException(msg='Target already exists',
295 code='target_already_exists',
296 component='iscsi')
92f5a8d4 297 settings = IscsiClient.instance(gateway_name=gateway).get_settings()
eafe8130
TL
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)
11fdf7f2
TL
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,
eafe8130 305 auth=None, portals=None, disks=None, clients=None, groups=None):
11fdf7f2
TL
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
1911f103
TL
312 validate_auth(auth)
313 for client in clients:
314 validate_auth(client['auth'])
315
92f5a8d4
TL
316 gateway = get_available_gateway()
317 config = IscsiClient.instance(gateway_name=gateway).get_config()
11fdf7f2
TL
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')
1911f103 326
92f5a8d4
TL
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)
eafe8130 332 IscsiTarget._validate(new_target_iqn, target_controls, portals, disks, groups, settings)
f6b5b4d7
TL
333 IscsiTarget._validate_delete(gateway, target_iqn, config, new_target_iqn, target_controls,
334 disks, clients, groups)
11fdf7f2
TL
335 config = IscsiTarget._delete(target_iqn, config, 0, 50, new_target_iqn, target_controls,
336 portals, disks, clients, groups)
eafe8130
TL
337 IscsiTarget._create(new_target_iqn, target_controls, acl_enabled, auth, portals, disks,
338 clients, groups, 50, 100, config, settings)
11fdf7f2
TL
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,
f91f0fd5 368 new_groups, group_id):
11fdf7f2
TL
369 deleted_groups.append(group_id)
370 IscsiClient.instance(gateway_name=gateway_name).delete_group(target_iqn,
371 group_id)
f91f0fd5
TL
372 else:
373 group = IscsiTarget._get_group(new_groups, group_id)
374
375 old_group_disks = set(target_config['groups'][group_id]['disks'].keys())
376 new_group_disks = {'{}/{}'.format(x['pool'], x['image']) for x in group['disks']}
377 local_deleted_disks = list(old_group_disks - new_group_disks)
378
379 old_group_members = set(target_config['groups'][group_id]['members'])
380 new_group_members = set(group['members'])
381 local_deleted_members = list(old_group_members - new_group_members)
382
383 if local_deleted_disks or local_deleted_members:
384 IscsiClient.instance(gateway_name=gateway_name).update_group(
385 target_iqn, group_id, local_deleted_members, local_deleted_disks)
11fdf7f2 386 TaskManager.current_task().inc_progress(task_progress_inc)
eafe8130 387 deleted_clients = []
f6b5b4d7
TL
388 deleted_client_luns = []
389 for client_iqn, client_config in target_config['clients'].items():
11fdf7f2 390 if IscsiTarget._client_deletion_required(target, new_target_iqn, new_target_controls,
f91f0fd5 391 new_clients, client_iqn):
eafe8130 392 deleted_clients.append(client_iqn)
11fdf7f2
TL
393 IscsiClient.instance(gateway_name=gateway_name).delete_client(target_iqn,
394 client_iqn)
f6b5b4d7
TL
395 else:
396 for image_id in list(client_config.get('luns', {}).keys()):
397 if IscsiTarget._client_lun_deletion_required(target, client_iqn, image_id,
f91f0fd5 398 new_clients, new_groups):
f6b5b4d7
TL
399 deleted_client_luns.append((client_iqn, image_id))
400 IscsiClient.instance(gateway_name=gateway_name).delete_client_lun(
401 target_iqn, client_iqn, image_id)
11fdf7f2
TL
402 TaskManager.current_task().inc_progress(task_progress_inc)
403 for image_id in target_config['disks']:
404 if IscsiTarget._target_lun_deletion_required(target, new_target_iqn,
f91f0fd5 405 new_target_controls, new_disks, image_id):
eafe8130 406 all_clients = target_config['clients'].keys()
f91f0fd5
TL
407 not_deleted_clients = [c for c in all_clients if c not in deleted_clients
408 and not IscsiTarget._client_in_group(target['groups'], c)
409 and not IscsiTarget._client_in_group(new_groups, c)]
eafe8130
TL
410 for client_iqn in not_deleted_clients:
411 client_image_ids = target_config['clients'][client_iqn]['luns'].keys()
412 for client_image_id in client_image_ids:
f6b5b4d7
TL
413 if image_id == client_image_id and \
414 (client_iqn, client_image_id) not in deleted_client_luns:
eafe8130
TL
415 IscsiClient.instance(gateway_name=gateway_name).delete_client_lun(
416 target_iqn, client_iqn, client_image_id)
11fdf7f2
TL
417 IscsiClient.instance(gateway_name=gateway_name).delete_target_lun(target_iqn,
418 image_id)
419 pool, image = image_id.split('/', 1)
420 IscsiClient.instance(gateway_name=gateway_name).delete_disk(pool, image)
421 TaskManager.current_task().inc_progress(task_progress_inc)
494da23a
TL
422 old_portals_by_host = IscsiTarget._get_portals_by_host(target['portals'])
423 new_portals_by_host = IscsiTarget._get_portals_by_host(new_portals)
424 for old_portal_host, old_portal_ip_list in old_portals_by_host.items():
425 if IscsiTarget._target_portal_deletion_required(old_portal_host,
426 old_portal_ip_list,
427 new_portals_by_host):
428 IscsiClient.instance(gateway_name=gateway_name).delete_gateway(target_iqn,
429 old_portal_host)
430 if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
11fdf7f2
TL
431 IscsiClient.instance(gateway_name=gateway_name).delete_target(target_iqn)
432 TaskManager.current_task().set_progress(task_progress_end)
433 return IscsiClient.instance(gateway_name=gateway_name).get_config()
434
435 @staticmethod
436 def _get_group(groups, group_id):
437 for group in groups:
438 if group['group_id'] == group_id:
439 return group
440 return None
441
442 @staticmethod
494da23a 443 def _group_deletion_required(target, new_target_iqn, new_target_controls,
f91f0fd5 444 new_groups, group_id):
494da23a 445 if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
11fdf7f2
TL
446 return True
447 new_group = IscsiTarget._get_group(new_groups, group_id)
448 if not new_group:
449 return True
11fdf7f2
TL
450 return False
451
452 @staticmethod
453 def _get_client(clients, client_iqn):
454 for client in clients:
455 if client['client_iqn'] == client_iqn:
456 return client
457 return None
458
459 @staticmethod
494da23a 460 def _client_deletion_required(target, new_target_iqn, new_target_controls,
f91f0fd5 461 new_clients, client_iqn):
494da23a 462 if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
11fdf7f2 463 return True
f6b5b4d7 464 new_client = IscsiTarget._get_client(new_clients, client_iqn)
11fdf7f2
TL
465 if not new_client:
466 return True
f91f0fd5
TL
467 return False
468
469 @staticmethod
470 def _client_in_group(groups, client_iqn):
471 for group in groups:
472 if client_iqn in group['members']:
11fdf7f2
TL
473 return True
474 return False
475
f6b5b4d7 476 @staticmethod
f91f0fd5 477 def _client_lun_deletion_required(target, client_iqn, image_id, new_clients, new_groups):
f6b5b4d7
TL
478 new_client = IscsiTarget._get_client(new_clients, client_iqn)
479 if not new_client:
480 return True
f91f0fd5
TL
481
482 # Disks inherited from groups must be considered
483 was_in_group = IscsiTarget._client_in_group(target['groups'], client_iqn)
484 is_in_group = IscsiTarget._client_in_group(new_groups, client_iqn)
485
486 if not was_in_group and is_in_group:
487 return True
488
489 if is_in_group:
490 return False
491
f6b5b4d7
TL
492 new_lun = IscsiTarget._get_disk(new_client.get('luns', []), image_id)
493 if not new_lun:
494 return True
f91f0fd5 495
f6b5b4d7
TL
496 old_client = IscsiTarget._get_client(target['clients'], client_iqn)
497 if not old_client:
498 return False
f91f0fd5 499
f6b5b4d7
TL
500 old_lun = IscsiTarget._get_disk(old_client.get('luns', []), image_id)
501 return new_lun != old_lun
502
11fdf7f2
TL
503 @staticmethod
504 def _get_disk(disks, image_id):
505 for disk in disks:
506 if '{}/{}'.format(disk['pool'], disk['image']) == image_id:
507 return disk
508 return None
509
510 @staticmethod
494da23a 511 def _target_lun_deletion_required(target, new_target_iqn, new_target_controls,
11fdf7f2 512 new_disks, image_id):
494da23a 513 if IscsiTarget._target_deletion_required(target, new_target_iqn, new_target_controls):
11fdf7f2
TL
514 return True
515 new_disk = IscsiTarget._get_disk(new_disks, image_id)
516 if not new_disk:
517 return True
518 old_disk = IscsiTarget._get_disk(target['disks'], image_id)
eafe8130
TL
519 new_disk_without_controls = deepcopy(new_disk)
520 new_disk_without_controls.pop('controls')
521 old_disk_without_controls = deepcopy(old_disk)
522 old_disk_without_controls.pop('controls')
523 if new_disk_without_controls != old_disk_without_controls:
11fdf7f2
TL
524 return True
525 return False
526
527 @staticmethod
494da23a
TL
528 def _target_portal_deletion_required(old_portal_host, old_portal_ip_list, new_portals_by_host):
529 if old_portal_host not in new_portals_by_host:
530 return True
531 if sorted(old_portal_ip_list) != sorted(new_portals_by_host[old_portal_host]):
532 return True
533 return False
534
535 @staticmethod
536 def _target_deletion_required(target, new_target_iqn, new_target_controls):
1911f103
TL
537 gateway = get_available_gateway()
538 settings = IscsiClient.instance(gateway_name=gateway).get_settings()
539
11fdf7f2
TL
540 if target['target_iqn'] != new_target_iqn:
541 return True
1911f103 542 if settings['api_version'] < 2 and target['target_controls'] != new_target_controls:
11fdf7f2 543 return True
11fdf7f2
TL
544 return False
545
546 @staticmethod
eafe8130 547 def _validate(target_iqn, target_controls, portals, disks, groups, settings):
11fdf7f2
TL
548 if not target_iqn:
549 raise DashboardException(msg='Target IQN is required',
550 code='target_iqn_required',
551 component='iscsi')
552
11fdf7f2
TL
553 minimum_gateways = max(1, settings['config']['minimum_gateways'])
554 portals_by_host = IscsiTarget._get_portals_by_host(portals)
555 if len(portals_by_host.keys()) < minimum_gateways:
556 if minimum_gateways == 1:
557 msg = 'At least one portal is required'
558 else:
559 msg = 'At least {} portals are required'.format(minimum_gateways)
560 raise DashboardException(msg=msg,
561 code='portals_required',
562 component='iscsi')
563
eafe8130
TL
564 # 'target_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 'target_controls_limits' in settings:
568 for target_control_name, target_control_value in target_controls.items():
569 limits = settings['target_controls_limits'].get(target_control_name)
570 if limits is not None:
571 min_value = limits.get('min')
572 if min_value is not None and target_control_value < min_value:
573 raise DashboardException(msg='Target control {} must be >= '
574 '{}'.format(target_control_name, min_value),
575 code='target_control_invalid_min',
576 component='iscsi')
577 max_value = limits.get('max')
578 if max_value is not None and target_control_value > max_value:
579 raise DashboardException(msg='Target control {} must be <= '
580 '{}'.format(target_control_name, max_value),
581 code='target_control_invalid_max',
582 component='iscsi')
583
92f5a8d4
TL
584 portal_names = [p['host'] for p in portals]
585 validate_rest_api(portal_names)
11fdf7f2
TL
586
587 for disk in disks:
588 pool = disk['pool']
589 image = disk['image']
590 backstore = disk['backstore']
591 required_rbd_features = settings['required_rbd_features'][backstore]
81eedcae 592 unsupported_rbd_features = settings['unsupported_rbd_features'][backstore]
11fdf7f2 593 IscsiTarget._validate_image(pool, image, backstore, required_rbd_features,
81eedcae
TL
594 unsupported_rbd_features)
595
eafe8130
TL
596 # 'disk_controls_limits' was introduced in ceph-iscsi > 3.2
597 # When using an older `ceph-iscsi` version these validations will
598 # NOT be executed beforehand
599 if 'disk_controls_limits' in settings:
600 for disk_control_name, disk_control_value in disk['controls'].items():
601 limits = settings['disk_controls_limits'][backstore].get(disk_control_name)
602 if limits is not None:
603 min_value = limits.get('min')
604 if min_value is not None and disk_control_value < min_value:
605 raise DashboardException(msg='Disk control {} must be >= '
606 '{}'.format(disk_control_name, min_value),
607 code='disk_control_invalid_min',
608 component='iscsi')
609 max_value = limits.get('max')
610 if max_value is not None and disk_control_value > max_value:
611 raise DashboardException(msg='Disk control {} must be <= '
612 '{}'.format(disk_control_name, max_value),
613 code='disk_control_invalid_max',
614 component='iscsi')
615
9f95a23c 616 initiators = [] # type: List[Any]
81eedcae
TL
617 for group in groups:
618 initiators = initiators + group['members']
619 if len(initiators) != len(set(initiators)):
620 raise DashboardException(msg='Each initiator can only be part of 1 group at a time',
621 code='initiator_in_multiple_groups',
622 component='iscsi')
11fdf7f2
TL
623
624 @staticmethod
81eedcae 625 def _validate_image(pool, image, backstore, required_rbd_features, unsupported_rbd_features):
11fdf7f2
TL
626 try:
627 ioctx = mgr.rados.open_ioctx(pool)
628 try:
629 with rbd.Image(ioctx, image) as img:
630 if img.features() & required_rbd_features != required_rbd_features:
631 raise DashboardException(msg='Image {} cannot be exported using {} '
632 'backstore because required features are '
633 'missing (required features are '
634 '{})'.format(image,
635 backstore,
636 format_bitmask(
637 required_rbd_features)),
638 code='image_missing_required_features',
639 component='iscsi')
81eedcae 640 if img.features() & unsupported_rbd_features != 0:
11fdf7f2
TL
641 raise DashboardException(msg='Image {} cannot be exported using {} '
642 'backstore because it contains unsupported '
81eedcae 643 'features ('
11fdf7f2
TL
644 '{})'.format(image,
645 backstore,
646 format_bitmask(
81eedcae 647 unsupported_rbd_features)),
11fdf7f2
TL
648 code='image_contains_unsupported_features',
649 component='iscsi')
650
651 except rbd.ImageNotFound:
652 raise DashboardException(msg='Image {} does not exist'.format(image),
653 code='image_does_not_exist',
654 component='iscsi')
655 except rados.ObjectNotFound:
656 raise DashboardException(msg='Pool {} does not exist'.format(pool),
657 code='pool_does_not_exist',
658 component='iscsi')
659
f6b5b4d7
TL
660 @staticmethod
661 def _validate_delete(gateway, target_iqn, config, new_target_iqn=None, new_target_controls=None,
662 new_disks=None, new_clients=None, new_groups=None):
663 new_target_controls = new_target_controls or {}
664 new_disks = new_disks or []
665 new_clients = new_clients or []
666 new_groups = new_groups or []
667
668 target_config = config['targets'][target_iqn]
669 target = IscsiTarget._config_to_target(target_iqn, config)
f6b5b4d7
TL
670 for client_iqn in list(target_config['clients'].keys()):
671 if IscsiTarget._client_deletion_required(target, new_target_iqn, new_target_controls,
f91f0fd5 672 new_clients, client_iqn):
f6b5b4d7
TL
673 client_info = IscsiClient.instance(gateway_name=gateway).get_clientinfo(target_iqn,
674 client_iqn)
675 if client_info.get('state', {}).get('LOGGED_IN', []):
676 raise DashboardException(msg="Client '{}' cannot be deleted until it's logged "
677 "out".format(client_iqn),
678 code='client_logged_in',
679 component='iscsi')
680
eafe8130
TL
681 @staticmethod
682 def _update_targetauth(config, target_iqn, auth, gateway_name):
683 # Target level authentication was introduced in ceph-iscsi config v11
684 if config['version'] > 10:
685 user = auth['user']
686 password = auth['password']
687 mutual_user = auth['mutual_user']
688 mutual_password = auth['mutual_password']
689 IscsiClient.instance(gateway_name=gateway_name).update_targetauth(target_iqn,
690 user,
691 password,
692 mutual_user,
693 mutual_password)
694
695 @staticmethod
696 def _update_targetacl(target_config, target_iqn, acl_enabled, gateway_name):
697 if not target_config or target_config['acl_enabled'] != acl_enabled:
698 targetauth_action = ('enable_acl' if acl_enabled else 'disable_acl')
699 IscsiClient.instance(gateway_name=gateway_name).update_targetacl(target_iqn,
700 targetauth_action)
701
1911f103 702 @staticmethod
f6b5b4d7
TL
703 def _is_auth_equal(auth_config, auth):
704 return auth['user'] == auth_config['username'] and \
705 auth['password'] == auth_config['password'] and \
706 auth['mutual_user'] == auth_config['mutual_username'] and \
707 auth['mutual_password'] == auth_config['mutual_password']
1911f103 708
11fdf7f2
TL
709 @staticmethod
710 def _create(target_iqn, target_controls, acl_enabled,
eafe8130
TL
711 auth, portals, disks, clients, groups,
712 task_progress_begin, task_progress_end, config, settings):
11fdf7f2
TL
713 target_config = config['targets'].get(target_iqn, None)
714 TaskManager.current_task().set_progress(task_progress_begin)
715 portals_by_host = IscsiTarget._get_portals_by_host(portals)
716 n_hosts = len(portals_by_host)
717 n_disks = len(disks)
718 n_clients = len(clients)
719 n_groups = len(groups)
720 task_progress_steps = n_hosts + n_disks + n_clients + n_groups
721 task_progress_inc = 0
722 if task_progress_steps != 0:
723 task_progress_inc = int((task_progress_end - task_progress_begin) / task_progress_steps)
724 try:
725 gateway_name = portals[0]['host']
726 if not target_config:
727 IscsiClient.instance(gateway_name=gateway_name).create_target(target_iqn,
728 target_controls)
494da23a
TL
729 for host, ip_list in portals_by_host.items():
730 if not target_config or host not in target_config['portals']:
11fdf7f2
TL
731 IscsiClient.instance(gateway_name=gateway_name).create_gateway(target_iqn,
732 host,
733 ip_list)
494da23a 734 TaskManager.current_task().inc_progress(task_progress_inc)
eafe8130 735
1911f103
TL
736 if not target_config or \
737 acl_enabled != target_config['acl_enabled'] or \
738 not IscsiTarget._is_auth_equal(target_config['auth'], auth):
739 if acl_enabled:
740 IscsiTarget._update_targetauth(config, target_iqn, auth, gateway_name)
741 IscsiTarget._update_targetacl(target_config, target_iqn, acl_enabled,
742 gateway_name)
743 else:
744 IscsiTarget._update_targetacl(target_config, target_iqn, acl_enabled,
745 gateway_name)
746 IscsiTarget._update_targetauth(config, target_iqn, auth, gateway_name)
eafe8130 747
11fdf7f2
TL
748 for disk in disks:
749 pool = disk['pool']
750 image = disk['image']
751 image_id = '{}/{}'.format(pool, image)
eafe8130
TL
752 backstore = disk['backstore']
753 wwn = disk.get('wwn')
754 lun = disk.get('lun')
11fdf7f2 755 if image_id not in config['disks']:
11fdf7f2
TL
756 IscsiClient.instance(gateway_name=gateway_name).create_disk(pool,
757 image,
eafe8130
TL
758 backstore,
759 wwn)
11fdf7f2
TL
760 if not target_config or image_id not in target_config['disks']:
761 IscsiClient.instance(gateway_name=gateway_name).create_target_lun(target_iqn,
eafe8130
TL
762 image_id,
763 lun)
764
765 controls = disk['controls']
766 d_conf_controls = {}
767 if image_id in config['disks']:
768 d_conf_controls = config['disks'][image_id]['controls']
769 disk_default_controls = settings['disk_default_controls'][backstore]
770 for old_control in d_conf_controls.keys():
771 # If control was removed, restore the default value
772 if old_control not in controls:
773 controls[old_control] = disk_default_controls[old_control]
774
775 if (image_id not in config['disks'] or d_conf_controls != controls) and controls:
776 IscsiClient.instance(gateway_name=gateway_name).reconfigure_disk(pool,
777 image,
778 controls)
11fdf7f2
TL
779 TaskManager.current_task().inc_progress(task_progress_inc)
780 for client in clients:
781 client_iqn = client['client_iqn']
782 if not target_config or client_iqn not in target_config['clients']:
783 IscsiClient.instance(gateway_name=gateway_name).create_client(target_iqn,
784 client_iqn)
f6b5b4d7
TL
785 if not target_config or client_iqn not in target_config['clients'] or \
786 not IscsiTarget._is_auth_equal(target_config['clients'][client_iqn]['auth'],
787 client['auth']):
11fdf7f2
TL
788 user = client['auth']['user']
789 password = client['auth']['password']
790 m_user = client['auth']['mutual_user']
791 m_password = client['auth']['mutual_password']
792 IscsiClient.instance(gateway_name=gateway_name).create_client_auth(
793 target_iqn, client_iqn, user, password, m_user, m_password)
eafe8130
TL
794 for lun in client['luns']:
795 pool = lun['pool']
796 image = lun['image']
797 image_id = '{}/{}'.format(pool, image)
f91f0fd5
TL
798 # Disks inherited from groups must be considered
799 group_disks = []
800 for group in groups:
801 if client_iqn in group['members']:
802 group_disks = ['{}/{}'.format(x['pool'], x['image'])
803 for x in group['disks']]
eafe8130 804 if not target_config or client_iqn not in target_config['clients'] or \
f91f0fd5
TL
805 (image_id not in target_config['clients'][client_iqn]['luns']
806 and image_id not in group_disks):
eafe8130
TL
807 IscsiClient.instance(gateway_name=gateway_name).create_client_lun(
808 target_iqn, client_iqn, image_id)
11fdf7f2
TL
809 TaskManager.current_task().inc_progress(task_progress_inc)
810 for group in groups:
811 group_id = group['group_id']
812 members = group['members']
813 image_ids = []
814 for disk in group['disks']:
815 image_ids.append('{}/{}'.format(disk['pool'], disk['image']))
f91f0fd5
TL
816
817 if target_config and group_id in target_config['groups']:
818 old_members = target_config['groups'][group_id]['members']
819 old_disks = target_config['groups'][group_id]['disks'].keys()
820
821 if not target_config or group_id not in target_config['groups'] or \
822 list(set(group['members']) - set(old_members)) or \
823 list(set(image_ids) - set(old_disks)):
11fdf7f2
TL
824 IscsiClient.instance(gateway_name=gateway_name).create_group(
825 target_iqn, group_id, members, image_ids)
826 TaskManager.current_task().inc_progress(task_progress_inc)
827 if target_controls:
828 if not target_config or target_controls != target_config['controls']:
829 IscsiClient.instance(gateway_name=gateway_name).reconfigure_target(
830 target_iqn, target_controls)
831 TaskManager.current_task().set_progress(task_progress_end)
832 except RequestException as e:
833 if e.content:
834 content = json.loads(e.content)
835 content_message = content.get('message')
836 if content_message:
837 raise DashboardException(msg=content_message, component='iscsi')
838 raise DashboardException(e=e, component='iscsi')
839
840 @staticmethod
841 def _config_to_target(target_iqn, config):
842 target_config = config['targets'][target_iqn]
843 portals = []
494da23a
TL
844 for host, portal_config in target_config['portals'].items():
845 for portal_ip in portal_config['portal_ip_addresses']:
11fdf7f2
TL
846 portal = {
847 'host': host,
848 'ip': portal_ip
849 }
850 portals.append(portal)
851 portals = IscsiTarget._sorted_portals(portals)
852 disks = []
853 for target_disk in target_config['disks']:
854 disk_config = config['disks'][target_disk]
855 disk = {
856 'pool': disk_config['pool'],
857 'image': disk_config['image'],
858 'controls': disk_config['controls'],
eafe8130
TL
859 'backstore': disk_config['backstore'],
860 'wwn': disk_config['wwn']
11fdf7f2 861 }
eafe8130
TL
862 # lun_id was introduced in ceph-iscsi config v11
863 if config['version'] > 10:
864 disk['lun'] = target_config['disks'][target_disk]['lun_id']
11fdf7f2
TL
865 disks.append(disk)
866 disks = IscsiTarget._sorted_disks(disks)
867 clients = []
868 for client_iqn, client_config in target_config['clients'].items():
869 luns = []
870 for client_lun in client_config['luns'].keys():
871 pool, image = client_lun.split('/', 1)
872 lun = {
873 'pool': pool,
874 'image': image
875 }
876 luns.append(lun)
877 user = client_config['auth']['username']
878 password = client_config['auth']['password']
879 mutual_user = client_config['auth']['mutual_username']
880 mutual_password = client_config['auth']['mutual_password']
881 client = {
882 'client_iqn': client_iqn,
883 'luns': luns,
884 'auth': {
885 'user': user,
886 'password': password,
887 'mutual_user': mutual_user,
888 'mutual_password': mutual_password
889 }
890 }
891 clients.append(client)
892 clients = IscsiTarget._sorted_clients(clients)
893 groups = []
894 for group_id, group_config in target_config['groups'].items():
895 group_disks = []
896 for group_disk_key, _ in group_config['disks'].items():
897 pool, image = group_disk_key.split('/', 1)
898 group_disk = {
899 'pool': pool,
900 'image': image
901 }
902 group_disks.append(group_disk)
903 group = {
904 'group_id': group_id,
905 'disks': group_disks,
906 'members': group_config['members'],
907 }
908 groups.append(group)
909 groups = IscsiTarget._sorted_groups(groups)
910 target_controls = target_config['controls']
11fdf7f2
TL
911 acl_enabled = target_config['acl_enabled']
912 target = {
913 'target_iqn': target_iqn,
914 'portals': portals,
915 'disks': disks,
916 'clients': clients,
917 'groups': groups,
918 'target_controls': target_controls,
919 'acl_enabled': acl_enabled
920 }
eafe8130
TL
921 # Target level authentication was introduced in ceph-iscsi config v11
922 if config['version'] > 10:
923 target_user = target_config['auth']['username']
924 target_password = target_config['auth']['password']
925 target_mutual_user = target_config['auth']['mutual_username']
926 target_mutual_password = target_config['auth']['mutual_password']
927 target['auth'] = {
928 'user': target_user,
929 'password': target_password,
930 'mutual_user': target_mutual_user,
931 'mutual_password': target_mutual_password
932 }
11fdf7f2
TL
933 return target
934
eafe8130
TL
935 @staticmethod
936 def _is_executing(target_iqn):
937 executing_tasks, _ = TaskManager.list()
938 for t in executing_tasks:
939 if t.name.startswith('iscsi/target') and t.metadata.get('target_iqn') == target_iqn:
940 return True
941 return False
942
11fdf7f2
TL
943 @staticmethod
944 def _set_info(target):
945 if not target['portals']:
946 return
947 target_iqn = target['target_iqn']
eafe8130
TL
948 # During task execution, additional info is not available
949 if IscsiTarget._is_executing(target_iqn):
950 return
92f5a8d4
TL
951 # If any portal is down, additional info is not available
952 for portal in target['portals']:
953 try:
954 IscsiClient.instance(gateway_name=portal['host']).ping()
955 except (IscsiGatewayDoesNotExist, RequestException):
956 return
11fdf7f2 957 gateway_name = target['portals'][0]['host']
eafe8130
TL
958 try:
959 target_info = IscsiClient.instance(gateway_name=gateway_name).get_targetinfo(
960 target_iqn)
961 target['info'] = target_info
962 for client in target['clients']:
963 client_iqn = client['client_iqn']
964 client_info = IscsiClient.instance(gateway_name=gateway_name).get_clientinfo(
965 target_iqn, client_iqn)
966 client['info'] = client_info
967 except RequestException as e:
968 # Target/Client has been removed in the meanwhile (e.g. using gwcli)
969 if e.status_code != 404:
970 raise e
11fdf7f2
TL
971
972 @staticmethod
973 def _sorted_portals(portals):
974 portals = portals or []
975 return sorted(portals, key=lambda p: '{}.{}'.format(p['host'], p['ip']))
976
977 @staticmethod
978 def _sorted_disks(disks):
979 disks = disks or []
980 return sorted(disks, key=lambda d: '{}.{}'.format(d['pool'], d['image']))
981
982 @staticmethod
983 def _sorted_clients(clients):
984 clients = clients or []
985 for client in clients:
986 client['luns'] = sorted(client['luns'],
987 key=lambda d: '{}.{}'.format(d['pool'], d['image']))
988 return sorted(clients, key=lambda c: c['client_iqn'])
989
990 @staticmethod
991 def _sorted_groups(groups):
992 groups = groups or []
993 for group in groups:
994 group['disks'] = sorted(group['disks'],
995 key=lambda d: '{}.{}'.format(d['pool'], d['image']))
996 group['members'] = sorted(group['members'])
997 return sorted(groups, key=lambda g: g['group_id'])
998
999 @staticmethod
1000 def _get_portals_by_host(portals):
9f95a23c
TL
1001 # type: (List[dict]) -> Dict[str, List[str]]
1002 portals_by_host = {} # type: Dict[str, List[str]]
11fdf7f2
TL
1003 for portal in portals:
1004 host = portal['host']
1005 ip = portal['ip']
1006 if host not in portals_by_host:
1007 portals_by_host[host] = []
1008 portals_by_host[host].append(ip)
1009 return portals_by_host
92f5a8d4
TL
1010
1011
1012def get_available_gateway():
1013 gateways = IscsiGatewaysConfig.get_gateways_config()['gateways']
1014 if not gateways:
1015 raise DashboardException(msg='There are no gateways defined',
1016 code='no_gateways_defined',
1017 component='iscsi')
1018 for gateway in gateways:
1019 try:
1020 IscsiClient.instance(gateway_name=gateway).ping()
1021 return gateway
1022 except RequestException:
1023 pass
1024 raise DashboardException(msg='There are no gateways available',
1025 code='no_gateways_available',
1026 component='iscsi')
1027
1028
1029def validate_rest_api(gateways):
1030 for gateway in gateways:
1031 try:
1032 IscsiClient.instance(gateway_name=gateway).ping()
1033 except RequestException:
1034 raise DashboardException(msg='iSCSI REST Api not available for gateway '
1035 '{}'.format(gateway),
1036 code='ceph_iscsi_rest_api_not_available_for_gateway',
1037 component='iscsi')
1911f103
TL
1038
1039
1040def validate_auth(auth):
1041 username_regex = re.compile(r'^[\w\.:@_-]{8,64}$')
1042 password_regex = re.compile(r'^[\w@\-_\/]{12,16}$')
1043 result = True
1044
1045 if auth['user'] or auth['password']:
1046 result = bool(username_regex.match(auth['user'])) and \
1047 bool(password_regex.match(auth['password']))
1048
1049 if auth['mutual_user'] or auth['mutual_password']:
1050 result = result and bool(username_regex.match(auth['mutual_user'])) and \
1051 bool(password_regex.match(auth['mutual_password'])) and auth['user']
1052
1053 if not result:
1054 raise DashboardException(msg='Bad authentication',
1055 code='target_bad_auth',
1056 component='iscsi')