1 # -*- coding: utf-8 -*-
2 # pylint: disable=too-many-branches
3 # pylint: disable=too-many-lines
4 from __future__
import absolute_import
6 from copy
import deepcopy
14 from . import ApiController
, UiApiController
, RESTController
, BaseController
, Endpoint
,\
15 ReadPermission
, UpdatePermission
, Task
17 from ..rest_client
import RequestException
18 from ..security
import Scope
19 from ..services
.iscsi_client
import IscsiClient
20 from ..services
.iscsi_cli
import IscsiGatewaysConfig
21 from ..services
.iscsi_config
import IscsiGatewayDoesNotExist
22 from ..services
.rbd
import format_bitmask
23 from ..services
.tcmu_service
import TcmuService
24 from ..exceptions
import DashboardException
25 from ..tools
import str_to_bool
, TaskManager
28 from typing
import Any
, Dict
, List
, no_type_check
30 no_type_check
= object() # Just for type checking
33 @UiApiController('/iscsi', Scope
.ISCSI
)
34 class IscsiUi(BaseController
):
36 REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION
= 10
37 REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION
= 11
43 status
= {'available': False}
45 gateway
= get_available_gateway()
46 except DashboardException
as e
:
47 status
['message'] = str(e
)
50 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
51 if config
['version'] < IscsiUi
.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION
or \
52 config
['version'] > IscsiUi
.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION
:
53 status
['message'] = 'Unsupported `ceph-iscsi` config version. ' \
54 'Expected >= {} and <= {} but found' \
55 ' {}.'.format(IscsiUi
.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION
,
56 IscsiUi
.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION
,
59 status
['available'] = True
60 except RequestException
as e
:
63 content
= json
.loads(e
.content
)
64 content_message
= content
.get('message')
66 content_message
= e
.content
68 status
['message'] = content_message
75 gateway
= get_available_gateway()
76 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
78 'ceph_iscsi_config_version': config
['version']
84 gateway
= get_available_gateway()
85 settings
= IscsiClient
.instance(gateway_name
=gateway
).get_settings()
86 if 'target_controls_limits' in settings
:
87 target_default_controls
= settings
['target_default_controls']
88 for ctrl_k
, ctrl_v
in target_default_controls
.items():
89 limits
= settings
['target_controls_limits'].get(ctrl_k
, {})
90 if 'type' not in limits
:
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
:
105 limits
['type'] = 'int'
106 settings
['disk_controls_limits'][backstore
][ctrl_k
] = limits
113 gateways_config
= IscsiGatewaysConfig
.get_gateways_config()
114 for name
in gateways_config
['gateways']:
116 ip_addresses
= IscsiClient
.instance(gateway_name
=name
).get_ip_addresses()
117 portals
.append({'name': name
, 'ip_addresses': ip_addresses
['data']})
118 except RequestException
:
120 return sorted(portals
, key
=lambda p
: '{}.{}'.format(p
['name'], p
['ip_addresses']))
127 gateways_names
= IscsiGatewaysConfig
.get_gateways_config()['gateways'].keys()
129 for gateway_name
in gateways_names
:
131 config
= IscsiClient
.instance(gateway_name
=gateway_name
).get_config()
133 except RequestException
:
137 for gateway_name
in gateways_names
:
139 'name': gateway_name
,
141 'num_targets': 'n/a',
142 'num_sessions': 'n/a'
145 IscsiClient
.instance(gateway_name
=gateway_name
).ping()
146 gateway
['state'] = 'up'
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'
156 gateway
['num_targets'] = len([target
for _
, target
in config
['targets'].items()
157 if gateway_name
in target
['portals']])
158 result_gateways
.append(gateway
)
162 tcmu_info
= TcmuService
.get_iscsi_info()
163 for _
, disk_config
in config
['disks'].items():
165 'pool': disk_config
['pool'],
166 'image': disk_config
['image'],
167 'backstore': disk_config
['backstore'],
168 'optimized_since': None,
170 'stats_history': None
172 tcmu_image_info
= TcmuService
.get_image_info(image
['pool'],
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
)
185 'gateways': sorted(result_gateways
, key
=lambda g
: g
['name']),
186 'images': sorted(result_images
, key
=lambda i
: '{}/{}'.format(i
['pool'], i
['image']))
190 @ApiController('/iscsi', Scope
.ISCSI
)
191 class Iscsi(BaseController
):
193 @Endpoint('GET', 'discoveryauth')
195 def get_discoveryauth(self
):
196 gateway
= get_available_gateway()
197 return self
._get
_discoveryauth
(gateway
)
199 @Endpoint('PUT', 'discoveryauth')
201 def set_discoveryauth(self
, user
, password
, mutual_user
, mutual_password
):
204 'password': password
,
205 'mutual_user': mutual_user
,
206 'mutual_password': mutual_password
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
,
217 return self
._get
_discoveryauth
(gateway
)
219 def _get_discoveryauth(self
, gateway
):
220 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
221 user
= config
['discovery_auth']['username']
222 password
= config
['discovery_auth']['password']
223 mutual_user
= config
['discovery_auth']['mutual_username']
224 mutual_password
= config
['discovery_auth']['mutual_password']
227 'password': password
,
228 'mutual_user': mutual_user
,
229 'mutual_password': mutual_password
233 def iscsi_target_task(name
, metadata
, wait_for
=2.0):
234 return Task("iscsi/target/{}".format(name
), metadata
, wait_for
)
237 @ApiController('/iscsi/target', Scope
.ISCSI
)
238 class IscsiTarget(RESTController
):
241 gateway
= get_available_gateway()
242 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
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
)
250 def get(self
, target_iqn
):
251 gateway
= get_available_gateway()
252 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
253 if target_iqn
not in config
['targets']:
254 raise cherrypy
.HTTPError(404)
255 target
= IscsiTarget
._config
_to
_target
(target_iqn
, config
)
256 IscsiTarget
._set
_info
(target
)
259 @iscsi_target_task('delete', {'target_iqn': '{target_iqn}'})
260 def delete(self
, target_iqn
):
261 gateway
= get_available_gateway()
262 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
263 if target_iqn
not in config
['targets']:
264 raise DashboardException(msg
='Target does not exist',
265 code
='target_does_not_exist',
267 portal_names
= list(config
['targets'][target_iqn
]['portals'].keys())
268 validate_rest_api(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',
276 IscsiTarget
._delete
(target_iqn
, config
, 0, 100)
278 @iscsi_target_task('create', {'target_iqn': '{target_iqn}'})
279 def create(self
, target_iqn
=None, target_controls
=None, acl_enabled
=None,
280 auth
=None, portals
=None, disks
=None, clients
=None, groups
=None):
281 target_controls
= target_controls
or {}
282 portals
= portals
or []
284 clients
= clients
or []
285 groups
= groups
or []
288 for client
in clients
:
289 validate_auth(client
['auth'])
291 gateway
= get_available_gateway()
292 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
293 if target_iqn
in config
['targets']:
294 raise DashboardException(msg
='Target already exists',
295 code
='target_already_exists',
297 settings
= IscsiClient
.instance(gateway_name
=gateway
).get_settings()
298 IscsiTarget
._validate
(target_iqn
, target_controls
, portals
, disks
, groups
, settings
)
300 IscsiTarget
._create
(target_iqn
, target_controls
, acl_enabled
, auth
, portals
, disks
,
301 clients
, groups
, 0, 100, config
, settings
)
303 @iscsi_target_task('edit', {'target_iqn': '{target_iqn}'})
304 def set(self
, target_iqn
, new_target_iqn
=None, target_controls
=None, acl_enabled
=None,
305 auth
=None, portals
=None, disks
=None, clients
=None, groups
=None):
306 target_controls
= target_controls
or {}
307 portals
= IscsiTarget
._sorted
_portals
(portals
)
308 disks
= IscsiTarget
._sorted
_disks
(disks
)
309 clients
= IscsiTarget
._sorted
_clients
(clients
)
310 groups
= IscsiTarget
._sorted
_groups
(groups
)
313 for client
in clients
:
314 validate_auth(client
['auth'])
316 gateway
= get_available_gateway()
317 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
318 if target_iqn
not in config
['targets']:
319 raise DashboardException(msg
='Target does not exist',
320 code
='target_does_not_exist',
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',
327 settings
= IscsiClient
.instance(gateway_name
=gateway
).get_settings()
328 new_portal_names
= {p
['host'] for p
in portals
}
329 old_portal_names
= set(config
['targets'][target_iqn
]['portals'].keys())
330 deleted_portal_names
= list(old_portal_names
- new_portal_names
)
331 validate_rest_api(deleted_portal_names
)
332 IscsiTarget
._validate
(new_target_iqn
, target_controls
, portals
, disks
, groups
, settings
)
333 IscsiTarget
._validate
_delete
(gateway
, target_iqn
, config
, new_target_iqn
, target_controls
,
334 disks
, clients
, groups
)
335 config
= IscsiTarget
._delete
(target_iqn
, config
, 0, 50, new_target_iqn
, target_controls
,
336 portals
, disks
, clients
, groups
)
337 IscsiTarget
._create
(new_target_iqn
, target_controls
, acl_enabled
, auth
, portals
, disks
,
338 clients
, groups
, 50, 100, config
, settings
)
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,
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 []
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',
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]
366 for group_id
in list(target_config
['groups'].keys()):
367 if IscsiTarget
._group
_deletion
_required
(target
, new_target_iqn
, new_target_controls
,
368 new_groups
, group_id
, new_clients
,
370 deleted_groups
.append(group_id
)
371 IscsiClient
.instance(gateway_name
=gateway_name
).delete_group(target_iqn
,
373 TaskManager
.current_task().inc_progress(task_progress_inc
)
375 deleted_client_luns
= []
376 for client_iqn
, client_config
in target_config
['clients'].items():
377 if IscsiTarget
._client
_deletion
_required
(target
, new_target_iqn
, new_target_controls
,
378 new_clients
, client_iqn
,
380 deleted_clients
.append(client_iqn
)
381 IscsiClient
.instance(gateway_name
=gateway_name
).delete_client(target_iqn
,
384 for image_id
in list(client_config
.get('luns', {}).keys()):
385 if IscsiTarget
._client
_lun
_deletion
_required
(target
, client_iqn
, image_id
,
387 deleted_client_luns
.append((client_iqn
, image_id
))
388 IscsiClient
.instance(gateway_name
=gateway_name
).delete_client_lun(
389 target_iqn
, client_iqn
, image_id
)
390 TaskManager
.current_task().inc_progress(task_progress_inc
)
391 for image_id
in target_config
['disks']:
392 if IscsiTarget
._target
_lun
_deletion
_required
(target
, new_target_iqn
,
394 new_disks
, image_id
):
395 all_clients
= target_config
['clients'].keys()
396 not_deleted_clients
= [c
for c
in all_clients
if c
not in deleted_clients
]
397 for client_iqn
in not_deleted_clients
:
398 client_image_ids
= target_config
['clients'][client_iqn
]['luns'].keys()
399 for client_image_id
in client_image_ids
:
400 if image_id
== client_image_id
and \
401 (client_iqn
, client_image_id
) not in deleted_client_luns
:
402 IscsiClient
.instance(gateway_name
=gateway_name
).delete_client_lun(
403 target_iqn
, client_iqn
, client_image_id
)
404 IscsiClient
.instance(gateway_name
=gateway_name
).delete_target_lun(target_iqn
,
406 pool
, image
= image_id
.split('/', 1)
407 IscsiClient
.instance(gateway_name
=gateway_name
).delete_disk(pool
, image
)
408 TaskManager
.current_task().inc_progress(task_progress_inc
)
409 old_portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(target
['portals'])
410 new_portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(new_portals
)
411 for old_portal_host
, old_portal_ip_list
in old_portals_by_host
.items():
412 if IscsiTarget
._target
_portal
_deletion
_required
(old_portal_host
,
414 new_portals_by_host
):
415 IscsiClient
.instance(gateway_name
=gateway_name
).delete_gateway(target_iqn
,
417 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
418 IscsiClient
.instance(gateway_name
=gateway_name
).delete_target(target_iqn
)
419 TaskManager
.current_task().set_progress(task_progress_end
)
420 return IscsiClient
.instance(gateway_name
=gateway_name
).get_config()
423 def _get_group(groups
, group_id
):
425 if group
['group_id'] == group_id
:
430 def _group_deletion_required(target
, new_target_iqn
, new_target_controls
,
431 new_groups
, group_id
, new_clients
, new_disks
):
432 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
434 new_group
= IscsiTarget
._get
_group
(new_groups
, group_id
)
437 old_group
= IscsiTarget
._get
_group
(target
['groups'], group_id
)
438 if new_group
!= old_group
:
440 # Check if any client inside this group has changed
441 for client_iqn
in new_group
['members']:
442 if IscsiTarget
._client
_deletion
_required
(target
, new_target_iqn
, new_target_controls
,
443 new_clients
, client_iqn
,
446 # Check if any disk inside this group has changed
447 for disk
in new_group
['disks']:
448 image_id
= '{}/{}'.format(disk
['pool'], disk
['image'])
449 if IscsiTarget
._target
_lun
_deletion
_required
(target
, new_target_iqn
,
451 new_disks
, image_id
):
456 def _get_client(clients
, client_iqn
):
457 for client
in clients
:
458 if client
['client_iqn'] == client_iqn
:
463 def _client_deletion_required(target
, new_target_iqn
, new_target_controls
,
464 new_clients
, client_iqn
, deleted_groups
):
465 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
467 new_client
= IscsiTarget
._get
_client
(new_clients
, client_iqn
)
470 # Check if client belongs to a groups that has been deleted
471 for group
in target
['groups']:
472 if group
['group_id'] in deleted_groups
and client_iqn
in group
['members']:
477 def _client_lun_deletion_required(target
, client_iqn
, image_id
, new_clients
):
478 new_client
= IscsiTarget
._get
_client
(new_clients
, client_iqn
)
481 new_lun
= IscsiTarget
._get
_disk
(new_client
.get('luns', []), image_id
)
484 old_client
= IscsiTarget
._get
_client
(target
['clients'], client_iqn
)
487 old_lun
= IscsiTarget
._get
_disk
(old_client
.get('luns', []), image_id
)
488 return new_lun
!= old_lun
491 def _get_disk(disks
, image_id
):
493 if '{}/{}'.format(disk
['pool'], disk
['image']) == image_id
:
498 def _target_lun_deletion_required(target
, new_target_iqn
, new_target_controls
,
499 new_disks
, image_id
):
500 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
502 new_disk
= IscsiTarget
._get
_disk
(new_disks
, image_id
)
505 old_disk
= IscsiTarget
._get
_disk
(target
['disks'], image_id
)
506 new_disk_without_controls
= deepcopy(new_disk
)
507 new_disk_without_controls
.pop('controls')
508 old_disk_without_controls
= deepcopy(old_disk
)
509 old_disk_without_controls
.pop('controls')
510 if new_disk_without_controls
!= old_disk_without_controls
:
515 def _target_portal_deletion_required(old_portal_host
, old_portal_ip_list
, new_portals_by_host
):
516 if old_portal_host
not in new_portals_by_host
:
518 if sorted(old_portal_ip_list
) != sorted(new_portals_by_host
[old_portal_host
]):
523 def _target_deletion_required(target
, new_target_iqn
, new_target_controls
):
524 gateway
= get_available_gateway()
525 settings
= IscsiClient
.instance(gateway_name
=gateway
).get_settings()
527 if target
['target_iqn'] != new_target_iqn
:
529 if settings
['api_version'] < 2 and target
['target_controls'] != new_target_controls
:
534 def _validate(target_iqn
, target_controls
, portals
, disks
, groups
, settings
):
536 raise DashboardException(msg
='Target IQN is required',
537 code
='target_iqn_required',
540 minimum_gateways
= max(1, settings
['config']['minimum_gateways'])
541 portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(portals
)
542 if len(portals_by_host
.keys()) < minimum_gateways
:
543 if minimum_gateways
== 1:
544 msg
= 'At least one portal is required'
546 msg
= 'At least {} portals are required'.format(minimum_gateways
)
547 raise DashboardException(msg
=msg
,
548 code
='portals_required',
551 # 'target_controls_limits' was introduced in ceph-iscsi > 3.2
552 # When using an older `ceph-iscsi` version these validations will
553 # NOT be executed beforehand
554 if 'target_controls_limits' in settings
:
555 for target_control_name
, target_control_value
in target_controls
.items():
556 limits
= settings
['target_controls_limits'].get(target_control_name
)
557 if limits
is not None:
558 min_value
= limits
.get('min')
559 if min_value
is not None and target_control_value
< min_value
:
560 raise DashboardException(msg
='Target control {} must be >= '
561 '{}'.format(target_control_name
, min_value
),
562 code
='target_control_invalid_min',
564 max_value
= limits
.get('max')
565 if max_value
is not None and target_control_value
> max_value
:
566 raise DashboardException(msg
='Target control {} must be <= '
567 '{}'.format(target_control_name
, max_value
),
568 code
='target_control_invalid_max',
571 portal_names
= [p
['host'] for p
in portals
]
572 validate_rest_api(portal_names
)
576 image
= disk
['image']
577 backstore
= disk
['backstore']
578 required_rbd_features
= settings
['required_rbd_features'][backstore
]
579 unsupported_rbd_features
= settings
['unsupported_rbd_features'][backstore
]
580 IscsiTarget
._validate
_image
(pool
, image
, backstore
, required_rbd_features
,
581 unsupported_rbd_features
)
583 # 'disk_controls_limits' was introduced in ceph-iscsi > 3.2
584 # When using an older `ceph-iscsi` version these validations will
585 # NOT be executed beforehand
586 if 'disk_controls_limits' in settings
:
587 for disk_control_name
, disk_control_value
in disk
['controls'].items():
588 limits
= settings
['disk_controls_limits'][backstore
].get(disk_control_name
)
589 if limits
is not None:
590 min_value
= limits
.get('min')
591 if min_value
is not None and disk_control_value
< min_value
:
592 raise DashboardException(msg
='Disk control {} must be >= '
593 '{}'.format(disk_control_name
, min_value
),
594 code
='disk_control_invalid_min',
596 max_value
= limits
.get('max')
597 if max_value
is not None and disk_control_value
> max_value
:
598 raise DashboardException(msg
='Disk control {} must be <= '
599 '{}'.format(disk_control_name
, max_value
),
600 code
='disk_control_invalid_max',
603 initiators
= [] # type: List[Any]
605 initiators
= initiators
+ group
['members']
606 if len(initiators
) != len(set(initiators
)):
607 raise DashboardException(msg
='Each initiator can only be part of 1 group at a time',
608 code
='initiator_in_multiple_groups',
612 def _validate_image(pool
, image
, backstore
, required_rbd_features
, unsupported_rbd_features
):
614 ioctx
= mgr
.rados
.open_ioctx(pool
)
616 with rbd
.Image(ioctx
, image
) as img
:
617 if img
.features() & required_rbd_features
!= required_rbd_features
:
618 raise DashboardException(msg
='Image {} cannot be exported using {} '
619 'backstore because required features are '
620 'missing (required features are '
624 required_rbd_features
)),
625 code
='image_missing_required_features',
627 if img
.features() & unsupported_rbd_features
!= 0:
628 raise DashboardException(msg
='Image {} cannot be exported using {} '
629 'backstore because it contains unsupported '
634 unsupported_rbd_features
)),
635 code
='image_contains_unsupported_features',
638 except rbd
.ImageNotFound
:
639 raise DashboardException(msg
='Image {} does not exist'.format(image
),
640 code
='image_does_not_exist',
642 except rados
.ObjectNotFound
:
643 raise DashboardException(msg
='Pool {} does not exist'.format(pool
),
644 code
='pool_does_not_exist',
648 def _validate_delete(gateway
, target_iqn
, config
, new_target_iqn
=None, new_target_controls
=None,
649 new_disks
=None, new_clients
=None, new_groups
=None):
650 new_target_controls
= new_target_controls
or {}
651 new_disks
= new_disks
or []
652 new_clients
= new_clients
or []
653 new_groups
= new_groups
or []
655 target_config
= config
['targets'][target_iqn
]
656 target
= IscsiTarget
._config
_to
_target
(target_iqn
, config
)
658 for group_id
in list(target_config
['groups'].keys()):
659 if IscsiTarget
._group
_deletion
_required
(target
, new_target_iqn
, new_target_controls
,
660 new_groups
, group_id
, new_clients
,
662 deleted_groups
.append(group_id
)
663 for client_iqn
in list(target_config
['clients'].keys()):
664 if IscsiTarget
._client
_deletion
_required
(target
, new_target_iqn
, new_target_controls
,
665 new_clients
, client_iqn
, deleted_groups
):
666 client_info
= IscsiClient
.instance(gateway_name
=gateway
).get_clientinfo(target_iqn
,
668 if client_info
.get('state', {}).get('LOGGED_IN', []):
669 raise DashboardException(msg
="Client '{}' cannot be deleted until it's logged "
670 "out".format(client_iqn
),
671 code
='client_logged_in',
675 def _update_targetauth(config
, target_iqn
, auth
, gateway_name
):
676 # Target level authentication was introduced in ceph-iscsi config v11
677 if config
['version'] > 10:
679 password
= auth
['password']
680 mutual_user
= auth
['mutual_user']
681 mutual_password
= auth
['mutual_password']
682 IscsiClient
.instance(gateway_name
=gateway_name
).update_targetauth(target_iqn
,
689 def _update_targetacl(target_config
, target_iqn
, acl_enabled
, gateway_name
):
690 if not target_config
or target_config
['acl_enabled'] != acl_enabled
:
691 targetauth_action
= ('enable_acl' if acl_enabled
else 'disable_acl')
692 IscsiClient
.instance(gateway_name
=gateway_name
).update_targetacl(target_iqn
,
696 def _is_auth_equal(auth_config
, auth
):
697 return auth
['user'] == auth_config
['username'] and \
698 auth
['password'] == auth_config
['password'] and \
699 auth
['mutual_user'] == auth_config
['mutual_username'] and \
700 auth
['mutual_password'] == auth_config
['mutual_password']
703 def _create(target_iqn
, target_controls
, acl_enabled
,
704 auth
, portals
, disks
, clients
, groups
,
705 task_progress_begin
, task_progress_end
, config
, settings
):
706 target_config
= config
['targets'].get(target_iqn
, None)
707 TaskManager
.current_task().set_progress(task_progress_begin
)
708 portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(portals
)
709 n_hosts
= len(portals_by_host
)
711 n_clients
= len(clients
)
712 n_groups
= len(groups
)
713 task_progress_steps
= n_hosts
+ n_disks
+ n_clients
+ n_groups
714 task_progress_inc
= 0
715 if task_progress_steps
!= 0:
716 task_progress_inc
= int((task_progress_end
- task_progress_begin
) / task_progress_steps
)
718 gateway_name
= portals
[0]['host']
719 if not target_config
:
720 IscsiClient
.instance(gateway_name
=gateway_name
).create_target(target_iqn
,
722 for host
, ip_list
in portals_by_host
.items():
723 if not target_config
or host
not in target_config
['portals']:
724 IscsiClient
.instance(gateway_name
=gateway_name
).create_gateway(target_iqn
,
727 TaskManager
.current_task().inc_progress(task_progress_inc
)
729 if not target_config
or \
730 acl_enabled
!= target_config
['acl_enabled'] or \
731 not IscsiTarget
._is
_auth
_equal
(target_config
['auth'], auth
):
733 IscsiTarget
._update
_targetauth
(config
, target_iqn
, auth
, gateway_name
)
734 IscsiTarget
._update
_targetacl
(target_config
, target_iqn
, acl_enabled
,
737 IscsiTarget
._update
_targetacl
(target_config
, target_iqn
, acl_enabled
,
739 IscsiTarget
._update
_targetauth
(config
, target_iqn
, auth
, gateway_name
)
743 image
= disk
['image']
744 image_id
= '{}/{}'.format(pool
, image
)
745 backstore
= disk
['backstore']
746 wwn
= disk
.get('wwn')
747 lun
= disk
.get('lun')
748 if image_id
not in config
['disks']:
749 IscsiClient
.instance(gateway_name
=gateway_name
).create_disk(pool
,
753 if not target_config
or image_id
not in target_config
['disks']:
754 IscsiClient
.instance(gateway_name
=gateway_name
).create_target_lun(target_iqn
,
758 controls
= disk
['controls']
760 if image_id
in config
['disks']:
761 d_conf_controls
= config
['disks'][image_id
]['controls']
762 disk_default_controls
= settings
['disk_default_controls'][backstore
]
763 for old_control
in d_conf_controls
.keys():
764 # If control was removed, restore the default value
765 if old_control
not in controls
:
766 controls
[old_control
] = disk_default_controls
[old_control
]
768 if (image_id
not in config
['disks'] or d_conf_controls
!= controls
) and controls
:
769 IscsiClient
.instance(gateway_name
=gateway_name
).reconfigure_disk(pool
,
772 TaskManager
.current_task().inc_progress(task_progress_inc
)
773 for client
in clients
:
774 client_iqn
= client
['client_iqn']
775 if not target_config
or client_iqn
not in target_config
['clients']:
776 IscsiClient
.instance(gateway_name
=gateway_name
).create_client(target_iqn
,
778 if not target_config
or client_iqn
not in target_config
['clients'] or \
779 not IscsiTarget
._is
_auth
_equal
(target_config
['clients'][client_iqn
]['auth'],
781 user
= client
['auth']['user']
782 password
= client
['auth']['password']
783 m_user
= client
['auth']['mutual_user']
784 m_password
= client
['auth']['mutual_password']
785 IscsiClient
.instance(gateway_name
=gateway_name
).create_client_auth(
786 target_iqn
, client_iqn
, user
, password
, m_user
, m_password
)
787 for lun
in client
['luns']:
790 image_id
= '{}/{}'.format(pool
, image
)
791 if not target_config
or client_iqn
not in target_config
['clients'] or \
792 image_id
not in target_config
['clients'][client_iqn
]['luns']:
793 IscsiClient
.instance(gateway_name
=gateway_name
).create_client_lun(
794 target_iqn
, client_iqn
, image_id
)
795 TaskManager
.current_task().inc_progress(task_progress_inc
)
797 group_id
= group
['group_id']
798 members
= group
['members']
800 for disk
in group
['disks']:
801 image_ids
.append('{}/{}'.format(disk
['pool'], disk
['image']))
802 if not target_config
or group_id
not in target_config
['groups']:
803 IscsiClient
.instance(gateway_name
=gateway_name
).create_group(
804 target_iqn
, group_id
, members
, image_ids
)
805 TaskManager
.current_task().inc_progress(task_progress_inc
)
807 if not target_config
or target_controls
!= target_config
['controls']:
808 IscsiClient
.instance(gateway_name
=gateway_name
).reconfigure_target(
809 target_iqn
, target_controls
)
810 TaskManager
.current_task().set_progress(task_progress_end
)
811 except RequestException
as e
:
813 content
= json
.loads(e
.content
)
814 content_message
= content
.get('message')
816 raise DashboardException(msg
=content_message
, component
='iscsi')
817 raise DashboardException(e
=e
, component
='iscsi')
820 def _config_to_target(target_iqn
, config
):
821 target_config
= config
['targets'][target_iqn
]
823 for host
, portal_config
in target_config
['portals'].items():
824 for portal_ip
in portal_config
['portal_ip_addresses']:
829 portals
.append(portal
)
830 portals
= IscsiTarget
._sorted
_portals
(portals
)
832 for target_disk
in target_config
['disks']:
833 disk_config
= config
['disks'][target_disk
]
835 'pool': disk_config
['pool'],
836 'image': disk_config
['image'],
837 'controls': disk_config
['controls'],
838 'backstore': disk_config
['backstore'],
839 'wwn': disk_config
['wwn']
841 # lun_id was introduced in ceph-iscsi config v11
842 if config
['version'] > 10:
843 disk
['lun'] = target_config
['disks'][target_disk
]['lun_id']
845 disks
= IscsiTarget
._sorted
_disks
(disks
)
847 for client_iqn
, client_config
in target_config
['clients'].items():
849 for client_lun
in client_config
['luns'].keys():
850 pool
, image
= client_lun
.split('/', 1)
856 user
= client_config
['auth']['username']
857 password
= client_config
['auth']['password']
858 mutual_user
= client_config
['auth']['mutual_username']
859 mutual_password
= client_config
['auth']['mutual_password']
861 'client_iqn': client_iqn
,
865 'password': password
,
866 'mutual_user': mutual_user
,
867 'mutual_password': mutual_password
870 clients
.append(client
)
871 clients
= IscsiTarget
._sorted
_clients
(clients
)
873 for group_id
, group_config
in target_config
['groups'].items():
875 for group_disk_key
, _
in group_config
['disks'].items():
876 pool
, image
= group_disk_key
.split('/', 1)
881 group_disks
.append(group_disk
)
883 'group_id': group_id
,
884 'disks': group_disks
,
885 'members': group_config
['members'],
888 groups
= IscsiTarget
._sorted
_groups
(groups
)
889 target_controls
= target_config
['controls']
890 acl_enabled
= target_config
['acl_enabled']
892 'target_iqn': target_iqn
,
897 'target_controls': target_controls
,
898 'acl_enabled': acl_enabled
900 # Target level authentication was introduced in ceph-iscsi config v11
901 if config
['version'] > 10:
902 target_user
= target_config
['auth']['username']
903 target_password
= target_config
['auth']['password']
904 target_mutual_user
= target_config
['auth']['mutual_username']
905 target_mutual_password
= target_config
['auth']['mutual_password']
908 'password': target_password
,
909 'mutual_user': target_mutual_user
,
910 'mutual_password': target_mutual_password
915 def _is_executing(target_iqn
):
916 executing_tasks
, _
= TaskManager
.list()
917 for t
in executing_tasks
:
918 if t
.name
.startswith('iscsi/target') and t
.metadata
.get('target_iqn') == target_iqn
:
923 def _set_info(target
):
924 if not target
['portals']:
926 target_iqn
= target
['target_iqn']
927 # During task execution, additional info is not available
928 if IscsiTarget
._is
_executing
(target_iqn
):
930 # If any portal is down, additional info is not available
931 for portal
in target
['portals']:
933 IscsiClient
.instance(gateway_name
=portal
['host']).ping()
934 except (IscsiGatewayDoesNotExist
, RequestException
):
936 gateway_name
= target
['portals'][0]['host']
938 target_info
= IscsiClient
.instance(gateway_name
=gateway_name
).get_targetinfo(
940 target
['info'] = target_info
941 for client
in target
['clients']:
942 client_iqn
= client
['client_iqn']
943 client_info
= IscsiClient
.instance(gateway_name
=gateway_name
).get_clientinfo(
944 target_iqn
, client_iqn
)
945 client
['info'] = client_info
946 except RequestException
as e
:
947 # Target/Client has been removed in the meanwhile (e.g. using gwcli)
948 if e
.status_code
!= 404:
952 def _sorted_portals(portals
):
953 portals
= portals
or []
954 return sorted(portals
, key
=lambda p
: '{}.{}'.format(p
['host'], p
['ip']))
957 def _sorted_disks(disks
):
959 return sorted(disks
, key
=lambda d
: '{}.{}'.format(d
['pool'], d
['image']))
962 def _sorted_clients(clients
):
963 clients
= clients
or []
964 for client
in clients
:
965 client
['luns'] = sorted(client
['luns'],
966 key
=lambda d
: '{}.{}'.format(d
['pool'], d
['image']))
967 return sorted(clients
, key
=lambda c
: c
['client_iqn'])
970 def _sorted_groups(groups
):
971 groups
= groups
or []
973 group
['disks'] = sorted(group
['disks'],
974 key
=lambda d
: '{}.{}'.format(d
['pool'], d
['image']))
975 group
['members'] = sorted(group
['members'])
976 return sorted(groups
, key
=lambda g
: g
['group_id'])
979 def _get_portals_by_host(portals
):
980 # type: (List[dict]) -> Dict[str, List[str]]
981 portals_by_host
= {} # type: Dict[str, List[str]]
982 for portal
in portals
:
983 host
= portal
['host']
985 if host
not in portals_by_host
:
986 portals_by_host
[host
] = []
987 portals_by_host
[host
].append(ip
)
988 return portals_by_host
991 def get_available_gateway():
992 gateways
= IscsiGatewaysConfig
.get_gateways_config()['gateways']
994 raise DashboardException(msg
='There are no gateways defined',
995 code
='no_gateways_defined',
997 for gateway
in gateways
:
999 IscsiClient
.instance(gateway_name
=gateway
).ping()
1001 except RequestException
:
1003 raise DashboardException(msg
='There are no gateways available',
1004 code
='no_gateways_available',
1008 def validate_rest_api(gateways
):
1009 for gateway
in gateways
:
1011 IscsiClient
.instance(gateway_name
=gateway
).ping()
1012 except RequestException
:
1013 raise DashboardException(msg
='iSCSI REST Api not available for gateway '
1014 '{}'.format(gateway
),
1015 code
='ceph_iscsi_rest_api_not_available_for_gateway',
1019 def validate_auth(auth
):
1020 username_regex
= re
.compile(r
'^[\w\.:@_-]{8,64}$')
1021 password_regex
= re
.compile(r
'^[\w@\-_\/]{12,16}$')
1024 if auth
['user'] or auth
['password']:
1025 result
= bool(username_regex
.match(auth
['user'])) and \
1026 bool(password_regex
.match(auth
['password']))
1028 if auth
['mutual_user'] or auth
['mutual_password']:
1029 result
= result
and bool(username_regex
.match(auth
['mutual_user'])) and \
1030 bool(password_regex
.match(auth
['mutual_password'])) and auth
['user']
1033 raise DashboardException(msg
='Bad authentication',
1034 code
='target_bad_auth',