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
25
from nova import exception
26
from nova import flags
27
from nova import log as logging
28
from nova.openstack.common import cfg
29
from nova import utils
30
from nova.volume import iscsi
33
LOG = logging.getLogger(__name__)
36
cfg.StrOpt('volume_group',
37
default='nova-volumes',
38
help='Name for the VG that will contain exported volumes'),
39
cfg.StrOpt('num_shell_tries',
41
help='number of times to attempt to run flakey shell commands'),
42
cfg.StrOpt('num_iscsi_scan_tries',
44
help='number of times to rescan iSCSI target to find volume'),
45
cfg.IntOpt('iscsi_num_targets',
47
help='Number of iscsi target ids per host'),
48
cfg.StrOpt('iscsi_target_prefix',
49
default='iqn.2010-10.org.openstack:',
50
help='prefix for iscsi volumes'),
51
cfg.StrOpt('iscsi_ip_address',
53
help='use this ip for iscsi'),
54
cfg.IntOpt('iscsi_port',
56
help='The port that the iSCSI daemon is listening on'),
57
cfg.StrOpt('rbd_pool',
59
help='the rbd pool in which volumes are stored'),
63
FLAGS.register_opts(volume_opts)
66
class VolumeDriver(object):
67
"""Executes commands relating to Volumes."""
68
def __init__(self, execute=utils.execute, *args, **kwargs):
69
# NOTE(vish): db is set by Manager
71
self.set_execute(execute)
73
def set_execute(self, execute):
74
self._execute = execute
76
def _try_execute(self, *command, **kwargs):
77
# NOTE(vish): Volume commands can partially fail due to timing, but
78
# running them a second time on failure will usually
83
self._execute(*command, **kwargs)
85
except exception.ProcessExecutionError:
87
if tries >= FLAGS.num_shell_tries:
89
LOG.exception(_("Recovering from a failed execute. "
90
"Try number %s"), tries)
91
time.sleep(tries ** 2)
93
def check_for_setup_error(self):
94
"""Returns an error if prerequisites aren't met"""
95
out, err = self._execute('vgs', '--noheadings', '-o', 'name',
97
volume_groups = out.split()
98
if not FLAGS.volume_group in volume_groups:
99
raise exception.Error(_("volume group %s doesn't exist")
100
% FLAGS.volume_group)
102
def _create_volume(self, volume_name, sizestr):
103
self._try_execute('lvcreate', '-L', sizestr, '-n',
104
volume_name, FLAGS.volume_group, run_as_root=True)
106
def _copy_volume(self, srcstr, deststr, size_in_g):
107
self._execute('dd', 'if=%s' % srcstr, 'of=%s' % deststr,
108
'count=%d' % (size_in_g * 1024), 'bs=1M',
111
def _volume_not_present(self, volume_name):
112
path_name = '%s/%s' % (FLAGS.volume_group, volume_name)
114
self._try_execute('lvdisplay', path_name, run_as_root=True)
115
except Exception as e:
116
# If the volume isn't present
120
def _delete_volume(self, volume, size_in_g):
121
"""Deletes a logical volume."""
122
# zero out old volumes to prevent data leaking between users
123
# TODO(ja): reclaiming space should be done lazy and low priority
124
self._copy_volume('/dev/zero', self.local_path(volume), size_in_g)
125
self._try_execute('lvremove', '-f', "%s/%s" %
127
self._escape_snapshot(volume['name'])),
130
def _sizestr(self, size_in_g):
131
if int(size_in_g) == 0:
133
return '%sG' % size_in_g
135
# Linux LVM reserves name that starts with snapshot, so that
136
# such volume name can't be created. Mangle it.
137
def _escape_snapshot(self, snapshot_name):
138
if not snapshot_name.startswith('snapshot'):
140
return '_' + snapshot_name
142
def create_volume(self, volume):
143
"""Creates a logical volume. Can optionally return a Dictionary of
144
changes to the volume object to be persisted."""
145
self._create_volume(volume['name'], self._sizestr(volume['size']))
147
def create_volume_from_snapshot(self, volume, snapshot):
148
"""Creates a volume from a snapshot."""
149
self._create_volume(volume['name'], self._sizestr(volume['size']))
150
self._copy_volume(self.local_path(snapshot), self.local_path(volume),
151
snapshot['volume_size'])
153
def delete_volume(self, volume):
154
"""Deletes a logical volume."""
155
if self._volume_not_present(volume['name']):
156
# If the volume isn't present, then don't attempt to delete
159
# TODO(yamahata): lvm can't delete origin volume only without
160
# deleting derived snapshots. Can we do something fancy?
161
out, err = self._execute('lvdisplay', '--noheading',
163
'%s/%s' % (FLAGS.volume_group,
166
# fake_execute returns None resulting unit test error
169
if (out[0] == 'o') or (out[0] == 'O'):
170
raise exception.VolumeIsBusy(volume_name=volume['name'])
172
self._delete_volume(volume, volume['size'])
174
def create_snapshot(self, snapshot):
175
"""Creates a snapshot."""
176
orig_lv_name = "%s/%s" % (FLAGS.volume_group, snapshot['volume_name'])
177
self._try_execute('lvcreate', '-L',
178
self._sizestr(snapshot['volume_size']),
179
'--name', self._escape_snapshot(snapshot['name']),
180
'--snapshot', orig_lv_name, run_as_root=True)
182
def delete_snapshot(self, snapshot):
183
"""Deletes a snapshot."""
184
if self._volume_not_present(self._escape_snapshot(snapshot['name'])):
185
# If the snapshot isn't present, then don't attempt to delete
188
# TODO(yamahata): zeroing out the whole snapshot triggers COW.
190
self._delete_volume(snapshot, snapshot['volume_size'])
192
def local_path(self, volume):
193
# NOTE(vish): stops deprecation warning
194
escaped_group = FLAGS.volume_group.replace('-', '--')
195
escaped_name = self._escape_snapshot(volume['name']).replace('-', '--')
196
return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
198
def ensure_export(self, context, volume):
199
"""Synchronously recreates an export for a logical volume."""
200
raise NotImplementedError()
202
def create_export(self, context, volume):
203
"""Exports the volume. Can optionally return a Dictionary of changes
204
to the volume object to be persisted."""
205
raise NotImplementedError()
207
def remove_export(self, context, volume):
208
"""Removes an export for a logical volume."""
209
raise NotImplementedError()
211
def check_for_export(self, context, volume_id):
212
"""Make sure volume is exported."""
213
raise NotImplementedError()
215
def initialize_connection(self, volume, connector):
216
"""Allow connection to connector and return connection info."""
217
raise NotImplementedError()
219
def terminate_connection(self, volume, connector):
220
"""Disallow connection from connector"""
221
raise NotImplementedError()
223
def get_volume_stats(self, refresh=False):
224
"""Return the current state of the volume service. If 'refresh' is
225
True, run the update first."""
228
def do_setup(self, context):
229
"""Any initialization the volume driver does while starting"""
233
class ISCSIDriver(VolumeDriver):
234
"""Executes commands relating to ISCSI volumes.
236
We make use of model provider properties as follows:
238
``provider_location``
239
if present, contains the iSCSI target information in the same
240
format as an ietadm discovery
241
i.e. '<ip>:<port>,<portal> <target IQN>'
244
if present, contains a space-separated triple:
245
'<auth method> <auth username> <auth password>'.
246
`CHAP` is the only auth_method in use at the moment.
249
def __init__(self, *args, **kwargs):
250
self.tgtadm = iscsi.get_target_admin()
251
super(ISCSIDriver, self).__init__(*args, **kwargs)
253
def set_execute(self, execute):
254
super(ISCSIDriver, self).set_execute(execute)
255
self.tgtadm.set_execute(execute)
257
def ensure_export(self, context, volume):
258
"""Synchronously recreates an export for a logical volume."""
260
iscsi_target = self.db.volume_get_iscsi_target_num(context,
262
except exception.NotFound:
263
LOG.info(_("Skipping ensure_export. No iscsi_target " +
264
"provisioned for volume: %d"), volume['id'])
267
iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
268
volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
270
self.tgtadm.new_target(iscsi_name, iscsi_target, check_exit_code=False)
271
self.tgtadm.new_logicalunit(iscsi_target, 0, volume_path,
272
check_exit_code=False)
274
def _ensure_iscsi_targets(self, context, host):
275
"""Ensure that target ids have been created in datastore."""
276
host_iscsi_targets = self.db.iscsi_target_count_by_host(context, host)
277
if host_iscsi_targets >= FLAGS.iscsi_num_targets:
279
# NOTE(vish): Target ids start at 1, not 0.
280
for target_num in xrange(1, FLAGS.iscsi_num_targets + 1):
281
target = {'host': host, 'target_num': target_num}
282
self.db.iscsi_target_create_safe(context, target)
284
def create_export(self, context, volume):
285
"""Creates an export for a logical volume."""
286
self._ensure_iscsi_targets(context, volume['host'])
287
iscsi_target = self.db.volume_allocate_iscsi_target(context,
290
iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
291
volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
293
self.tgtadm.new_target(iscsi_name, iscsi_target)
294
self.tgtadm.new_logicalunit(iscsi_target, 0, volume_path)
297
if FLAGS.iscsi_helper == 'tgtadm':
301
model_update['provider_location'] = _iscsi_location(
302
FLAGS.iscsi_ip_address, iscsi_target, iscsi_name, lun)
305
def remove_export(self, context, volume):
306
"""Removes an export for a logical volume."""
308
iscsi_target = self.db.volume_get_iscsi_target_num(context,
310
except exception.NotFound:
311
LOG.info(_("Skipping remove_export. No iscsi_target " +
312
"provisioned for volume: %d"), volume['id'])
316
# ietadm show will exit with an error
317
# this export has already been removed
318
self.tgtadm.show_target(iscsi_target)
319
except Exception as e:
320
LOG.info(_("Skipping remove_export. No iscsi_target " +
321
"is presently exported for volume: %d"), volume['id'])
324
self.tgtadm.delete_logicalunit(iscsi_target, 0)
325
self.tgtadm.delete_target(iscsi_target)
327
def _do_iscsi_discovery(self, volume):
328
#TODO(justinsb): Deprecate discovery and use stored info
329
#NOTE(justinsb): Discovery won't work with CHAP-secured targets (?)
330
LOG.warn(_("ISCSI provider_location not stored, using discovery"))
332
volume_name = volume['name']
334
(out, _err) = self._execute('iscsiadm', '-m', 'discovery',
335
'-t', 'sendtargets', '-p', volume['host'],
337
for target in out.splitlines():
338
if FLAGS.iscsi_ip_address in target and volume_name in target:
342
def _get_iscsi_properties(self, volume):
343
"""Gets iscsi configuration
345
We ideally get saved information in the volume entity, but fall back
346
to discovery if need be. Discovery may be completely removed in future
349
:target_discovered: boolean indicating whether discovery was used
351
:target_iqn: the IQN of the iSCSI target
353
:target_portal: the portal of the iSCSI target
355
:target_lun: the lun of the iSCSI target
357
:volume_id: the id of the volume (currently used by xen)
359
:auth_method:, :auth_username:, :auth_password:
361
the authentication details. Right now, either auth_method is not
362
present meaning no authentication, or auth_method == `CHAP`
363
meaning use CHAP with the specified credentials.
368
location = volume['provider_location']
371
# provider_location is the same format as iSCSI discovery output
372
properties['target_discovered'] = False
374
location = self._do_iscsi_discovery(volume)
377
raise exception.Error(_("Could not find iSCSI export "
381
LOG.debug(_("ISCSI Discovery: Found %s") % (location))
382
properties['target_discovered'] = True
384
results = location.split(" ")
385
properties['target_portal'] = results[0].split(",")[0]
386
properties['target_iqn'] = results[1]
388
properties['target_lun'] = int(results[2])
389
except (IndexError, ValueError):
390
if FLAGS.iscsi_helper == 'tgtadm':
391
properties['target_lun'] = 1
393
properties['target_lun'] = 0
395
properties['volume_id'] = volume['id']
397
auth = volume['provider_auth']
399
(auth_method, auth_username, auth_secret) = auth.split()
401
properties['auth_method'] = auth_method
402
properties['auth_username'] = auth_username
403
properties['auth_password'] = auth_secret
407
def _run_iscsiadm(self, iscsi_properties, iscsi_command):
408
(out, err) = self._execute('iscsiadm', '-m', 'node', '-T',
409
iscsi_properties['target_iqn'],
410
'-p', iscsi_properties['target_portal'],
411
*iscsi_command, run_as_root=True)
412
LOG.debug("iscsiadm %s: stdout=%s stderr=%s" %
413
(iscsi_command, out, err))
416
def _iscsiadm_update(self, iscsi_properties, property_key, property_value):
417
iscsi_command = ('--op', 'update', '-n', property_key,
418
'-v', property_value)
419
return self._run_iscsiadm(iscsi_properties, iscsi_command)
421
def initialize_connection(self, volume, connector):
422
"""Initializes the connection and returns connection info.
424
The iscsi driver returns a driver_volume_type of 'iscsi'.
425
The format of the driver data is defined in _get_iscsi_properties.
426
Example return value::
429
'driver_volume_type': 'iscsi'
431
'target_discovered': True,
432
'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001',
433
'target_portal': '127.0.0.0.1:3260',
440
iscsi_properties = self._get_iscsi_properties(volume)
442
'driver_volume_type': 'iscsi',
443
'data': iscsi_properties
446
def terminate_connection(self, volume, connector):
449
def check_for_export(self, context, volume_id):
450
"""Make sure volume is exported."""
452
tid = self.db.volume_get_iscsi_target_num(context, volume_id)
454
self.tgtadm.show_target(tid)
455
except exception.ProcessExecutionError, e:
456
# Instances remount read-only in this case.
457
# /etc/init.d/iscsitarget restart and rebooting nova-volume
458
# is better since ensure_export() works at boot time.
459
LOG.error(_("Cannot confirm exported volume "
460
"id:%(volume_id)s.") % locals())
464
class FakeISCSIDriver(ISCSIDriver):
465
"""Logs calls instead of executing."""
466
def __init__(self, *args, **kwargs):
467
super(FakeISCSIDriver, self).__init__(execute=self.fake_execute,
470
def check_for_setup_error(self):
471
"""No setup necessary in fake mode."""
474
def initialize_connection(self, volume, connector):
476
'driver_volume_type': 'iscsi',
480
def terminate_connection(self, volume, connector):
484
def fake_execute(cmd, *_args, **_kwargs):
485
"""Execute that simply logs the command."""
486
LOG.debug(_("FAKE ISCSI: %s"), cmd)
490
class RBDDriver(VolumeDriver):
491
"""Implements RADOS block device (RBD) volume commands"""
493
def check_for_setup_error(self):
494
"""Returns an error if prerequisites aren't met"""
495
(stdout, stderr) = self._execute('rados', 'lspools')
496
pools = stdout.split("\n")
497
if not FLAGS.rbd_pool in pools:
498
raise exception.Error(_("rbd has no pool %s") %
501
def create_volume(self, volume):
502
"""Creates a logical volume."""
503
if int(volume['size']) == 0:
506
size = int(volume['size']) * 1024
507
self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
508
'--size', size, 'create', volume['name'])
510
def delete_volume(self, volume):
511
"""Deletes a logical volume."""
512
self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
513
'rm', volume['name'])
515
def create_snapshot(self, snapshot):
516
"""Creates an rbd snapshot"""
517
self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
518
'snap', 'create', '--snap', snapshot['name'],
519
snapshot['volume_name'])
521
def delete_snapshot(self, snapshot):
522
"""Deletes an rbd snapshot"""
523
self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
524
'snap', 'rm', '--snap', snapshot['name'],
525
snapshot['volume_name'])
527
def local_path(self, volume):
528
"""Returns the path of the rbd volume."""
529
# This is the same as the remote path
530
# since qemu accesses it directly.
531
return "rbd:%s/%s" % (FLAGS.rbd_pool, volume['name'])
533
def ensure_export(self, context, volume):
534
"""Synchronously recreates an export for a logical volume."""
537
def create_export(self, context, volume):
538
"""Exports the volume"""
541
def remove_export(self, context, volume):
542
"""Removes an export for a logical volume"""
545
def initialize_connection(self, volume, connector):
547
'driver_volume_type': 'rbd',
549
'name': '%s/%s' % (FLAGS.rbd_pool, volume['name'])
553
def terminate_connection(self, volume, connector):
557
class SheepdogDriver(VolumeDriver):
558
"""Executes commands relating to Sheepdog Volumes"""
560
def check_for_setup_error(self):
561
"""Returns an error if prerequisites aren't met"""
563
#NOTE(francois-charlier) Since 0.24 'collie cluster info -r'
564
# gives short output, but for compatibility reason we won't
565
# use it and just check if 'running' is in the output.
566
(out, err) = self._execute('collie', 'cluster', 'info')
567
if not 'running' in out.split():
568
raise exception.Error(_("Sheepdog is not working: %s") % out)
569
except exception.ProcessExecutionError:
570
raise exception.Error(_("Sheepdog is not working"))
572
def create_volume(self, volume):
573
"""Creates a sheepdog volume"""
574
self._try_execute('qemu-img', 'create',
575
"sheepdog:%s" % volume['name'],
576
self._sizestr(volume['size']))
578
def create_volume_from_snapshot(self, volume, snapshot):
579
"""Creates a sheepdog volume from a snapshot."""
580
self._try_execute('qemu-img', 'create', '-b',
581
"sheepdog:%s:%s" % (snapshot['volume_name'],
583
"sheepdog:%s" % volume['name'])
585
def delete_volume(self, volume):
586
"""Deletes a logical volume"""
587
self._try_execute('collie', 'vdi', 'delete', volume['name'])
589
def create_snapshot(self, snapshot):
590
"""Creates a sheepdog snapshot"""
591
self._try_execute('qemu-img', 'snapshot', '-c', snapshot['name'],
592
"sheepdog:%s" % snapshot['volume_name'])
594
def delete_snapshot(self, snapshot):
595
"""Deletes a sheepdog snapshot"""
596
self._try_execute('collie', 'vdi', 'delete', snapshot['volume_name'],
597
'-s', snapshot['name'])
599
def local_path(self, volume):
600
return "sheepdog:%s" % volume['name']
602
def ensure_export(self, context, volume):
603
"""Safely and synchronously recreates an export for a logical volume"""
606
def create_export(self, context, volume):
607
"""Exports the volume"""
610
def remove_export(self, context, volume):
611
"""Removes an export for a logical volume"""
614
def initialize_connection(self, volume, connector):
616
'driver_volume_type': 'sheepdog',
618
'name': volume['name']
622
def terminate_connection(self, volume, connector):
626
class LoggingVolumeDriver(VolumeDriver):
627
"""Logs and records calls, for unit tests."""
629
def check_for_setup_error(self):
632
def create_volume(self, volume):
633
self.log_action('create_volume', volume)
635
def delete_volume(self, volume):
636
self.log_action('delete_volume', volume)
638
def local_path(self, volume):
639
print "local_path not implemented"
640
raise NotImplementedError()
642
def ensure_export(self, context, volume):
643
self.log_action('ensure_export', volume)
645
def create_export(self, context, volume):
646
self.log_action('create_export', volume)
648
def remove_export(self, context, volume):
649
self.log_action('remove_export', volume)
651
def initialize_connection(self, volume, connector):
652
self.log_action('initialize_connection', volume)
654
def terminate_connection(self, volume, connector):
655
self.log_action('terminate_connection', volume)
657
def check_for_export(self, context, volume_id):
658
self.log_action('check_for_export', volume_id)
664
LoggingVolumeDriver._LOGS = []
667
def log_action(action, parameters):
668
"""Logs the command."""
669
LOG.debug(_("LoggingVolumeDriver: %s") % (action))
672
log_dictionary = dict(parameters)
673
log_dictionary['action'] = action
674
LOG.debug(_("LoggingVolumeDriver: %s") % (log_dictionary))
675
LoggingVolumeDriver._LOGS.append(log_dictionary)
679
return LoggingVolumeDriver._LOGS
682
def logs_like(action, **kwargs):
684
for entry in LoggingVolumeDriver._LOGS:
685
if entry['action'] != action:
688
for k, v in kwargs.iteritems():
689
if entry.get(k) != v:
693
matches.append(entry)
697
def _iscsi_location(ip, target, iqn, lun=None):
698
return "%s:%s,%s %s %s" % (ip, FLAGS.iscsi_port, target, iqn, lun)