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