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