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