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