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