1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2010 United States Government as represented by the
4
# Administrator of the National Aeronautics and Space Administration.
7
# Licensed under the Apache License, Version 2.0 (the "License"); you may
8
# not use this file except in compliance with the License. You may obtain
9
# a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing, software
14
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
# License for the specific language governing permissions and limitations
19
Driver for Linux servers running LVM.
27
from oslo.config import cfg
29
from cinder import exception
30
from cinder import flags
31
from cinder.image import image_utils
32
from cinder.openstack.common import log as logging
33
from cinder import utils
34
from cinder.volume import driver
35
from cinder.volume import iscsi
37
LOG = logging.getLogger(__name__)
40
cfg.StrOpt('volume_group',
41
default='cinder-volumes',
42
help='Name for the VG that will contain exported volumes'),
43
cfg.StrOpt('volume_clear',
45
help='Method used to wipe old volumes (valid options are: '
46
'none, zero, shred)'),
47
cfg.IntOpt('volume_clear_size',
49
help='Size in MiB to wipe at start of old volumes. 0 => all'),
50
cfg.StrOpt('pool_size',
52
help='Size of thin provisioning pool '
53
'(None uses entire cinder VG)'),
54
cfg.IntOpt('lvm_mirrors',
56
help='If set, create lvms with multiple mirrors. Note that '
57
'this requires lvm_mirrors + 2 pvs with available space'),
61
FLAGS.register_opts(volume_opts)
64
class LVMVolumeDriver(driver.VolumeDriver):
65
"""Executes commands relating to Volumes."""
66
def __init__(self, *args, **kwargs):
67
super(LVMVolumeDriver, self).__init__(*args, **kwargs)
68
self.configuration.append_config_values(volume_opts)
70
def check_for_setup_error(self):
71
"""Returns an error if prerequisites aren't met"""
72
out, err = self._execute('vgs', '--noheadings', '-o', 'name',
74
volume_groups = out.split()
75
if self.configuration.volume_group not in volume_groups:
76
exception_message = (_("volume group %s doesn't exist")
77
% self.configuration.volume_group)
78
raise exception.VolumeBackendAPIException(data=exception_message)
80
def _create_volume(self, volume_name, sizestr):
81
cmd = ['lvcreate', '-L', sizestr, '-n', volume_name,
82
self.configuration.volume_group]
83
if self.configuration.lvm_mirrors:
84
cmd += ['-m', self.configuration.lvm_mirrors, '--nosync']
85
terras = int(sizestr[:-1]) / 1024.0
87
rsize = int(2 ** math.ceil(math.log(terras) / math.log(2)))
88
# NOTE(vish): Next power of two for region size. See:
89
# http://red.ht/U2BPOD
90
cmd += ['-R', str(rsize)]
92
self._try_execute(*cmd, run_as_root=True)
94
def _copy_volume(self, srcstr, deststr, size_in_g, clearing=False):
95
# Use O_DIRECT to avoid thrashing the system buffer cache
96
extra_flags = ['iflag=direct', 'oflag=direct']
98
# Check whether O_DIRECT is supported
100
self._execute('dd', 'count=0', 'if=%s' % srcstr, 'of=%s' % deststr,
101
*extra_flags, run_as_root=True)
102
except exception.ProcessExecutionError:
105
# If the volume is being unprovisioned then
106
# request the data is persisted before returning,
107
# so that it's not discarded from the cache.
108
if clearing and not extra_flags:
109
extra_flags.append('conv=fdatasync')
112
self._execute('dd', 'if=%s' % srcstr, 'of=%s' % deststr,
113
'count=%d' % (size_in_g * 1024), 'bs=1M',
114
*extra_flags, run_as_root=True)
116
def _volume_not_present(self, volume_name):
117
path_name = '%s/%s' % (self.configuration.volume_group, volume_name)
119
self._try_execute('lvdisplay', path_name, run_as_root=True)
120
except Exception as e:
121
# If the volume isn't present
125
def _delete_volume(self, volume, size_in_g):
126
"""Deletes a logical volume."""
127
# zero out old volumes to prevent data leaking between users
128
# TODO(ja): reclaiming space should be done lazy and low priority
129
dev_path = self.local_path(volume)
130
if os.path.exists(dev_path):
131
self.clear_volume(volume)
133
self._try_execute('lvremove', '-f', "%s/%s" %
134
(self.configuration.volume_group,
135
self._escape_snapshot(volume['name'])),
138
def _sizestr(self, size_in_g):
139
if int(size_in_g) == 0:
141
return '%sG' % size_in_g
143
# Linux LVM reserves name that starts with snapshot, so that
144
# such volume name can't be created. Mangle it.
145
def _escape_snapshot(self, snapshot_name):
146
if not snapshot_name.startswith('snapshot'):
148
return '_' + snapshot_name
150
def create_volume(self, volume):
151
"""Creates a logical volume. Can optionally return a Dictionary of
152
changes to the volume object to be persisted."""
153
self._create_volume(volume['name'], self._sizestr(volume['size']))
155
def create_volume_from_snapshot(self, volume, snapshot):
156
"""Creates a volume from a snapshot."""
157
self._create_volume(volume['name'], self._sizestr(volume['size']))
158
self._copy_volume(self.local_path(snapshot), self.local_path(volume),
159
snapshot['volume_size'])
161
def delete_volume(self, volume):
162
"""Deletes a logical volume."""
163
if self._volume_not_present(volume['name']):
164
# If the volume isn't present, then don't attempt to delete
167
# TODO(yamahata): lvm can't delete origin volume only without
168
# deleting derived snapshots. Can we do something fancy?
169
out, err = self._execute('lvdisplay', '--noheading',
171
'%s/%s' % (self.configuration.volume_group,
174
# fake_execute returns None resulting unit test error
177
if (out[0] == 'o') or (out[0] == 'O'):
178
raise exception.VolumeIsBusy(volume_name=volume['name'])
180
self._delete_volume(volume, volume['size'])
182
def clear_volume(self, volume):
183
"""unprovision old volumes to prevent data leaking between users."""
185
vol_path = self.local_path(volume)
186
size_in_g = volume.get('size')
187
size_in_m = self.configuration.volume_clear_size
192
if self.configuration.volume_clear == 'none':
195
LOG.info(_("Performing secure delete on volume: %s") % volume['id'])
197
if self.configuration.volume_clear == 'zero':
199
return self._copy_volume('/dev/zero', vol_path, size_in_g,
202
clear_cmd = ['shred', '-n0', '-z', '-s%dMiB' % size_in_m]
203
elif self.configuration.volume_clear == 'shred':
204
clear_cmd = ['shred', '-n3']
206
clear_cmd.append('-s%dMiB' % size_in_m)
208
LOG.error(_("Error unrecognized volume_clear option: %s"),
209
self.configuration.volume_clear)
212
clear_cmd.append(vol_path)
213
self._execute(*clear_cmd, run_as_root=True)
215
def create_snapshot(self, snapshot):
216
"""Creates a snapshot."""
217
orig_lv_name = "%s/%s" % (self.configuration.volume_group,
218
snapshot['volume_name'])
219
self._try_execute('lvcreate', '-L',
220
self._sizestr(snapshot['volume_size']),
221
'--name', self._escape_snapshot(snapshot['name']),
222
'--snapshot', orig_lv_name, run_as_root=True)
224
def delete_snapshot(self, snapshot):
225
"""Deletes a snapshot."""
226
if self._volume_not_present(self._escape_snapshot(snapshot['name'])):
227
# If the snapshot isn't present, then don't attempt to delete
230
# TODO(yamahata): zeroing out the whole snapshot triggers COW.
232
self._delete_volume(snapshot, snapshot['volume_size'])
234
def local_path(self, volume):
235
# NOTE(vish): stops deprecation warning
236
escaped_group = self.configuration.volume_group.replace('-', '--')
237
escaped_name = self._escape_snapshot(volume['name']).replace('-', '--')
238
return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
240
def copy_image_to_volume(self, context, volume, image_service, image_id):
241
"""Fetch the image from image_service and write it to the volume."""
242
image_utils.fetch_to_raw(context,
245
self.local_path(volume))
247
def copy_volume_to_image(self, context, volume, image_service, image_meta):
248
"""Copy the volume to the specified image."""
249
image_utils.upload_volume(context,
252
self.local_path(volume))
254
def create_cloned_volume(self, volume, src_vref):
255
"""Creates a clone of the specified volume."""
256
LOG.info(_('Creating clone of volume: %s') % src_vref['id'])
257
volume_name = FLAGS.volume_name_template % src_vref['id']
258
temp_id = 'tmp-snap-%s' % src_vref['id']
259
temp_snapshot = {'volume_name': volume_name,
260
'size': src_vref['size'],
261
'volume_size': src_vref['size'],
262
'name': 'clone-snap-%s' % src_vref['id'],
264
self.create_snapshot(temp_snapshot)
265
self._create_volume(volume['name'], self._sizestr(volume['size']))
267
self._copy_volume(self.local_path(temp_snapshot),
268
self.local_path(volume),
271
self.delete_snapshot(temp_snapshot)
273
def clone_image(self, volume, image_location):
276
def backup_volume(self, context, backup, backup_service):
277
"""Create a new backup from an existing volume."""
278
volume = self.db.volume_get(context, backup['volume_id'])
279
volume_path = self.local_path(volume)
280
with utils.temporary_chown(volume_path):
281
with utils.file_open(volume_path) as volume_file:
282
backup_service.backup(backup, volume_file)
284
def restore_backup(self, context, backup, volume, backup_service):
285
"""Restore an existing backup to a new or existing volume."""
286
volume_path = self.local_path(volume)
287
with utils.temporary_chown(volume_path):
288
with utils.file_open(volume_path, 'wb') as volume_file:
289
backup_service.restore(backup, volume['id'], volume_file)
292
class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver):
293
"""Executes commands relating to ISCSI volumes.
295
We make use of model provider properties as follows:
297
``provider_location``
298
if present, contains the iSCSI target information in the same
299
format as an ietadm discovery
300
i.e. '<ip>:<port>,<portal> <target IQN>'
303
if present, contains a space-separated triple:
304
'<auth method> <auth username> <auth password>'.
305
`CHAP` is the only auth_method in use at the moment.
308
def __init__(self, *args, **kwargs):
309
self.tgtadm = iscsi.get_target_admin()
310
super(LVMISCSIDriver, self).__init__(*args, **kwargs)
312
def set_execute(self, execute):
313
super(LVMISCSIDriver, self).set_execute(execute)
314
self.tgtadm.set_execute(execute)
316
def ensure_export(self, context, volume):
317
"""Synchronously recreates an export for a logical volume."""
318
# NOTE(jdg): tgtadm doesn't use the iscsi_targets table
319
# TODO(jdg): In the future move all of the dependent stuff into the
320
# cooresponding target admin class
322
if isinstance(self.tgtadm, iscsi.LioAdm):
324
volume_info = self.db.volume_get(context, volume['id'])
327
auth_pass) = volume_info['provider_auth'].split(' ', 3)
328
chap_auth = self._iscsi_authentication(auth_method,
331
except exception.NotFound:
332
LOG.debug("volume_info:", volume_info)
333
LOG.info(_("Skipping ensure_export. No iscsi_target "
334
"provision for volume: %s"), volume['id'])
337
iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
338
volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
341
self.tgtadm.create_iscsi_target(iscsi_name, iscsi_target,
342
0, volume_path, chap_auth,
343
check_exit_code=False)
346
if not isinstance(self.tgtadm, iscsi.TgtAdm):
348
iscsi_target = self.db.volume_get_iscsi_target_num(
351
except exception.NotFound:
352
LOG.info(_("Skipping ensure_export. No iscsi_target "
353
"provisioned for volume: %s"), volume['id'])
356
iscsi_target = 1 # dummy value when using TgtAdm
360
# Check for https://bugs.launchpad.net/cinder/+bug/1065702
362
volume_name = volume['name']
363
if (volume['provider_location'] is not None and
364
volume['name'] not in volume['provider_location']):
366
msg = _('Detected inconsistency in provider_location id')
368
old_name = self._fix_id_migration(context, volume)
369
if 'in-use' in volume['status']:
370
volume_name = old_name
373
iscsi_name = "%s%s" % (self.configuration.iscsi_target_prefix,
375
volume_path = "/dev/%s/%s" % (self.configuration.volume_group,
378
# NOTE(jdg): For TgtAdm case iscsi_name is the ONLY param we need
379
# should clean this all up at some point in the future
380
self.tgtadm.create_iscsi_target(iscsi_name, iscsi_target,
381
0, volume_path, chap_auth,
382
check_exit_code=False,
385
def _fix_id_migration(self, context, volume):
386
"""Fix provider_location and dev files to address bug 1065702.
388
For volumes that the provider_location has NOT been updated
389
and are not currently in-use we'll create a new iscsi target
390
and remove the persist file.
392
If the volume is in-use, we'll just stick with the old name
393
and when detach is called we'll feed back into ensure_export
394
again if necessary and fix things up then.
396
Details at: https://bugs.launchpad.net/cinder/+bug/1065702
400
pattern = re.compile(r":|\s")
401
fields = pattern.split(volume['provider_location'])
404
volume['provider_location'] = \
405
volume['provider_location'].replace(old_name, volume['name'])
406
model_update['provider_location'] = volume['provider_location']
408
self.db.volume_update(context, volume['id'], model_update)
411
os.chdir('/dev/%s' % self.configuration.volume_group)
414
(out, err) = self._execute('readlink', old_name)
415
except exception.ProcessExecutionError:
416
link_path = '/dev/%s/%s' % (self.configuration.volume_group,
418
LOG.debug(_('Symbolic link %s not found') % link_path)
422
rel_path = out.rstrip()
425
rel_path, volume['name'],
430
def _ensure_iscsi_targets(self, context, host):
431
"""Ensure that target ids have been created in datastore."""
432
# NOTE(jdg): tgtadm doesn't use the iscsi_targets table
433
# TODO(jdg): In the future move all of the dependent stuff into the
434
# cooresponding target admin class
435
if not isinstance(self.tgtadm, iscsi.TgtAdm):
436
host_iscsi_targets = self.db.iscsi_target_count_by_host(context,
438
if host_iscsi_targets >= self.configuration.iscsi_num_targets:
441
# NOTE(vish): Target ids start at 1, not 0.
442
target_end = self.configuration.iscsi_num_targets + 1
443
for target_num in xrange(1, target_end):
444
target = {'host': host, 'target_num': target_num}
445
self.db.iscsi_target_create_safe(context, target)
447
def create_export(self, context, volume):
448
"""Creates an export for a logical volume."""
450
iscsi_name = "%s%s" % (self.configuration.iscsi_target_prefix,
452
volume_path = "/dev/%s/%s" % (self.configuration.volume_group,
456
# TODO(jdg): In the future move all of the dependent stuff into the
457
# cooresponding target admin class
458
if not isinstance(self.tgtadm, iscsi.TgtAdm):
460
self._ensure_iscsi_targets(context, volume['host'])
461
iscsi_target = self.db.volume_allocate_iscsi_target(context,
465
lun = 1 # For tgtadm the controller is lun 0, dev starts at lun 1
466
iscsi_target = 0 # NOTE(jdg): Not used by tgtadm
468
# Use the same method to generate the username and the password.
469
chap_username = utils.generate_username()
470
chap_password = utils.generate_password()
471
chap_auth = self._iscsi_authentication('IncomingUser', chap_username,
473
# NOTE(jdg): For TgtAdm case iscsi_name is the ONLY param we need
474
# should clean this all up at some point in the future
475
tid = self.tgtadm.create_iscsi_target(iscsi_name,
480
model_update['provider_location'] = self._iscsi_location(
481
self.configuration.iscsi_ip_address, tid, iscsi_name, lun)
482
model_update['provider_auth'] = self._iscsi_authentication(
483
'CHAP', chap_username, chap_password)
486
def remove_export(self, context, volume):
487
"""Removes an export for a logical volume."""
488
# NOTE(jdg): tgtadm doesn't use the iscsi_targets table
489
# TODO(jdg): In the future move all of the dependent stuff into the
490
# cooresponding target admin class
492
if isinstance(self.tgtadm, iscsi.LioAdm):
494
iscsi_target = self.db.volume_get_iscsi_target_num(
497
except exception.NotFound:
498
LOG.info(_("Skipping remove_export. No iscsi_target "
499
"provisioned for volume: %s"), volume['id'])
502
self.tgtadm.remove_iscsi_target(iscsi_target, 0, volume['id'])
506
elif not isinstance(self.tgtadm, iscsi.TgtAdm):
508
iscsi_target = self.db.volume_get_iscsi_target_num(
511
except exception.NotFound:
512
LOG.info(_("Skipping remove_export. No iscsi_target "
513
"provisioned for volume: %s"), volume['id'])
520
# NOTE: provider_location may be unset if the volume hasn't
522
location = volume['provider_location'].split(' ')
525
# ietadm show will exit with an error
526
# this export has already been removed
527
self.tgtadm.show_target(iscsi_target, iqn=iqn)
529
except Exception as e:
530
LOG.info(_("Skipping remove_export. No iscsi_target "
531
"is presently exported for volume: %s"), volume['id'])
534
self.tgtadm.remove_iscsi_target(iscsi_target, 0, volume['id'])
536
def get_volume_stats(self, refresh=False):
537
"""Get volume status.
539
If 'refresh' is True, run update the stats first."""
541
self._update_volume_status()
545
def _update_volume_status(self):
546
"""Retrieve status info from volume group."""
548
LOG.debug(_("Updating volume status"))
551
# Note(zhiteng): These information are driver/backend specific,
552
# each driver may define these values in its own config options
553
# or fetch from driver specific configuration file.
554
backend_name = self.configuration.safe_get('volume_backend_name')
555
data["volume_backend_name"] = backend_name or 'LVM_iSCSI'
556
data["vendor_name"] = 'Open Source'
557
data["driver_version"] = '1.0'
558
data["storage_protocol"] = 'iSCSI'
560
data['total_capacity_gb'] = 0
561
data['free_capacity_gb'] = 0
562
data['reserved_percentage'] = self.configuration.reserved_percentage
563
data['QoS_support'] = False
566
out, err = self._execute('vgs', '--noheadings', '--nosuffix',
567
'--unit=G', '-o', 'name,size,free',
568
self.configuration.volume_group,
570
except exception.ProcessExecutionError as exc:
571
LOG.error(_("Error retrieving volume status: "), exc.stderr)
576
data['total_capacity_gb'] = float(volume[1])
577
data['free_capacity_gb'] = float(volume[2])
581
def _iscsi_location(self, ip, target, iqn, lun=None):
582
return "%s:%s,%s %s %s" % (ip, self.configuration.iscsi_port,
585
def _iscsi_authentication(self, chap, name, password):
586
return "%s %s %s" % (chap, name, password)
589
class ThinLVMVolumeDriver(LVMISCSIDriver):
590
"""Subclass for thin provisioned LVM's."""
591
def __init__(self, *args, **kwargs):
592
super(ThinLVMVolumeDriver, self).__init__(*args, **kwargs)
594
def check_for_setup_error(self):
595
"""Returns an error if prerequisites aren't met"""
596
out, err = self._execute('lvs', '--option',
597
'name', '--noheadings',
599
pool_name = "%s-pool" % FLAGS.volume_group
600
if pool_name not in out:
601
if not FLAGS.pool_size:
602
out, err = self._execute('vgs', FLAGS.volume_group,
603
'--noheadings', '--options',
604
'name,size', run_as_root=True)
605
size = re.sub(r'[\.][\d][\d]', '', out.split()[1])
607
size = "%s" % FLAGS.pool_size
609
pool_path = '%s/%s' % (FLAGS.volume_group, pool_name)
610
out, err = self._execute('lvcreate', '-T', '-L', size,
611
pool_path, run_as_root=True)
613
def _do_lvm_snapshot(self, src_lvm_name, dest_vref, is_cinder_snap=True):
615
new_name = self._escape_snapshot(dest_vref['name'])
617
new_name = dest_vref['name']
619
self._try_execute('lvcreate', '-s', '-n', new_name,
620
src_lvm_name, run_as_root=True)
622
def create_volume(self, volume):
623
"""Creates a logical volume. Can optionally return a Dictionary of
624
changes to the volume object to be persisted."""
625
sizestr = self._sizestr(volume['size'])
626
vg_name = ("%s/%s-pool" % (FLAGS.volume_group, FLAGS.volume_group))
627
self._try_execute('lvcreate', '-T', '-V', sizestr, '-n',
628
volume['name'], vg_name, run_as_root=True)
630
def delete_volume(self, volume):
631
"""Deletes a logical volume."""
632
if self._volume_not_present(volume['name']):
634
self._try_execute('lvremove', '-f', "%s/%s" %
636
self._escape_snapshot(volume['name'])),
639
def create_cloned_volume(self, volume, src_vref):
640
"""Creates a clone of the specified volume."""
641
LOG.info(_('Creating clone of volume: %s') % src_vref['id'])
642
orig_lv_name = "%s/%s" % (FLAGS.volume_group, src_vref['name'])
643
self._do_lvm_snapshot(orig_lv_name, volume, False)
645
def create_snapshot(self, snapshot):
646
"""Creates a snapshot of a volume."""
647
orig_lv_name = "%s/%s" % (FLAGS.volume_group, snapshot['volume_name'])
648
self._do_lvm_snapshot(orig_lv_name, snapshot)