1 # -*- coding: utf-8 -*-
2 # pylint: disable=too-many-branches
3 from __future__
import absolute_import
5 from copy
import deepcopy
12 from . import ApiController
, UiApiController
, RESTController
, BaseController
, Endpoint
,\
13 ReadPermission
, UpdatePermission
, Task
15 from ..rest_client
import RequestException
16 from ..security
import Scope
17 from ..services
.iscsi_client
import IscsiClient
18 from ..services
.iscsi_cli
import IscsiGatewaysConfig
19 from ..services
.rbd
import format_bitmask
20 from ..services
.tcmu_service
import TcmuService
21 from ..exceptions
import DashboardException
22 from ..tools
import TaskManager
25 @UiApiController('/iscsi', Scope
.ISCSI
)
26 class IscsiUi(BaseController
):
28 REQUIRED_CEPH_ISCSI_CONFIG_VERSION
= 10
33 status
= {'available': False}
34 gateways
= IscsiGatewaysConfig
.get_gateways_config()['gateways']
36 status
['message'] = 'There are no gateways defined'
39 for gateway
in gateways
:
41 IscsiClient
.instance(gateway_name
=gateway
).ping()
42 except RequestException
:
43 status
['message'] = 'Gateway {} is inaccessible'.format(gateway
)
45 config
= IscsiClient
.instance().get_config()
46 if config
['version'] != IscsiUi
.REQUIRED_CEPH_ISCSI_CONFIG_VERSION
:
47 status
['message'] = 'Unsupported `ceph-iscsi` config version. Expected {} but ' \
48 'found {}.'.format(IscsiUi
.REQUIRED_CEPH_ISCSI_CONFIG_VERSION
,
51 status
['available'] = True
52 except RequestException
as e
:
55 content
= json
.loads(e
.content
)
56 content_message
= content
.get('message')
58 content_message
= e
.content
60 status
['message'] = content_message
67 return IscsiClient
.instance().get_settings()
73 gateways_config
= IscsiGatewaysConfig
.get_gateways_config()
74 for name
in gateways_config
['gateways']:
75 ip_addresses
= IscsiClient
.instance(gateway_name
=name
).get_ip_addresses()
76 portals
.append({'name': name
, 'ip_addresses': ip_addresses
['data']})
77 return sorted(portals
, key
=lambda p
: '{}.{}'.format(p
['name'], p
['ip_addresses']))
84 gateways_names
= IscsiGatewaysConfig
.get_gateways_config()['gateways'].keys()
86 for gateway_name
in gateways_names
:
88 config
= IscsiClient
.instance(gateway_name
=gateway_name
).get_config()
90 except RequestException
:
94 for gateway_name
in gateways_names
:
102 IscsiClient
.instance(gateway_name
=gateway_name
).ping()
103 gateway
['state'] = 'up'
105 gateway
['num_sessions'] = 0
106 if gateway_name
in config
['gateways']:
107 gatewayinfo
= IscsiClient
.instance(
108 gateway_name
=gateway_name
).get_gatewayinfo()
109 gateway
['num_sessions'] = gatewayinfo
['num_sessions']
110 except RequestException
:
111 gateway
['state'] = 'down'
113 gateway
['num_targets'] = len([target
for _
, target
in config
['targets'].items()
114 if gateway_name
in target
['portals']])
115 result_gateways
.append(gateway
)
119 tcmu_info
= TcmuService
.get_iscsi_info()
120 for _
, disk_config
in config
['disks'].items():
122 'pool': disk_config
['pool'],
123 'image': disk_config
['image'],
124 'backstore': disk_config
['backstore'],
125 'optimized_since': None,
127 'stats_history': None
129 tcmu_image_info
= TcmuService
.get_image_info(image
['pool'],
133 if 'optimized_since' in tcmu_image_info
:
134 image
['optimized_since'] = tcmu_image_info
['optimized_since']
135 if 'stats' in tcmu_image_info
:
136 image
['stats'] = tcmu_image_info
['stats']
137 if 'stats_history' in tcmu_image_info
:
138 image
['stats_history'] = tcmu_image_info
['stats_history']
139 result_images
.append(image
)
142 'gateways': sorted(result_gateways
, key
=lambda g
: g
['name']),
143 'images': sorted(result_images
, key
=lambda i
: '{}/{}'.format(i
['pool'], i
['image']))
147 @ApiController('/iscsi', Scope
.ISCSI
)
148 class Iscsi(BaseController
):
150 @Endpoint('GET', 'discoveryauth')
152 def get_discoveryauth(self
):
153 return self
._get
_discoveryauth
()
155 @Endpoint('PUT', 'discoveryauth')
157 def set_discoveryauth(self
, user
, password
, mutual_user
, mutual_password
):
158 IscsiClient
.instance().update_discoveryauth(user
, password
, mutual_user
, mutual_password
)
159 return self
._get
_discoveryauth
()
161 def _get_discoveryauth(self
):
162 config
= IscsiClient
.instance().get_config()
163 user
= config
['discovery_auth']['username']
164 password
= config
['discovery_auth']['password']
165 mutual_user
= config
['discovery_auth']['mutual_username']
166 mutual_password
= config
['discovery_auth']['mutual_password']
169 'password': password
,
170 'mutual_user': mutual_user
,
171 'mutual_password': mutual_password
175 def iscsi_target_task(name
, metadata
, wait_for
=2.0):
176 return Task("iscsi/target/{}".format(name
), metadata
, wait_for
)
179 @ApiController('/iscsi/target', Scope
.ISCSI
)
180 class IscsiTarget(RESTController
):
183 config
= IscsiClient
.instance().get_config()
185 for target_iqn
in config
['targets'].keys():
186 target
= IscsiTarget
._config
_to
_target
(target_iqn
, config
)
187 IscsiTarget
._set
_info
(target
)
188 targets
.append(target
)
191 def get(self
, target_iqn
):
192 config
= IscsiClient
.instance().get_config()
193 if target_iqn
not in config
['targets']:
194 raise cherrypy
.HTTPError(404)
195 target
= IscsiTarget
._config
_to
_target
(target_iqn
, config
)
196 IscsiTarget
._set
_info
(target
)
199 @iscsi_target_task('delete', {'target_iqn': '{target_iqn}'})
200 def delete(self
, target_iqn
):
201 config
= IscsiClient
.instance().get_config()
202 if target_iqn
not in config
['targets']:
203 raise DashboardException(msg
='Target does not exist',
204 code
='target_does_not_exist',
206 if target_iqn
not in config
['targets']:
207 raise DashboardException(msg
='Target does not exist',
208 code
='target_does_not_exist',
210 IscsiTarget
._delete
(target_iqn
, config
, 0, 100)
212 @iscsi_target_task('create', {'target_iqn': '{target_iqn}'})
213 def create(self
, target_iqn
=None, target_controls
=None, acl_enabled
=None,
214 portals
=None, disks
=None, clients
=None, groups
=None):
215 target_controls
= target_controls
or {}
216 portals
= portals
or []
218 clients
= clients
or []
219 groups
= groups
or []
221 config
= IscsiClient
.instance().get_config()
222 if target_iqn
in config
['targets']:
223 raise DashboardException(msg
='Target already exists',
224 code
='target_already_exists',
226 IscsiTarget
._validate
(target_iqn
, portals
, disks
, groups
)
227 IscsiTarget
._create
(target_iqn
, target_controls
, acl_enabled
, portals
, disks
, clients
,
228 groups
, 0, 100, config
)
230 @iscsi_target_task('edit', {'target_iqn': '{target_iqn}'})
231 def set(self
, target_iqn
, new_target_iqn
=None, target_controls
=None, acl_enabled
=None,
232 portals
=None, disks
=None, clients
=None, groups
=None):
233 target_controls
= target_controls
or {}
234 portals
= IscsiTarget
._sorted
_portals
(portals
)
235 disks
= IscsiTarget
._sorted
_disks
(disks
)
236 clients
= IscsiTarget
._sorted
_clients
(clients
)
237 groups
= IscsiTarget
._sorted
_groups
(groups
)
239 config
= IscsiClient
.instance().get_config()
240 if target_iqn
not in config
['targets']:
241 raise DashboardException(msg
='Target does not exist',
242 code
='target_does_not_exist',
244 if target_iqn
!= new_target_iqn
and new_target_iqn
in config
['targets']:
245 raise DashboardException(msg
='Target IQN already in use',
246 code
='target_iqn_already_in_use',
248 IscsiTarget
._validate
(new_target_iqn
, portals
, disks
, groups
)
249 config
= IscsiTarget
._delete
(target_iqn
, config
, 0, 50, new_target_iqn
, target_controls
,
250 portals
, disks
, clients
, groups
)
251 IscsiTarget
._create
(new_target_iqn
, target_controls
, acl_enabled
, portals
, disks
, clients
,
252 groups
, 50, 100, config
)
255 def _delete(target_iqn
, config
, task_progress_begin
, task_progress_end
, new_target_iqn
=None,
256 new_target_controls
=None, new_portals
=None, new_disks
=None, new_clients
=None,
258 new_target_controls
= new_target_controls
or {}
259 new_portals
= new_portals
or []
260 new_disks
= new_disks
or []
261 new_clients
= new_clients
or []
262 new_groups
= new_groups
or []
264 TaskManager
.current_task().set_progress(task_progress_begin
)
265 target_config
= config
['targets'][target_iqn
]
266 if not target_config
['portals'].keys():
267 raise DashboardException(msg
="Cannot delete a target that doesn't contain any portal",
268 code
='cannot_delete_target_without_portals',
270 target
= IscsiTarget
._config
_to
_target
(target_iqn
, config
)
271 n_groups
= len(target_config
['groups'])
272 n_clients
= len(target_config
['clients'])
273 n_target_disks
= len(target_config
['disks'])
274 task_progress_steps
= n_groups
+ n_clients
+ n_target_disks
275 task_progress_inc
= 0
276 if task_progress_steps
!= 0:
277 task_progress_inc
= int((task_progress_end
- task_progress_begin
) / task_progress_steps
)
278 gateway_name
= list(target_config
['portals'].keys())[0]
280 for group_id
in list(target_config
['groups'].keys()):
281 if IscsiTarget
._group
_deletion
_required
(target
, new_target_iqn
, new_target_controls
,
282 new_groups
, group_id
, new_clients
,
284 deleted_groups
.append(group_id
)
285 IscsiClient
.instance(gateway_name
=gateway_name
).delete_group(target_iqn
,
287 TaskManager
.current_task().inc_progress(task_progress_inc
)
288 for client_iqn
in list(target_config
['clients'].keys()):
289 if IscsiTarget
._client
_deletion
_required
(target
, new_target_iqn
, new_target_controls
,
290 new_clients
, client_iqn
,
291 new_groups
, deleted_groups
):
292 IscsiClient
.instance(gateway_name
=gateway_name
).delete_client(target_iqn
,
294 TaskManager
.current_task().inc_progress(task_progress_inc
)
295 for image_id
in target_config
['disks']:
296 if IscsiTarget
._target
_lun
_deletion
_required
(target
, new_target_iqn
,
298 new_disks
, image_id
):
299 IscsiClient
.instance(gateway_name
=gateway_name
).delete_target_lun(target_iqn
,
301 pool
, image
= image_id
.split('/', 1)
302 IscsiClient
.instance(gateway_name
=gateway_name
).delete_disk(pool
, image
)
303 TaskManager
.current_task().inc_progress(task_progress_inc
)
304 old_portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(target
['portals'])
305 new_portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(new_portals
)
306 for old_portal_host
, old_portal_ip_list
in old_portals_by_host
.items():
307 if IscsiTarget
._target
_portal
_deletion
_required
(old_portal_host
,
309 new_portals_by_host
):
310 IscsiClient
.instance(gateway_name
=gateway_name
).delete_gateway(target_iqn
,
312 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
313 IscsiClient
.instance(gateway_name
=gateway_name
).delete_target(target_iqn
)
314 TaskManager
.current_task().set_progress(task_progress_end
)
315 return IscsiClient
.instance(gateway_name
=gateway_name
).get_config()
318 def _get_group(groups
, group_id
):
320 if group
['group_id'] == group_id
:
325 def _group_deletion_required(target
, new_target_iqn
, new_target_controls
,
326 new_groups
, group_id
, new_clients
, new_disks
):
327 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
329 new_group
= IscsiTarget
._get
_group
(new_groups
, group_id
)
332 old_group
= IscsiTarget
._get
_group
(target
['groups'], group_id
)
333 if new_group
!= old_group
:
335 # Check if any client inside this group has changed
336 for client_iqn
in new_group
['members']:
337 if IscsiTarget
._client
_deletion
_required
(target
, new_target_iqn
, new_target_controls
,
338 new_clients
, client_iqn
,
341 # Check if any disk inside this group has changed
342 for disk
in new_group
['disks']:
343 image_id
= '{}/{}'.format(disk
['pool'], disk
['image'])
344 if IscsiTarget
._target
_lun
_deletion
_required
(target
, new_target_iqn
,
346 new_disks
, image_id
):
351 def _get_client(clients
, client_iqn
):
352 for client
in clients
:
353 if client
['client_iqn'] == client_iqn
:
358 def _client_deletion_required(target
, new_target_iqn
, new_target_controls
,
359 new_clients
, client_iqn
, new_groups
, deleted_groups
):
360 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
362 new_client
= deepcopy(IscsiTarget
._get
_client
(new_clients
, client_iqn
))
365 # Disks inherited from groups must be considered
366 for group
in new_groups
:
367 if client_iqn
in group
['members']:
368 new_client
['luns'] += group
['disks']
369 old_client
= IscsiTarget
._get
_client
(target
['clients'], client_iqn
)
370 if new_client
!= old_client
:
372 # Check if client belongs to a groups that has been deleted
373 for group
in target
['groups']:
374 if group
['group_id'] in deleted_groups
and client_iqn
in group
['members']:
379 def _get_disk(disks
, image_id
):
381 if '{}/{}'.format(disk
['pool'], disk
['image']) == image_id
:
386 def _target_lun_deletion_required(target
, new_target_iqn
, new_target_controls
,
387 new_disks
, image_id
):
388 if IscsiTarget
._target
_deletion
_required
(target
, new_target_iqn
, new_target_controls
):
390 new_disk
= IscsiTarget
._get
_disk
(new_disks
, image_id
)
393 old_disk
= IscsiTarget
._get
_disk
(target
['disks'], image_id
)
394 if new_disk
!= old_disk
:
399 def _target_portal_deletion_required(old_portal_host
, old_portal_ip_list
, new_portals_by_host
):
400 if old_portal_host
not in new_portals_by_host
:
402 if sorted(old_portal_ip_list
) != sorted(new_portals_by_host
[old_portal_host
]):
407 def _target_deletion_required(target
, new_target_iqn
, new_target_controls
):
408 if target
['target_iqn'] != new_target_iqn
:
410 if target
['target_controls'] != new_target_controls
:
415 def _validate(target_iqn
, portals
, disks
, groups
):
417 raise DashboardException(msg
='Target IQN is required',
418 code
='target_iqn_required',
421 settings
= IscsiClient
.instance().get_settings()
422 minimum_gateways
= max(1, settings
['config']['minimum_gateways'])
423 portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(portals
)
424 if len(portals_by_host
.keys()) < minimum_gateways
:
425 if minimum_gateways
== 1:
426 msg
= 'At least one portal is required'
428 msg
= 'At least {} portals are required'.format(minimum_gateways
)
429 raise DashboardException(msg
=msg
,
430 code
='portals_required',
433 for portal
in portals
:
434 gateway_name
= portal
['host']
436 IscsiClient
.instance(gateway_name
=gateway_name
).ping()
437 except RequestException
:
438 raise DashboardException(msg
='iSCSI REST Api not available for gateway '
439 '{}'.format(gateway_name
),
440 code
='ceph_iscsi_rest_api_not_available_for_gateway',
445 image
= disk
['image']
446 backstore
= disk
['backstore']
447 required_rbd_features
= settings
['required_rbd_features'][backstore
]
448 unsupported_rbd_features
= settings
['unsupported_rbd_features'][backstore
]
449 IscsiTarget
._validate
_image
(pool
, image
, backstore
, required_rbd_features
,
450 unsupported_rbd_features
)
454 initiators
= initiators
+ group
['members']
455 if len(initiators
) != len(set(initiators
)):
456 raise DashboardException(msg
='Each initiator can only be part of 1 group at a time',
457 code
='initiator_in_multiple_groups',
461 def _validate_image(pool
, image
, backstore
, required_rbd_features
, unsupported_rbd_features
):
463 ioctx
= mgr
.rados
.open_ioctx(pool
)
465 with rbd
.Image(ioctx
, image
) as img
:
466 if img
.features() & required_rbd_features
!= required_rbd_features
:
467 raise DashboardException(msg
='Image {} cannot be exported using {} '
468 'backstore because required features are '
469 'missing (required features are '
473 required_rbd_features
)),
474 code
='image_missing_required_features',
476 if img
.features() & unsupported_rbd_features
!= 0:
477 raise DashboardException(msg
='Image {} cannot be exported using {} '
478 'backstore because it contains unsupported '
483 unsupported_rbd_features
)),
484 code
='image_contains_unsupported_features',
487 except rbd
.ImageNotFound
:
488 raise DashboardException(msg
='Image {} does not exist'.format(image
),
489 code
='image_does_not_exist',
491 except rados
.ObjectNotFound
:
492 raise DashboardException(msg
='Pool {} does not exist'.format(pool
),
493 code
='pool_does_not_exist',
497 def _create(target_iqn
, target_controls
, acl_enabled
,
498 portals
, disks
, clients
, groups
,
499 task_progress_begin
, task_progress_end
, config
):
500 target_config
= config
['targets'].get(target_iqn
, None)
501 TaskManager
.current_task().set_progress(task_progress_begin
)
502 portals_by_host
= IscsiTarget
._get
_portals
_by
_host
(portals
)
503 n_hosts
= len(portals_by_host
)
505 n_clients
= len(clients
)
506 n_groups
= len(groups
)
507 task_progress_steps
= n_hosts
+ n_disks
+ n_clients
+ n_groups
508 task_progress_inc
= 0
509 if task_progress_steps
!= 0:
510 task_progress_inc
= int((task_progress_end
- task_progress_begin
) / task_progress_steps
)
512 gateway_name
= portals
[0]['host']
513 if not target_config
:
514 IscsiClient
.instance(gateway_name
=gateway_name
).create_target(target_iqn
,
516 for host
, ip_list
in portals_by_host
.items():
517 if not target_config
or host
not in target_config
['portals']:
518 IscsiClient
.instance(gateway_name
=gateway_name
).create_gateway(target_iqn
,
521 TaskManager
.current_task().inc_progress(task_progress_inc
)
522 targetauth_action
= ('enable_acl' if acl_enabled
else 'disable_acl')
523 IscsiClient
.instance(gateway_name
=gateway_name
).update_targetauth(target_iqn
,
527 image
= disk
['image']
528 image_id
= '{}/{}'.format(pool
, image
)
529 if image_id
not in config
['disks']:
530 backstore
= disk
['backstore']
531 IscsiClient
.instance(gateway_name
=gateway_name
).create_disk(pool
,
534 if not target_config
or image_id
not in target_config
['disks']:
535 IscsiClient
.instance(gateway_name
=gateway_name
).create_target_lun(target_iqn
,
537 controls
= disk
['controls']
539 IscsiClient
.instance(gateway_name
=gateway_name
).reconfigure_disk(pool
,
542 TaskManager
.current_task().inc_progress(task_progress_inc
)
543 for client
in clients
:
544 client_iqn
= client
['client_iqn']
545 if not target_config
or client_iqn
not in target_config
['clients']:
546 IscsiClient
.instance(gateway_name
=gateway_name
).create_client(target_iqn
,
548 for lun
in client
['luns']:
551 image_id
= '{}/{}'.format(pool
, image
)
552 IscsiClient
.instance(gateway_name
=gateway_name
).create_client_lun(
553 target_iqn
, client_iqn
, image_id
)
554 user
= client
['auth']['user']
555 password
= client
['auth']['password']
556 m_user
= client
['auth']['mutual_user']
557 m_password
= client
['auth']['mutual_password']
558 IscsiClient
.instance(gateway_name
=gateway_name
).create_client_auth(
559 target_iqn
, client_iqn
, user
, password
, m_user
, m_password
)
560 TaskManager
.current_task().inc_progress(task_progress_inc
)
562 group_id
= group
['group_id']
563 members
= group
['members']
565 for disk
in group
['disks']:
566 image_ids
.append('{}/{}'.format(disk
['pool'], disk
['image']))
567 if not target_config
or group_id
not in target_config
['groups']:
568 IscsiClient
.instance(gateway_name
=gateway_name
).create_group(
569 target_iqn
, group_id
, members
, image_ids
)
570 TaskManager
.current_task().inc_progress(task_progress_inc
)
572 if not target_config
or target_controls
!= target_config
['controls']:
573 IscsiClient
.instance(gateway_name
=gateway_name
).reconfigure_target(
574 target_iqn
, target_controls
)
575 TaskManager
.current_task().set_progress(task_progress_end
)
576 except RequestException
as e
:
578 content
= json
.loads(e
.content
)
579 content_message
= content
.get('message')
581 raise DashboardException(msg
=content_message
, component
='iscsi')
582 raise DashboardException(e
=e
, component
='iscsi')
585 def _config_to_target(target_iqn
, config
):
586 target_config
= config
['targets'][target_iqn
]
588 for host
, portal_config
in target_config
['portals'].items():
589 for portal_ip
in portal_config
['portal_ip_addresses']:
594 portals
.append(portal
)
595 portals
= IscsiTarget
._sorted
_portals
(portals
)
597 for target_disk
in target_config
['disks']:
598 disk_config
= config
['disks'][target_disk
]
600 'pool': disk_config
['pool'],
601 'image': disk_config
['image'],
602 'controls': disk_config
['controls'],
603 'backstore': disk_config
['backstore']
606 disks
= IscsiTarget
._sorted
_disks
(disks
)
608 for client_iqn
, client_config
in target_config
['clients'].items():
610 for client_lun
in client_config
['luns'].keys():
611 pool
, image
= client_lun
.split('/', 1)
617 user
= client_config
['auth']['username']
618 password
= client_config
['auth']['password']
619 mutual_user
= client_config
['auth']['mutual_username']
620 mutual_password
= client_config
['auth']['mutual_password']
622 'client_iqn': client_iqn
,
626 'password': password
,
627 'mutual_user': mutual_user
,
628 'mutual_password': mutual_password
631 clients
.append(client
)
632 clients
= IscsiTarget
._sorted
_clients
(clients
)
634 for group_id
, group_config
in target_config
['groups'].items():
636 for group_disk_key
, _
in group_config
['disks'].items():
637 pool
, image
= group_disk_key
.split('/', 1)
642 group_disks
.append(group_disk
)
644 'group_id': group_id
,
645 'disks': group_disks
,
646 'members': group_config
['members'],
649 groups
= IscsiTarget
._sorted
_groups
(groups
)
650 target_controls
= target_config
['controls']
651 for key
, value
in target_controls
.items():
652 if isinstance(value
, bool):
653 target_controls
[key
] = 'Yes' if value
else 'No'
654 acl_enabled
= target_config
['acl_enabled']
656 'target_iqn': target_iqn
,
661 'target_controls': target_controls
,
662 'acl_enabled': acl_enabled
667 def _set_info(target
):
668 if not target
['portals']:
670 target_iqn
= target
['target_iqn']
671 gateway_name
= target
['portals'][0]['host']
672 target_info
= IscsiClient
.instance(gateway_name
=gateway_name
).get_targetinfo(target_iqn
)
673 target
['info'] = target_info
674 for client
in target
['clients']:
675 client_iqn
= client
['client_iqn']
676 client_info
= IscsiClient
.instance(gateway_name
=gateway_name
).get_clientinfo(
677 target_iqn
, client_iqn
)
678 client
['info'] = client_info
681 def _sorted_portals(portals
):
682 portals
= portals
or []
683 return sorted(portals
, key
=lambda p
: '{}.{}'.format(p
['host'], p
['ip']))
686 def _sorted_disks(disks
):
688 return sorted(disks
, key
=lambda d
: '{}.{}'.format(d
['pool'], d
['image']))
691 def _sorted_clients(clients
):
692 clients
= clients
or []
693 for client
in clients
:
694 client
['luns'] = sorted(client
['luns'],
695 key
=lambda d
: '{}.{}'.format(d
['pool'], d
['image']))
696 return sorted(clients
, key
=lambda c
: c
['client_iqn'])
699 def _sorted_groups(groups
):
700 groups
= groups
or []
702 group
['disks'] = sorted(group
['disks'],
703 key
=lambda d
: '{}.{}'.format(d
['pool'], d
['image']))
704 group
['members'] = sorted(group
['members'])
705 return sorted(groups
, key
=lambda g
: g
['group_id'])
708 def _get_portals_by_host(portals
):
710 for portal
in portals
:
711 host
= portal
['host']
713 if host
not in portals_by_host
:
714 portals_by_host
[host
] = []
715 portals_by_host
[host
].append(ip
)
716 return portals_by_host