1 # Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. |
|
2 # |
|
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may |
|
4 # not use this file except in compliance with the License. You may obtain |
|
5 # a copy of the License at |
|
6 # |
|
7 # http://www.apache.org/licenses/LICENSE-2.0 |
|
8 # |
|
9 # Unless required by applicable law or agreed to in writing, software |
|
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
|
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
|
12 # License for the specific language governing permissions and limitations |
|
13 # under the License. |
|
14 """ |
|
15 ZFS Storage Appliance Cinder Volume Driver |
|
16 """ |
|
17 import ast |
|
18 import base64 |
|
19 |
|
20 from oslo_config import cfg |
|
21 from oslo_log import log |
|
22 from oslo_utils import units |
|
23 |
|
24 from cinder import exception |
|
25 from cinder.i18n import _, _LE, _LW |
|
26 from cinder.volume import driver |
|
27 from cinder.volume.drivers.san import san |
|
28 from cinder.volume.drivers.zfssa import zfssarest |
|
29 from cinder.volume import volume_types |
|
30 |
|
31 CONF = cfg.CONF |
|
32 LOG = log.getLogger(__name__) |
|
33 |
|
34 ZFSSA_OPTS = [ |
|
35 cfg.StrOpt('zfssa_pool', |
|
36 help='Storage pool name.'), |
|
37 cfg.StrOpt('zfssa_project', |
|
38 help='Project name.'), |
|
39 cfg.StrOpt('zfssa_lun_volblocksize', default='8k', |
|
40 choices=['512', '1k', '2k', '4k', '8k', '16k', '32k', '64k', |
|
41 '128k'], |
|
42 help='Block size.'), |
|
43 cfg.BoolOpt('zfssa_lun_sparse', default=False, |
|
44 help='Flag to enable sparse (thin-provisioned): True, False.'), |
|
45 cfg.StrOpt('zfssa_lun_compression', default='off', |
|
46 choices=['off', 'lzjb', 'gzip-2', 'gzip', 'gzip-9'], |
|
47 help='Data compression.'), |
|
48 cfg.StrOpt('zfssa_lun_logbias', default='latency', |
|
49 choices=['latency', 'throughput'], |
|
50 help='Synchronous write bias.'), |
|
51 cfg.StrOpt('zfssa_initiator_group', default='', |
|
52 help='iSCSI initiator group.'), |
|
53 cfg.StrOpt('zfssa_initiator', default='', |
|
54 help='iSCSI initiator IQNs. (comma separated)'), |
|
55 cfg.StrOpt('zfssa_initiator_user', default='', |
|
56 help='iSCSI initiator CHAP user.'), |
|
57 cfg.StrOpt('zfssa_initiator_password', default='', |
|
58 help='iSCSI initiator CHAP password.', secret=True), |
|
59 cfg.StrOpt('zfssa_initiator_config', default='', |
|
60 help='iSCSI initiators configuration.'), |
|
61 cfg.StrOpt('zfssa_target_group', default='tgt-grp', |
|
62 help='iSCSI target group name.'), |
|
63 cfg.StrOpt('zfssa_target_user', default='', |
|
64 help='iSCSI target CHAP user.'), |
|
65 cfg.StrOpt('zfssa_target_password', default='', |
|
66 help='iSCSI target CHAP password.', secret=True), |
|
67 cfg.StrOpt('zfssa_target_portal', |
|
68 help='iSCSI target portal (Data-IP:Port, w.x.y.z:3260).'), |
|
69 cfg.StrOpt('zfssa_target_interfaces', |
|
70 help='Network interfaces of iSCSI targets. (comma separated)'), |
|
71 cfg.IntOpt('zfssa_rest_timeout', |
|
72 help='REST connection timeout. (seconds)') |
|
73 |
|
74 ] |
|
75 |
|
76 CONF.register_opts(ZFSSA_OPTS) |
|
77 |
|
78 ZFSSA_LUN_SPECS = {'zfssa:volblocksize', |
|
79 'zfssa:sparse', |
|
80 'zfssa:compression', |
|
81 'zfssa:logbias'} |
|
82 |
|
83 |
|
84 def factory_zfssa(): |
|
85 return zfssarest.ZFSSAApi() |
|
86 |
|
87 |
|
88 class ZFSSAISCSIDriver(driver.ISCSIDriver): |
|
89 """ZFSSA Cinder volume driver""" |
|
90 |
|
91 VERSION = '1.0.0' |
|
92 protocol = 'iSCSI' |
|
93 |
|
94 def __init__(self, *args, **kwargs): |
|
95 super(ZFSSAISCSIDriver, self).__init__(*args, **kwargs) |
|
96 self.configuration.append_config_values(ZFSSA_OPTS) |
|
97 self.configuration.append_config_values(san.san_opts) |
|
98 self.zfssa = None |
|
99 self._stats = None |
|
100 |
|
101 def _get_target_alias(self): |
|
102 """return target alias""" |
|
103 return self.configuration.zfssa_target_group |
|
104 |
|
105 def do_setup(self, context): |
|
106 """Setup - create multiple elements. |
|
107 |
|
108 Project, initiators, initiatorgroup, target and targetgroup. |
|
109 """ |
|
110 lcfg = self.configuration |
|
111 msg = (_('Connecting to host: %s.') % lcfg.san_ip) |
|
112 LOG.info(msg) |
|
113 self.zfssa = factory_zfssa() |
|
114 self.zfssa.set_host(lcfg.san_ip, timeout=lcfg.zfssa_rest_timeout) |
|
115 auth_str = base64.encodestring('%s:%s' % |
|
116 (lcfg.san_login, |
|
117 lcfg.san_password))[:-1] |
|
118 self.zfssa.login(auth_str) |
|
119 self.zfssa.create_project(lcfg.zfssa_pool, lcfg.zfssa_project, |
|
120 compression=lcfg.zfssa_lun_compression, |
|
121 logbias=lcfg.zfssa_lun_logbias) |
|
122 |
|
123 if (lcfg.zfssa_initiator_config != ''): |
|
124 initiator_config = ast.literal_eval(lcfg.zfssa_initiator_config) |
|
125 for initiator_group in initiator_config: |
|
126 zfssa_initiator_group = initiator_group |
|
127 for zfssa_initiator in initiator_config[zfssa_initiator_group]: |
|
128 self.zfssa.create_initiator(zfssa_initiator['iqn'], |
|
129 zfssa_initiator_group + '-' + |
|
130 zfssa_initiator['iqn'], |
|
131 chapuser= |
|
132 zfssa_initiator['user'], |
|
133 chapsecret= |
|
134 zfssa_initiator['password']) |
|
135 if (zfssa_initiator_group != 'default'): |
|
136 self.zfssa.add_to_initiatorgroup( |
|
137 zfssa_initiator['iqn'], |
|
138 zfssa_initiator_group) |
|
139 else: |
|
140 LOG.warning(_LW('zfssa_initiator_config not found. ' |
|
141 'Using deprecated configuration options.')) |
|
142 if (lcfg.zfssa_initiator != '' and |
|
143 (lcfg.zfssa_initiator_group == '' or |
|
144 lcfg.zfssa_initiator_group == 'default')): |
|
145 LOG.warning(_LW('zfssa_initiator: %(ini)s' |
|
146 ' wont be used on ' |
|
147 'zfssa_initiator_group= %(inigrp)s.') |
|
148 % {'ini': lcfg.zfssa_initiator, |
|
149 'inigrp': lcfg.zfssa_initiator_group}) |
|
150 |
|
151 # Setup initiator and initiator group |
|
152 if (lcfg.zfssa_initiator != '' and |
|
153 lcfg.zfssa_initiator_group != '' and |
|
154 lcfg.zfssa_initiator_group != 'default'): |
|
155 for initiator in lcfg.zfssa_initiator.split(','): |
|
156 self.zfssa.create_initiator( |
|
157 initiator, lcfg.zfssa_initiator_group + '-' + |
|
158 initiator, chapuser=lcfg.zfssa_initiator_user, |
|
159 chapsecret=lcfg.zfssa_initiator_password) |
|
160 self.zfssa.add_to_initiatorgroup( |
|
161 initiator, lcfg.zfssa_initiator_group) |
|
162 |
|
163 # Parse interfaces |
|
164 interfaces = [] |
|
165 for interface in lcfg.zfssa_target_interfaces.split(','): |
|
166 if interface == '': |
|
167 continue |
|
168 interfaces.append(interface) |
|
169 |
|
170 # Setup target and target group |
|
171 iqn = self.zfssa.create_target( |
|
172 self._get_target_alias(), |
|
173 interfaces, |
|
174 tchapuser=lcfg.zfssa_target_user, |
|
175 tchapsecret=lcfg.zfssa_target_password) |
|
176 |
|
177 self.zfssa.add_to_targetgroup(iqn, lcfg.zfssa_target_group) |
|
178 |
|
179 def check_for_setup_error(self): |
|
180 """Check that driver can login. |
|
181 |
|
182 Check also pool, project, initiators, initiatorgroup, target and |
|
183 targetgroup. |
|
184 """ |
|
185 lcfg = self.configuration |
|
186 |
|
187 self.zfssa.verify_pool(lcfg.zfssa_pool) |
|
188 self.zfssa.verify_project(lcfg.zfssa_pool, lcfg.zfssa_project) |
|
189 |
|
190 if (lcfg.zfssa_initiator_config != ''): |
|
191 initiator_config = ast.literal_eval(lcfg.zfssa_initiator_config) |
|
192 for initiator_group in initiator_config: |
|
193 zfssa_initiator_group = initiator_group |
|
194 for zfssa_initiator in initiator_config[zfssa_initiator_group]: |
|
195 self.zfssa.verify_initiator(zfssa_initiator['iqn']) |
|
196 else: |
|
197 if (lcfg.zfssa_initiator != '' and |
|
198 lcfg.zfssa_initiator_group != '' and |
|
199 lcfg.zfssa_initiator_group != 'default'): |
|
200 for initiator in lcfg.zfssa_initiator.split(','): |
|
201 self.zfssa.verify_initiator(initiator) |
|
202 |
|
203 self.zfssa.verify_target(self._get_target_alias()) |
|
204 |
|
205 def _get_provider_info(self, volume): |
|
206 """return provider information""" |
|
207 lcfg = self.configuration |
|
208 lun = self.zfssa.get_lun(lcfg.zfssa_pool, |
|
209 lcfg.zfssa_project, volume['name']) |
|
210 iqn = self.zfssa.get_target(self._get_target_alias()) |
|
211 loc = "%s %s %s" % (lcfg.zfssa_target_portal, iqn, lun['number']) |
|
212 LOG.debug('_get_provider_info: provider_location: %s' % loc) |
|
213 provider = {'provider_location': loc} |
|
214 if lcfg.zfssa_target_user != '' and lcfg.zfssa_target_password != '': |
|
215 provider['provider_auth'] = ('CHAP %s %s' % |
|
216 lcfg.zfssa_target_user, |
|
217 lcfg.zfssa_target_password) |
|
218 |
|
219 return provider |
|
220 |
|
221 def create_volume(self, volume): |
|
222 """Create a volume on ZFSSA""" |
|
223 LOG.debug('zfssa.create_volume: volume=' + volume['name']) |
|
224 lcfg = self.configuration |
|
225 volsize = str(volume['size']) + 'g' |
|
226 specs = self._get_voltype_specs(volume) |
|
227 self.zfssa.create_lun(lcfg.zfssa_pool, |
|
228 lcfg.zfssa_project, |
|
229 volume['name'], |
|
230 volsize, |
|
231 lcfg.zfssa_target_group, |
|
232 specs) |
|
233 |
|
234 def delete_volume(self, volume): |
|
235 """Deletes a volume with the given volume['name'].""" |
|
236 LOG.debug('zfssa.delete_volume: name=' + volume['name']) |
|
237 lcfg = self.configuration |
|
238 lun2del = self.zfssa.get_lun(lcfg.zfssa_pool, |
|
239 lcfg.zfssa_project, |
|
240 volume['name']) |
|
241 # Delete clone temp snapshot. see create_cloned_volume() |
|
242 if 'origin' in lun2del and 'id' in volume: |
|
243 if lun2del['nodestroy']: |
|
244 self.zfssa.set_lun_props(lcfg.zfssa_pool, |
|
245 lcfg.zfssa_project, |
|
246 volume['name'], |
|
247 nodestroy=False) |
|
248 |
|
249 tmpsnap = 'tmp-snapshot-%s' % volume['id'] |
|
250 if lun2del['origin']['snapshot'] == tmpsnap: |
|
251 self.zfssa.delete_snapshot(lcfg.zfssa_pool, |
|
252 lcfg.zfssa_project, |
|
253 lun2del['origin']['share'], |
|
254 lun2del['origin']['snapshot']) |
|
255 return |
|
256 |
|
257 self.zfssa.delete_lun(pool=lcfg.zfssa_pool, |
|
258 project=lcfg.zfssa_project, |
|
259 lun=volume['name']) |
|
260 |
|
261 def create_snapshot(self, snapshot): |
|
262 """Creates a snapshot with the given snapshot['name'] of the |
|
263 snapshot['volume_name'] |
|
264 """ |
|
265 LOG.debug('zfssa.create_snapshot: snapshot=' + snapshot['name']) |
|
266 lcfg = self.configuration |
|
267 self.zfssa.create_snapshot(lcfg.zfssa_pool, |
|
268 lcfg.zfssa_project, |
|
269 snapshot['volume_name'], |
|
270 snapshot['name']) |
|
271 |
|
272 def delete_snapshot(self, snapshot): |
|
273 """Deletes a snapshot.""" |
|
274 LOG.debug('zfssa.delete_snapshot: snapshot=' + snapshot['name']) |
|
275 lcfg = self.configuration |
|
276 has_clones = self.zfssa.has_clones(lcfg.zfssa_pool, |
|
277 lcfg.zfssa_project, |
|
278 snapshot['volume_name'], |
|
279 snapshot['name']) |
|
280 if has_clones: |
|
281 LOG.error(_LE('Snapshot %s: has clones') % snapshot['name']) |
|
282 raise exception.SnapshotIsBusy(snapshot_name=snapshot['name']) |
|
283 |
|
284 self.zfssa.delete_snapshot(lcfg.zfssa_pool, |
|
285 lcfg.zfssa_project, |
|
286 snapshot['volume_name'], |
|
287 snapshot['name']) |
|
288 |
|
289 def create_volume_from_snapshot(self, volume, snapshot): |
|
290 """Creates a volume from a snapshot - clone a snapshot""" |
|
291 LOG.debug('zfssa.create_volume_from_snapshot: volume=' + |
|
292 volume['name']) |
|
293 LOG.debug('zfssa.create_volume_from_snapshot: snapshot=' + |
|
294 snapshot['name']) |
|
295 if not self._verify_clone_size(snapshot, volume['size'] * units.Gi): |
|
296 exception_msg = (_('Error verifying clone size on ' |
|
297 'Volume clone: %(clone)s ' |
|
298 'Size: %(size)d on' |
|
299 'Snapshot: %(snapshot)s') |
|
300 % {'clone': volume['name'], |
|
301 'size': volume['size'], |
|
302 'snapshot': snapshot['name']}) |
|
303 LOG.error(exception_msg) |
|
304 raise exception.InvalidInput(reason=exception_msg) |
|
305 |
|
306 lcfg = self.configuration |
|
307 self.zfssa.clone_snapshot(lcfg.zfssa_pool, |
|
308 lcfg.zfssa_project, |
|
309 snapshot['volume_name'], |
|
310 snapshot['name'], |
|
311 volume['name']) |
|
312 |
|
313 def _update_volume_status(self): |
|
314 """Retrieve status info from volume group.""" |
|
315 LOG.debug("Updating volume status") |
|
316 self._stats = None |
|
317 data = {} |
|
318 backend_name = self.configuration.safe_get('volume_backend_name') |
|
319 data["volume_backend_name"] = backend_name or self.__class__.__name__ |
|
320 data["vendor_name"] = 'Oracle' |
|
321 data["driver_version"] = self.VERSION |
|
322 data["storage_protocol"] = self.protocol |
|
323 |
|
324 lcfg = self.configuration |
|
325 (avail, total) = self.zfssa.get_pool_stats(lcfg.zfssa_pool) |
|
326 if avail is None or total is None: |
|
327 return |
|
328 |
|
329 data['total_capacity_gb'] = int(total) / units.Gi |
|
330 data['free_capacity_gb'] = int(avail) / units.Gi |
|
331 data['reserved_percentage'] = 0 |
|
332 data['QoS_support'] = False |
|
333 self._stats = data |
|
334 |
|
335 def get_volume_stats(self, refresh=False): |
|
336 """Get volume status. |
|
337 If 'refresh' is True, run update the stats first. |
|
338 """ |
|
339 if refresh: |
|
340 self._update_volume_status() |
|
341 return self._stats |
|
342 |
|
343 def create_export(self, context, volume): |
|
344 pass |
|
345 |
|
346 def remove_export(self, context, volume): |
|
347 pass |
|
348 |
|
349 def ensure_export(self, context, volume): |
|
350 pass |
|
351 |
|
352 def copy_image_to_volume(self, context, volume, image_service, image_id): |
|
353 self.ensure_export(context, volume) |
|
354 super(ZFSSAISCSIDriver, self).copy_image_to_volume( |
|
355 context, volume, image_service, image_id) |
|
356 |
|
357 def extend_volume(self, volume, new_size): |
|
358 """Driver entry point to extent volume size.""" |
|
359 LOG.debug('extend_volume: volume name: %s' % volume['name']) |
|
360 lcfg = self.configuration |
|
361 self.zfssa.set_lun_props(lcfg.zfssa_pool, |
|
362 lcfg.zfssa_project, |
|
363 volume['name'], |
|
364 volsize=new_size * units.Gi) |
|
365 |
|
366 def create_cloned_volume(self, volume, src_vref): |
|
367 """Create a clone of the specified volume.""" |
|
368 zfssa_snapshot = {'volume_name': src_vref['name'], |
|
369 'name': 'tmp-snapshot-%s' % volume['id']} |
|
370 self.create_snapshot(zfssa_snapshot) |
|
371 try: |
|
372 self.create_volume_from_snapshot(volume, zfssa_snapshot) |
|
373 except exception.VolumeBackendAPIException: |
|
374 LOG.error(_LE('Clone Volume:' |
|
375 '%(volume)s failed from source volume:' |
|
376 '%(src_vref)s') |
|
377 % {'volume': volume['name'], |
|
378 'src_vref': src_vref['name']}) |
|
379 # Cleanup snapshot |
|
380 self.delete_snapshot(zfssa_snapshot) |
|
381 |
|
382 def local_path(self, volume): |
|
383 """Not implemented""" |
|
384 pass |
|
385 |
|
386 def backup_volume(self, context, backup, backup_service): |
|
387 """Not implemented""" |
|
388 pass |
|
389 |
|
390 def restore_backup(self, context, backup, volume, backup_service): |
|
391 """Not implemented""" |
|
392 pass |
|
393 |
|
394 def _verify_clone_size(self, snapshot, size): |
|
395 """Check whether the clone size is the same as the parent volume""" |
|
396 lcfg = self.configuration |
|
397 lun = self.zfssa.get_lun(lcfg.zfssa_pool, |
|
398 lcfg.zfssa_project, |
|
399 snapshot['volume_name']) |
|
400 return lun['size'] == size |
|
401 |
|
402 def initialize_connection(self, volume, connector): |
|
403 lcfg = self.configuration |
|
404 init_groups = self.zfssa.get_initiator_initiatorgroup( |
|
405 connector['initiator']) |
|
406 for initiator_group in init_groups: |
|
407 self.zfssa.set_lun_initiatorgroup(lcfg.zfssa_pool, |
|
408 lcfg.zfssa_project, |
|
409 volume['name'], |
|
410 initiator_group) |
|
411 iscsi_properties = {} |
|
412 provider = self._get_provider_info(volume) |
|
413 (target_portal, iqn, lun) = provider['provider_location'].split() |
|
414 iscsi_properties['target_discovered'] = False |
|
415 iscsi_properties['target_portal'] = target_portal |
|
416 iscsi_properties['target_iqn'] = iqn |
|
417 iscsi_properties['target_lun'] = lun |
|
418 iscsi_properties['volume_id'] = volume['id'] |
|
419 |
|
420 if 'provider_auth' in provider: |
|
421 (auth_method, auth_username, auth_password) = provider[ |
|
422 'provider_auth'].split() |
|
423 iscsi_properties['auth_method'] = auth_method |
|
424 iscsi_properties['auth_username'] = auth_username |
|
425 iscsi_properties['auth_password'] = auth_password |
|
426 |
|
427 return { |
|
428 'driver_volume_type': 'iscsi', |
|
429 'data': iscsi_properties |
|
430 } |
|
431 |
|
432 def terminate_connection(self, volume, connector, **kwargs): |
|
433 """Driver entry point to terminate a connection for a volume.""" |
|
434 LOG.debug('terminate_connection: volume name: %s.' % volume['name']) |
|
435 lcfg = self.configuration |
|
436 self.zfssa.set_lun_initiatorgroup(lcfg.zfssa_pool, |
|
437 lcfg.zfssa_project, |
|
438 volume['name'], |
|
439 '') |
|
440 |
|
441 def _get_voltype_specs(self, volume): |
|
442 """Get specs suitable for volume creation.""" |
|
443 vtype = volume.get('volume_type_id', None) |
|
444 extra_specs = None |
|
445 if vtype: |
|
446 extra_specs = volume_types.get_volume_type_extra_specs(vtype) |
|
447 |
|
448 return self._get_specs(extra_specs) |
|
449 |
|
450 def _get_specs(self, xspecs): |
|
451 """Return a dict with extra specs and/or config values.""" |
|
452 result = {} |
|
453 for spc in ZFSSA_LUN_SPECS: |
|
454 val = None |
|
455 prop = spc.split(':')[1] |
|
456 cfg = 'zfssa_lun_' + prop |
|
457 if xspecs: |
|
458 val = xspecs.pop(spc, None) |
|
459 |
|
460 if val is None: |
|
461 val = self.configuration.safe_get(cfg) |
|
462 |
|
463 if val is not None and val != '': |
|
464 result.update({prop: val}) |
|
465 |
|
466 return result |
|