]>
Commit | Line | Data |
---|---|---|
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 |
5 | from __future__ import absolute_import |
6 | ||
11fdf7f2 | 7 | import json |
f67539c2 TL |
8 | import re |
9 | from copy import deepcopy | |
11fdf7f2 | 10 | |
f67539c2 | 11 | import cherrypy |
11fdf7f2 TL |
12 | import rados |
13 | import rbd | |
14 | ||
11fdf7f2 | 15 | from .. import mgr |
f67539c2 | 16 | from ..exceptions import DashboardException |
11fdf7f2 TL |
17 | from ..rest_client import RequestException |
18 | from ..security import Scope | |
11fdf7f2 | 19 | from ..services.iscsi_cli import IscsiGatewaysConfig |
f67539c2 | 20 | from ..services.iscsi_client import IscsiClient |
92f5a8d4 | 21 | from ..services.iscsi_config import IscsiGatewayDoesNotExist |
11fdf7f2 TL |
22 | from ..services.rbd import format_bitmask |
23 | from ..services.tcmu_service import TcmuService | |
f67539c2 TL |
24 | from ..tools import TaskManager, str_to_bool |
25 | from . import ApiController, BaseController, ControllerDoc, Endpoint, \ | |
26 | EndpointDoc, ReadPermission, RESTController, Task, UiApiController, \ | |
27 | UpdatePermission | |
11fdf7f2 | 28 | |
9f95a23c TL |
29 | try: |
30 | from typing import Any, Dict, List, no_type_check | |
31 | except ImportError: | |
32 | no_type_check = object() # Just for type checking | |
33 | ||
f67539c2 TL |
34 | ISCSI_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) | |
43 | class 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 | 201 | class 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 | ||
252 | def 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 |
258 | class 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 | ||
1032 | def 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 | ||
1049 | def 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 | ||
1060 | def 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') |