1 # -*- coding: utf-8 -*-
2 # pylint: disable=too-many-branches
3 from __future__
import absolute_import
5 from copy
import deepcopy
13 from . import ApiController
, UiApiController
, RESTController
, BaseController
, Endpoint
,\
14 ReadPermission
, UpdatePermission
, Task
16 from ..rest_client
import RequestException
17 from ..security
import Scope
18 from ..services
.iscsi_client
import IscsiClient
19 from ..services
.iscsi_cli
import IscsiGatewaysConfig
20 from ..services
.iscsi_config
import IscsiGatewayDoesNotExist
21 from ..services
.rbd
import format_bitmask
22 from ..services
.tcmu_service
import TcmuService
23 from ..exceptions
import DashboardException
24 from ..tools
import str_to_bool
, TaskManager
27 from typing
import Any
, Dict
, List
, no_type_check
29 no_type_check
= object() # Just for type checking
32 @UiApiController('/iscsi', Scope
.ISCSI
)
33 class IscsiUi(BaseController
):
35 REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION
= 10
36 REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION
= 11
42 status
= {'available': False}
44 gateway
= get_available_gateway()
45 except DashboardException
as e
:
46 status
['message'] = str(e
)
49 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
50 if config
['version'] < IscsiUi
.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION
or \
51 config
['version'] > IscsiUi
.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION
:
52 status
['message'] = 'Unsupported `ceph-iscsi` config version. ' \
53 'Expected >= {} and <= {} but found' \
54 ' {}.'.format(IscsiUi
.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION
,
55 IscsiUi
.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION
,
58 status
['available'] = True
59 except RequestException
as e
:
62 content
= json
.loads(e
.content
)
63 content_message
= content
.get('message')
65 content_message
= e
.content
67 status
['message'] = content_message
74 gateway
= get_available_gateway()
75 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
77 'ceph_iscsi_config_version': config
['version']
83 gateway
= get_available_gateway()
84 settings
= IscsiClient
.instance(gateway_name
=gateway
).get_settings()
85 if 'target_controls_limits' in settings
:
86 target_default_controls
= settings
['target_default_controls']
87 for ctrl_k
, ctrl_v
in target_default_controls
.items():
88 limits
= settings
['target_controls_limits'].get(ctrl_k
, {})
89 if 'type' not in limits
:
91 limits
['type'] = 'int'
92 # backward compatibility
93 if target_default_controls
[ctrl_k
] in ['Yes', 'No']:
94 limits
['type'] = 'bool'
95 target_default_controls
[ctrl_k
] = str_to_bool(ctrl_v
)
96 settings
['target_controls_limits'][ctrl_k
] = limits
97 if 'disk_controls_limits' in settings
:
98 for backstore
, disk_controls_limits
in settings
['disk_controls_limits'].items():
99 disk_default_controls
= settings
['disk_default_controls'][backstore
]
100 for ctrl_k
, ctrl_v
in disk_default_controls
.items():
101 limits
= disk_controls_limits
.get(ctrl_k
, {})
102 if 'type' not in limits
:
104 limits
['type'] = 'int'
105 settings
['disk_controls_limits'][backstore
][ctrl_k
] = limits
112 gateways_config
= IscsiGatewaysConfig
.get_gateways_config()
113 for name
in gateways_config
['gateways']:
115 ip_addresses
= IscsiClient
.instance(gateway_name
=name
).get_ip_addresses()
116 portals
.append({'name': name
, 'ip_addresses': ip_addresses
['data']})
117 except RequestException
:
119 return sorted(portals
, key
=lambda p
: '{}.{}'.format(p
['name'], p
['ip_addresses']))
126 gateways_names
= IscsiGatewaysConfig
.get_gateways_config()['gateways'].keys()
128 for gateway_name
in gateways_names
:
130 config
= IscsiClient
.instance(gateway_name
=gateway_name
).get_config()
132 except RequestException
:
136 for gateway_name
in gateways_names
:
138 'name': gateway_name
,
140 'num_targets': 'n/a',
141 'num_sessions': 'n/a'
144 IscsiClient
.instance(gateway_name
=gateway_name
).ping()
145 gateway
['state'] = 'up'
147 gateway
['num_sessions'] = 0
148 if gateway_name
in config
['gateways']:
149 gatewayinfo
= IscsiClient
.instance(
150 gateway_name
=gateway_name
).get_gatewayinfo()
151 gateway
['num_sessions'] = gatewayinfo
['num_sessions']
152 except RequestException
:
153 gateway
['state'] = 'down'
155 gateway
['num_targets'] = len([target
for _
, target
in config
['targets'].items()
156 if gateway_name
in target
['portals']])
157 result_gateways
.append(gateway
)
161 tcmu_info
= TcmuService
.get_iscsi_info()
162 for _
, disk_config
in config
['disks'].items():
164 'pool': disk_config
['pool'],
165 'image': disk_config
['image'],
166 'backstore': disk_config
['backstore'],
167 'optimized_since': None,
169 'stats_history': None
171 tcmu_image_info
= TcmuService
.get_image_info(image
['pool'],
175 if 'optimized_since' in tcmu_image_info
:
176 image
['optimized_since'] = tcmu_image_info
['optimized_since']
177 if 'stats' in tcmu_image_info
:
178 image
['stats'] = tcmu_image_info
['stats']
179 if 'stats_history' in tcmu_image_info
:
180 image
['stats_history'] = tcmu_image_info
['stats_history']
181 result_images
.append(image
)
184 'gateways': sorted(result_gateways
, key
=lambda g
: g
['name']),
185 'images': sorted(result_images
, key
=lambda i
: '{}/{}'.format(i
['pool'], i
['image']))
189 @ApiController('/iscsi', Scope
.ISCSI
)
190 class Iscsi(BaseController
):
192 @Endpoint('GET', 'discoveryauth')
194 def get_discoveryauth(self
):
195 gateway
= get_available_gateway()
196 return self
._get
_discoveryauth
(gateway
)
198 @Endpoint('PUT', 'discoveryauth')
200 def set_discoveryauth(self
, user
, password
, mutual_user
, mutual_password
):
203 'password': password
,
204 'mutual_user': mutual_user
,
205 'mutual_password': mutual_password
208 gateway
= get_available_gateway()
209 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
210 gateway_names
= list(config
['gateways'].keys())
211 validate_rest_api(gateway_names
)
212 IscsiClient
.instance(gateway_name
=gateway
).update_discoveryauth(user
,
216 return self
._get
_discoveryauth
(gateway
)
218 def _get_discoveryauth(self
, gateway
):
219 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
220 user
= config
['discovery_auth']['username']
221 password
= config
['discovery_auth']['password']
222 mutual_user
= config
['discovery_auth']['mutual_username']
223 mutual_password
= config
['discovery_auth']['mutual_password']
226 'password': password
,
227 'mutual_user': mutual_user
,
228 'mutual_password': mutual_password
232 def iscsi_target_task(name
, metadata
, wait_for
=2.0):
233 return Task("iscsi/target/{}".format(name
), metadata
, wait_for
)
236 @ApiController('/iscsi/target', Scope
.ISCSI
)
237 class IscsiTarget(RESTController
):
240 gateway
= get_available_gateway()
241 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
243 for target_iqn
in config
['targets'].keys():
244 target
= IscsiTarget
._config
_to
_target
(target_iqn
, config
)
245 IscsiTarget
._set
_info
(target
)
246 targets
.append(target
)
249 def get(self
, target_iqn
):
250 gateway
= get_available_gateway()
251 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
252 if target_iqn
not in config
['targets']:
253 raise cherrypy
.HTTPError(404)
254 target
= IscsiTarget
._config
_to
_target
(target_iqn
, config
)
255 IscsiTarget
._set
_info
(target
)
258 @iscsi_target_task('delete', {'target_iqn': '{target_iqn}'})
259 def delete(self
, target_iqn
):
260 gateway
= get_available_gateway()
261 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
262 if target_iqn
not in config
['targets']:
263 raise DashboardException(msg
='Target does not exist',
264 code
='target_does_not_exist',
266 portal_names
= list(config
['targets'][target_iqn
]['portals'].keys())
267 validate_rest_api(portal_names
)
269 portal_name
= portal_names
[0]
270 target_info
= IscsiClient
.instance(gateway_name
=portal_name
).get_targetinfo(target_iqn
)
271 if target_info
['num_sessions'] > 0:
272 raise DashboardException(msg
='Target has active sessions',
273 code
='target_has_active_sessions',
275 IscsiTarget
._delete
(target_iqn
, config
, 0, 100)
277 @iscsi_target_task('create', {'target_iqn': '{target_iqn}'})
278 def create(self
, target_iqn
=None, target_controls
=None, acl_enabled
=None,
279 auth
=None, portals
=None, disks
=None, clients
=None, groups
=None):
280 target_controls
= target_controls
or {}
281 portals
= portals
or []
283 clients
= clients
or []
284 groups
= groups
or []
287 for client
in clients
:
288 validate_auth(client
['auth'])
290 gateway
= get_available_gateway()
291 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
292 if target_iqn
in config
['targets']:
293 raise DashboardException(msg
='Target already exists',
294 code
='target_already_exists',
296 settings
= IscsiClient
.instance(gateway_name
=gateway
).get_settings()
297 IscsiTarget
._validate
(target_iqn
, target_controls
, portals
, disks
, groups
, settings
)
299 IscsiTarget
._create
(target_iqn
, target_controls
, acl_enabled
, auth
, portals
, disks
,
300 clients
, groups
, 0, 100, config
, settings
)
302 @iscsi_target_task('edit', {'target_iqn': '{target_iqn}'})
303 def set(self
, target_iqn
, new_target_iqn
=None, target_controls
=None, acl_enabled
=None,
304 auth
=None, portals
=None, disks
=None, clients
=None, groups
=None):
305 target_controls
= target_controls
or {}
306 portals
= IscsiTarget
._sorted
_portals
(portals
)
307 disks
= IscsiTarget
._sorted
_disks
(disks
)
308 clients
= IscsiTarget
._sorted
_clients
(clients
)
309 groups
= IscsiTarget
._sorted
_groups
(groups
)
312 for client
in clients
:
313 validate_auth(client
['auth'])
315 gateway
= get_available_gateway()
316 config
= IscsiClient
.instance(gateway_name
=gateway
).get_config()
317 if target_iqn
not in config
['targets']:
318 raise DashboardException(msg
='Target does not exist',
319 code
='target_does_not_exist',
321 if target_iqn
!= new_target_iqn
and new_target_iqn
in config
['targets']:
322 raise DashboardException(msg
='Target IQN already in use',
323 code
='target_iqn_already_in_use',
326 settings
= IscsiClient
.instance(gateway_name
=gateway
).get_settings()
327 new_portal_names
= {p
['host'] for p
in portals
}
328 old_portal_names
= set(config
['targets'][target_iqn
]['portals'].keys())
329 deleted_portal_names
= list(old_portal_names
- new_portal_names
)
330 validate_rest_api(deleted_portal_names
)
331 IscsiTarget
._validate
(new_target_iqn
, target_controls
, portals
, disks
, groups
, settings
)
332 config
= IscsiTarget
._delete
(target_iqn
, config
, 0, 50, new_target_iqn
, target_controls
,
333 portals
, disks
, clients
, groups
)
334 IscsiTarget
._create
(new_target_iqn
, target_controls
, acl_enabled
, auth
, portals
, disks
,
335 clients
, groups
, 50, 100, config
, settings
)
338 def _delete(target_iqn
, config
, task_progress_begin
, task_progress_end
, new_target_iqn
=None,
339 new_target_controls
=None, new_portals
=None, new_disks
=None, new_clients
=None,
341 new_target_controls
= new_target_controls
or {}
342 new_portals
= new_portals
or []
343 new_disks
= new_disks
or []
344 new_clients
= new_clients
or []
345 new_groups
= new_groups
or []
347 TaskManager
.current_task().set_progress(task_progress_begin
)
348 target_config
= config
['targets'][target_iqn
]
349 if not target_config
['portals'].keys():
350 raise DashboardException(msg
="Cannot delete a target that doesn't contain any portal",
351 code
='cannot_delete_target_without_portals',
353 target
= IscsiTarget
._config
_to
_target
(target_iqn
, config
)
354 n_groups
= len(target_config
['groups'])
355 n_clients
= len(target_config
['clients'])
356 n_target_disks
= len(target_config
['disks'])
357 task_progress_steps
= n_groups
+ n_clients
+ n_target_disks
358 task_progress_inc
= 0
359 if task_progress_steps
!= 0:
360 task_progress_inc
= int((task_progress_end
- task_progress_begin
) / task_progress_steps
)
361 gateway_name
= list(target_config
['portals'].keys())[0]
363 for group_id
in list(target_config
['groups'].keys()):
364 if IscsiTarget
._group
_deletion
_required
(target
, new_target_iqn
, new_target_controls
,
365 new_groups
, group_id
, new_clients
,
367 deleted_groups
.append(group_id
)
368 IscsiClient
.instance(gateway_name
=gateway_name
).delete_group(target_iqn
,
370 TaskManager
.current_task().inc_progress(task_progress_inc
)
372 for client_iqn
in list(target_config
['clients'].keys()):
373 if IscsiTarget
._client
_deletion
_required
(target
, new_target_iqn
, new_target_controls
,
374 new_clients
, client_iqn
,
375 new_groups
, deleted_groups
):
376 deleted_clients
.append(client_iqn
)
377 IscsiClient
.instance(gateway_name
=gateway_name
).delete_client(target_iqn
,
379 TaskManager
.current_task().inc_progress(task_progress_inc
)
380 for image_id
in target_config
['disks']:
381 if IscsiTarget
._target
_lun
_deletion
_required
(target
, new_target_iqn
,
383 new_disks
, image_id
):
384 all_clients
= target_config
['clients'].keys()
385 not_deleted_clients
= [c
for c
in all_clients
if c
not in deleted_clients
]
386 for client_iqn
in not_deleted_clients
:
387 client_image_ids
= target_config
['clients'][client_iqn
]['luns'].keys()
388 for client_image_id
in client_image_ids
:
389 if image_id
== client_image_id
:
390 IscsiClient
.instance(gateway_name
=gateway_name
).delete_client_lun(
391 target_iqn
, client_iqn
, client_image_id
)
392 IscsiClient
.instance(gateway_name
=gateway_name
).delete_target_lun(target_iqn
,
394 pool
, image
= image_id
.split('/', 1)
395 IscsiClient
.instance(gateway_name
=gateway_name
).delete_disk(pool
, image
)
396 TaskManager
.current_task().inc_progress(task_progress_inc
)
397 old_portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(target
['portals'])
398 new_portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(new_portals
)
399 for old_portal_host
, old_portal_ip_list
in old_portals_by_host
.items():
400 if IscsiTarget
._target
_portal
_deletion
_required
(old_portal_host
,
402 new_portals_by_host
):
403 IscsiClient
.instance(gateway_name
=gateway_name
).delete_gateway(target_iqn
,
405 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
406 IscsiClient
.instance(gateway_name
=gateway_name
).delete_target(target_iqn
)
407 TaskManager
.current_task().set_progress(task_progress_end
)
408 return IscsiClient
.instance(gateway_name
=gateway_name
).get_config()
411 def _get_group(groups
, group_id
):
413 if group
['group_id'] == group_id
:
418 def _group_deletion_required(target
, new_target_iqn
, new_target_controls
,
419 new_groups
, group_id
, new_clients
, new_disks
):
420 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
422 new_group
= IscsiTarget
._get
_group
(new_groups
, group_id
)
425 old_group
= IscsiTarget
._get
_group
(target
['groups'], group_id
)
426 if new_group
!= old_group
:
428 # Check if any client inside this group has changed
429 for client_iqn
in new_group
['members']:
430 if IscsiTarget
._client
_deletion
_required
(target
, new_target_iqn
, new_target_controls
,
431 new_clients
, client_iqn
,
434 # Check if any disk inside this group has changed
435 for disk
in new_group
['disks']:
436 image_id
= '{}/{}'.format(disk
['pool'], disk
['image'])
437 if IscsiTarget
._target
_lun
_deletion
_required
(target
, new_target_iqn
,
439 new_disks
, image_id
):
444 def _get_client(clients
, client_iqn
):
445 for client
in clients
:
446 if client
['client_iqn'] == client_iqn
:
451 def _client_deletion_required(target
, new_target_iqn
, new_target_controls
,
452 new_clients
, client_iqn
, new_groups
, deleted_groups
):
453 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
455 new_client
= deepcopy(IscsiTarget
._get
_client
(new_clients
, client_iqn
))
458 # Disks inherited from groups must be considered
459 for group
in new_groups
:
460 if client_iqn
in group
['members']:
461 new_client
['luns'] += group
['disks']
462 old_client
= IscsiTarget
._get
_client
(target
['clients'], client_iqn
)
463 if new_client
!= old_client
:
465 # Check if client belongs to a groups that has been deleted
466 for group
in target
['groups']:
467 if group
['group_id'] in deleted_groups
and client_iqn
in group
['members']:
472 def _get_disk(disks
, image_id
):
474 if '{}/{}'.format(disk
['pool'], disk
['image']) == image_id
:
479 def _target_lun_deletion_required(target
, new_target_iqn
, new_target_controls
,
480 new_disks
, image_id
):
481 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
483 new_disk
= IscsiTarget
._get
_disk
(new_disks
, image_id
)
486 old_disk
= IscsiTarget
._get
_disk
(target
['disks'], image_id
)
487 new_disk_without_controls
= deepcopy(new_disk
)
488 new_disk_without_controls
.pop('controls')
489 old_disk_without_controls
= deepcopy(old_disk
)
490 old_disk_without_controls
.pop('controls')
491 if new_disk_without_controls
!= old_disk_without_controls
:
496 def _target_portal_deletion_required(old_portal_host
, old_portal_ip_list
, new_portals_by_host
):
497 if old_portal_host
not in new_portals_by_host
:
499 if sorted(old_portal_ip_list
) != sorted(new_portals_by_host
[old_portal_host
]):
504 def _target_deletion_required(target
, new_target_iqn
, new_target_controls
):
505 gateway
= get_available_gateway()
506 settings
= IscsiClient
.instance(gateway_name
=gateway
).get_settings()
508 if target
['target_iqn'] != new_target_iqn
:
510 if settings
['api_version'] < 2 and target
['target_controls'] != new_target_controls
:
515 def _validate(target_iqn
, target_controls
, portals
, disks
, groups
, settings
):
517 raise DashboardException(msg
='Target IQN is required',
518 code
='target_iqn_required',
521 minimum_gateways
= max(1, settings
['config']['minimum_gateways'])
522 portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(portals
)
523 if len(portals_by_host
.keys()) < minimum_gateways
:
524 if minimum_gateways
== 1:
525 msg
= 'At least one portal is required'
527 msg
= 'At least {} portals are required'.format(minimum_gateways
)
528 raise DashboardException(msg
=msg
,
529 code
='portals_required',
532 # 'target_controls_limits' was introduced in ceph-iscsi > 3.2
533 # When using an older `ceph-iscsi` version these validations will
534 # NOT be executed beforehand
535 if 'target_controls_limits' in settings
:
536 for target_control_name
, target_control_value
in target_controls
.items():
537 limits
= settings
['target_controls_limits'].get(target_control_name
)
538 if limits
is not None:
539 min_value
= limits
.get('min')
540 if min_value
is not None and target_control_value
< min_value
:
541 raise DashboardException(msg
='Target control {} must be >= '
542 '{}'.format(target_control_name
, min_value
),
543 code
='target_control_invalid_min',
545 max_value
= limits
.get('max')
546 if max_value
is not None and target_control_value
> max_value
:
547 raise DashboardException(msg
='Target control {} must be <= '
548 '{}'.format(target_control_name
, max_value
),
549 code
='target_control_invalid_max',
552 portal_names
= [p
['host'] for p
in portals
]
553 validate_rest_api(portal_names
)
557 image
= disk
['image']
558 backstore
= disk
['backstore']
559 required_rbd_features
= settings
['required_rbd_features'][backstore
]
560 unsupported_rbd_features
= settings
['unsupported_rbd_features'][backstore
]
561 IscsiTarget
._validate
_image
(pool
, image
, backstore
, required_rbd_features
,
562 unsupported_rbd_features
)
564 # 'disk_controls_limits' was introduced in ceph-iscsi > 3.2
565 # When using an older `ceph-iscsi` version these validations will
566 # NOT be executed beforehand
567 if 'disk_controls_limits' in settings
:
568 for disk_control_name
, disk_control_value
in disk
['controls'].items():
569 limits
= settings
['disk_controls_limits'][backstore
].get(disk_control_name
)
570 if limits
is not None:
571 min_value
= limits
.get('min')
572 if min_value
is not None and disk_control_value
< min_value
:
573 raise DashboardException(msg
='Disk control {} must be >= '
574 '{}'.format(disk_control_name
, min_value
),
575 code
='disk_control_invalid_min',
577 max_value
= limits
.get('max')
578 if max_value
is not None and disk_control_value
> max_value
:
579 raise DashboardException(msg
='Disk control {} must be <= '
580 '{}'.format(disk_control_name
, max_value
),
581 code
='disk_control_invalid_max',
584 initiators
= [] # type: List[Any]
586 initiators
= initiators
+ group
['members']
587 if len(initiators
) != len(set(initiators
)):
588 raise DashboardException(msg
='Each initiator can only be part of 1 group at a time',
589 code
='initiator_in_multiple_groups',
593 def _validate_image(pool
, image
, backstore
, required_rbd_features
, unsupported_rbd_features
):
595 ioctx
= mgr
.rados
.open_ioctx(pool
)
597 with rbd
.Image(ioctx
, image
) as img
:
598 if img
.features() & required_rbd_features
!= required_rbd_features
:
599 raise DashboardException(msg
='Image {} cannot be exported using {} '
600 'backstore because required features are '
601 'missing (required features are '
605 required_rbd_features
)),
606 code
='image_missing_required_features',
608 if img
.features() & unsupported_rbd_features
!= 0:
609 raise DashboardException(msg
='Image {} cannot be exported using {} '
610 'backstore because it contains unsupported '
615 unsupported_rbd_features
)),
616 code
='image_contains_unsupported_features',
619 except rbd
.ImageNotFound
:
620 raise DashboardException(msg
='Image {} does not exist'.format(image
),
621 code
='image_does_not_exist',
623 except rados
.ObjectNotFound
:
624 raise DashboardException(msg
='Pool {} does not exist'.format(pool
),
625 code
='pool_does_not_exist',
629 def _update_targetauth(config
, target_iqn
, auth
, gateway_name
):
630 # Target level authentication was introduced in ceph-iscsi config v11
631 if config
['version'] > 10:
633 password
= auth
['password']
634 mutual_user
= auth
['mutual_user']
635 mutual_password
= auth
['mutual_password']
636 IscsiClient
.instance(gateway_name
=gateway_name
).update_targetauth(target_iqn
,
643 def _update_targetacl(target_config
, target_iqn
, acl_enabled
, gateway_name
):
644 if not target_config
or target_config
['acl_enabled'] != acl_enabled
:
645 targetauth_action
= ('enable_acl' if acl_enabled
else 'disable_acl')
646 IscsiClient
.instance(gateway_name
=gateway_name
).update_targetacl(target_iqn
,
650 def _is_auth_equal(target_auth
, auth
):
651 return auth
['user'] == target_auth
['username'] and \
652 auth
['password'] == target_auth
['password'] and \
653 auth
['mutual_user'] == target_auth
['mutual_username'] and \
654 auth
['mutual_password'] == target_auth
['mutual_password']
657 def _create(target_iqn
, target_controls
, acl_enabled
,
658 auth
, portals
, disks
, clients
, groups
,
659 task_progress_begin
, task_progress_end
, config
, settings
):
660 target_config
= config
['targets'].get(target_iqn
, None)
661 TaskManager
.current_task().set_progress(task_progress_begin
)
662 portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(portals
)
663 n_hosts
= len(portals_by_host
)
665 n_clients
= len(clients
)
666 n_groups
= len(groups
)
667 task_progress_steps
= n_hosts
+ n_disks
+ n_clients
+ n_groups
668 task_progress_inc
= 0
669 if task_progress_steps
!= 0:
670 task_progress_inc
= int((task_progress_end
- task_progress_begin
) / task_progress_steps
)
672 gateway_name
= portals
[0]['host']
673 if not target_config
:
674 IscsiClient
.instance(gateway_name
=gateway_name
).create_target(target_iqn
,
676 for host
, ip_list
in portals_by_host
.items():
677 if not target_config
or host
not in target_config
['portals']:
678 IscsiClient
.instance(gateway_name
=gateway_name
).create_gateway(target_iqn
,
681 TaskManager
.current_task().inc_progress(task_progress_inc
)
683 if not target_config
or \
684 acl_enabled
!= target_config
['acl_enabled'] or \
685 not IscsiTarget
._is
_auth
_equal
(target_config
['auth'], auth
):
687 IscsiTarget
._update
_targetauth
(config
, target_iqn
, auth
, gateway_name
)
688 IscsiTarget
._update
_targetacl
(target_config
, target_iqn
, acl_enabled
,
691 IscsiTarget
._update
_targetacl
(target_config
, target_iqn
, acl_enabled
,
693 IscsiTarget
._update
_targetauth
(config
, target_iqn
, auth
, gateway_name
)
697 image
= disk
['image']
698 image_id
= '{}/{}'.format(pool
, image
)
699 backstore
= disk
['backstore']
700 wwn
= disk
.get('wwn')
701 lun
= disk
.get('lun')
702 if image_id
not in config
['disks']:
703 IscsiClient
.instance(gateway_name
=gateway_name
).create_disk(pool
,
707 if not target_config
or image_id
not in target_config
['disks']:
708 IscsiClient
.instance(gateway_name
=gateway_name
).create_target_lun(target_iqn
,
712 controls
= disk
['controls']
714 if image_id
in config
['disks']:
715 d_conf_controls
= config
['disks'][image_id
]['controls']
716 disk_default_controls
= settings
['disk_default_controls'][backstore
]
717 for old_control
in d_conf_controls
.keys():
718 # If control was removed, restore the default value
719 if old_control
not in controls
:
720 controls
[old_control
] = disk_default_controls
[old_control
]
722 if (image_id
not in config
['disks'] or d_conf_controls
!= controls
) and controls
:
723 IscsiClient
.instance(gateway_name
=gateway_name
).reconfigure_disk(pool
,
726 TaskManager
.current_task().inc_progress(task_progress_inc
)
727 for client
in clients
:
728 client_iqn
= client
['client_iqn']
729 if not target_config
or client_iqn
not in target_config
['clients']:
730 IscsiClient
.instance(gateway_name
=gateway_name
).create_client(target_iqn
,
732 user
= client
['auth']['user']
733 password
= client
['auth']['password']
734 m_user
= client
['auth']['mutual_user']
735 m_password
= client
['auth']['mutual_password']
736 IscsiClient
.instance(gateway_name
=gateway_name
).create_client_auth(
737 target_iqn
, client_iqn
, user
, password
, m_user
, m_password
)
738 for lun
in client
['luns']:
741 image_id
= '{}/{}'.format(pool
, image
)
742 if not target_config
or client_iqn
not in target_config
['clients'] or \
743 image_id
not in target_config
['clients'][client_iqn
]['luns']:
744 IscsiClient
.instance(gateway_name
=gateway_name
).create_client_lun(
745 target_iqn
, client_iqn
, image_id
)
746 TaskManager
.current_task().inc_progress(task_progress_inc
)
748 group_id
= group
['group_id']
749 members
= group
['members']
751 for disk
in group
['disks']:
752 image_ids
.append('{}/{}'.format(disk
['pool'], disk
['image']))
753 if not target_config
or group_id
not in target_config
['groups']:
754 IscsiClient
.instance(gateway_name
=gateway_name
).create_group(
755 target_iqn
, group_id
, members
, image_ids
)
756 TaskManager
.current_task().inc_progress(task_progress_inc
)
758 if not target_config
or target_controls
!= target_config
['controls']:
759 IscsiClient
.instance(gateway_name
=gateway_name
).reconfigure_target(
760 target_iqn
, target_controls
)
761 TaskManager
.current_task().set_progress(task_progress_end
)
762 except RequestException
as e
:
764 content
= json
.loads(e
.content
)
765 content_message
= content
.get('message')
767 raise DashboardException(msg
=content_message
, component
='iscsi')
768 raise DashboardException(e
=e
, component
='iscsi')
771 def _config_to_target(target_iqn
, config
):
772 target_config
= config
['targets'][target_iqn
]
774 for host
, portal_config
in target_config
['portals'].items():
775 for portal_ip
in portal_config
['portal_ip_addresses']:
780 portals
.append(portal
)
781 portals
= IscsiTarget
._sorted
_portals
(portals
)
783 for target_disk
in target_config
['disks']:
784 disk_config
= config
['disks'][target_disk
]
786 'pool': disk_config
['pool'],
787 'image': disk_config
['image'],
788 'controls': disk_config
['controls'],
789 'backstore': disk_config
['backstore'],
790 'wwn': disk_config
['wwn']
792 # lun_id was introduced in ceph-iscsi config v11
793 if config
['version'] > 10:
794 disk
['lun'] = target_config
['disks'][target_disk
]['lun_id']
796 disks
= IscsiTarget
._sorted
_disks
(disks
)
798 for client_iqn
, client_config
in target_config
['clients'].items():
800 for client_lun
in client_config
['luns'].keys():
801 pool
, image
= client_lun
.split('/', 1)
807 user
= client_config
['auth']['username']
808 password
= client_config
['auth']['password']
809 mutual_user
= client_config
['auth']['mutual_username']
810 mutual_password
= client_config
['auth']['mutual_password']
812 'client_iqn': client_iqn
,
816 'password': password
,
817 'mutual_user': mutual_user
,
818 'mutual_password': mutual_password
821 clients
.append(client
)
822 clients
= IscsiTarget
._sorted
_clients
(clients
)
824 for group_id
, group_config
in target_config
['groups'].items():
826 for group_disk_key
, _
in group_config
['disks'].items():
827 pool
, image
= group_disk_key
.split('/', 1)
832 group_disks
.append(group_disk
)
834 'group_id': group_id
,
835 'disks': group_disks
,
836 'members': group_config
['members'],
839 groups
= IscsiTarget
._sorted
_groups
(groups
)
840 target_controls
= target_config
['controls']
841 acl_enabled
= target_config
['acl_enabled']
843 'target_iqn': target_iqn
,
848 'target_controls': target_controls
,
849 'acl_enabled': acl_enabled
851 # Target level authentication was introduced in ceph-iscsi config v11
852 if config
['version'] > 10:
853 target_user
= target_config
['auth']['username']
854 target_password
= target_config
['auth']['password']
855 target_mutual_user
= target_config
['auth']['mutual_username']
856 target_mutual_password
= target_config
['auth']['mutual_password']
859 'password': target_password
,
860 'mutual_user': target_mutual_user
,
861 'mutual_password': target_mutual_password
866 def _is_executing(target_iqn
):
867 executing_tasks
, _
= TaskManager
.list()
868 for t
in executing_tasks
:
869 if t
.name
.startswith('iscsi/target') and t
.metadata
.get('target_iqn') == target_iqn
:
874 def _set_info(target
):
875 if not target
['portals']:
877 target_iqn
= target
['target_iqn']
878 # During task execution, additional info is not available
879 if IscsiTarget
._is
_executing
(target_iqn
):
881 # If any portal is down, additional info is not available
882 for portal
in target
['portals']:
884 IscsiClient
.instance(gateway_name
=portal
['host']).ping()
885 except (IscsiGatewayDoesNotExist
, RequestException
):
887 gateway_name
= target
['portals'][0]['host']
889 target_info
= IscsiClient
.instance(gateway_name
=gateway_name
).get_targetinfo(
891 target
['info'] = target_info
892 for client
in target
['clients']:
893 client_iqn
= client
['client_iqn']
894 client_info
= IscsiClient
.instance(gateway_name
=gateway_name
).get_clientinfo(
895 target_iqn
, client_iqn
)
896 client
['info'] = client_info
897 except RequestException
as e
:
898 # Target/Client has been removed in the meanwhile (e.g. using gwcli)
899 if e
.status_code
!= 404:
903 def _sorted_portals(portals
):
904 portals
= portals
or []
905 return sorted(portals
, key
=lambda p
: '{}.{}'.format(p
['host'], p
['ip']))
908 def _sorted_disks(disks
):
910 return sorted(disks
, key
=lambda d
: '{}.{}'.format(d
['pool'], d
['image']))
913 def _sorted_clients(clients
):
914 clients
= clients
or []
915 for client
in clients
:
916 client
['luns'] = sorted(client
['luns'],
917 key
=lambda d
: '{}.{}'.format(d
['pool'], d
['image']))
918 return sorted(clients
, key
=lambda c
: c
['client_iqn'])
921 def _sorted_groups(groups
):
922 groups
= groups
or []
924 group
['disks'] = sorted(group
['disks'],
925 key
=lambda d
: '{}.{}'.format(d
['pool'], d
['image']))
926 group
['members'] = sorted(group
['members'])
927 return sorted(groups
, key
=lambda g
: g
['group_id'])
930 def _get_portals_by_host(portals
):
931 # type: (List[dict]) -> Dict[str, List[str]]
932 portals_by_host
= {} # type: Dict[str, List[str]]
933 for portal
in portals
:
934 host
= portal
['host']
936 if host
not in portals_by_host
:
937 portals_by_host
[host
] = []
938 portals_by_host
[host
].append(ip
)
939 return portals_by_host
942 def get_available_gateway():
943 gateways
= IscsiGatewaysConfig
.get_gateways_config()['gateways']
945 raise DashboardException(msg
='There are no gateways defined',
946 code
='no_gateways_defined',
948 for gateway
in gateways
:
950 IscsiClient
.instance(gateway_name
=gateway
).ping()
952 except RequestException
:
954 raise DashboardException(msg
='There are no gateways available',
955 code
='no_gateways_available',
959 def validate_rest_api(gateways
):
960 for gateway
in gateways
:
962 IscsiClient
.instance(gateway_name
=gateway
).ping()
963 except RequestException
:
964 raise DashboardException(msg
='iSCSI REST Api not available for gateway '
965 '{}'.format(gateway
),
966 code
='ceph_iscsi_rest_api_not_available_for_gateway',
970 def validate_auth(auth
):
971 username_regex
= re
.compile(r
'^[\w\.:@_-]{8,64}$')
972 password_regex
= re
.compile(r
'^[\w@\-_\/]{12,16}$')
975 if auth
['user'] or auth
['password']:
976 result
= bool(username_regex
.match(auth
['user'])) and \
977 bool(password_regex
.match(auth
['password']))
979 if auth
['mutual_user'] or auth
['mutual_password']:
980 result
= result
and bool(username_regex
.match(auth
['mutual_user'])) and \
981 bool(password_regex
.match(auth
['mutual_password'])) and auth
['user']
984 raise DashboardException(msg
='Bad authentication',
985 code
='target_bad_auth',